diff --git a/package.json b/package.json index f7e24a0a..50fa2d19 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "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" }, diff --git a/src/main/base/win.ts b/src/main/base/win.ts index 292d5562..e27b4145 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; @@ -189,6 +191,8 @@ export class Win { * TODO: Broadcast the remote so that /web-remote/ can connect * https://github.com/ciderapp/Apple-Music-Electron/blob/818ed18940ff600d76eb59d22016723a75885cd5/resources/functions/handler.js#L1173 */ + const ws = new wsapi() + ws.InitWebSockets() const remote = express(); remote.use(express.static(path.join(this.paths.srcPath, "./web-remote/"))) remote.listen(this.remotePort, () => { diff --git a/src/main/base/wsapi.ts b/src/main/base/wsapi.ts new file mode 100644 index 00000000..fc346e76 --- /dev/null +++ b/src/main/base/wsapi.ts @@ -0,0 +1,291 @@ +// @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 { + private standa2rdResponse (status, data, message, type: string = "generic") { + this.status = status; + this.message = message; + this.data = data; + this.type = type; + } + + 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/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..1519c3bf 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -610,10 +610,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) => { diff --git a/src/renderer/views/main.ejs b/src/renderer/views/main.ejs index c952abf6..242b5337 100644 --- a/src/renderer/views/main.ejs +++ b/src/renderer/views/main.ejs @@ -710,5 +710,6 @@ + diff --git a/src/web-remote/index.js b/src/web-remote/index.js index 4956cd0b..7b520f8c 100644 --- a/src/web-remote/index.js +++ b/src/web-remote/index.js @@ -504,6 +504,7 @@ var app = new Vue({ } socket.onmessage = (e) => { + console.log(e.data) const response = JSON.parse(e.data); switch (response.type) { default: console.log(response);