diff options
Diffstat (limited to 'backend.js')
-rw-r--r-- | backend.js | 291 |
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 |