diff --git a/package.json b/package.json index 8df62a3b..68a09948 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "description": "A new look into listening and enjoying music in style and performance.", "license": "MIT", "main": "./build/index.js", - "author": "Cider Collective (https://cider.sh)", + "author": "Cider Collective (https://cider.sh)", "repository": "https://github.com/ciderapp/Cider.git", "bugs": { "url": "https://github.com/ciderapp/Cider/issues?q=is%3Aopen+is%3Aissue+label%3Abug" @@ -45,11 +45,12 @@ "run-script-os": "^1.1.6", "source-map-support": "^0.5.21", "v8-compile-cache": "^2.3.0", - "ws": "^8.4.0", + "ws": "^8.4.2", "xml2js": "^0.4.23", "youtube-search-without-api-key": "^1.0.7" }, "devDependencies": { + "@types/discord-rpc": "^4.0.0", "@types/express": "^4.17.13", "electron": "https://github.com/castlabs/electron-releases.git", "electron-builder": "^22.14.5", diff --git a/src/main/base/plugins.ts b/src/main/base/plugins.ts index 34980706..be87e89f 100644 --- a/src/main/base/plugins.ts +++ b/src/main/base/plugins.ts @@ -23,7 +23,7 @@ export default class PluginHandler { if (plugins[file] || plugin.name in plugins) { console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`); } else { - plugins[file] = new plugin(); + plugins[file] = new plugin(electron.app); } } }); @@ -33,16 +33,16 @@ export default class PluginHandler { if (fs.existsSync(this.userPluginsPath)) { fs.readdirSync(this.userPluginsPath).forEach(file => { if (file.endsWith('.ts') || file.endsWith('.js')) { - const plugin = require(path.join(this.userPluginsPath, file)); + const plugin = require(path.join(this.userPluginsPath, file)).default; if (plugins[file] || plugin in plugins) { - console.log(`[${plugin.default}] Plugin already loaded / Duplicate Class Name`); + console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`); } else { - plugins[file] = new plugin.default(); + plugins[file] = new plugin(electron.app); } } }); } - + console.log('loaded plugins:', JSON.stringify(plugins)) return plugins; } @@ -54,4 +54,4 @@ export default class PluginHandler { } } -} \ No newline at end of file +} diff --git a/src/main/base/win.ts b/src/main/base/win.ts index 11b7cd9b..b7d351c0 100644 --- a/src/main/base/win.ts +++ b/src/main/base/win.ts @@ -9,6 +9,8 @@ import * as fs from "fs"; import { Stream } from "stream"; import * as qrcode from "qrcode-terminal"; import * as os from "os"; +import {wsapi} from "./wsapi"; + export class Win { win: any | undefined = null; app: any | undefined = null; @@ -83,6 +85,8 @@ export class Win { this.options.height = windowState.height; // Start the webserver for the browser window to load + const ws = new wsapi() + ws.InitWebSockets() this.startWebServer(); this.win = new electron.BrowserWindow(this.options); diff --git a/src/main/base/wsapi.ts b/src/main/base/wsapi.ts new file mode 100644 index 00000000..ff1d96b9 --- /dev/null +++ b/src/main/base/wsapi.ts @@ -0,0 +1,284 @@ +// @ts-nocheck + +import * as ws from "ws"; +import * as http from "http"; +import * as https from "https"; +import * as url from "url"; +import * as fs from "fs"; +import * as path from "path"; +import * as electron from "electron"; +const WebSocket = ws; +const WebSocketServer = ws.Server; + +private class standardResponse { + status: number; + message: string; + data: any; + type: string; +} + +export class wsapi { + port: any = 26369 + wss: any = null + clients: [] + createId() { + // create random guid + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, + v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + public async InitWebSockets () { + electron.ipcMain.on('wsapi-updatePlaybackState', (event, arg) => { + wsapi.updatePlaybackState(arg); + }) + + electron.ipcMain.on('wsapi-returnQueue', (event, arg) => { + wsapi.returnQueue(JSON.parse(arg)); + }); + + electron.ipcMain.on('wsapi-returnSearch', (event, arg) => { + console.log("SEARCH") + wsapi.returnSearch(JSON.parse(arg)); + }); + + electron.ipcMain.on('wsapi-returnSearchLibrary', (event, arg) => { + wsapi.returnSearchLibrary(JSON.parse(arg)); + }); + + electron.ipcMain.on('wsapi-returnDynamic', (event, arg, type) => { + wsapi.returnDynamic(JSON.parse(arg), type); + }); + + electron.ipcMain.on('wsapi-returnMusicKitApi', (event, arg, method) => { + wsapi.returnMusicKitApi(JSON.parse(arg), method); + }); + + electron.ipcMain.on('wsapi-returnLyrics', (event, arg) => { + wsapi.returnLyrics(JSON.parse(arg)); + }); + this.wss = new WebSocketServer({ + port: this.port, + perMessageDeflate: { + zlibDeflateOptions: { + // See zlib defaults. + chunkSize: 1024, + memLevel: 7, + level: 3 + }, + zlibInflateOptions: { + chunkSize: 10 * 1024 + }, + // Other options settable: + clientNoContextTakeover: true, // Defaults to negotiated value. + serverNoContextTakeover: true, // Defaults to negotiated value. + serverMaxWindowBits: 10, // Defaults to negotiated value. + // Below options specified as default values. + concurrencyLimit: 10, // Limits zlib concurrency for perf. + threshold: 1024 // Size (in bytes) below which messages + // should not be compressed if context takeover is disabled. + } + }) + console.log(`WebSocketServer started on port: ${this.port}`); + + const defaultResponse = new standardResponse(0, {}, "OK"); + + + this.wss.on('connection', function connection(ws) { + ws.id = wsapi.createId(); + console.log(`Client ${ws.id} connected`) + wsapi.clients.push(ws); + ws.on('message', function incoming(message) { + + }); + // ws on message + ws.on('message', function incoming(message) { + let data = JSON.parse(message); + let response = new standardResponse(0, {}, "OK");; + if (data.action) { + data.action.toLowerCase(); + } + switch (data.action) { + default: + response.message = "Action not found"; + break; + case "identify": + response.message = "Thanks for identifying!" + response.data = { + id: ws.id + } + ws.identity = { + name: data.name, + author: data.author, + description: data.description, + version: data.version + } + break; + case "play-next": + electron.app.win.webContents.executeJavaScript(`wsapi.playNext(\`${data.type}\`,\`${data.id}\`)`); + response.message = "Play Next"; + break; + case "play-later": + electron.app.win.webContents.executeJavaScript(`wsapi.playLater(\`${data.type}\`,\`${data.id}\`)`); + response.message = "Play Later"; + break; + case "quick-play": + electron.app.win.webContents.executeJavaScript(`wsapi.quickPlay(\`${data.term}\`)`); + response.message = "Quick Play"; + break; + case "get-lyrics": + electron.app.win.webContents.executeJavaScript(`wsapi.getLyrics()`); + break; + case "shuffle": + electron.app.win.webContents.executeJavaScript(`wsapi.toggleShuffle()`); + break; + case "set-shuffle": + if(data.shuffle == true) { + electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().shuffleMode = 1`); + }else{ + electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().shuffleMode = 0`); + } + break; + case "repeat": + electron.app.win.webContents.executeJavaScript(`wsapi.toggleRepeat()`); + break; + case "seek": + electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().seekToTime(${parseFloat(data.time)})`); + response.message = "Seek"; + break; + case "pause": + electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().pause()`); + response.message = "Paused"; + break; + case "play": + electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().play()`); + response.message = "Playing"; + break; + case "stop": + electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().stop()`); + response.message = "Stopped"; + break; + case "volume": + electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().volume = ${parseFloat(data.volume)}`); + response.message = "Volume"; + break; + case "mute": + electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().mute()`); + response.message = "Muted"; + break; + case "unmute": + electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().unmute()`); + response.message = "Unmuted"; + break; + case "next": + electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().skipToNextItem()`); + response.message = "Next"; + break; + case "previous": + electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().skipToPreviousItem()`); + response.message = "Previous"; + break; + case "musickit-api": + electron.app.win.webContents.executeJavaScript(`wsapi.musickitApi(\`${data.method}\`, \`${data.id}\`, ${JSON.stringify(data.params)})`); + break; + case "musickit-library-api": + break; + case "set-autoplay": + electron.app.win.webContents.executeJavaScript(`wsapi.setAutoplay(${data.autoplay})`); + break; + case "queue-move": + electron.app.win.webContents.executeJavaScript(`wsapi.moveQueueItem(${data.from},${data.to})`); + break; + case "get-queue": + electron.app.win.webContents.executeJavaScript(`wsapi.getQueue()`); + break; + case "search": + if (!data.limit) { + data.limit = 10; + } + electron.app.win.webContents.executeJavaScript(`wsapi.search(\`${data.term}\`, \`${data.limit}\`)`); + break; + case "library-search": + if (!data.limit) { + data.limit = 10; + } + electron.app.win.webContents.executeJavaScript(`wsapi.searchLibrary(\`${data.term}\`, \`${data.limit}\`)`); + break; + case "show-window": + electron.app.win.show() + break; + case "hide-window": + electron.app.win.hide() + break; + case "play-mediaitem": + electron.app.win.webContents.executeJavaScript(`wsapi.playTrackById(${data.id}, \`${data.kind}\`)`); + response.message = "Playing track"; + break; + case "get-status": + response.data = { + isAuthorized: true + }; + response.message = "Status"; + break; + case "get-currentmediaitem": + electron.app.win.webContents.executeJavaScript(`wsapi.getPlaybackState()`); + break; + } + ws.send(JSON.stringify(response)); + }); + + ws.on('close', function close() { + // remove client from list + wsapi.clients.splice(wsapi.clients.indexOf(ws), 1); + console.log(`Client ${ws.id} disconnected`); + }); + ws.send(JSON.stringify(defaultResponse)); + }); + } + sendToClient(id) { + // replace the clients.forEach with a filter to find the client that requested + } + updatePlaybackState(attr) { + const response = new standardResponse(0, attr, "OK", "playbackStateUpdate"); + wsapi.clients.forEach(function each(client) { + client.send(JSON.stringify(response)); + }); + } + returnMusicKitApi(results, method) { + const response = new standardResponse(0, results, "OK", `musickitapi.${method}`); + wsapi.clients.forEach(function each(client) { + client.send(JSON.stringify(response)); + }); + } + returnDynamic(results, type) { + const response = new standardResponse(0, results, "OK", type); + wsapi.clients.forEach(function each(client) { + client.send(JSON.stringify(response)); + }); + } + returnLyrics(results) { + const response = new standardResponse(0, results, "OK", "lyrics"); + wsapi.clients.forEach(function each(client) { + client.send(JSON.stringify(response)); + }); + } + returnSearch(results) { + const response = new standardResponse(0, results, "OK", "searchResults"); + wsapi.clients.forEach(function each(client) { + client.send(JSON.stringify(response)); + }); + } + returnSearchLibrary(results) { + const response = new standardResponse(0, results, "OK", "searchResultsLibrary"); + wsapi.clients.forEach(function each(client) { + client.send(JSON.stringify(response)); + }); + } + returnQueue(queue) { + const response = new standardResponse(0, queue, "OK", "queue"); + wsapi.clients.forEach(function each(client) { + client.send(JSON.stringify(response)); + }); + } +} \ No newline at end of file diff --git a/src/main/index.ts b/src/main/index.ts index 8e802ca4..30ed265b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -11,7 +11,6 @@ import {AppEvents} from "./base/app"; import PluginHandler from "./base/plugins"; // const test = new PluginHandler(); - const config = new ConfigStore(); const App = new AppEvents(config.store); const Cider = new Win(electron.app, config.store) @@ -23,17 +22,21 @@ const plug = new PluginHandler(); electron.app.on('ready', () => { App.ready(); - plug.callPlugins('onReady'); console.log('[Cider] Application is Ready. Creating Window.') if (!electron.app.isPackaged) { console.info('[Cider] Running in development mode.') require('vue-devtools').install() } - electron.components.whenReady().then(() => { - Cider.createWindow(); + + electron.components.whenReady().then(async () => { + await Cider.createWindow() + plug.callPlugins('onReady', Cider); + + }) + }); /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -110,4 +113,4 @@ electron.app.on('before-quit', () => { // console.warn("[InstanceHandler] Existing Instance is Blocking Second Instance."); // app.quit(); // app.isQuiting = true -// } \ No newline at end of file +// } diff --git a/src/main/plugins/examplePlugin.ts b/src/main/plugins/Extras/examplePlugin.ts similarity index 81% rename from src/main/plugins/examplePlugin.ts rename to src/main/plugins/Extras/examplePlugin.ts index 9c7bb8e4..78a194a9 100644 --- a/src/main/plugins/examplePlugin.ts +++ b/src/main/plugins/Extras/examplePlugin.ts @@ -1,5 +1,10 @@ let i = 1, k = 1; export default class ExamplePlugin { + /** + * Private variables for interaction in plugins + */ + private _win: any; + private _app: any; /** * Base Plugin Details (Eventually implemented into a GUI in settings) @@ -12,14 +17,16 @@ export default class ExamplePlugin { /** * Runs on plugin load (Currently run on application start) */ - constructor() { - - } + constructor(app: any) { + this._app = app; + console.log('Example plugin loaded'); + } /** * Runs on app ready */ - onReady(): void { + onReady(win: any): void { + this._win = win; console.log('Example plugin ready'); } @@ -48,4 +55,4 @@ export default class ExamplePlugin { k++ } -} \ No newline at end of file +} diff --git a/src/main/plugins/Extras/sendSongToTitlebar.ts b/src/main/plugins/Extras/sendSongToTitlebar.ts new file mode 100644 index 00000000..540e32f4 --- /dev/null +++ b/src/main/plugins/Extras/sendSongToTitlebar.ts @@ -0,0 +1,37 @@ +export default class sendSongToTitlebar { + /** + * Base Plugin Details (Eventually implemented into a GUI in settings) + */ + public name: string = 'sendSongToTitlebar'; + public description: string = 'Sets the app\'s titlebar to the Song title'; + public version: string = '0.0.1'; + public author: string = 'Cider Collective (credit to 8times9 via #147)'; + /** + * Runs on plugin load (Currently run on application start) + */ + private _win: any; + private _app: any; + constructor() {} + /** + * Runs on app ready + */ + onReady(win: any): void { + this._win = win; + } + /** + * Runs on app stop + */ + onBeforeQuit(): void {} + /** + * Runs on playback State Change + * @param attributes Music Attributes (attributes.state = current state) + */ + onPlaybackStateDidChange(attributes: any): void { + this._win.win.setTitle(`${(attributes != null && attributes.name != null && attributes.name.length > 0) ? (attributes.name + " - ") : ''}Cider`) + } + /** + * Runs on song change + * @param attributes Music Attributes + */ + onNowPlayingItemDidChange(attributes: object): void {} +} \ No newline at end of file diff --git a/src/main/plugins/discordrpc.ts b/src/main/plugins/discordrpc.ts new file mode 100644 index 00000000..599560df --- /dev/null +++ b/src/main/plugins/discordrpc.ts @@ -0,0 +1,205 @@ +import * as DiscordRPC from 'discord-rpc' +export default class DiscordRPCPlugin { + /** + * Private variables for interaction in plugins + */ + private _win: any; + private _app: any; + private _discord: any; + private connect(clientId: any) { + this._discord = { isConnected: false }; + if (this._win.store.store.general.discord_rpc == 0 || this._discord.isConnected) return; + + DiscordRPC.register(clientId) // Apparently needed for ask to join, join, spectate etc. + const client = new DiscordRPC.Client({ transport: "ipc" }); + this._discord = Object.assign(client, { error: false, activityCache: null, isConnected: false }); + + // Login to Discord + this._discord.login({ clientId }) + .then(() => { + this._discord.isConnected = true; + }) + .catch((e : any) => console.error(`[DiscordRPC][connect] ${e}`)); + + this._discord.on('ready', () => { + console.log(`[DiscordRPC][connect] Successfully Connected to Discord. Authed for user: ${client.user.username} (${client.user.id})`); + }) + + // Handles Errors + this._discord.on('error', (err: any) => { + console.error(`[DiscordRPC] ${err}`); + this.disconnect() + this._discord.isConnected = false; + }); + } + + /** + * Disconnects from Discord RPC + */ + private disconnect() { + if (this._win.store.store.general.discord_rpc == 0 || !this._discord.isConnected) return; + + try { + this._discord.destroy().then(() => { + this._discord.isConnected = false; + console.log('[DiscordRPC][disconnect] Disconnected from discord.') + }).catch((e : any) => console.error(`[DiscordRPC][disconnect] ${e}`)); + } catch (err) { + console.error(err) + } + } + + /** + * Sets the activity of the client + * @param {object} attributes + */ + private updateActivity(attributes : any) { + if (this._win.store.store.general.discord_rpc == 0) return; + + if (!this._discord.isConnected) { + this._discord.clearActivity().catch((e : any) => console.error(`[DiscordRPC][updateActivity] ${e}`)); + return; + } + + // console.log('[DiscordRPC][updateActivity] Updating Discord Activity.') + + const listenURL = `https://cider.sh/p?s&id=${attributes.playParams.id}` // cider://play/s/[id] (for song) + //console.log(attributes) + + interface ActObject { + details?: any, + state?: any, + startTimestamp?: any, + endTimestamp?: any, + largeImageKey? : any, + largeImageText?: any, + smallImageKey?: any, + smallImageText?: any, + instance: true, + buttons?: [ + { label: "Listen on Cider", url?: any }, + ] + } + + let ActivityObject : ActObject | null = { + details: attributes.name, + state: `by ${attributes.artistName}`, + startTimestamp: attributes.startTime, + endTimestamp: attributes.endTime, + largeImageKey : (attributes.artwork.url.replace('{w}', '1024').replace('{h}', '1024')) ?? 'cider', + largeImageText: attributes.albumName, + smallImageKey: (attributes.status ? 'play' : 'pause'), + smallImageText: (attributes.status ? 'Playing' : 'Paused'), + instance: true, + buttons: [ + { label: "Listen on Cider", url: listenURL }, + ] + }; + if (ActivityObject.largeImageKey == "" || ActivityObject.largeImageKey == null) { + ActivityObject.largeImageKey = (this._win.store.store.general.discord_rpc == 1) ? "cider" : "logo" + } + + // Remove the pause/play icon and test for clear activity on pause + if (this._win.store.store.general.discordClearActivityOnPause == 1) { + delete ActivityObject.smallImageKey + delete ActivityObject.smallImageText + } + + // Deletes the timestamp if its not greater than 0 + if (!((new Date(attributes.endTime)).getTime() > 0)) { + delete ActivityObject.startTimestamp + delete ActivityObject.endTimestamp + } + + // Artist check + if (!attributes.artistName) { + delete ActivityObject.state + } + + // Album text check + if (!ActivityObject.largeImageText || ActivityObject.largeImageText.length < 2) { + delete ActivityObject.largeImageText + } + + // Checks if the name is greater than 128 because some songs can be that long + if (ActivityObject.details.length > 128) { + ActivityObject.details = ActivityObject.details.substring(0, 125) + '...' + } + + + + + // Check if its pausing (false) or playing (true) + if (!attributes.status) { + if (this._win.store.store.general.discordClearActivityOnPause == 1) { + this._discord.clearActivity().catch((e : any) => console.error(`[DiscordRPC][clearActivity] ${e}`)); + ActivityObject = null + } else { + delete ActivityObject.startTimestamp + delete ActivityObject.endTimestamp + ActivityObject.smallImageKey = 'pause' + ActivityObject.smallImageText = 'Paused' + } + } + + + if (ActivityObject && ActivityObject !== this._discord.activityCache && ActivityObject.details && ActivityObject.state) { + try { + // console.log(`[DiscordRPC][setActivity] Setting activity to ${JSON.stringify(ActivityObject)}`); + this._discord.setActivity(ActivityObject) + this._discord.activityCache = ActivityObject + } catch (err) { + console.error(`[DiscordRPC][setActivity] ${err}`) + } + + } + } + + + /** + * Base Plugin Details (Eventually implemented into a GUI in settings) + */ + public name: string = 'DiscordRPCPlugin'; + public description: string = 'Discord RPC plugin for Cider'; + public version: string = '0.0.1'; + public author: string = 'vapormusic / Cider Collective'; + + /** + * Runs on plugin load (Currently run on application start) + */ + constructor(app: any) { + this._app = app; + } + + /** + * Runs on app ready + */ + onReady(win: any): void { + this._win = win; + this.connect((this._win.store.store.general.discord_rpc == 1) ? '911790844204437504' : '886578863147192350'); + } + + /** + * Runs on app stop + */ + onBeforeQuit(): void { + console.log('Example plugin stopped'); + } + + /** + * Runs on playback State Change + * @param attributes Music Attributes (attributes.state = current state) + */ + onPlaybackStateDidChange(attributes: object): void { + this.updateActivity(attributes) + } + + /** + * Runs on song change + * @param attributes Music Attributes + */ + onNowPlayingItemDidChange(attributes: object): void { + this.updateActivity(attributes) + } + +} diff --git a/src/renderer/.jsbeautifyrc b/src/renderer/.jsbeautifyrc new file mode 100644 index 00000000..bde13199 --- /dev/null +++ b/src/renderer/.jsbeautifyrc @@ -0,0 +1,5 @@ +{ + "js": { + "beautify.ignore": "src/renderer/index.js" + } +} \ No newline at end of file diff --git a/src/renderer/WSAPI_Interop.js b/src/renderer/WSAPI_Interop.js new file mode 100644 index 00000000..c7143b37 --- /dev/null +++ b/src/renderer/WSAPI_Interop.js @@ -0,0 +1,103 @@ +const wsapi = { + cache: {playParams: {id: 0}, status: null, remainingTime: 0}, + playbackCache: {status: null, time: Date.now()}, + search(term, limit) { + MusicKit.getInstance().api.search(term, {limit: limit, types: 'songs,artists,albums'}).then((results)=>{ + ipcRenderer.send('wsapi-returnSearch', JSON.stringify(results)) + }) + }, + searchLibrary(term, limit) { + MusicKit.getInstance().api.library.search(term, {limit: limit, types: 'library-songs,library-artists,library-albums'}).then((results)=>{ + ipcRenderer.send('wsapi-returnSearchLibrary', JSON.stringify(results)) + }) + }, + getAttributes: function () { + const mk = MusicKit.getInstance(); + const nowPlayingItem = mk.nowPlayingItem; + const isPlayingExport = mk.isPlaying; + const remainingTimeExport = mk.currentPlaybackTimeRemaining; + const attributes = (nowPlayingItem != null ? nowPlayingItem.attributes : {}); + + attributes.status = isPlayingExport ? isPlayingExport : false; + attributes.name = attributes.name ? attributes.name : 'No Title Found'; + attributes.artwork = attributes.artwork ? attributes.artwork : {url: ''}; + attributes.artwork.url = attributes.artwork.url ? attributes.artwork.url : ''; + attributes.playParams = attributes.playParams ? attributes.playParams : {id: 'no-id-found'}; + attributes.playParams.id = attributes.playParams.id ? attributes.playParams.id : 'no-id-found'; + attributes.albumName = attributes.albumName ? attributes.albumName : ''; + attributes.artistName = attributes.artistName ? attributes.artistName : ''; + attributes.genreNames = attributes.genreNames ? attributes.genreNames : []; + attributes.remainingTime = remainingTimeExport ? (remainingTimeExport * 1000) : 0; + attributes.durationInMillis = attributes.durationInMillis ? attributes.durationInMillis : 0; + attributes.startTime = Date.now(); + attributes.endTime = attributes.endTime ? attributes.endTime : Date.now(); + attributes.volume = mk.volume; + attributes.shuffleMode = mk.shuffleMode; + attributes.repeatMode = mk.repeatMode; + attributes.autoplayEnabled = mk.autoplayEnabled; + return attributes + }, + moveQueueItem(oldPosition, newPosition) { + MusicKit.getInstance().queue._queueItems.splice(newPosition,0,MusicKit.getInstance().queue._queueItems.splice(oldPosition,1)[0]) + MusicKit.getInstance().queue._reindex() + }, + setAutoplay(value) { + MusicKit.getInstance().autoplayEnabled = value + }, + returnDynamic(data, type) { + ipcRenderer.send('wsapi-returnDynamic', JSON.stringify(data), type) + }, + musickitApi(method, id, params) { + MusicKit.getInstance().api[method](id, params).then((results)=>{ + ipcRenderer.send('wsapi-returnMusicKitApi', JSON.stringify(results), method) + }) + }, + getPlaybackState () { + ipcRenderer.send('wsapi-updatePlaybackState', MusicKitInterop.getAttributes()); + }, + getLyrics() { + return [] + _lyrics.GetLyrics(1, false) + }, + getQueue() { + ipcRenderer.send('wsapi-returnQueue', JSON.stringify(MusicKit.getInstance().queue)) + }, + playNext(type, id) { + var request = {} + request[type] = id + MusicKit.getInstance().playNext(request) + }, + playLater(type, id) { + var request = {} + request[type] = id + MusicKit.getInstance().playLater(request) + }, + love() { + + }, + playTrackById(id, kind = "song") { + MusicKit.getInstance().setQueue({ [kind]: id }).then(function (queue) { + MusicKit.getInstance().play() + }) + }, + quickPlay(term) { + // Quick play by song name + MusicKit.getInstance().api.search(term, { limit: 2, types: 'songs' }).then(function (data) { + MusicKit.getInstance().setQueue({ song: data["songs"][0]["id"] }).then(function (queue) { + MusicKit.getInstance().play() + }) + }) + }, + toggleShuffle() { + MusicKit.getInstance().shuffleMode = MusicKit.getInstance().shuffleMode === 0 ? 1 : 0 + }, + toggleRepeat() { + if(MusicKit.getInstance().repeatMode == 0) { + MusicKit.getInstance().repeatMode = 2 + }else if(MusicKit.getInstance().repeatMode == 2){ + MusicKit.getInstance().repeatMode = 1 + }else{ + MusicKit.getInstance().repeatMode = 0 + } + } +} \ No newline at end of file diff --git a/src/renderer/index.js b/src/renderer/index.js index 61e69fa1..611d27d8 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -249,6 +249,11 @@ const app = new Vue({ start: 0, end: 0 }, + v3: { + requestBody: { + platform: "web" + } + }, tmpVar: [], notification: false, chrome: { @@ -610,10 +615,16 @@ const app = new Vue({ } }) + this.mk.addEventListener(MusicKit.Events.playbackStateDidChange, ()=>{ + ipcRenderer.send('wsapi-updatePlaybackState', wsapi.getAttributes()); + }) + this.mk.addEventListener(MusicKit.Events.playbackTimeDidChange, (a) => { self.lyriccurrenttime = self.mk.currentPlaybackTime this.currentSongInfo = a self.playerLCD.playbackDuration = (self.mk.currentPlaybackTime) + // wsapi + ipcRenderer.send('wsapi-updatePlaybackState', wsapi.getAttributes()); }) this.mk.addEventListener(MusicKit.Events.nowPlayingItemDidChange, (a) => { @@ -875,15 +886,18 @@ const app = new Vue({ }) } }, - async showCollection(response, title, type) { + async showCollection(response, title, type, requestBody = {}) { let self = this + console.log(response) + this.collectionList.requestBody = {} this.collectionList.response = response this.collectionList.title = title this.collectionList.type = type + this.collectionList.requestBody = requestBody app.appRoute("collection-list") }, async showArtistView(artist, title, view) { - let response = (await app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/artists/${artist}/view/${view}`)).data + let response = (await app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/artists/${artist}/view/${view}`,{}, {includeResponseMeta: !0})).data console.log(response) await this.showCollection(response, title, "artists") }, @@ -892,7 +906,8 @@ const app = new Vue({ await this.showCollection(response, title, "record-labels") }, async showSearchView(term, group, title) { - let response = await app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/search?term=${term}`, { + + let requestBody = { platform: "web", groups: group, types: "activities,albums,apple-curators,artists,curators,editorial-items,music-movies,music-videos,playlists,songs,stations,tv-episodes,uploaded-videos,record-labels", @@ -918,14 +933,18 @@ const app = new Vue({ resource: ["autos"] }, groups: group + } + let response = await app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/search?term=${term}`, requestBody , { + includeResponseMeta: !0 }) + console.log('searchres', response) let responseFormat = { data: response.data.results[group].data, - next: response.data.results[group].data, + next: response.data.results[group].next, groups: group } - await this.showCollection(responseFormat, title, "search") + await this.showCollection(responseFormat, title, "search", requestBody) }, async getPlaylistContinuous(response, transient = false) { response = response.data.data[0] @@ -1134,7 +1153,10 @@ const app = new Vue({ window.location.hash = `${kind}/${id}` document.querySelector("#app-content").scrollTop = 0 } else if (!kind.toString().includes("radioStation") && !kind.toString().includes("song") && !kind.toString().includes("musicVideo") && !kind.toString().includes("uploadedVideo") && !kind.toString().includes("music-movie")) { - let params = { extend: "editorialVideo" } + let params = { + extend: "offers,editorialVideo", + "views": "appears-on,more-by-artist,related-videos,other-versions,you-might-also-like,video-extras,audio-extras", + } app.page = (kind) + "_" + (id); app.getTypeFromID((kind), (id), (isLibrary), params); window.location.hash = `${kind}/${id}${isLibrary ? "/" + isLibrary : ''}` @@ -2878,7 +2900,7 @@ const app = new Vue({ } id = item.id } - let response = await this.mk.api.v3.music(`/v1/me/ratings/${type}?platform=web&ids=${type.includes('library') ? item.id : id}}`) + let response = await this.mk.api.v3.music(`/v1/me/ratings/${type}?platform=web&ids=${type.includes('library') ? item.id : id}`) if (response.data.data.length != 0) { let value = response.data.data[0].attributes.value return value @@ -3073,7 +3095,6 @@ const app = new Vue({ items: [{ "icon": "./assets/feather/list.svg", "name": "Add to Playlist...", - "hidden": true, "action": function() { app.promptAddToPlaylist() } diff --git a/src/renderer/style.less b/src/renderer/style.less index 11db2fca..65b5389a 100644 --- a/src/renderer/style.less +++ b/src/renderer/style.less @@ -18,6 +18,7 @@ --navbarHeight: 48px; --selected: rgb(130 130 130 / 30%); --selected-click: rgb(80 80 80 / 30%); + --hover: rgb(200 200 200 / 10%); --keyColor: #fa586a; --keyColor-rgb: 250, 88, 106; --keyColor-rollover: #ff8a9c; @@ -254,6 +255,32 @@ input[type="text"], input[type="number"] { } } +.artworkMaterial { + position: relative; + height:100%; + width:100%; + overflow: hidden; + pointer-events: none; + + >img { + position: absolute; + width: 200%; + opacity: 0.5; + filter: brightness(200%) blur(180px) saturate(280%) contrast(2); + } + + >img:first-child { + top:0; + left:0; + } + + >img:last-child { + bottom:0; + right: 0; + transform: rotate(180deg); + } +} + [artwork-hidden] { transition: opacity .25s var(--appleEase); @@ -867,6 +894,13 @@ input[type=range].web-slider::-webkit-slider-runnable-track { } .app-chrome .app-chrome-item.volume > input[type=range]::-webkit-slider-thumb { + -webkit-appearance: none; + height: 14px; + width: 14px; + border-radius: 50%; + background: rgb(50 50 50); + cursor: default; + box-shadow: inset 0px 0px 0px 1px rgba(255, 255, 255, 0.4); transition: all var(--appleTransition); } @@ -880,10 +914,6 @@ input[type=range].web-slider::-webkit-slider-runnable-track { transform: scale(1); } -.app-chrome .app-chrome-item.volume > input[type=range] { - width: 100%; -} - .app-chrome .app-chrome-item.volume > input[type=range] { -webkit-appearance: none; height: 4px; @@ -891,16 +921,7 @@ input[type=range].web-slider::-webkit-slider-runnable-track { border-radius: 5px; background-size: 70% 100%; background-repeat: no-repeat; -} - -.app-chrome .app-chrome-item.volume > input[type=range]::-webkit-slider-thumb { - -webkit-appearance: none; - height: 14px; - width: 14px; - border-radius: 50%; - background: rgb(50 50 50); - cursor: default; - box-shadow: inset 0px 0px 0px 1px rgba(255, 255, 255, 0.4); + width: 100%, } .app-chrome .app-chrome-item.volume > input[type=range]::-webkit-slider-runnable-track { @@ -1018,6 +1039,15 @@ input[type=range].web-slider::-webkit-slider-runnable-track { //margin-bottom: -3px; } } + + .explicit-icon { + background-image: url("./assets/explicit.svg"); + height: 9px; + width: 36px; + filter: contrast(0); + background-repeat: no-repeat; + margin-left: 3px; + } } .app-chrome .app-chrome-item > .app-playback-controls .song-duration p { @@ -1133,6 +1163,34 @@ input[type=range].web-slider::-webkit-slider-runnable-track { justify-content: center; align-items: center; filter: contrast(0.8); + + .lcdMenu { + height: 100%; + width: 100%; + padding: 0px; + margin: 0px; + background: transparent; + border: 0px; + appearance: none; + display: flex; + justify-content: center; + align-items: center; + border-radius: 6px; + + &:focus { + outline: none; + } + &:hover { + background: var(--hover); + } + &:active { + background: var(--selected-click); + transform: scale(0.95); + } + .svg-icon { + --url: url('views/svg/more.svg')!important; + } + } } .app-chrome .app-chrome-item > .app-playback-controls .playback-info { @@ -1177,7 +1235,57 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb { /* Window is smaller <= 1023px width */ @media only screen and (max-width: 1023px) { .display--small { - display: inherit !important; + display: inherit !important;; + + .slider { + width: 100%; + z-index: 1; + } + + .input-container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + padding-bottom: 10px; + } + + input[type=range] { + -webkit-appearance: none; + height: 4px; + background: rgba(255, 255, 255, 0.4); + border-radius: 5px; + background-size: 70% 100%; + background-repeat: no-repeat; + + &::-webkit-slider-thumb { + -webkit-appearance: none; + height: 14px; + width: 14px; + border-radius: 50%; + background: rgb(50 50 50); + cursor: default; + box-shadow: inset 0px 0px 0px 1px rgba(255, 255, 255, 0.4); + transition: all var(--appleTransition); + } + + &::-webkit-slider-thumb:hover { + background-image: radial-gradient(var(--keyColor) 2px, transparent 3px, transparent 10px); + transform: scale(1.2); + } + + &::-webkit-slider-thumb:active { + background-image: radial-gradient(var(--keyColor) 3px, transparent 4px, transparent 10px); + transform: scale(1); + } + + &::-webkit-slider-runnable-track { + -webkit-appearance: none; + box-shadow: none; + border: none; + background: transparent; + } + } } .display--large { @@ -1874,6 +1982,36 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb { /* Cider */ +.more-btn-round { + border-radius: 100%; + background: rgba(100, 100, 100, 0.5); + box-shadow: var(--ciderShadow-Generic); + width: 32px; + height: 32px; + border: 0px; + cursor: pointer; + z-index: 5; + display: flex; + justify-content: center; + align-items: center; + + &:hover { + filter: brightness(125%); + } + + &:active { + filter: brightness(75%); + transform: scale(0.98); + transition: transform 0s var(--appleEase), box-shadow 0.2s var(--appleEase); + } + + .svg-icon { + width: 100%; + background: #eee; + --url: url("./views/svg/more.svg"); + } +} + .about-page { .teamBtn { display: flex; @@ -1929,6 +2067,14 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb { &.md-btn-block { display: block; + width:100%; + } + + &.md-btn-glyph { + display:flex; + align-items: center; + justify-content: center; + width: 100%; } &.md-btn-primary { @@ -2376,115 +2522,171 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb { .playlist-page { --bgColor: transparent; padding: 0px; - background: linear-gradient(180deg, var(--bgColor) 32px, var(--bgColor) 59px, transparent 60px, transparent 100%); + //background: linear-gradient(180deg, var(--bgColor) 32px, var(--bgColor) 18px, transparent 60px, transparent 100%); top: 0; padding-top: var(--navigationBarHeight); .playlist-body { - padding: var(--contentInnerPadding); + padding: 0px var(--contentInnerPadding) 0px var(--contentInnerPadding); + } + + .floating-header { + position: sticky; + top: 0; + left: 0; + border-bottom: 1px solid rgba(200, 200, 200, 0.05); + z-index: 6; + padding: 0px 1em; + backdrop-filter: blur(32px); + background: rgba(24, 24, 24, 0.15); + top: var(--navigationBarHeight); + transition: opacity 0.1s var(--appleEase); } .playlist-display { padding: var(--contentInnerPadding); min-height: 300px; + position: relative; - .playlist-info { - flex-shrink: unset; + .artworkContainer { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + margin: 0; + padding: 0; + -webkit-mask-image: radial-gradient(at top left, black, transparent 70%), radial-gradient(at top right, black, transparent 70%), linear-gradient(180deg, rgb(200 200 200), transparent 98%); + opacity: .7; + animation: playlistArtworkFadeIn 1s var(--appleEase); + + .artworkMaterial>img { + filter: brightness(100%) blur(80px) saturate(100%) contrast(1); + object-position: center; + object-fit: cover; + width: 100%; + height: 100%; + transform: unset; + } + } + + .playlistInfo { + z-index: 1; + position: absolute; + bottom: 0; + left: 0; + right: 0; + top: 0; display: flex; - flex-flow: column; - justify-content: flex-end; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; - .playlist-name { - font-weight: 700; - font-size: 1.6rem; - margin-bottom: 6px; - margin-right: 6px; - flex-shrink: unset; + >.row { + width: calc(100% - 32px); } - .nameEdit { - font-weight: 700; - font-size: 1.6rem; - margin-bottom: 6px; - margin-right: 6px; + .playlist-info { flex-shrink: unset; - background: transparent; - border: 0px; - color: inherit; - font-family: inherit; - } + display: flex; + flex-flow: column; + justify-content: flex-end; - .playlist-artist { - font-size: 20px; - margin-bottom: 6px; - margin-right: 6px; - flex-shrink: unset; - } - - .playlist-desc { - box-sizing: border-box; - font-size: 14px; - flex-shrink: unset; - margin-right: 5px; - max-height: 100px; - position: relative; - - .content { - height: 100px; - -webkit-mask-image: -webkit-gradient(linear, left 50%, left 90%, from(rgba(0, 0, 0, 1)), to(rgba(0, 0, 0, 0))); + .playlist-name { + font-weight: 700; + font-size: 1.6rem; + margin-bottom: 6px; + margin-right: 6px; + flex-shrink: unset; } - .more-btn { - appearance: none; - position: absolute; - right: 0; - bottom: 0; - padding: 0 5px; - font-size: 14px; - color: var(--keyColor); - background-color: transparent; + .nameEdit { + font-weight: 700; + font-size: 1.6rem; + margin-bottom: 6px; + margin-right: 6px; + flex-shrink: unset; + background: transparent; border: 0px; - cursor: pointer; - width: 100%; - height: 100%; - overflow: hidden; - display: flex; - justify-content: flex-end; - align-items: flex-end; - font-weight: 600; + color: inherit; font-family: inherit; - text-transform: uppercase; } - } - .playlist-desc-expanded { - box-sizing: border-box; - font-size: 14px; - position: relative; + .playlist-artist { + font-size: 20px; + margin-bottom: 6px; + margin-right: 6px; + flex-shrink: unset; + } - .more-btn { - appearance: none; - position: absolute; - right: 0; - bottom: 0; - padding: 0 5px; + .playlist-desc { + box-sizing: border-box; font-size: 14px; - color: var(--keyColor); - background-color: transparent; - border: 0px; - cursor: pointer; - width: 100%; - height: 100%; - overflow: hidden; - display: flex; - justify-content: flex-end; - align-items: flex-end; - font-weight: 600; - font-family: inherit; - text-transform: uppercase; + flex-shrink: unset; + margin-right: 5px; + max-height: 100px; + position: relative; + + .content { + height: 100px; + -webkit-mask-image: -webkit-gradient(linear, left 50%, left 90%, from(rgba(0, 0, 0, 1)), to(rgba(0, 0, 0, 0))); + } + + .more-btn { + appearance: none; + position: absolute; + right: 0; + bottom: 0; + padding: 0 5px; + font-size: 14px; + color: var(--keyColor); + background-color: transparent; + border: 0px; + cursor: pointer; + width: 100%; + height: 100%; + overflow: hidden; + display: flex; + justify-content: flex-end; + align-items: flex-end; + font-weight: 600; + font-family: inherit; + text-transform: uppercase; + } + } + + .playlist-desc-expanded { + box-sizing: border-box; + font-size: 14px; + position: relative; + + .more-btn { + appearance: none; + position: absolute; + right: 0; + bottom: 0; + padding: 0 5px; + font-size: 14px; + color: var(--keyColor); + background-color: transparent; + border: 0px; + cursor: pointer; + width: 100%; + height: 100%; + overflow: hidden; + display: flex; + justify-content: flex-end; + align-items: flex-end; + font-weight: 600; + font-family: inherit; + text-transform: uppercase; + } } } } + + } .friends-info { @@ -2517,26 +2719,6 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb { } } - .playlist-more { - border-radius: 100%; - background: var(--keyColor); - box-shadow: var(--ciderShadow-Generic); - width: 36px; - height: 36px; - float: right; - border: 0px; - cursor: pointer; - z-index: 5; - - &:hover { - background: var(--keyColor-rollover); - } - - &:active { - background: var(--keyColor-pressed); - } - } - .playlist-time { font-size: 0.9em; margin: 6px; @@ -2544,6 +2726,14 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb { } } +@keyframes playlistArtworkFadeIn { + 0%{ + opacity: 0; + } + 100%{ + opacity: 0.7; + } +} // Collection Page .collection-page { padding-bottom: 128px; @@ -2586,8 +2776,21 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb { padding: 0px; top: 0; + .floating-header { + position: sticky; + top: 0; + left: 0; + border-bottom: 1px solid rgba(200, 200, 200, 0.05); + z-index: 6; + padding: 0px 1em; + backdrop-filter: blur(32px); + background: rgba(24, 24, 24, 0.15); + top: var(--navigationBarHeight); + transition: opacity 0.1s var(--appleEase); + } + .artist-header { - background: linear-gradient(45deg, var(--keyColor), #0e0e0e); + //background: linear-gradient(45deg, var(--keyColor), #0e0e0e); color: white; display: flex; align-items: center; @@ -2595,26 +2798,36 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb { min-height: 400px; position: relative; - .artist-more { - border-radius: 100%; - background: var(--keyColor); - box-shadow: var(--ciderShadow-Generic); - width: 36px; - height: 36px; + .header-content { + z-index: 1; + } + + .artworkContainer { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + margin: 0; + padding: 0; + -webkit-mask-image: radial-gradient(at top left, black, transparent 70%), radial-gradient(at top right, black, transparent 70%), linear-gradient(180deg, rgb(200 200 200), transparent 98%); + opacity: .7; + animation: playlistArtworkFadeIn 1s var(--appleEase); + + .artworkMaterial>img { + filter: brightness(100%) blur(80px) saturate(100%) contrast(1); + object-position: center; + object-fit: cover; + width: 100%; + height: 100%; + transform: unset; + } + } + + .more-btn-round { position: absolute; bottom: 26px; right: 32px; - border: 0px; - cursor: pointer; - z-index: 5; - - &:hover { - background: var(--keyColor-rollover); - } - - &:active { - background: var(--keyColor-pressed); - } } .animated { @@ -2681,28 +2894,31 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb { } } + .artist-play { + width: 36px; + height: 36px; + background: var(--keyColor); + border-radius: 100%; + box-shadow: var(--mediaItemShadow); + display: none; + cursor: pointer; + appearance: none; + border: 0px; + padding: 0px; + + &:hover { + background: var(--keyColor-rollover); + } + + &:active { + background: var(--keyColor-pressed); + } + } .artist-title { + .artist-play { - width: 36px; - height: 36px; - background: var(--keyColor); - border-radius: 100%; - margin: 14px; - box-shadow: var(--mediaItemShadow); - display: none; - cursor: pointer; - appearance: none; - border: 0px; - padding: 0px; transform: translateY(3px); - - &:hover { - background: var(--keyColor-rollover); - } - - &:active { - background: var(--keyColor-pressed); - } + margin: 14px; } &.artist-animation-on { @@ -2720,7 +2936,8 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb { } .artist-body { - padding: var(--contentInnerPadding); + padding: 0px var(--contentInnerPadding) 0px var(--contentInnerPadding); + margin-top: -48px; } .showmoreless { diff --git a/src/renderer/views/components/artwork-material.ejs b/src/renderer/views/components/artwork-material.ejs new file mode 100644 index 00000000..3cfe9891 --- /dev/null +++ b/src/renderer/views/components/artwork-material.ejs @@ -0,0 +1,37 @@ + + + \ No newline at end of file diff --git a/src/renderer/views/components/mediaitem-info.ejs b/src/renderer/views/components/mediaitem-info.ejs new file mode 100644 index 00000000..e69de29b diff --git a/src/renderer/views/components/mediaitem-scroller-horizontal.ejs b/src/renderer/views/components/mediaitem-scroller-horizontal.ejs index ef2f21d2..e1413c7c 100644 --- a/src/renderer/views/components/mediaitem-scroller-horizontal.ejs +++ b/src/renderer/views/components/mediaitem-scroller-horizontal.ejs @@ -1,6 +1,7 @@ + +<%- include('components/artwork-material') %> <%- include('components/menu-panel') %> @@ -712,5 +716,6 @@ + diff --git a/src/renderer/views/pages/artist-feed.ejs b/src/renderer/views/pages/artist-feed.ejs index b1147153..b8baf96f 100644 --- a/src/renderer/views/pages/artist-feed.ejs +++ b/src/renderer/views/pages/artist-feed.ejs @@ -1,5 +1,30 @@