123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231 |
- // 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'
- import {
- isGroup,
- isTrack,
- findItemObject,
- flattenGrouplike,
- getFlatGroupList,
- getFlatTrackList,
- getItemPath
- } from './playlist-utils.js'
- const referenceDataSymbol = Symbol('Restored reference data')
- function getPlayerInfo(queuePlayer) {
- const { player } = queuePlayer
- return {
- time: queuePlayer.time,
- isLooping: player.isLooping,
- isPaused: player.isPaused,
- volume: player.volume
- }
- }
- export function saveBackend(backend) {
- return {
- queuePlayers: backend.queuePlayers.map(QP => ({
- id: QP.id,
- playingTrack: saveItemReference(QP.playingTrack),
- queuedTracks: QP.queueGrouplike.items.map(saveItemReference),
- pauseNextTrack: QP.pauseNextTrack,
- playerInfo: getPlayerInfo(QP)
- }))
- }
- }
- export async function restoreBackend(backend, data) {
- // console.log('restoring 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.id = qpData.id
- QP.queueGrouplike.items = qpData.queuedTracks.map(refData => restoreNewItem(refData))
- 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, playerInfo.time || 0, playerInfo.isPaused)
- }
- export 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 findItemObject, 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 possibleChoices = getFlatTrackList({items: playlists})
- 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 = findItemObject(playingDataToRestore, possibleChoices)
- 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 findItemObject(refData, possibleChoices) || track
- })
- QP.emit('queue updated')
- }
- }
- export function saveItemReference(item) {
- // Utility function to generate reference data for a track or grouplike,
- // according to the format taken by findItemObject.
- if (isTrack(item)) {
- return {
- name: item.name,
- path: getItemPath(item).slice(0, -1).map(group => group.name),
- downloaderArg: item.downloaderArg
- }
- } else if (isGroup(item)) {
- return {
- name: item.name,
- path: getItemPath(item).slice(0, -1).map(group => group.name),
- items: item.items.map(saveItemReference)
- }
- } else if (item) {
- return item
- } else {
- return null
- }
- }
- export function restoreNewItem(referenceData, playlists) {
- // Utility function to restore a new item. If you're restoring tracks
- // already present in a backend, use the specific function for that,
- // updateRestoredTracksUsingPlaylists.
- //
- // This function takes a playlists array like the function for restoring
- // tracks in a backend, but in this function, it's optional: if not provided,
- // it will simply skip searching for a resembling track and return a new
- // track object right away.
- let found
- if (playlists) {
- let possibleChoices
- if (referenceData.downloaderArg) {
- possibleChoices = getFlatTrackList({items: playlists})
- } else if (referenceData.items) {
- possibleChoices = getFlatGroupList({items: playlists})
- }
- if (possibleChoices) {
- found = findItemObject(referenceData, possibleChoices)
- }
- }
- if (found) {
- return found
- } else if (referenceData.downloaderArg) {
- return {
- [referenceDataSymbol]: referenceData,
- name: referenceData.name,
- downloaderArg: referenceData.downloaderArg
- }
- } else if (referenceData.items) {
- return {
- [referenceDataSymbol]: referenceData,
- name: referenceData.name,
- items: referenceData.items.map(item => restoreNewItem(item, playlists))
- }
- } else {
- return {
- [referenceDataSymbol]: referenceData,
- name: referenceData.name
- }
- }
- }
- export 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
- }
|