From 84c49e453336d6105655edd08e93bab071c0fc3b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 26 Apr 2021 13:15:09 -0300 Subject: synchronize shared sources on join + other stuff --- socket.js | 157 +++++++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 110 insertions(+), 47 deletions(-) (limited to 'socket.js') diff --git a/socket.js b/socket.js index bc35c76..a40dc97 100644 --- a/socket.js +++ b/socket.js @@ -40,6 +40,7 @@ const { const { getTimeStringsFromSec, + parseWithoutPrototype, silenceEvents } = require('./general-util') @@ -67,7 +68,7 @@ function serializePartySource(item) { } } -function deserializePartySource(source) { +function deserializePartySource(source, parent = null) { // Reconstruct a party source into the ordinary group/track format. const recursive = source => { @@ -81,9 +82,16 @@ function deserializePartySource(source) { } const top = recursive(source) - return (isGroup(top) + + const item = (isGroup(top) ? updateGroupFormat(top) : updateTrackFormat(top)) + + if (parent) { + item[parentSymbol] = parent + } + + return item } function serializeCommandToData(command) { @@ -94,7 +102,11 @@ function serializeCommandToData(command) { function deserializeDataToCommand(data) { // Turn data received from a socket into a command that can be processed as // an action to apply to the mtui backend. - return JSON.parse(data) + return parseWithoutPrototype(data) +} + +function namePartySources(nickname) { + return `Party Sources - ${nickname}` } function isItemRef(ref) { @@ -144,8 +156,15 @@ function validateCommand(command) { switch (command.sender) { case 'server': switch (command.code) { - case 'initialize-backend': - return typeof command.backend === 'object' + case 'initialize-party': + return ( + typeof command.backend === 'object' && + typeof command.socketInfo === 'object' && + Object.values(command.socketInfo).every(info => ( + typeof info.nickname === 'string' && + Array.isArray(info.sharedSources) + )) + ) case 'set-socket-id': return typeof command.socketId === 'string' } @@ -290,23 +309,36 @@ function makeSocketServer() { const server = new net.Server() const socketMap = Object.create(null) + // Keeps track of details to share with newly joining sockets for + // synchronization. + const socketInfoMap = Object.create(null) + server.canonicalBackend = null // -> queue player id -> array: socket - const readyToResume = {} - const donePlaying = {} + const readyToResume = Object.create(null) + const donePlaying = Object.create(null) server.on('connection', socket => { const socketId = shortid.generate() - socketMap[socketId] = socket + const socketInfo = { + hasAnnouncedJoin: false, + nickname: DEFAULT_NICKNAME, - let hasAnnouncedJoin = false - let nickname = DEFAULT_NICKNAME + // Unlike in client code, this isn't an array of actual playlist items; + // rather, it's the intermediary format used when transferring between + // client and server. + sharedSources: [] + } + + socketMap[socketId] = socket + socketInfoMap[socketId] = socketInfo socket.on('close', () => { if (socketId in socketMap) { delete socketMap[socketId] + delete socketInfoMap[socketId] } }) @@ -322,7 +354,7 @@ function makeSocketServer() { command.sender = 'client' command.senderSocketId = socketId - command.senderNickname = nickname + command.senderNickname = socketInfo.nickname if (!validateCommand(command)) { return @@ -331,7 +363,7 @@ function makeSocketServer() { // If the socket hasn't announced its joining yet, it only has access to // a few commands. - if (!hasAnnouncedJoin) { + if (!socketInfo.hasAnnouncedJoin) { if (![ 'announce-join', 'set-nickname' @@ -414,22 +446,30 @@ function makeSocketServer() { // Also attach the old nickname for display in log messages. if (command.code === 'set-nickname') { - command.oldNickname = nickname - command.senderNickname = nickname - nickname = command.nickname + command.oldNickname = socketInfo.nickname + command.senderNickname = socketInfo.nickname + socketInfo.nickname = command.nickname + } + + // If it's a 'share-with-party' command, keep track of the item being + // shared, so we can synchronize newly joining sockets with it. + + if (command.code === 'share-with-party') { + const { sharedSources } = socketInfoMap[socketId] + sharedSources.push(command.item) } // If it's an 'announce-join' command, mark the variable for this! if (command.code === 'announce-join') { - hasAnnouncedJoin = true; + socketInfo.hasAnnouncedJoin = true; } // If the socket hasn't announced its joining yet, don't relay the // command. (Since hasAnnouncedJoin gets set above, 'announce-join' - // will meet this condition.) + // will pass this condition.) - if (!hasAnnouncedJoin) { + if (!socketInfo.hasAnnouncedJoin) { return } @@ -458,8 +498,9 @@ function makeSocketServer() { socket.write(serializeCommandToData({ sender: 'server', - code: 'initialize-backend', - backend: savedBackend + code: 'initialize-party', + backend: savedBackend, + socketInfo: socketInfoMap }) + '\n') }) @@ -509,16 +550,16 @@ function attachBackendToSocketClient(backend, client) { let hasAnnouncedJoin = false - const partyGrouplike = { - name: `Party Sources - ${client.nickname}`, + const sharedSources = { + name: namePartySources(client.nickname), isPartySources: true, items: [] } - const partyGrouplikeMap = Object.create(null) + const socketInfoMap = Object.create(null) const getPlaylistSources = () => - partyGrouplike.items.map(item => item[originalSymbol]) + sharedSources.items.map(item => item[originalSymbol]) backend.setHasAnnouncedJoin(false) backend.setAlwaysStartPaused(true) @@ -556,7 +597,7 @@ function attachBackendToSocketClient(backend, client) { case 'distribute-queue': actionmsg = `distributed ${itemToMessage(command.topItem)} across the queue ${command.opts.how}` break - case 'initialize-backend': + case 'initialize-party': return case 'play': actionmsg = `started playing ${itemToMessage(command.track)}` @@ -647,10 +688,31 @@ function attachBackendToSocketClient(backend, client) { switch (command.code) { case 'set-socket-id': client.socketId = command.socketId - partyGrouplikeMap[command.socketId] = partyGrouplike - backend.loadPartyGrouplike(client.socketId, partyGrouplike) + socketInfoMap[command.socketId] = { + nickname: client.nickname, + sharedSources + } + backend.loadSharedSources(command.socketId, sharedSources) return - case 'initialize-backend': + case 'initialize-party': + for (const [ socketId, info ] of Object.entries(command.socketInfo)) { + const nickname = info.nickname + + const sharedSources = { + name: namePartySources(nickname), + isPartySources: true + } + + sharedSources.items = info.sharedSources.map( + item => deserializePartySource(item, sharedSources)) + + socketInfoMap[socketId] = { + nickname, + sharedSources + } + + backend.loadSharedSources(socketId, sharedSources) + } await restoreBackend(backend, command.backend) backend.on('playing', QP => { QP.once('received time data', () => { @@ -668,13 +730,16 @@ function attachBackendToSocketClient(backend, client) { switch (command.code) { case 'announce-join': { - const partyGrouplike = { - name: `Party Sources - ${command.senderNickname}`, + const sharedSources = { + name: namePartySources(command.senderNickname), isPartySources: true, items: [] } - partyGrouplikeMap[command.senderSocketId] = partyGrouplike - backend.loadPartyGrouplike(command.senderSocketId, partyGrouplike) + socketInfoMap[command.senderSocketId] = { + nickname: command.senderNickname, + sharedSources + } + backend.loadSharedSources(command.senderSocketId, sharedSources) return } case 'clear-queue': @@ -733,11 +798,10 @@ function attachBackendToSocketClient(backend, client) { if (QP) silenceEvents(QP, ['seek-to'], () => QP.seekTo(command.time)) return case 'set-nickname': { - const partyGrouplike = partyGrouplikeMap[command.senderSocketId] - if (partyGrouplike) { - partyGrouplike.name = `Party Sources - ${command.senderNickname}` - backend.partyGrouplikeUpdated(client.socketId, partyGrouplike) - } + const info = socketInfoMap[command.senderSocketId] + info.nickname = command.senderNickname + info.sharedSources.name = namePartySources(command.senderNickname) + backend.sharedSourcesUpdated(client.socketId, info.sharedSources) return } case 'set-pause': { @@ -755,11 +819,10 @@ function attachBackendToSocketClient(backend, client) { return } case 'share-with-party': { - const deserialized = deserializePartySource(command.item) - const partyGrouplike = partyGrouplikeMap[command.senderSocketId] - deserialized[parentSymbol] = partyGrouplike - partyGrouplike.items.push(deserialized) - backend.partyGrouplikeUpdated(command.senderSocketId, partyGrouplike) + const { sharedSources } = socketInfoMap[command.senderSocketId] + const deserialized = deserializePartySource(command.item, sharedSources) + sharedSources.items.push(deserialized) + backend.sharedSourcesUpdated(command.senderSocketId, sharedSources) return } case 'stop-playing': @@ -861,7 +924,7 @@ function attachBackendToSocketClient(backend, client) { backend.on('set party nickname', nickname => { let oldNickname = client.nickname - partyGrouplike.name = `Party Sources - ${nickname}` + sharedSources.name = namePartySources(nickname) client.nickname = nickname client.sendCommand({code: 'set-nickname', nickname, oldNickname}) }) @@ -898,15 +961,15 @@ function attachBackendToSocketClient(backend, client) { }) backend.on('share with party', item => { - if (partyGrouplike.items.every(x => x[originalSymbol] !== item)) { + if (sharedSources.items.every(x => x[originalSymbol] !== item)) { const serialized = serializePartySource(item) const deserialized = deserializePartySource(serialized) - deserialized[parentSymbol] = partyGrouplike + deserialized[parentSymbol] = sharedSources deserialized[originalSymbol] = item - partyGrouplike.items.push(deserialized) - backend.partyGrouplikeUpdated(client.socketId, partyGrouplike) + sharedSources.items.push(deserialized) + backend.sharedSourcesUpdated(client.socketId, sharedSources) updateRestoredTracksUsingPlaylists(backend, getPlaylistSources()) -- cgit 1.3.0-6-gf8a5