diff options
-rw-r--r-- | backend.js | 47 | ||||
-rwxr-xr-x | index.js | 14 | ||||
-rw-r--r-- | package-lock.json | 13 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | players.js | 10 | ||||
-rw-r--r-- | serialized-backend.js | 7 | ||||
-rw-r--r-- | socket.js | 130 |
7 files changed, 193 insertions, 29 deletions
diff --git a/backend.js b/backend.js index d2d0138..f3d8cfd 100644 --- a/backend.js +++ b/backend.js @@ -8,6 +8,7 @@ const { getMetadataReaderFor } = require('./metadata-readers') const { getPlayer } = require('./players') const RecordStore = require('./record-store') const os = require('os') +const shortid = require('shortid') const { getTimeStringsFromSec, @@ -62,6 +63,8 @@ class QueuePlayer extends EventEmitter { }) { super() + this.id = shortid.generate() + this.player = null this.playingTrack = null this.queueGrouplike = {name: 'Queue', isTheQueue: true, items: []} @@ -69,6 +72,7 @@ class QueuePlayer extends EventEmitter { this.playedTrackToEnd = false this.timeData = null + this.time = null this.getPlayer = getPlayer this.getRecordFor = getRecordFor @@ -86,6 +90,7 @@ class QueuePlayer extends EventEmitter { this.player.on('printStatusLine', data => { if (this.playingTrack) { this.timeData = data + this.time = data.curSecTotal this.emit('received time data', data, this) } }) @@ -414,6 +419,7 @@ class QueuePlayer extends EventEmitter { } this.timeData = null + this.time = null this.playingTrack = item this.emit('playing', this.playingTrack, oldTrack, this) @@ -504,6 +510,7 @@ class QueuePlayer extends EventEmitter { const oldTrack = this.playingTrack this.playingTrack = null this.timeData = null + this.time = null this.emit('playing', null, oldTrack, this) } } @@ -513,51 +520,69 @@ class QueuePlayer extends EventEmitter { } seekAhead(seconds) { + this.time += seconds this.player.seekAhead(seconds) + this.emit('seek-ahead', +seconds) } seekBack(seconds) { + this.time -= seconds this.player.seekBack(seconds) + this.emit('seek-back', +seconds) + } + + seekTo(timeInSecs) { + this.time = timeInSecs + this.player.seekTo(timeInSecs) + this.emit('seek-to', +timeInSecs) } togglePause() { this.player.togglePause() + this.emit('toggle-pause') } setPause(value) { this.player.setPause(value) + this.emit('set-pause', !!value) } toggleLoop() { this.player.toggleLoop() + this.emit('toggle-loop') } setLoop(value) { this.player.setLoop(value) + this.emit('set-loop', !!value) } volUp(amount = 10) { this.player.volUp(amount) + this.emit('vol-up', +amount) } volDown(amount = 10) { this.player.volDown(amount) + this.emit('vol-down', +amount) } setVolume(value) { this.player.setVolume(value) + this.emit('set-volume', +value) } setVolumeMultiplier(value) { - this.player.setVolumeMultiplier(value); + this.player.setVolumeMultiplier(value) } fadeIn() { - return this.player.fadeIn(); + return this.player.fadeIn() } setPauseNextTrack(value) { this.pauseNextTrack = !!value + this.emit('set-pause-next-track', !!value) } get remainingTracks() { @@ -632,6 +657,24 @@ class Backend extends EventEmitter { this.queuePlayers.push(queuePlayer) this.emit('added queue player', queuePlayer) + for (const event of [ + 'playing', + 'seek-ahead', + 'seek-back', + 'toggle-pause', + 'set-pause', + 'toggle-loop', + 'set-loop', + 'vol-up', + 'vol-down', + 'set-volume', + 'set-pause-next-track' + ]) { + queuePlayer.on(event, (...data) => { + this.emit(event, queuePlayer, ...data) + }) + } + return queuePlayer } diff --git a/index.js b/index.js index b0db6cd..03b5bb1 100755 --- a/index.js +++ b/index.js @@ -75,7 +75,7 @@ async function main() { 'player-options': {type: 'series'}, 'stress-test': {type: 'flag'}, 'socket-client': {type: 'value'}, - 'socket-server': {type: 'flag'}, + 'socket-server': {type: 'value'}, 'telnet-server': {type: 'flag'}, [parseOptions.handleDashless](option) { playlistSources.push(option) @@ -147,20 +147,26 @@ async function main() { appElement.attachAsServerHost(telnetServer) } + let socketClient let socketServer if (options['socket-server']) { socketServer = makeSocketServer() attachSocketServerToBackend(socketServer, backend) - socketServer.listen(1255) + socketServer.listen(options['socket-server']) + + socketClient = makeSocketClient() + socketClient.socket.connect(options['socket-server']) } - let socketClient if (options['socket-client']) { socketClient = makeSocketClient() + socketClient.socket.connect(options['socket-client']) + } + + if (socketClient) { attachBackendToSocketClient(backend, socketClient, { getPlaylistSources: () => appElement.playlistSources }) - socketClient.socket.connect(1255) } if (options['stress-test']) { diff --git a/package-lock.json b/package-lock.json index a4ec0d1..f984dce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,11 @@ "minimist": "^1.2.5" } }, + "nanoid": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", + "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" + }, "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", @@ -82,6 +87,14 @@ "truncate-utf8-bytes": "^1.0.0" } }, + "shortid": { + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.15.tgz", + "integrity": "sha512-5EaCy2mx2Jgc/Fdn9uuDuNIIfWBpzY4XIlhoqtXF6qsf+/+SGZ+FxDdX/ZsMZiWupIWNqAEmiNY4RC+LSmCeOw==", + "requires": { + "nanoid": "^2.1.0" + } + }, "temp-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", diff --git a/package.json b/package.json index 7e33ce0..094f101 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "node-natural-sort": "^0.8.7", "open": "^7.0.3", "sanitize-filename": "^1.6.3", + "shortid": "^2.2.15", "tempy": "^0.2.1", "tui-lib": "^0.2.1", "tui-text-editor": "^0.3.1", diff --git a/players.js b/players.js index 6624f0f..2056ed7 100644 --- a/players.js +++ b/players.js @@ -224,13 +224,15 @@ module.exports.ControllableMPVPlayer = class extends module.exports.MPVPlayer { } setPause(val) { - this.isPaused = !!val - this.sendCommand('set', 'pause', this.isPaused) + if (!!val !== this.isPaused) { + this.togglePause() + } } setLoop(val) { - this.isLooping = !!val - this.sendCommand('set', 'loop', this.isLooping) + if (!!val !== this.isLooping) { + this.toggleLoop() + } } async kill() { diff --git a/serialized-backend.js b/serialized-backend.js index 13bb2b9..041f668 100644 --- a/serialized-backend.js +++ b/serialized-backend.js @@ -30,9 +30,9 @@ const { const referenceDataSymbol = Symbol('Restored reference data') function getPlayerInfo(queuePlayer) { - const { player, timeData } = queuePlayer + const { player } = queuePlayer return { - time: timeData && timeData.curSecTotal, + time: queuePlayer.time, isLooping: player.isLooping, isPaused: player.isPaused, volume: player.volume @@ -56,6 +56,7 @@ function saveBackend(backend) { return { queuePlayers: backend.queuePlayers.map(QP => ({ + id: QP.id, playingTrack: referenceTrack(QP.playingTrack), queuedTracks: QP.queueGrouplike.items.map(referenceTrack), pauseNextTrack: QP.pauseNextTrack, @@ -74,6 +75,8 @@ async function restoreBackend(backend, data) { const QP = await backend.addQueuePlayer() QP[referenceDataSymbol] = qpData + QP.id = qpData.id + QP.queueGrouplike.items = qpData.queuedTracks.map(refData => ({ [referenceDataSymbol]: refData, name: refData.name, diff --git a/socket.js b/socket.js index 5bfe80a..ae8ef87 100644 --- a/socket.js +++ b/socket.js @@ -48,8 +48,23 @@ function validateCommand(command) { case 'initialize-backend': return typeof command.backend === 'object' } - break + // No break here; servers can send commands which typically come from + // clients too. case 'client': + switch (command.code) { + case 'seek-to': + return ( + typeof command.queuePlayer === 'string' && + typeof command.time === 'number' + ) + case 'set-pause': + return ( + typeof command.queuePlayer === 'string' && + typeof command.paused === 'boolean' + ) + case 'status': + return typeof command.status === 'string' + } break } @@ -70,7 +85,7 @@ function makeSocketServer() { server.canonicalBackend = null - function receivedData(data) { + function receivedData(socket, data) { // Parse data as a command and validate it. If invalid, drop this data. let command @@ -80,14 +95,45 @@ function makeSocketServer() { return } + command.sender = 'client' + if (!validateCommand(command)) { return } + // If it's a status command, respond appropriately, and return so that it + // is not relayed. + + if (command.code === 'status') { + switch (command.status) { + case 'sync-playback': + for (const QP of server.canonicalBackend.queuePlayers) { + if (QP.timeData) { + socket.write(JSON.stringify({ + sender: 'server', + code: 'seek-to', + queuePlayer: QP.id, + time: QP.timeData.curSecTotal + }) + '\n') + socket.write(JSON.stringify({ + sender: 'server', + code: 'set-pause', + queuePlayer: QP.id, + paused: false + })) + } + } + + break + } + + return + } + // Relay the data to client sockets. for (const socket of sockets) { - socket.write(command) + socket.write(JSON.stringify(command)) } } @@ -100,12 +146,20 @@ function makeSocketServer() { } }) - socket.on('data', receivedData) + socket.on('data', data => receivedData(socket, data)) + + const savedBackend = saveBackend(server.canonicalBackend) + + for (const qpData of savedBackend.queuePlayers) { + if (qpData.playerInfo) { + qpData.playerInfo.isPaused = true + } + } socket.write(JSON.stringify({ sender: 'server', code: 'initialize-backend', - backend: saveBackend(server.canonicalBackend) + backend: savedBackend })) }) @@ -129,18 +183,20 @@ function makeSocketClient() { // Same sort of "guarding" deserialization/validation as in the server // code, because it's possible the client and server backends mismatch. - let command - try { - command = deserializeDataToCommand(data) - } catch (error) { - return - } + for (const line of data.toString().split('\n')) { + let command + try { + command = deserializeDataToCommand(line) + } catch (error) { + return + } - if (!validateCommand(command)) { - return - } + if (!validateCommand(command)) { + return + } - client.emit('command', command) + client.emit('command', command) + } }) return client @@ -160,11 +216,51 @@ function attachBackendToSocketClient(backend, client, { await restoreBackend(backend, command.backend) // TODO: does this need to be called here? updateRestoredTracksUsingPlaylists(backend, getPlaylistSources()) - break + backend.on('playing', QP => { + QP.once('received time data', () => { + client.sendCommand({code: 'status', status: 'sync-playback'}) + }) + }) + return + } + // Again, no pause. Client commands can come from the server. + case 'client': { + let QP = ( + command.queuePlayer && + backend.queuePlayers.find(QP => QP.id === command.queuePlayer) + ) + + switch (command.code) { + case 'seek-to': + if (QP) QP.seekTo(command.time) + return + case 'set-pause': + if (QP) QP.setPause(command.paused) + return } - break + } } }) + + backend.on('toggle-pause', queuePlayer => { + client.sendCommand({ + code: 'set-pause', + queuePlayer: queuePlayer.id, + paused: queuePlayer.player.isPaused + }) + }) + + function handleSeek(queuePlayer) { + client.sendCommand({ + code: 'seek-to', + queuePlayer: queuePlayer.id, + time: queuePlayer.time + }) + } + + backend.on('seek-ahead', handleSeek) + backend.on('seek-back', handleSeek) + backend.on('seek-to', handleSeek) } function attachSocketServerToBackend(server, backend) { |