« get me outta code hell

serialized-backend.js - mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
path: root/serialized-backend.js
blob: b43fa4930ecfdec61595ce98804366a0a457725e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// 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 } = queuePlayer
  return {
    time: queuePlayer.time,
    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 => ({
      id: QP.id,
      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.id = qpData.id

      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
}