From bb96788a3ab48776229985ca0a02f9c5c124b65a Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 15 May 2023 16:06:04 -0300 Subject: WIP socket shenanigans [!!!] this commit is mostly trash lol [!!!] Editor's note: Okay, after rebasing this commit is *super* spooky! It's interacting with a bunch of stuff that was previously handled in a merge commit and the revised changes may or may not be totally broken. If in doubt, assume this commit is the root of all evil (probably). --- backend.js | 34 +++++++---- index.js | 88 ++++++++++++++++---------- serialized-backend.js | 2 + socket.js | 166 ++++++++++++++++++++++++++++++++++++++++++++++---- todo.txt | 12 ++++ 5 files changed, 246 insertions(+), 56 deletions(-) diff --git a/backend.js b/backend.js index 93de487..ffa620a 100644 --- a/backend.js +++ b/backend.js @@ -11,7 +11,7 @@ import shortid from 'shortid' import {getDownloaderFor} from './downloaders.js' import {getMetadataReaderFor} from './metadata-readers.js' -import {getPlayer} from './players.js' +import {getPlayer, GhostPlayer} from './players.js' import RecordStore from './record-store.js' import { @@ -381,12 +381,6 @@ class QueuePlayer extends EventEmitter { throw new Error('Attempted to play before a player was loaded') } - let playingThisTrack = true - this.emit('playing new track') - this.once('playing new track', () => { - playingThisTrack = false - }) - // If it's a group, play the first track. if (isGroup(item)) { item = flattenGrouplike(item).items[0] @@ -402,13 +396,18 @@ class QueuePlayer extends EventEmitter { return } - playTrack: { + let playingThisTrack = true + this.emit('playing new track') + this.once('playing new track', () => { + playingThisTrack = false + }) + + if (this.player instanceof GhostPlayer) { + await this.#ghostPlay(item, startTime) + } else if (!item.downloaderArg) { // No downloader argument? That's no good - stop here. // TODO: An error icon on this item, or something??? - if (!item.downloaderArg) { - break playTrack - } - + } else { // If, by the time the track is downloaded, we're playing something // different from when the download started, assume that we just want to // keep listening to whatever new thing we started. @@ -452,6 +451,17 @@ class QueuePlayer extends EventEmitter { } } + async #ghostPlay(item, startTime) { + // If we're playing off a GhostPlayer, strip down the whole process. + // Downloading is totally unnecessary, for example. + + this.timeData = null + this.time = null + this.playingTrack = item + this.emit('playing', this.playingTrack) + await this.player.playFile('-', startTime) + } + playNext(track, automaticallyQueueNextTrack = false) { if (!track) return false diff --git a/index.js b/index.js index 8888d48..1f25b0f 100755 --- a/index.js +++ b/index.js @@ -104,33 +104,68 @@ async function main() { process.exit(1) } - const backend = new Backend({ - playerName: options['player'], - playerOptions: options['player-options'] - }) + const backendConfig = + (options['socket-server'] + ? { + playerName: 'ghost', + } + : { + playerName: options['player'], + playerOptions: options['player-options'], + }) + + const appConfig = + (options['socket-server'] + ? { + showPartyControls: true, + canControlPlayback: false, + canControlQueue: false, + canControlQueuePlayers: false, + canProcessMetadata: false, + } + : options['socket-client'] + ? { + showPartyControls: true, + } + : {}) - const result = await backend.setup() - if (result.error) { - console.error(result.error) + const backend = new Backend(backendConfig) + + const setupResult = await backend.setup() + if (setupResult.error) { + console.error(setupResult.error) process.exit(1) } - backend.on('playing', track => { - if (track) { - writeFile(backend.rootDirectory + '/current-track.txt', - getItemPathString(track)) - writeFile(backend.rootDirectory + '/current-track.json', - JSON.stringify(track, null, 2)) - } - }) + if (options['socket-server']) { + const socketServer = makeSocketServer() + attachSocketServerToBackend(socketServer, backend) + socketServer.listen(options['socket-server']) + + const socketClient = makeSocketClient() + attachBackendToSocketClient(backend, socketClient) + socketClient.socket.connect(options['socket-server']) + + backend.setPartyNickname('Internal Client') + backend.announceJoinParty() + } + + if (!options['socket-server']) { + backend.on('playing', track => { + if (track) { + writeFile(backend.rootDirectory + '/current-track.txt', + getItemPathString(track)) + writeFile(backend.rootDirectory + '/current-track.json', + JSON.stringify(track, null, 2)) + } + }) + } const { appElement, dirtyTerminal, flushable, root } = await setupClient({ backend, screenInterface: new CommandLineInterface(), writable: process.stdout, - appConfig: { - showPartyControls: !!(options['socket-server'] || options['socket-client']) - } + appConfig, }) appElement.on('quitRequested', () => { @@ -152,7 +187,7 @@ async function main() { root.renderNow() }) - if (playlistSources.length === 0) { + if (!options['socket-server'] && playlistSources.length === 0) { if (jsonConfig.defaultPlaylists) { playlistSources.push(...jsonConfig.defaultPlaylists) } else { @@ -181,26 +216,13 @@ async function main() { appElement.attachAsServerHost(telnetServer) } - let socketClient - let socketServer - if (options['socket-server']) { - socketServer = makeSocketServer() - attachSocketServerToBackend(socketServer, backend) - socketServer.listen(options['socket-server']) - - socketClient = makeSocketClient() - socketClient.socket.connect(options['socket-server']) - } - if (options['socket-client']) { - socketClient = makeSocketClient() + const socketClient = makeSocketClient() const [ p1, p2 ] = options['socket-client'].split(':') const host = p2 && p1 const port = p2 ? p2 : p1 socketClient.socket.connect(port, host) - } - if (socketClient) { attachBackendToSocketClient(backend, socketClient) let nickname = process.env.USER diff --git a/serialized-backend.js b/serialized-backend.js index 7ae5e9d..575ba8f 100644 --- a/serialized-backend.js +++ b/serialized-backend.js @@ -56,6 +56,8 @@ export function saveBackend(backend) { } export async function restoreBackend(backend, data) { + // console.log('restoring backend:', data) + if (data.queuePlayers) { if (data.queuePlayers.length === 0) { return diff --git a/socket.js b/socket.js index c91a1af..525be82 100644 --- a/socket.js +++ b/socket.js @@ -239,7 +239,11 @@ function validateCommand(command) { command.sender === 'server' ) || !command.startingTrack ) - case 'share-with-party': + case 'added queue player': + return ( + typeof command.id === 'string' + ) + case 'share with party': return ( typeof command.item === 'string' || Array.isArray(command.item) @@ -389,8 +393,6 @@ export function makeSocketServer() { if (readySockets && !readySockets.includes(socketId)) { readySockets.push(socketId) if (readySockets.length === Object.keys(socketMap).length) { - const QP = server.canonicalBackend.queuePlayers.find(QP => QP.id === command.queuePlayer) - silenceEvents(QP, ['set-pause'], () => QP.setPause(false)) for (const socket of Object.values(socketMap)) { socket.write(serializeCommandToData({ sender: 'server', @@ -641,6 +643,9 @@ export function attachBackendToSocketClient(backend, client) { case 'unqueue': actionmsg = `removed ${itemToMessage(command.topItem)} from the queue` break + case 'added queue player': + actionmsg = `created a new playback queue` + break case 'status': isVerbose = true switch (command.status) { @@ -707,11 +712,14 @@ export function attachBackendToSocketClient(backend, client) { backend.loadSharedSources(socketId, sharedSources) } await restoreBackend(backend, command.backend) - backend.on('playing', QP => { - QP.once('received time data', () => { - client.sendCommand({code: 'status', status: 'sync-playback'}) - }) - }) + attachPlaybackBackendListeners() + // Commented out as part of a merge commit catching up + // socket-mtui with main. Spooky! // + // backend.on('playing', QP => { + // QP.once('received time data', () => { + // client.sendCommand({code: 'status', status: 'sync-playback'}) + // }) + // }) return } // Again, no break. Client commands can come from the server. @@ -766,9 +774,9 @@ export function attachBackendToSocketClient(backend, client) { queuePlayer: QP.id }) }) - silenceEvents(QP, ['playing'], () => QP.play( - restoreNewItem(command.track, getPlaylistSources()) - )) + silenceEvents(QP, ['playing'], () => { + QP.play(restoreNewItem(command.track, getPlaylistSources())) + }) } return case 'queue': @@ -798,6 +806,8 @@ export function attachBackendToSocketClient(backend, client) { return } case 'set-pause': { + // All this code looks very scary??? + /* // TODO: there's an event leak here when toggling pause while // nothing is playing let playingThisTrack = true @@ -809,6 +819,15 @@ export function attachBackendToSocketClient(backend, client) { if (QP) silenceEvents(QP, ['set-pause'], () => QP.setPause(command.paused)) } }, command.startingTrack ? 500 : 0) + */ + silenceEvents(QP, ['set pause'], () => QP.setPause(command.paused)) + return + } + case 'added queue player': { + silenceEvents(backend, ['added queue player'], () => { + const QP = backend.addQueuePlayer() + QP.id = command.id + }) return } case 'share-with-party': { @@ -972,6 +991,131 @@ export function attachBackendToSocketClient(backend, client) { }) } }) + + backend.on('set party nickname', nickname => { + let oldNickname = client.nickname + sharedSources.name = namePartySources(nickname) + client.nickname = nickname + client.sendCommand({code: 'set nickname', nickname, oldNickname}) + }) + + function attachPlaybackBackendListeners() { + backend.on('QP: clear queue', queuePlayer => { + client.sendCommand({ + code: 'clear queue', + queuePlayer: queuePlayer.id + }) + }) + + backend.on('QP: clear queue past', (queuePlayer, track) => { + client.sendCommand({ + code: 'clear queue past', + queuePlayer: queuePlayer.id, + track: saveItemReference(track) + }) + }) + + backend.on('QP: clear queue up to', (queuePlayer, track) => { + client.sendCommand({ + code: 'clear queue up to', + queuePlayer: queuePlayer.id, + track: saveItemReference(track) + }) + }) + + backend.on('QP: distribute queue', (queuePlayer, topItem, opts) => { + client.sendCommand({ + code: 'distribute queue', + queuePlayer: queuePlayer.id, + topItem: saveItemReference(topItem), + opts + }) + }) + + backend.on('QP: done playing', queuePlayer => { + client.sendCommand({ + code: 'status', + status: 'done playing', + queuePlayer: queuePlayer.id + }) + }) + + backend.on('QP: playing', (queuePlayer, track) => { + if (track) { + client.sendCommand({ + code: 'play', + queuePlayer: queuePlayer.id, + track: saveItemReference(track) + }) + queuePlayer.once('received time data', data => { + client.sendCommand({ + code: 'status', + status: 'ready to resume', + queuePlayer: queuePlayer.id + }) + }) + } else { + client.sendCommand({ + code: 'stop playing', + queuePlayer: queuePlayer.id + }) + } + }) + + backend.on('QP: queue', (queuePlayer, topItem, afterItem, opts) => { + client.sendCommand({ + code: 'queue', + queuePlayer: queuePlayer.id, + topItem: saveItemReference(topItem), + afterItem: saveItemReference(afterItem), + opts + }) + }) + + function handleSeek(queuePlayer) { + client.sendCommand({ + code: 'seek to', + queuePlayer: queuePlayer.id, + time: queuePlayer.time + }) + } + + backend.on('QP: seek ahead', handleSeek) + backend.on('QP: seek back', handleSeek) + backend.on('QP: seek to', handleSeek) + + backend.on('QP: shuffle queue', queuePlayer => { + client.sendCommand({ + code: 'restore queue', + why: 'shuffle', + queuePlayer: queuePlayer.id, + tracks: queuePlayer.queueGrouplike.items.map(saveItemReference) + }) + }) + + backend.on('QP: toggle pause', queuePlayer => { + client.sendCommand({ + code: 'set pause', + queuePlayer: queuePlayer.id, + paused: queuePlayer.player.isPaused + }) + }) + + backend.on('QP: unqueue', (queuePlayer, topItem) => { + client.sendCommand({ + code: 'unqueue', + queuePlayer: queuePlayer.id, + topItem: saveItemReference(topItem) + }) + }) + + backend.on('added queue player', (queuePlayer) => { + client.sendCommand({ + code: 'added queue player', + id: queuePlayer.id, + }) + }) + } } export function attachSocketServerToBackend(server, backend) { diff --git a/todo.txt b/todo.txt index e718ce0..253ea8c 100644 --- a/todo.txt +++ b/todo.txt @@ -754,6 +754,18 @@ TODO: There should be a way for the server to handle disputes between two clients' own "done playing" events (or the GHOST PLAYER reaching the playback time provided when the track was first shared). +TODO: Implement a waaaay better socat system, particularly one which waits for + feedback when a command is sent and returns that. This has to be special- + coded for mpv since there isn't a generalized standard, so it should make + use of the existing Socat class, not replace it outright. + +TODO: Use above socat system to keep "pinging" the socket until a response is + received - mpv doesn't make the socket immediately available. I think if + we wait for a pong response before allowing any actual commands to go + through, we can avoid weirdness with commands being dropped beacuse they + were sent too early. For now we just use a time-based delay on the base + Socat class, which is a hack. + TODO: When you're navigating down (or up) a menu, if that menu's got a scrollbar *and* is divided into sections, passing a divider line should try to scroll the whole newly active section into view! This way you get -- cgit 1.3.0-6-gf8a5