serialized-backend.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. // Tools for serializing a backend into a JSON-stringifiable object format,
  2. // and for deserializing this format and loading its contained data into an
  3. // existing backend instance.
  4. //
  5. // Serialized data includes the list of queue players and each player's state
  6. // (queued items, playback position, etc).
  7. //
  8. // Serialized backend data can be used for a variety of purposes, such as
  9. // writing the data to a file and saving it for later use, or transferring
  10. // it over an internet connection to synchronize playback with a friend.
  11. // (The code in socket.js exists to automate this process, as well as to
  12. // provide a link so that changes to the queue or playback are synchronized
  13. // in real-time.)
  14. //
  15. // TODO: Changes might be necessary all throughout the program to support
  16. // having any number of objects refer to "the same track", as will likely be
  17. // the case when restoring from a serialized backend. One way to handle this
  18. // would be to (perhaps through the existing record store code) keep a handle
  19. // on each of "the same track", which would be accessed by something like a
  20. // serialized ID (ala symbols), or maybe just the track name / source URL.
  21. 'use strict'
  22. import {
  23. isGroup,
  24. isTrack,
  25. findItemObject,
  26. flattenGrouplike,
  27. getFlatGroupList,
  28. getFlatTrackList,
  29. getItemPath
  30. } from './playlist-utils.js'
  31. const referenceDataSymbol = Symbol('Restored reference data')
  32. function getPlayerInfo(queuePlayer) {
  33. const { player } = queuePlayer
  34. return {
  35. time: queuePlayer.time,
  36. isLooping: player.isLooping,
  37. isPaused: player.isPaused,
  38. volume: player.volume
  39. }
  40. }
  41. export function saveBackend(backend) {
  42. return {
  43. queuePlayers: backend.queuePlayers.map(QP => ({
  44. id: QP.id,
  45. playingTrack: saveItemReference(QP.playingTrack),
  46. queuedTracks: QP.queueGrouplike.items.map(saveItemReference),
  47. pauseNextTrack: QP.pauseNextTrack,
  48. playerInfo: getPlayerInfo(QP)
  49. }))
  50. }
  51. }
  52. export async function restoreBackend(backend, data) {
  53. // console.log('restoring backend:', data)
  54. if (data.queuePlayers) {
  55. if (data.queuePlayers.length === 0) {
  56. return
  57. }
  58. for (const qpData of data.queuePlayers) {
  59. const QP = await backend.addQueuePlayer()
  60. QP[referenceDataSymbol] = qpData
  61. QP.id = qpData.id
  62. QP.queueGrouplike.items = qpData.queuedTracks.map(refData => restoreNewItem(refData))
  63. QP.player.setVolume(qpData.playerInfo.volume)
  64. QP.player.setLoop(qpData.playerInfo.isLooping)
  65. QP.on('playing', () => {
  66. QP[referenceDataSymbol].playingTrack = null
  67. QP[referenceDataSymbol].playerInfo = null
  68. })
  69. }
  70. // We remove the old queue players after the new ones have been added,
  71. // because the backend won't let us ever have less than one queue player
  72. // at a time.
  73. while (backend.queuePlayers.length !== data.queuePlayers.length) {
  74. backend.removeQueuePlayer(backend.queuePlayers[0])
  75. }
  76. }
  77. }
  78. async function restorePlayingTrack(queuePlayer, playedTrack, playerInfo) {
  79. const QP = queuePlayer
  80. await QP.stopPlaying()
  81. QP.play(playedTrack, playerInfo.time || 0, playerInfo.isPaused)
  82. }
  83. export function updateRestoredTracksUsingPlaylists(backend, playlists) {
  84. // Utility function to restore the "identities" of tracks (i.e. which objects
  85. // they are represented by) queued or playing in the provided backend,
  86. // pulling possible track identities from the provided playlists.
  87. //
  88. // How well provided tracks resemble the ones existing in the backend (which
  89. // have not already been replaced by an existing track) is calculated with
  90. // the algorithm implemented in findItemObject, combining all provided
  91. // playlists (simply putting them all in a group) to allow the algorithm to
  92. // choose from all playlists equally at once.
  93. //
  94. // This function should be called after restoring a playlist and whenever
  95. // a new source playlist is added (a new tab opened, etc).
  96. //
  97. // TODO: Though this helps to combat issues with restoring track identities
  98. // when restoring from a saved backend, it could be expanded to restore from
  99. // closed sources as well (reference data would have to be automatically
  100. // saved on the tracks independently of save/restore in order to support
  101. // this sort of functionality). Note this would still face difficulties with
  102. // opening two identical playlists (i.e. the same playlist twice), since then
  103. // identities would be equally correctly picked from either source; this is
  104. // an inevitable issue with the way identities are resolved, but could be
  105. // lessened in the UI by simply opening a new view (rather than a whole new
  106. // load, with new track identities) when a playlist is opened twice at once.
  107. const possibleChoices = getFlatTrackList({items: playlists})
  108. for (const QP of backend.queuePlayers) {
  109. let playingDataToRestore
  110. const qpData = (QP[referenceDataSymbol] || {})
  111. const waitingTrackData = qpData.playingTrack
  112. if (waitingTrackData) {
  113. playingDataToRestore = waitingTrackData
  114. } else if (QP.playingTrack) {
  115. playingDataToRestore = QP.playingTrack[referenceDataSymbol]
  116. }
  117. if (playingDataToRestore) {
  118. const found = findItemObject(playingDataToRestore, possibleChoices)
  119. if (found) {
  120. restorePlayingTrack(QP, found, qpData.playerInfo || getPlayerInfo(QP))
  121. }
  122. }
  123. QP.queueGrouplike.items = QP.queueGrouplike.items.map(track => {
  124. const refData = track[referenceDataSymbol]
  125. if (!refData) {
  126. return track
  127. }
  128. return findItemObject(refData, possibleChoices) || track
  129. })
  130. QP.emit('queue updated')
  131. }
  132. }
  133. export function saveItemReference(item) {
  134. // Utility function to generate reference data for a track or grouplike,
  135. // according to the format taken by findItemObject.
  136. if (isTrack(item)) {
  137. return {
  138. name: item.name,
  139. path: getItemPath(item).slice(0, -1).map(group => group.name),
  140. downloaderArg: item.downloaderArg
  141. }
  142. } else if (isGroup(item)) {
  143. return {
  144. name: item.name,
  145. path: getItemPath(item).slice(0, -1).map(group => group.name),
  146. items: item.items.map(saveItemReference)
  147. }
  148. } else if (item) {
  149. return item
  150. } else {
  151. return null
  152. }
  153. }
  154. export function restoreNewItem(referenceData, playlists) {
  155. // Utility function to restore a new item. If you're restoring tracks
  156. // already present in a backend, use the specific function for that,
  157. // updateRestoredTracksUsingPlaylists.
  158. //
  159. // This function takes a playlists array like the function for restoring
  160. // tracks in a backend, but in this function, it's optional: if not provided,
  161. // it will simply skip searching for a resembling track and return a new
  162. // track object right away.
  163. let found
  164. if (playlists) {
  165. let possibleChoices
  166. if (referenceData.downloaderArg) {
  167. possibleChoices = getFlatTrackList({items: playlists})
  168. } else if (referenceData.items) {
  169. possibleChoices = getFlatGroupList({items: playlists})
  170. }
  171. if (possibleChoices) {
  172. found = findItemObject(referenceData, possibleChoices)
  173. }
  174. }
  175. if (found) {
  176. return found
  177. } else if (referenceData.downloaderArg) {
  178. return {
  179. [referenceDataSymbol]: referenceData,
  180. name: referenceData.name,
  181. downloaderArg: referenceData.downloaderArg
  182. }
  183. } else if (referenceData.items) {
  184. return {
  185. [referenceDataSymbol]: referenceData,
  186. name: referenceData.name,
  187. items: referenceData.items.map(item => restoreNewItem(item, playlists))
  188. }
  189. } else {
  190. return {
  191. [referenceDataSymbol]: referenceData,
  192. name: referenceData.name
  193. }
  194. }
  195. }
  196. export function getWaitingTrackData(queuePlayer) {
  197. // Utility function to get reference data for the track which is currently
  198. // waiting to be played, once a resembling track is found. This should only
  199. // be used to reflect that data in the user interface.
  200. return (queuePlayer[referenceDataSymbol] || {}).playingTrack
  201. }