« 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/backend.js
diff options
context:
space:
mode:
Diffstat (limited to 'backend.js')
-rw-r--r--backend.js240
1 files changed, 192 insertions, 48 deletions
diff --git a/backend.js b/backend.js
index 4142026..a491f00 100644
--- a/backend.js
+++ b/backend.js
@@ -7,9 +7,11 @@ import {readFile, writeFile} from 'node:fs/promises'
 import EventEmitter from 'node:events'
 import os from 'node:os'
 
+import shortid from 'shortid'
+
 import {getDownloaderFor} from './downloaders.js'
 import {getMetadataReaderFor} from './metadata-readers.js'
-import {getPlayer} from './players.js'
+import {getPlayer, GhostPlayer} from './players.js'
 import RecordStore from './record-store.js'
 
 import {
@@ -58,6 +60,8 @@ class QueuePlayer extends EventEmitter {
   }) {
     super()
 
+    this.id = shortid.generate()
+
     this.player = null
     this.playingTrack = null
     this.queueGrouplike = {name: 'Queue', isTheQueue: true, items: []}
@@ -65,6 +69,10 @@ class QueuePlayer extends EventEmitter {
     this.queueEndMode = 'end' // end, loop, shuffle
     this.playedTrackToEnd = false
     this.timeData = null
+    this.time = null
+
+    this.alwaysStartPaused = false
+    this.waitWhenDonePlaying = false
 
     this.getPlayer = getPlayer
     this.getRecordFor = getRecordFor
@@ -83,7 +91,8 @@ class QueuePlayer extends EventEmitter {
       if (this.playingTrack) {
         const oldTimeData = this.timeData
         this.timeData = data
-        this.emit('received time data', data, oldTimeData, this)
+        this.time = data.curSecTotal
+        this.emit('received time data', data, oldTimeData)
       }
     })
 
@@ -158,6 +167,7 @@ class QueuePlayer extends EventEmitter {
     }
 
     recursivelyAddTracks(topItem)
+    this.emit('queue', topItem, afterItem, {movePlayingTrack})
     this.emitQueueUpdated()
 
     // This is the first new track, if a group was queued.
@@ -166,9 +176,12 @@ class QueuePlayer extends EventEmitter {
     return newTrack
   }
 
-  distributeQueue(grouplike, {how = 'evenly', rangeEnd = 'end-of-queue'}) {
-    if (isTrack(grouplike)) {
-      grouplike = {items: [grouplike]}
+  distributeQueue(topItem, {how = 'evenly', rangeEnd = 'end-of-queue'} = {}) {
+    let grouplike
+    if (isTrack(topItem)) {
+      grouplike = {items: [topItem]}
+    } else {
+      grouplike = topItem
     }
 
     const { items } = this.queueGrouplike
@@ -220,6 +233,7 @@ class QueuePlayer extends EventEmitter {
       }
     }
 
+    this.emit('distribute queue', topItem, {how, rangeEnd})
     this.emitQueueUpdated()
   }
 
@@ -264,11 +278,17 @@ class QueuePlayer extends EventEmitter {
     }
 
     recursivelyUnqueueTracks(topItem)
+    this.emit('unqueue', topItem)
     this.emitQueueUpdated()
 
     return focusItem
   }
 
+  replaceAllItems(newItems) {
+    this.queueGrouplike.items = newItems
+    this.emitQueueUpdated()
+  }
+
   clearQueuePast(track) {
     const { items } = this.queueGrouplike
     const index = items.indexOf(track) + 1
@@ -281,6 +301,7 @@ class QueuePlayer extends EventEmitter {
       items.splice(index)
     }
 
+    this.emit('clear queue past', track)
     this.emitQueueUpdated()
   }
 
@@ -297,6 +318,7 @@ class QueuePlayer extends EventEmitter {
       items.splice(startIndex, endIndex - startIndex)
     }
 
+    this.emit('clear queue up to', track)
     this.emitQueueUpdated()
   }
 
@@ -329,6 +351,7 @@ class QueuePlayer extends EventEmitter {
     const remainingItems = queue.items.slice(index)
     const newItems = initialItems.concat(shuffleArray(remainingItems))
     queue.items = newItems
+    this.emit('shuffle queue')
     this.emitQueueUpdated()
   }
 
@@ -337,6 +360,7 @@ class QueuePlayer extends EventEmitter {
     // the track that's currently playing).
     this.queueGrouplike.items = this.queueGrouplike.items
       .filter(item => item === this.playingTrack)
+    this.emit('clear queue')
     this.emitQueueUpdated()
   }
 
@@ -352,18 +376,11 @@ class QueuePlayer extends EventEmitter {
     this.clearPlayingTrack()
   }
 
-
-  async play(item, startTime = 0) {
+  async play(item, startTime = 0, forceStartPaused = false) {
     if (this.player === null) {
       throw new Error('Attempted to play before a player was loaded')
     }
 
-    let playingThisTrack = true
-    this.emit('playing new track')
-    this.once('playing new track', () => {
-      playingThisTrack = false
-    })
-
     // If it's a group, play the first track.
     if (isGroup(item)) {
       item = flattenGrouplike(item).items[0]
@@ -379,13 +396,18 @@ class QueuePlayer extends EventEmitter {
       return
     }
 
-    playTrack: {
+    let playingThisTrack = true
+    this.emit('playing new track')
+    this.once('playing new track', () => {
+      playingThisTrack = false
+    })
+
+    if (this.player instanceof GhostPlayer) {
+      await this.#ghostPlay(item, startTime)
+    } else if (!item.downloaderArg) {
       // No downloader argument? That's no good - stop here.
       // TODO: An error icon on this item, or something???
-      if (!item.downloaderArg) {
-        break playTrack
-      }
-
+    } else {
       // If, by the time the track is downloaded, we're playing something
       // different from when the download started, assume that we just want to
       // keep listening to whatever new thing we started.
@@ -399,11 +421,15 @@ class QueuePlayer extends EventEmitter {
       }
 
       this.timeData = null
+      this.time = null
       this.playingTrack = item
-      this.emit('playing', this.playingTrack, oldTrack, startTime, this)
+      this.emit('playing details', this.playingTrack, oldTrack, startTime)
+      this.emit('playing', this.playingTrack)
 
       await this.player.kill()
-      if (this.playedTrackToEnd) {
+      if (this.alwaysStartPaused || forceStartPaused) {
+        this.player.setPause(true)
+      } else if (this.playedTrackToEnd) {
         this.player.setPause(this.pauseNextTrack)
         this.pauseNextTrack = false
         this.playedTrackToEnd = false
@@ -418,10 +444,24 @@ class QueuePlayer extends EventEmitter {
 
     if (playingThisTrack) {
       this.playedTrackToEnd = true
-      this.playNext(item)
+      this.emit('done playing', this.playingTrack)
+      if (!this.waitWhenDonePlaying) {
+        this.playNext(item)
+      }
     }
   }
 
+  async #ghostPlay(item, startTime) {
+    // If we're playing off a GhostPlayer, strip down the whole process.
+    // Downloading is totally unnecessary, for example.
+
+    this.timeData = null
+    this.time = null
+    this.playingTrack = item
+    this.emit('playing', this.playingTrack)
+    await this.player.playFile('-', startTime)
+  }
+
   playNext(track, automaticallyQueueNextTrack = false) {
     if (!track) return false
 
@@ -537,7 +577,9 @@ class QueuePlayer extends EventEmitter {
       const oldTrack = this.playingTrack
       this.playingTrack = null
       this.timeData = null
-      this.emit('playing', null, oldTrack, 0, this)
+      this.time = null
+      this.emit('playing details', null, oldTrack, 0)
+      this.emit('playing', null)
     }
   }
 
@@ -546,11 +588,25 @@ class QueuePlayer extends EventEmitter {
   }
 
   seekAhead(seconds) {
+    this.time += seconds
     this.player.seekAhead(seconds)
+    this.emit('seek ahead', +seconds)
   }
 
   seekBack(seconds) {
+    if (this.time < seconds) {
+      this.time = 0
+    } else {
+      this.time -= seconds
+    }
     this.player.seekBack(seconds)
+    this.emit('seek back', +seconds)
+  }
+
+  seekTo(timeInSecs) {
+    this.time = timeInSecs
+    this.player.seekTo(timeInSecs)
+    this.emit('seek to', +timeInSecs)
   }
 
   seekTo(seconds) {
@@ -563,47 +619,61 @@ class QueuePlayer extends EventEmitter {
 
   togglePause() {
     this.player.togglePause()
+    this.emit('toggle pause')
   }
 
   setPause(value) {
     this.player.setPause(value)
+    this.emit('set pause', !!value)
   }
 
   toggleLoop() {
     this.player.toggleLoop()
+    this.emit('toggle loop')
   }
 
   setLoop(value) {
     this.player.setLoop(value)
+    this.emit('set loop', !!value)
   }
 
-  volUp(amount = 10) {
+  volumeUp(amount = 10) {
     this.player.volUp(amount)
+    this.emit('volume up', +amount)
   }
 
-  volDown(amount = 10) {
+  volumeDown(amount = 10) {
     this.player.volDown(amount)
+    this.emit('volume down', +amount)
   }
 
   setVolume(value) {
     this.player.setVolume(value)
+    this.emit('set volume', +value)
   }
 
   setVolumeMultiplier(value) {
-    this.player.setVolumeMultiplier(value);
+    this.player.setVolumeMultiplier(value)
   }
 
   fadeIn() {
-    return this.player.fadeIn();
+    return this.player.fadeIn()
   }
 
   setPauseNextTrack(value) {
     this.pauseNextTrack = !!value
+    this.emit('set pause next track', !!value)
   }
 
   setLoopQueueAtEnd(value) {
     this.loopQueueAtEnd = !!value
-    this.emit('set-loop-queue-at-end', !!value)
+    this.emit('set loop queue at end', !!value)
+  }
+
+  setDuration(duration) {
+    if (this.player.setDuration) {
+      setTimeout(() => this.player.setDuration(duration))
+    }
   }
 
   get remainingTracks() {
@@ -636,14 +706,24 @@ export default class Backend extends EventEmitter {
   } = {}) {
     super()
 
-    this.playerName = playerName;
-    this.playerOptions = playerOptions;
+    this.playerName = playerName
+    this.playerOptions = playerOptions
 
     if (playerOptions.length && !playerName) {
       throw new Error(`Must specify playerName to specify playerOptions`);
     }
 
     this.queuePlayers = []
+    this.alwaysStartPaused = false
+    this.waitWhenDonePlaying = false
+
+    this.hasAnnouncedJoin = false
+    this.sharedSourcesMap = Object.create(null)
+    this.sharedSourcesGrouplike = {
+      name: 'Shared Sources',
+      isPartySources: true,
+      items: []
+    }
 
     this.recordStore = new RecordStore()
     this.throttleMetadata = throttlePromise(10)
@@ -675,37 +755,50 @@ export default class Backend extends EventEmitter {
       return error
     }
 
+    queuePlayer.alwaysStartPaused = this.alwaysStartPaused
+    queuePlayer.waitWhenDonePlaying = this.waitWhenDonePlaying
+
     this.queuePlayers.push(queuePlayer)
     this.emit('added queue player', queuePlayer)
 
     for (const event of [
-      'playing',
+      'clear queue',
+      'clear queue past',
+      'clear queue up to',
+      'distribute queue',
       'done playing',
+      'playing',
+      'playing details',
       'queue',
-      'distribute-queue',
-      'unqueue',
-      'clear-queue-past',
-      'clear-queue-up-to',
-      'shuffle-queue',
-      'clear-queue',
       'queue updated',
-      'seek-ahead',
-      'seek-back',
-      'toggle-pause',
-      'set-pause',
-      'toggle-loop',
-      'set-loop',
-      'vol-up',
-      'vol-down',
-      'set-volume',
-      'set-pause-next-track',
-      'set-loop-queue-at-end'
+      'received time data',
+      'seek ahead',
+      'seek back',
+      'seek to',
+      'set loop',
+      'set loop queue at end',
+      'set pause',
+      'set pause next track',
+      'set volume',
+      'shuffle queue',
+      'toggle loop',
+      'toggle pause',
+      'unqueue',
+      'volume down',
+      'volume up',
     ]) {
       queuePlayer.on(event, (...data) => {
-        this.emit(event, queuePlayer, ...data)
+        this.emit('QP: ' + event, queuePlayer, ...data)
       })
     }
 
+    queuePlayer.on('playing', track => {
+      if (track) {
+        const metadata = this.getMetadataFor(track)
+        queuePlayer.setDuration(metadata?.duration)
+      }
+    })
+
     return queuePlayer
   }
 
@@ -802,6 +895,20 @@ export default class Backend extends EventEmitter {
     return {seconds, string, noticedMissingMetadata, approxSymbol}
   }
 
+  setAlwaysStartPaused(value) {
+    this.alwaysStartPaused = !!value
+    for (const queuePlayer of this.queuePlayers) {
+      queuePlayer.alwaysStartPaused = !!value
+    }
+  }
+
+  setWaitWhenDonePlaying(value) {
+    this.waitWhenDonePlaying = !!value
+    for (const queuePlayer of this.queuePlayers) {
+      queuePlayer.waitWhenDonePlaying = !!value
+    }
+  }
+
   async stopPlayingAll() {
     for (const queuePlayer of this.queuePlayers) {
       await queuePlayer.stopPlaying()
@@ -811,4 +918,41 @@ export default class Backend extends EventEmitter {
   async download(item) {
     return download(item, this.getRecordFor(item))
   }
+
+  showLogMessage(messageInfo) {
+    this.emit('log message', messageInfo)
+  }
+
+  setPartyNickname(nickname) {
+    this.emit('set party nickname', nickname)
+  }
+
+  announceJoinParty() {
+    this.emit('announce join party')
+  }
+
+  setHasAnnouncedJoin(hasAnnouncedJoin) {
+    this.hasAnnouncedJoin = hasAnnouncedJoin
+  }
+
+  loadSharedSources(socketId, sharedSources) {
+    if (socketId in this.sharedSourcesMap) {
+      return
+    }
+
+    this.sharedSourcesMap[socketId] = sharedSources
+
+    sharedSources[parentSymbol] = this.sharedSourcesGrouplike
+    this.sharedSourcesGrouplike.items.push(sharedSources)
+
+    this.emit('got shared sources', socketId, sharedSources)
+  }
+
+  sharedSourcesUpdated(socketId, sharedSources) {
+    this.emit('shared sources updated', socketId, sharedSources)
+  }
+
+  shareWithParty(item) {
+    this.emit('share with party', item)
+  }
 }