« 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.js291
1 files changed, 180 insertions, 111 deletions
diff --git a/backend.js b/backend.js
index 3d9c386..a491f00 100644
--- a/backend.js
+++ b/backend.js
@@ -3,32 +3,29 @@
 
 'use strict'
 
-const { getDownloaderFor } = require('./downloaders')
-const { getMetadataReaderFor } = require('./metadata-readers')
-const { getPlayer } = require('./players')
-const RecordStore = require('./record-store')
-const os = require('os')
-const shortid = require('shortid')
-
-const {
+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, GhostPlayer} from './players.js'
+import RecordStore from './record-store.js'
+
+import {
   getTimeStringsFromSec,
   shuffleArray,
-  throttlePromise
-} = require('./general-util')
+  throttlePromise,
+} from './general-util.js'
 
-const {
+import {
   isGroup,
   isTrack,
   flattenGrouplike,
-  getItemPathString,
-  parentSymbol
-} = require('./playlist-utils')
-
-const { promisify } = require('util')
-const EventEmitter = require('events')
-const fs = require('fs')
-const writeFile = promisify(fs.writeFile)
-const readFile = promisify(fs.readFile)
+  parentSymbol,
+} from './playlist-utils.js'
 
 async function download(item, record) {
   if (isGroup(item)) {
@@ -69,7 +66,7 @@ class QueuePlayer extends EventEmitter {
     this.playingTrack = null
     this.queueGrouplike = {name: 'Queue', isTheQueue: true, items: []}
     this.pauseNextTrack = false
-    this.loopQueueAtEnd = false
+    this.queueEndMode = 'end' // end, loop, shuffle
     this.playedTrackToEnd = false
     this.timeData = null
     this.time = null
@@ -92,9 +89,10 @@ class QueuePlayer extends EventEmitter {
 
     this.player.on('printStatusLine', data => {
       if (this.playingTrack) {
+        const oldTimeData = this.timeData
         this.timeData = data
         this.time = data.curSecTotal
-        this.emit('received time data', data, this)
+        this.emit('received time data', data, oldTimeData)
       }
     })
 
@@ -217,20 +215,6 @@ class QueuePlayer extends EventEmitter {
 
     const distributeSize = distributeEnd - distributeStart
 
-    const queueItem = (item, insertIndex) => {
-      if (items.includes(item)) {
-        /*
-        if (!movePlayingTrack && item === this.playingTrack) {
-          return
-        }
-        */
-        items.splice(items.indexOf(item), 1)
-      } else {
-        offset++
-      }
-      items.splice(insertIndex, 0, item)
-    }
-
     if (how === 'evenly') {
       let offset = 0
       for (const item of newTracks) {
@@ -249,7 +233,7 @@ class QueuePlayer extends EventEmitter {
       }
     }
 
-    this.emit('distribute-queue', topItem, {how, rangeEnd})
+    this.emit('distribute queue', topItem, {how, rangeEnd})
     this.emitQueueUpdated()
   }
 
@@ -317,7 +301,7 @@ class QueuePlayer extends EventEmitter {
       items.splice(index)
     }
 
-    this.emit('clear-queue-past', track)
+    this.emit('clear queue past', track)
     this.emitQueueUpdated()
   }
 
@@ -334,7 +318,7 @@ class QueuePlayer extends EventEmitter {
       items.splice(startIndex, endIndex - startIndex)
     }
 
-    this.emit('clear-queue-up-to', track)
+    this.emit('clear queue up to', track)
     this.emitQueueUpdated()
   }
 
@@ -358,14 +342,16 @@ class QueuePlayer extends EventEmitter {
     }
   }
 
-  shuffleQueue() {
+  shuffleQueue(pastPlayingTrackOnly = true) {
     const queue = this.queueGrouplike
-    const index = queue.items.indexOf(this.playingTrack) + 1 // This is 0 if no track is playing
+    const index = (pastPlayingTrackOnly
+      ? queue.items.indexOf(this.playingTrack) + 1 // This is 0 if no track is playing
+      : 0)
     const initialItems = queue.items.slice(0, index)
     const remainingItems = queue.items.slice(index)
     const newItems = initialItems.concat(shuffleArray(remainingItems))
     queue.items = newItems
-    this.emit('shuffle-queue')
+    this.emit('shuffle queue')
     this.emitQueueUpdated()
   }
 
@@ -374,7 +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.emit('clear queue')
     this.emitQueueUpdated()
   }
 
@@ -390,18 +376,11 @@ class QueuePlayer extends EventEmitter {
     this.clearPlayingTrack()
   }
 
-
-  async play(item, forceStartPaused) {
+  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]
@@ -417,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.
@@ -439,8 +423,8 @@ class QueuePlayer extends EventEmitter {
       this.timeData = null
       this.time = null
       this.playingTrack = item
-      this.emit('playing details', this.playingTrack, oldTrack, this)
-      this.emit('playing', this.playingTrack, oldTrack, this)
+      this.emit('playing details', this.playingTrack, oldTrack, startTime)
+      this.emit('playing', this.playingTrack)
 
       await this.player.kill()
       if (this.alwaysStartPaused || forceStartPaused) {
@@ -452,7 +436,7 @@ class QueuePlayer extends EventEmitter {
       } else {
         this.player.setPause(false)
       }
-      await this.player.playFile(downloadFile)
+      await this.player.playFile(downloadFile, startTime)
     }
 
     // playingThisTrack now means whether the track played through to the end
@@ -462,20 +446,31 @@ class QueuePlayer extends EventEmitter {
       this.playedTrackToEnd = true
       this.emit('done playing', this.playingTrack)
       if (!this.waitWhenDonePlaying) {
-        if (!this.playNext(item)) {
-          if (this.loopQueueAtEnd) {
-            this.playFirst()
-          } else {
-            this.clearPlayingTrack()
-          }
-        }
+        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
 
+    // Auto-queue is nice but it should only happen when the queue hasn't been
+    // explicitly set to loop.
+    automaticallyQueueNextTrack = (
+      automaticallyQueueNextTrack &&
+      this.queueEndMode === 'end')
+
     const queue = this.queueGrouplike
     let queueIndex = queue.items.indexOf(track)
     if (queueIndex === -1) return false
@@ -494,7 +489,7 @@ class QueuePlayer extends EventEmitter {
         this.queue(nextItem)
         queueIndex = queue.items.length - 1
       } else {
-        return false
+        return this.playNextAtQueueEnd()
       }
     }
 
@@ -540,14 +535,51 @@ class QueuePlayer extends EventEmitter {
     return false
   }
 
+  playNextAtQueueEnd() {
+    switch (this.queueEndMode) {
+      case 'loop':
+        this.playFirst()
+        return true
+      case 'shuffle':
+        this.shuffleQueue(false)
+        this.playFirst()
+        return true
+      case 'end':
+      default:
+        this.clearPlayingTrack()
+        return false
+    }
+  }
+
+  async playOrSeek(item, time) {
+    if (!isTrack(item)) {
+      // This only makes sense to call with individual tracks!
+      return
+    }
+
+    if (item === this.playingTrack) {
+      this.seekTo(time)
+    } else {
+      // Queue the track, but only if it's not already in the queue, so that we
+      // respect an existing queue order.
+      const queue = this.queueGrouplike
+      const queueIndex = queue.items.indexOf(item)
+      if (queueIndex === -1) {
+        this.queue(item, this.playingTrack)
+      }
+
+      this.play(item, time)
+    }
+  }
+
   clearPlayingTrack() {
     if (this.playingTrack !== null) {
       const oldTrack = this.playingTrack
       this.playingTrack = null
       this.timeData = null
       this.time = null
-      this.emit('playing details', null, oldTrack, this)
-      this.emit('playing', null, oldTrack, this)
+      this.emit('playing details', null, oldTrack, 0)
+      this.emit('playing', null)
     }
   }
 
@@ -558,7 +590,7 @@ class QueuePlayer extends EventEmitter {
   seekAhead(seconds) {
     this.time += seconds
     this.player.seekAhead(seconds)
-    this.emit('seek-ahead', +seconds)
+    this.emit('seek ahead', +seconds)
   }
 
   seekBack(seconds) {
@@ -568,48 +600,56 @@ class QueuePlayer extends EventEmitter {
       this.time -= seconds
     }
     this.player.seekBack(seconds)
-    this.emit('seek-back', +seconds)
+    this.emit('seek back', +seconds)
   }
 
   seekTo(timeInSecs) {
     this.time = timeInSecs
     this.player.seekTo(timeInSecs)
-    this.emit('seek-to', +timeInSecs)
+    this.emit('seek to', +timeInSecs)
+  }
+
+  seekTo(seconds) {
+    this.player.seekTo(seconds)
+  }
+
+  seekToStart() {
+    this.player.seekToStart()
   }
 
   togglePause() {
     this.player.togglePause()
-    this.emit('toggle-pause')
+    this.emit('toggle pause')
   }
 
   setPause(value) {
     this.player.setPause(value)
-    this.emit('set-pause', !!value)
+    this.emit('set pause', !!value)
   }
 
   toggleLoop() {
     this.player.toggleLoop()
-    this.emit('toggle-loop')
+    this.emit('toggle loop')
   }
 
   setLoop(value) {
     this.player.setLoop(value)
-    this.emit('set-loop', !!value)
+    this.emit('set loop', !!value)
   }
 
-  volUp(amount = 10) {
+  volumeUp(amount = 10) {
     this.player.volUp(amount)
-    this.emit('vol-up', +amount)
+    this.emit('volume up', +amount)
   }
 
-  volDown(amount = 10) {
+  volumeDown(amount = 10) {
     this.player.volDown(amount)
-    this.emit('vol-down', +amount)
+    this.emit('volume down', +amount)
   }
 
   setVolume(value) {
     this.player.setVolume(value)
-    this.emit('set-volume', +value)
+    this.emit('set volume', +value)
   }
 
   setVolumeMultiplier(value) {
@@ -622,12 +662,18 @@ class QueuePlayer extends EventEmitter {
 
   setPauseNextTrack(value) {
     this.pauseNextTrack = !!value
-    this.emit('set-pause-next-track', !!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() {
@@ -653,7 +699,7 @@ class QueuePlayer extends EventEmitter {
   }
 }
 
-class Backend extends EventEmitter {
+export default class Backend extends EventEmitter {
   constructor({
     playerName = null,
     playerOptions = []
@@ -672,6 +718,12 @@ class Backend extends EventEmitter {
     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)
@@ -710,33 +762,43 @@ class Backend extends EventEmitter {
     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
   }
 
@@ -873,17 +935,24 @@ class Backend extends EventEmitter {
     this.hasAnnouncedJoin = hasAnnouncedJoin
   }
 
-  loadPartyGrouplike(socketId, partyGrouplike) {
-    this.emit('got party grouplike', socketId, partyGrouplike)
+  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)
   }
 
-  shareWithParty(item) {
-    this.emit('share with party', item)
+  sharedSourcesUpdated(socketId, sharedSources) {
+    this.emit('shared sources updated', socketId, sharedSources)
   }
 
-  partyGrouplikeUpdated(socketId, partyGrouplike) {
-    this.emit('party grouplike updated', socketId, partyGrouplike)
+  shareWithParty(item) {
+    this.emit('share with party', item)
   }
 }
-
-module.exports = Backend