From 503a37ba4d7550f9c2ed1602e589a0142a20d10d Mon Sep 17 00:00:00 2001 From: Florrie Date: Fri, 10 Jul 2020 11:28:34 -0300 Subject: basic working backend save/restore & socket server Backend save/restore code (living in serialized-backend.js) has been well tested and shouldn't need much change going forward. Now we get to begin working on the actual synchronized-over-socket-server commands! --- serialized-backend.js | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 serialized-backend.js (limited to 'serialized-backend.js') diff --git a/serialized-backend.js b/serialized-backend.js new file mode 100644 index 0000000..13bb2b9 --- /dev/null +++ b/serialized-backend.js @@ -0,0 +1,187 @@ +// Tools for serializing a backend into a JSON-stringifiable object format, +// and for deserializing this format and loading its contained data into an +// existing backend instance. +// +// Serialized data includes the list of queue players and each player's state +// (queued items, playback position, etc). +// +// Serialized backend data can be used for a variety of purposes, such as +// writing the data to a file and saving it for later use, or transferring +// it over an internet connection to synchronize playback with a friend. +// (The code in socket.js exists to automate this process, as well as to +// provide a link so that changes to the queue or playback are synchronized +// in real-time.) +// +// TODO: Changes might be necessary all throughout the program to support +// having any number of objects refer to "the same track", as will likely be +// the case when restoring from a serialized backend. One way to handle this +// would be to (perhaps through the existing record store code) keep a handle +// on each of "the same track", which would be accessed by something like a +// serialized ID (ala symbols), or maybe just the track name / source URL. + +'use strict' + +const { + findTrackObject, + flattenGrouplike, + getItemPath +} = require('./playlist-utils') + +const referenceDataSymbol = Symbol('Restored reference data') + +function getPlayerInfo(queuePlayer) { + const { player, timeData } = queuePlayer + return { + time: timeData && timeData.curSecTotal, + isLooping: player.isLooping, + isPaused: player.isPaused, + volume: player.volume + } +} + +function saveBackend(backend) { + function referenceTrack(track) { + if (track) { + // This is the same format used as referenceData in findTrackObject + // (in playlist-utils.js). + return { + name: track.name, + downloaderArg: track.downloaderArg, + path: getItemPath(track).slice(0, -1).map(group => group.name) + } + } else { + return null + } + } + + return { + queuePlayers: backend.queuePlayers.map(QP => ({ + playingTrack: referenceTrack(QP.playingTrack), + queuedTracks: QP.queueGrouplike.items.map(referenceTrack), + pauseNextTrack: QP.pauseNextTrack, + playerInfo: getPlayerInfo(QP) + })) + } +} + +async function restoreBackend(backend, data) { + if (data.queuePlayers) { + if (data.queuePlayers.length === 0) { + return + } + + for (const qpData of data.queuePlayers) { + const QP = await backend.addQueuePlayer() + QP[referenceDataSymbol] = qpData + + QP.queueGrouplike.items = qpData.queuedTracks.map(refData => ({ + [referenceDataSymbol]: refData, + name: refData.name, + downloaderArg: refData.downloaderArg + })) + + QP.player.setVolume(qpData.playerInfo.volume) + QP.player.setLoop(qpData.playerInfo.isLooping) + + QP.on('playing', () => { + QP[referenceDataSymbol].playingTrack = null + QP[referenceDataSymbol].playerInfo = null + }) + } + + // We remove the old queue players after the new ones have been added, + // because the backend won't let us ever have less than one queue player + // at a time. + while (backend.queuePlayers.length !== data.queuePlayers.length) { + backend.removeQueuePlayer(backend.queuePlayers[0]) + } + } +} + +async function restorePlayingTrack(queuePlayer, playedTrack, playerInfo) { + const QP = queuePlayer + await QP.stopPlaying() + QP.play(playedTrack, true) + QP.once('received time data', () => { + if (QP.playingTrack === playedTrack) { + QP.player.seekTo(playerInfo.time) + if (!playerInfo.isPaused) { + QP.player.togglePause() + } + } + }) +} + +function updateRestoredTracksUsingPlaylists(backend, playlists) { + // Utility function to restore the "identities" of tracks (i.e. which objects + // they are represented by) queued or playing in the provided backend, + // pulling possible track identities from the provided playlists. + // + // How well provided tracks resemble the ones existing in the backend (which + // have not already been replaced by an existing track) is calculated with + // the algorithm implemented in findTrackObject, combining all provided + // playlists (simply putting them all in a group) to allow the algorithm to + // choose from all playlists equally at once. + // + // This function should be called after restoring a playlist and whenever + // a new source playlist is added (a new tab opened, etc). + // + // TODO: Though this helps to combat issues with restoring track identities + // when restoring from a saved backend, it could be expanded to restore from + // closed sources as well (reference data would have to be automatically + // saved on the tracks independently of save/restore in order to support + // this sort of functionality). Note this would still face difficulties with + // opening two identical playlists (i.e. the same playlist twice), since then + // identities would be equally correctly picked from either source; this is + // an inevitable issue with the way identities are resolved, but could be + // lessened in the UI by simply opening a new view (rather than a whole new + // load, with new track identities) when a playlist is opened twice at once. + + const combinedPlaylist = {items: playlists} + const flattenedPlaylist = flattenGrouplike(combinedPlaylist) + + for (const QP of backend.queuePlayers) { + let playingDataToRestore + + const qpData = (QP[referenceDataSymbol] || {}) + const waitingTrackData = qpData.playingTrack + if (waitingTrackData) { + playingDataToRestore = waitingTrackData + } else if (QP.playingTrack) { + playingDataToRestore = QP.playingTrack[referenceDataSymbol] + } + + if (playingDataToRestore) { + const found = findTrackObject(playingDataToRestore, combinedPlaylist, flattenedPlaylist) + if (found) { + restorePlayingTrack(QP, found, qpData.playerInfo || getPlayerInfo(QP)) + } + } + + QP.queueGrouplike.items = QP.queueGrouplike.items.map(track => { + const refData = track[referenceDataSymbol] + if (!refData) { + return track + } + + return findTrackObject(refData, combinedPlaylist, flattenedPlaylist) || track + }) + + QP.emit('queue updated') + } +} + +function getWaitingTrackData(queuePlayer) { + // Utility function to get reference data for the track which is currently + // waiting to be played, once a resembling track is found. This should only + // be used to reflect that data in the user interface. + + return (queuePlayer[referenceDataSymbol] || {}).playingTrack +} + +Object.assign(module.exports, { + saveBackend, + restoreBackend, + updateRestoredTracksUsingPlaylists, + getWaitingTrackData +}) -- cgit 1.3.0-6-gf8a5