« get me outta code hell

mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
path: root/serialized-backend.js
diff options
context:
space:
mode:
Diffstat (limited to 'serialized-backend.js')
-rw-r--r--serialized-backend.js180
1 files changed, 180 insertions, 0 deletions
diff --git a/serialized-backend.js b/serialized-backend.js
new file mode 100644
index 0000000..4ea3019
--- /dev/null
+++ b/serialized-backend.js
@@ -0,0 +1,180 @@
+// 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 {
+  findTrackObject,
+  flattenGrouplike,
+  getItemPath,
+} from './playlist-utils.js'
+
+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
+  }
+}
+
+export 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)
+    }))
+  }
+}
+
+export 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()
+      }
+    }
+  })
+}
+
+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 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')
+  }
+}
+
+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
+}