backend.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959
  1. // MTUI "server" - this just acts as the backend for mtui, controlling the
  2. // player, queue, etc. It's entirely independent from tui-lib/UI.
  3. 'use strict'
  4. import {readFile, writeFile} from 'node:fs/promises'
  5. import EventEmitter from 'node:events'
  6. import os from 'node:os'
  7. import shortid from 'shortid'
  8. import {getDownloaderFor} from './downloaders.js'
  9. import {getMetadataReaderFor} from './metadata-readers.js'
  10. import {getPlayer, GhostPlayer} from './players.js'
  11. import RecordStore from './record-store.js'
  12. import {
  13. getTimeStringsFromSec,
  14. shuffleArray,
  15. throttlePromise,
  16. } from './general-util.js'
  17. import {
  18. isGroup,
  19. isTrack,
  20. flattenGrouplike,
  21. parentSymbol,
  22. } from './playlist-utils.js'
  23. async function download(item, record) {
  24. if (isGroup(item)) {
  25. // TODO: Download all children (recursively), show a confirmation prompt
  26. // if there are a lot of items (remember to flatten).
  27. return
  28. }
  29. // You can't download things that aren't tracks!
  30. if (!isTrack(item)) {
  31. return
  32. }
  33. // Don't start downloading an item if we're already downloading it!
  34. if (record.downloading) {
  35. return
  36. }
  37. const arg = item.downloaderArg
  38. record.downloading = true
  39. try {
  40. return await getDownloaderFor(arg)(arg)
  41. } finally {
  42. record.downloading = false
  43. }
  44. }
  45. class QueuePlayer extends EventEmitter {
  46. constructor({
  47. getPlayer,
  48. getRecordFor
  49. }) {
  50. super()
  51. this.id = shortid.generate()
  52. this.player = null
  53. this.playingTrack = null
  54. this.queueGrouplike = {name: 'Queue', isTheQueue: true, items: []}
  55. this.pauseNextTrack = false
  56. this.queueEndMode = 'end' // end, loop, shuffle
  57. this.playedTrackToEnd = false
  58. this.timeData = null
  59. this.time = null
  60. this.alwaysStartPaused = false
  61. this.waitWhenDonePlaying = false
  62. this.getPlayer = getPlayer
  63. this.getRecordFor = getRecordFor
  64. }
  65. async setup() {
  66. this.player = await this.getPlayer()
  67. if (!this.player) {
  68. return {
  69. error: "Sorry, it doesn't look like there's an audio player installed on your computer. Can you try installing MPV (https://mpv.io) or SoX?"
  70. }
  71. }
  72. this.player.on('printStatusLine', data => {
  73. if (this.playingTrack) {
  74. const oldTimeData = this.timeData
  75. this.timeData = data
  76. this.time = data.curSecTotal
  77. this.emit('received time data', data, oldTimeData)
  78. }
  79. })
  80. return true
  81. }
  82. queue(topItem, afterItem = null, {movePlayingTrack = true} = {}) {
  83. const { items } = this.queueGrouplike
  84. const newTrackIndex = items.length
  85. // The position which new tracks should be added at, if afterItem is
  86. // passed.
  87. const afterIndex = afterItem && items.indexOf(afterItem)
  88. // Keeps track of how many tracks have been added; this is used so that
  89. // a whole group can be queued in order after a given item.
  90. let grouplikeOffset = 0
  91. // Keeps track of how many tracks have been removed (times -1); this is
  92. // used so we queue tracks at the intended spot.
  93. let removeOffset = 0
  94. const recursivelyAddTracks = item => {
  95. // For groups, just queue all children.
  96. if (isGroup(item)) {
  97. for (const child of item.items) {
  98. recursivelyAddTracks(child)
  99. }
  100. return
  101. }
  102. // If the item isn't a track, it can't be queued.
  103. if (!isTrack(item)) {
  104. return
  105. }
  106. // You can't put the same track in the queue twice - we automatically
  107. // remove the old entry. (You can't for a variety of technical reasons,
  108. // but basically you either have the display all bork'd, or new tracks
  109. // can't be added to the queue in the right order (because Object.assign
  110. // is needed to fix the display, but then you end up with a new object
  111. // that doesn't work with indexOf).)
  112. if (items.includes(item)) {
  113. // HOWEVER, if the "moveCurrentTrack" option is false, and that item
  114. // is the one that's currently playing, we won't do anything with it
  115. // at all.
  116. if (!movePlayingTrack && item === this.playingTrack) {
  117. return
  118. }
  119. const removeIndex = items.indexOf(item)
  120. items.splice(removeIndex, 1)
  121. // If the item we removed was positioned before the insertion index,
  122. // we need to shift that index back one, so it's placed after the same
  123. // intended track.
  124. if (removeIndex <= afterIndex) {
  125. removeOffset--
  126. }
  127. }
  128. if (afterItem === 'FRONT') {
  129. items.unshift(item)
  130. } else if (afterItem) {
  131. items.splice(afterIndex + 1 + grouplikeOffset + removeOffset, 0, item)
  132. } else {
  133. items.push(item)
  134. }
  135. grouplikeOffset++
  136. }
  137. recursivelyAddTracks(topItem)
  138. this.emit('queue', topItem, afterItem, {movePlayingTrack})
  139. this.emitQueueUpdated()
  140. // This is the first new track, if a group was queued.
  141. const newTrack = items[newTrackIndex]
  142. return newTrack
  143. }
  144. distributeQueue(topItem, {how = 'evenly', rangeEnd = 'end-of-queue'} = {}) {
  145. let grouplike
  146. if (isTrack(topItem)) {
  147. grouplike = {items: [topItem]}
  148. } else {
  149. grouplike = topItem
  150. }
  151. const { items } = this.queueGrouplike
  152. const newTracks = flattenGrouplike(grouplike).items.filter(isTrack)
  153. // Expressly do an initial pass and unqueue the items we want to queue -
  154. // otherwise they would mess with the math we do afterwords.
  155. for (const item of newTracks) {
  156. if (items.includes(item)) {
  157. /*
  158. if (!movePlayingTrack && item === this.playingTrack) {
  159. // NB: if uncommenting this code, splice item from newTracks and do
  160. // continue instead of return!
  161. return
  162. }
  163. */
  164. items.splice(items.indexOf(item), 1)
  165. }
  166. }
  167. const distributeStart = items.indexOf(this.playingTrack) + 1
  168. let distributeEnd
  169. if (rangeEnd === 'end-of-queue') {
  170. distributeEnd = items.length
  171. } else if (typeof rangeEnd === 'number') {
  172. distributeEnd = Math.min(items.length, rangeEnd)
  173. } else {
  174. throw new Error('Invalid rangeEnd: ' + rangeEnd)
  175. }
  176. const distributeSize = distributeEnd - distributeStart
  177. if (how === 'evenly') {
  178. let offset = 0
  179. for (const item of newTracks) {
  180. const insertIndex = distributeStart + Math.floor(offset)
  181. items.splice(insertIndex, 0, item)
  182. offset++
  183. offset += distributeSize / newTracks.length
  184. }
  185. } else if (how === 'randomly') {
  186. const indexes = newTracks.map(() => Math.floor(Math.random() * distributeSize))
  187. indexes.sort()
  188. for (let i = 0; i < newTracks.length; i++) {
  189. const item = newTracks[i]
  190. const insertIndex = distributeStart + indexes[i] + i
  191. items.splice(insertIndex, 0, item)
  192. }
  193. }
  194. this.emit('distribute queue', topItem, {how, rangeEnd})
  195. this.emitQueueUpdated()
  196. }
  197. unqueue(topItem, focusItem = null) {
  198. // This function has support to unqueue groups - it removes all tracks in
  199. // the group recursively. (You can never unqueue a group itself from the
  200. // queue listing because groups can't be added directly to the queue.)
  201. const { items } = this.queueGrouplike
  202. const recursivelyUnqueueTracks = item => {
  203. // For groups, just unqueue all children. (Groups themselves can't be
  204. // added to the queue, so we don't need to worry about removing them.)
  205. if (isGroup(item)) {
  206. for (const child of item.items) {
  207. recursivelyUnqueueTracks(child)
  208. }
  209. return
  210. }
  211. // Don't unqueue the currently-playing track - this usually causes more
  212. // trouble than it's worth.
  213. if (item === this.playingTrack) {
  214. return
  215. }
  216. // If we're unqueueing the item which is currently focused by the cursor,
  217. // just move the cursor ahead.
  218. if (item === focusItem) {
  219. focusItem = items[items.indexOf(focusItem) + 1]
  220. // ...Unless that puts it at past the end of the list, in which case, move
  221. // it behind the item we're removing.
  222. if (!focusItem) {
  223. focusItem = items[items.length - 2]
  224. }
  225. }
  226. if (items.includes(item)) {
  227. items.splice(items.indexOf(item), 1)
  228. }
  229. }
  230. recursivelyUnqueueTracks(topItem)
  231. this.emit('unqueue', topItem)
  232. this.emitQueueUpdated()
  233. return focusItem
  234. }
  235. replaceAllItems(newItems) {
  236. this.queueGrouplike.items = newItems
  237. this.emitQueueUpdated()
  238. }
  239. clearQueuePast(track) {
  240. const { items } = this.queueGrouplike
  241. const index = items.indexOf(track) + 1
  242. if (index < 0) {
  243. return
  244. } else if (index < items.indexOf(this.playingTrack)) {
  245. items.splice(index, items.length - index, this.playingTrack)
  246. } else {
  247. items.splice(index)
  248. }
  249. this.emit('clear queue past', track)
  250. this.emitQueueUpdated()
  251. }
  252. clearQueueUpTo(track) {
  253. const { items } = this.queueGrouplike
  254. const endIndex = items.indexOf(track)
  255. const startIndex = (this.playingTrack ? items.indexOf(this.playingTrack) + 1 : 0)
  256. if (endIndex < 0) {
  257. return
  258. } else if (endIndex < startIndex) {
  259. return
  260. } else {
  261. items.splice(startIndex, endIndex - startIndex)
  262. }
  263. this.emit('clear queue up to', track)
  264. this.emitQueueUpdated()
  265. }
  266. playSooner(item) {
  267. this.distributeQueue(item, {
  268. how: 'randomly',
  269. rangeEnd: this.queueGrouplike.items.indexOf(item)
  270. })
  271. }
  272. playLater(item) {
  273. this.skipIfCurrent(item)
  274. this.distributeQueue(item, {
  275. how: 'randomly'
  276. })
  277. }
  278. skipIfCurrent(track) {
  279. if (track === this.playingTrack) {
  280. this.playNext(track)
  281. }
  282. }
  283. shuffleQueue(pastPlayingTrackOnly = true) {
  284. const queue = this.queueGrouplike
  285. const index = (pastPlayingTrackOnly
  286. ? queue.items.indexOf(this.playingTrack) + 1 // This is 0 if no track is playing
  287. : 0)
  288. const initialItems = queue.items.slice(0, index)
  289. const remainingItems = queue.items.slice(index)
  290. const newItems = initialItems.concat(shuffleArray(remainingItems))
  291. queue.items = newItems
  292. this.emit('shuffle queue')
  293. this.emitQueueUpdated()
  294. }
  295. clearQueue() {
  296. // Clear the queue so that there aren't any items left in it (except for
  297. // the track that's currently playing).
  298. this.queueGrouplike.items = this.queueGrouplike.items
  299. .filter(item => item === this.playingTrack)
  300. this.emit('clear queue')
  301. this.emitQueueUpdated()
  302. }
  303. emitQueueUpdated() {
  304. this.emit('queue updated')
  305. }
  306. async stopPlaying() {
  307. // We emit this so the active play() call doesn't immediately start a new
  308. // track. We aren't *actually* about to play a new track.
  309. this.emit('playing new track')
  310. await this.player.kill()
  311. this.clearPlayingTrack()
  312. }
  313. async play(item, startTime = 0, forceStartPaused = false) {
  314. if (this.player === null) {
  315. throw new Error('Attempted to play before a player was loaded')
  316. }
  317. // If it's a group, play the first track.
  318. if (isGroup(item)) {
  319. item = flattenGrouplike(item).items[0]
  320. }
  321. // If there is no item (e.g. an empty group), well.. don't do anything.
  322. if (!item) {
  323. return
  324. }
  325. // If it's not a track, you can't play it.
  326. if (!isTrack(item)) {
  327. return
  328. }
  329. let playingThisTrack = true
  330. this.emit('playing new track')
  331. this.once('playing new track', () => {
  332. playingThisTrack = false
  333. })
  334. if (this.player instanceof GhostPlayer) {
  335. await this.#ghostPlay(item, startTime)
  336. } else if (!item.downloaderArg) {
  337. // No downloader argument? That's no good - stop here.
  338. // TODO: An error icon on this item, or something???
  339. } else {
  340. // If, by the time the track is downloaded, we're playing something
  341. // different from when the download started, assume that we just want to
  342. // keep listening to whatever new thing we started.
  343. const oldTrack = this.playingTrack
  344. const downloadFile = await this.download(item)
  345. if (this.playingTrack !== oldTrack) {
  346. return
  347. }
  348. this.timeData = null
  349. this.time = null
  350. this.playingTrack = item
  351. this.emit('playing details', this.playingTrack, oldTrack, startTime)
  352. this.emit('playing', this.playingTrack)
  353. await this.player.kill()
  354. if (this.alwaysStartPaused || forceStartPaused) {
  355. this.player.setPause(true)
  356. } else if (this.playedTrackToEnd) {
  357. this.player.setPause(this.pauseNextTrack)
  358. this.pauseNextTrack = false
  359. this.playedTrackToEnd = false
  360. } else {
  361. this.player.setPause(false)
  362. }
  363. await this.player.playFile(downloadFile, startTime)
  364. }
  365. // playingThisTrack now means whether the track played through to the end
  366. // (true), or was stopped by a different track being started (false).
  367. if (playingThisTrack) {
  368. this.playedTrackToEnd = true
  369. this.emit('done playing', this.playingTrack)
  370. if (!this.waitWhenDonePlaying) {
  371. this.playNext(item)
  372. }
  373. }
  374. }
  375. async #ghostPlay(item, startTime) {
  376. // If we're playing off a GhostPlayer, strip down the whole process.
  377. // Downloading is totally unnecessary, for example.
  378. this.timeData = null
  379. this.time = null
  380. this.playingTrack = item
  381. this.emit('playing', this.playingTrack)
  382. await this.player.playFile('-', startTime)
  383. }
  384. playNext(track, automaticallyQueueNextTrack = false) {
  385. if (!track) return false
  386. // Auto-queue is nice but it should only happen when the queue hasn't been
  387. // explicitly set to loop.
  388. automaticallyQueueNextTrack = (
  389. automaticallyQueueNextTrack &&
  390. this.queueEndMode === 'end')
  391. const queue = this.queueGrouplike
  392. let queueIndex = queue.items.indexOf(track)
  393. if (queueIndex === -1) return false
  394. queueIndex++
  395. if (queueIndex >= queue.items.length) {
  396. if (automaticallyQueueNextTrack) {
  397. const parent = track[parentSymbol]
  398. if (!parent) return false
  399. let index = parent.items.indexOf(track)
  400. let nextItem
  401. do {
  402. nextItem = parent.items[++index]
  403. } while (nextItem && !(isTrack(nextItem) || isGroup(nextItem)))
  404. if (!nextItem) return false
  405. this.queue(nextItem)
  406. queueIndex = queue.items.length - 1
  407. } else {
  408. return this.playNextAtQueueEnd()
  409. }
  410. }
  411. this.play(queue.items[queueIndex])
  412. return true
  413. }
  414. playPrevious(track, automaticallyQueuePreviousTrack = false) {
  415. if (!track) return false
  416. const queue = this.queueGrouplike
  417. let queueIndex = queue.items.indexOf(track)
  418. if (queueIndex === -1) return false
  419. queueIndex--
  420. if (queueIndex < 0) {
  421. if (automaticallyQueuePreviousTrack) {
  422. const parent = track[parentSymbol]
  423. if (!parent) return false
  424. let index = parent.items.indexOf(track)
  425. let previousItem
  426. do {
  427. previousItem = parent.items[--index]
  428. } while (previousItem && !(isTrack(previousItem) || isGroup(previousItem)))
  429. if (!previousItem) return false
  430. this.queue(previousItem, 'FRONT')
  431. queueIndex = 0
  432. } else {
  433. return false
  434. }
  435. }
  436. this.play(queue.items[queueIndex])
  437. return true
  438. }
  439. playFirst() {
  440. const queue = this.queueGrouplike
  441. if (queue.items.length) {
  442. this.play(queue.items[0])
  443. return true
  444. }
  445. return false
  446. }
  447. playNextAtQueueEnd() {
  448. switch (this.queueEndMode) {
  449. case 'loop':
  450. this.playFirst()
  451. return true
  452. case 'shuffle':
  453. this.shuffleQueue(false)
  454. this.playFirst()
  455. return true
  456. case 'end':
  457. default:
  458. this.clearPlayingTrack()
  459. return false
  460. }
  461. }
  462. async playOrSeek(item, time) {
  463. if (!isTrack(item)) {
  464. // This only makes sense to call with individual tracks!
  465. return
  466. }
  467. if (item === this.playingTrack) {
  468. this.seekTo(time)
  469. } else {
  470. // Queue the track, but only if it's not already in the queue, so that we
  471. // respect an existing queue order.
  472. const queue = this.queueGrouplike
  473. const queueIndex = queue.items.indexOf(item)
  474. if (queueIndex === -1) {
  475. this.queue(item, this.playingTrack)
  476. }
  477. this.play(item, time)
  478. }
  479. }
  480. clearPlayingTrack() {
  481. if (this.playingTrack !== null) {
  482. const oldTrack = this.playingTrack
  483. this.playingTrack = null
  484. this.timeData = null
  485. this.time = null
  486. this.emit('playing details', null, oldTrack, 0)
  487. this.emit('playing', null)
  488. }
  489. }
  490. async download(item) {
  491. return download(item, this.getRecordFor(item))
  492. }
  493. seekAhead(seconds) {
  494. this.time += seconds
  495. this.player.seekAhead(seconds)
  496. this.emit('seek ahead', +seconds)
  497. }
  498. seekBack(seconds) {
  499. if (this.time < seconds) {
  500. this.time = 0
  501. } else {
  502. this.time -= seconds
  503. }
  504. this.player.seekBack(seconds)
  505. this.emit('seek back', +seconds)
  506. }
  507. seekTo(timeInSecs) {
  508. this.time = timeInSecs
  509. this.player.seekTo(timeInSecs)
  510. this.emit('seek to', +timeInSecs)
  511. }
  512. seekTo(seconds) {
  513. this.player.seekTo(seconds)
  514. }
  515. seekToStart() {
  516. this.player.seekToStart()
  517. }
  518. togglePause() {
  519. this.player.togglePause()
  520. this.emit('toggle pause')
  521. }
  522. setPause(value) {
  523. this.player.setPause(value)
  524. this.emit('set pause', !!value)
  525. }
  526. toggleLoop() {
  527. this.player.toggleLoop()
  528. this.emit('toggle loop')
  529. }
  530. setLoop(value) {
  531. this.player.setLoop(value)
  532. this.emit('set loop', !!value)
  533. }
  534. volumeUp(amount = 10) {
  535. this.player.volUp(amount)
  536. this.emit('volume up', +amount)
  537. }
  538. volumeDown(amount = 10) {
  539. this.player.volDown(amount)
  540. this.emit('volume down', +amount)
  541. }
  542. setVolume(value) {
  543. this.player.setVolume(value)
  544. this.emit('set volume', +value)
  545. }
  546. setVolumeMultiplier(value) {
  547. this.player.setVolumeMultiplier(value)
  548. }
  549. fadeIn() {
  550. return this.player.fadeIn()
  551. }
  552. setPauseNextTrack(value) {
  553. this.pauseNextTrack = !!value
  554. this.emit('set pause next track', !!value)
  555. }
  556. setLoopQueueAtEnd(value) {
  557. this.loopQueueAtEnd = !!value
  558. this.emit('set loop queue at end', !!value)
  559. }
  560. setDuration(duration) {
  561. if (this.player.setDuration) {
  562. setTimeout(() => this.player.setDuration(duration))
  563. }
  564. }
  565. get remainingTracks() {
  566. const index = this.queueGrouplike.items.indexOf(this.playingTrack)
  567. const length = this.queueGrouplike.items.length
  568. if (index === -1) {
  569. return length
  570. } else {
  571. return length - index - 1
  572. }
  573. }
  574. get playSymbol() {
  575. if (this.player && this.playingTrack) {
  576. if (this.player.isPaused) {
  577. return '⏸'
  578. } else {
  579. return '▶'
  580. }
  581. } else {
  582. return '.'
  583. }
  584. }
  585. }
  586. export default class Backend extends EventEmitter {
  587. constructor({
  588. playerName = null,
  589. playerOptions = []
  590. } = {}) {
  591. super()
  592. this.playerName = playerName
  593. this.playerOptions = playerOptions
  594. if (playerOptions.length && !playerName) {
  595. throw new Error(`Must specify playerName to specify playerOptions`);
  596. }
  597. this.queuePlayers = []
  598. this.alwaysStartPaused = false
  599. this.waitWhenDonePlaying = false
  600. this.hasAnnouncedJoin = false
  601. this.sharedSourcesMap = Object.create(null)
  602. this.sharedSourcesGrouplike = {
  603. name: 'Shared Sources',
  604. isPartySources: true,
  605. items: []
  606. }
  607. this.recordStore = new RecordStore()
  608. this.throttleMetadata = throttlePromise(10)
  609. this.metadataDictionary = {}
  610. this.rootDirectory = os.homedir() + '/.mtui'
  611. this.metadataPath = this.rootDirectory + '/track-metadata.json'
  612. }
  613. async setup() {
  614. const error = await this.addQueuePlayer()
  615. if (error.error) {
  616. return error
  617. }
  618. await this.loadMetadata()
  619. return true
  620. }
  621. async addQueuePlayer() {
  622. const queuePlayer = new QueuePlayer({
  623. getPlayer: () => getPlayer(this.playerName, this.playerOptions),
  624. getRecordFor: item => this.getRecordFor(item)
  625. })
  626. const error = await queuePlayer.setup()
  627. if (error.error) {
  628. return error
  629. }
  630. queuePlayer.alwaysStartPaused = this.alwaysStartPaused
  631. queuePlayer.waitWhenDonePlaying = this.waitWhenDonePlaying
  632. this.queuePlayers.push(queuePlayer)
  633. this.emit('added queue player', queuePlayer)
  634. for (const event of [
  635. 'clear queue',
  636. 'clear queue past',
  637. 'clear queue up to',
  638. 'distribute queue',
  639. 'done playing',
  640. 'playing',
  641. 'playing details',
  642. 'queue',
  643. 'queue updated',
  644. 'received time data',
  645. 'seek ahead',
  646. 'seek back',
  647. 'seek to',
  648. 'set loop',
  649. 'set loop queue at end',
  650. 'set pause',
  651. 'set pause next track',
  652. 'set volume',
  653. 'shuffle queue',
  654. 'toggle loop',
  655. 'toggle pause',
  656. 'unqueue',
  657. 'volume down',
  658. 'volume up',
  659. ]) {
  660. queuePlayer.on(event, (...data) => {
  661. this.emit('QP: ' + event, queuePlayer, ...data)
  662. })
  663. }
  664. queuePlayer.on('playing', track => {
  665. if (track) {
  666. const metadata = this.getMetadataFor(track)
  667. queuePlayer.setDuration(metadata?.duration)
  668. }
  669. })
  670. return queuePlayer
  671. }
  672. removeQueuePlayer(queuePlayer) {
  673. if (this.queuePlayers.length > 1) {
  674. this.queuePlayers.splice(this.queuePlayers.indexOf(queuePlayer), 1)
  675. this.emit('removed queue player', queuePlayer)
  676. }
  677. }
  678. async readMetadata() {
  679. try {
  680. return JSON.parse(await readFile(this.metadataPath))
  681. } catch (error) {
  682. // Just stop. It's okay to fail to load metadata.
  683. return null
  684. }
  685. }
  686. async loadMetadata() {
  687. Object.assign(this.metadataDictionary, await this.readMetadata())
  688. }
  689. async saveMetadata() {
  690. const newData = Object.assign({}, await this.readMetadata(), this.metadataDictionary)
  691. await writeFile(this.metadataPath, JSON.stringify(newData))
  692. }
  693. getMetadataFor(item) {
  694. const key = this.metadataDictionary[item.downloaderArg]
  695. return this.metadataDictionary[key] || null
  696. }
  697. async processMetadata(item, reprocess = false, top = true) {
  698. let counter = 0
  699. if (isGroup(item)) {
  700. const results = await Promise.all(item.items.map(x => this.processMetadata(x, reprocess, false)))
  701. counter += results.reduce((acc, n) => acc + n, 0)
  702. } else if (isTrack(item)) process: {
  703. if (!reprocess && this.getMetadataFor(item)) {
  704. break process
  705. }
  706. await this.throttleMetadata(async () => {
  707. const filePath = await this.download(item)
  708. const metadataReader = getMetadataReaderFor(filePath)
  709. const data = await metadataReader(filePath)
  710. this.metadataDictionary[item.downloaderArg] = filePath
  711. this.metadataDictionary[filePath] = data
  712. })
  713. this.emit('processMetadata progress', this.throttleMetadata.queue.length)
  714. counter++
  715. }
  716. if (top) {
  717. await this.saveMetadata()
  718. }
  719. return counter
  720. }
  721. getRecordFor(item) {
  722. return this.recordStore.getRecord(item)
  723. }
  724. getDuration(item) {
  725. let noticedMissingMetadata = false
  726. const durationFn = (acc, track) => {
  727. const metadata = this.getMetadataFor(track)
  728. if (!metadata) noticedMissingMetadata = true
  729. return acc + (metadata && metadata.duration) || 0
  730. }
  731. let items
  732. if (isGroup(item)) {
  733. items = flattenGrouplike(item).items
  734. } else {
  735. items = [item]
  736. }
  737. const tracks = items.filter(isTrack)
  738. const seconds = tracks.reduce(durationFn, 0)
  739. let { duration: string } = getTimeStringsFromSec(0, seconds)
  740. const approxSymbol = noticedMissingMetadata ? '+' : ''
  741. string += approxSymbol
  742. return {seconds, string, noticedMissingMetadata, approxSymbol}
  743. }
  744. setAlwaysStartPaused(value) {
  745. this.alwaysStartPaused = !!value
  746. for (const queuePlayer of this.queuePlayers) {
  747. queuePlayer.alwaysStartPaused = !!value
  748. }
  749. }
  750. setWaitWhenDonePlaying(value) {
  751. this.waitWhenDonePlaying = !!value
  752. for (const queuePlayer of this.queuePlayers) {
  753. queuePlayer.waitWhenDonePlaying = !!value
  754. }
  755. }
  756. async stopPlayingAll() {
  757. for (const queuePlayer of this.queuePlayers) {
  758. await queuePlayer.stopPlaying()
  759. }
  760. }
  761. async download(item) {
  762. return download(item, this.getRecordFor(item))
  763. }
  764. showLogMessage(messageInfo) {
  765. this.emit('log message', messageInfo)
  766. }
  767. setPartyNickname(nickname) {
  768. this.emit('set party nickname', nickname)
  769. }
  770. announceJoinParty() {
  771. this.emit('announce join party')
  772. }
  773. setHasAnnouncedJoin(hasAnnouncedJoin) {
  774. this.hasAnnouncedJoin = hasAnnouncedJoin
  775. }
  776. loadSharedSources(socketId, sharedSources) {
  777. if (socketId in this.sharedSourcesMap) {
  778. return
  779. }
  780. this.sharedSourcesMap[socketId] = sharedSources
  781. sharedSources[parentSymbol] = this.sharedSourcesGrouplike
  782. this.sharedSourcesGrouplike.items.push(sharedSources)
  783. this.emit('got shared sources', socketId, sharedSources)
  784. }
  785. sharedSourcesUpdated(socketId, sharedSources) {
  786. this.emit('shared sources updated', socketId, sharedSources)
  787. }
  788. shareWithParty(item) {
  789. this.emit('share with party', item)
  790. }
  791. }