From 991b2f0a8280c31b93ad91d6a215b74183417352 Mon Sep 17 00:00:00 2001 From: Florrie Date: Sat, 11 Jul 2020 16:22:01 -0300 Subject: support queue controls over socket clients --- socket.js | 206 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 198 insertions(+), 8 deletions(-) (limited to 'socket.js') diff --git a/socket.js b/socket.js index ae8ef87..a972f0c 100644 --- a/socket.js +++ b/socket.js @@ -16,9 +16,15 @@ const net = require('net') const { saveBackend, restoreBackend, + saveItemReference, + restoreNewItem, updateRestoredTracksUsingPlaylists } = require('./serialized-backend') +const { + silenceEvents +} = require('./general-util') + function serializeCommandToData(command) { // Turn a command into a string/buffer that can be sent over a socket. return JSON.stringify(command) @@ -30,6 +36,38 @@ function deserializeDataToCommand(data) { return JSON.parse(data) } +function isItemRef(ref) { + if (ref === null || typeof ref !== 'object') { + return false + } + + // List of true/false/null. False means *invalid* reference data; null + // means *nonpresent* reference data. True means present and valid. + const conditionChecks = [ + 'name' in ref ? typeof ref.name === 'string' : null, + 'path' in ref ? Array.isArray(ref.path) && ref.path.every(n => typeof n === 'string') : null, + 'downloaderArg' in ref ? ( + !('items' in ref) && + typeof ref.downloaderArg === 'string' + ) : null, + 'items' in ref ? ( + !('downloaderArg' in ref) && + Array.isArray(ref.items) && + ref.items.every(isItemRef) + ) : null + ] + + if (conditionChecks.includes(false)) { + return false + } + + if (!conditionChecks.includes(true)) { + return false + } + + return true +} + function validateCommand(command) { // TODO: Could be used to validate "against" a backend, but for now it just // checks data types. @@ -52,6 +90,51 @@ function validateCommand(command) { // clients too. case 'client': switch (command.code) { + case 'clear-queue': + return typeof command.queuePlayer === 'string' + case 'clear-queue-past': + case 'clear-queue-up-to': + return ( + typeof command.queuePlayer === 'string' && + isItemRef(command.topItem) + ) + case 'distribute-queue': + return ( + typeof command.queuePlayer === 'string' && + isItemRef(command.topItem) && + (!command.opts || typeof command.opts === 'object' && ( + ( + !command.opts.how || + ['evenly', 'randomly'].includes(command.opts.how) + ) && + ( + !command.opts.rangeEnd || + ['end-of-queue'].includes(command.opts.rangeEnd) || + typeof command.opts.rangeEnd === 'number' + ) + )) + ) + case 'queue': + return ( + typeof command.queuePlayer === 'string' && + isItemRef(command.topItem) && + ( + isItemRef(command.afterItem) || + [null, 'FRONT'].includes(command.afterItem) + ) && + (!command.opts || typeof command.opts === 'object' && ( + ( + !command.opts.movePlayingTrack || + typeof command.opts.movePlayingTrack === 'boolean' + ) + )) + ) + case 'restore-queue': + return ( + typeof command.queuePlayer === 'string' && + Array.isArray(command.tracks) && + command.tracks.every(track => isItemRef(track)) + ) case 'seek-to': return ( typeof command.queuePlayer === 'string' && @@ -64,6 +147,11 @@ function validateCommand(command) { ) case 'status': return typeof command.status === 'string' + case 'unqueue': + return ( + typeof command.queuePlayer === 'string' && + isItemRef(command.topItem) + ) } break } @@ -119,7 +207,7 @@ function makeSocketServer() { sender: 'server', code: 'set-pause', queuePlayer: QP.id, - paused: false + paused: QP.player.isPaused })) } } @@ -130,9 +218,11 @@ function makeSocketServer() { return } - // Relay the data to client sockets. + // Relay the command to client sockets besides the sender. + + const otherSockets = sockets.filter(s => s !== socket) - for (const socket of sockets) { + for (const socket of otherSockets) { socket.write(JSON.stringify(command)) } } @@ -231,22 +321,98 @@ function attachBackendToSocketClient(backend, client, { ) switch (command.code) { + case 'clear-queue': + if (QP) silenceEvents(QP, ['clear-queue'], () => QP.clearQueue()) + return + case 'clear-queue-past': + if (QP) silenceEvents(QP, ['clear-queue-past'], () => QP.clearQueuePast( + restoreNewItem(command.topItem, getPlaylistSources()) + )) + return + case 'clear-queue-up-to': + if (QP) silenceEvents(QP, ['clear-queue-up-to'], () => QP.clearQueueUpTo( + restoreNewItem(command.topItem, getPlaylistSources()) + )) + return + case 'distribute-queue': + if (QP) silenceEvents(QP, ['distribute-queue'], () => QP.distributeQueue( + restoreNewItem(command.topItem, getPlaylistSources()), + { + how: command.opts.how, + rangeEnd: command.opts.rangeEnd + } + )) + return + case 'queue': + if (QP) silenceEvents(QP, ['queue'], () => QP.queue( + restoreNewItem(command.topItem, getPlaylistSources()), + isItemRef(command.afterItem) ? restoreNewItem(command.afterItem, getPlaylistSources()) : command.afterItem, + { + movePlayingTrack: command.opts.movePlayingTrack + } + )) + return + case 'restore-queue': + if (QP) { + QP.queueGrouplike.items = command.tracks.map(refData => restoreNewItem(refData)) + // TODO: target just the one queue player. hacks = illegal + updateRestoredTracksUsingPlaylists(backend, getPlaylistSources()) + } case 'seek-to': - if (QP) QP.seekTo(command.time) + if (QP) silenceEvents(QP, ['seek-to'], () => QP.seekTo(command.time)) return case 'set-pause': - if (QP) QP.setPause(command.paused) + if (QP) silenceEvents(QP, ['set-pause'], () => QP.setPause(command.paused)) + return + case 'unqueue': + if (QP) silenceEvents(QP, ['unqueue'], () => QP.unqueue( + restoreNewItem(command.topItem, getPlaylistSources()) + )) return } } } }) - backend.on('toggle-pause', queuePlayer => { + backend.on('clear-queue', queuePlayer => { client.sendCommand({ - code: 'set-pause', + code: 'clear-queue', + queuePlayer: queuePlayer.id + }) + }) + + backend.on('clear-queue-past', (queuePlayer, topItem) => { + client.sendCommand({ + code: 'clear-queue-past', queuePlayer: queuePlayer.id, - paused: queuePlayer.player.isPaused + topItem: saveItemReference(topItem) + }) + }) + + backend.on('clear-queue-up-to', (queuePlayer, topItem) => { + client.sendCommand({ + code: 'clear-queue-up-to', + queuePlayer: queuePlayer.id, + topItem: saveItemReference(topItem) + }) + }) + + backend.on('distribute-queue', (queuePlayer, topItem, opts) => { + client.sendCommand({ + code: 'distribute-queue', + queuePlayer: queuePlayer.id, + topItem: saveItemReference(topItem), + opts + }) + }) + + backend.on('queue', (queuePlayer, topItem, afterItem, opts) => { + client.sendCommand({ + code: 'queue', + queuePlayer: queuePlayer.id, + topItem: saveItemReference(topItem), + afterItem: saveItemReference(afterItem), + opts }) }) @@ -261,6 +427,30 @@ function attachBackendToSocketClient(backend, client, { backend.on('seek-ahead', handleSeek) backend.on('seek-back', handleSeek) backend.on('seek-to', handleSeek) + + backend.on('shuffle-queue', queuePlayer => { + client.sendCommand({ + code: 'restore-queue', + queuePlayer: queuePlayer.id, + tracks: queuePlayer.queueGrouplike.items.map(saveItemReference) + }) + }) + + backend.on('toggle-pause', queuePlayer => { + client.sendCommand({ + code: 'set-pause', + queuePlayer: queuePlayer.id, + paused: queuePlayer.player.isPaused + }) + }) + + backend.on('unqueue', (queuePlayer, topItem) => { + client.sendCommand({ + code: 'unqueue', + queuePlayer: queuePlayer.id, + topItem: saveItemReference(topItem) + }) + }) } function attachSocketServerToBackend(server, backend) { -- cgit 1.3.0-6-gf8a5