« get me outta code hell

WIP - support multiple players at once - 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:
authorFlorrie <towerofnix@gmail.com>2019-09-20 16:54:13 -0300
committerFlorrie <towerofnix@gmail.com>2019-09-20 16:54:13 -0300
commit6bad90e8e0db9c9273de984be53a1ca61b4d8a24 (patch)
tree45e5f2a74a4fe9f6d939590a5d547be939071596 /backend.js
parenta5d3b710eb46e58708b8dbb51f5231ba534561fd (diff)
WIP - support multiple players at once
Currently bug-free and doesn't change anything about existing mtui
behavior! Meta N to create a new player, meta up/down to switch between
which one you're interacting with. Each player has its own queue.
Eventually (soon(TM)) there'll be much better UI to go with all this!
Diffstat (limited to 'backend.js')
-rw-r--r--backend.js321
1 files changed, 188 insertions, 133 deletions
diff --git a/backend.js b/backend.js
index 57910e9..e8601c5 100644
--- a/backend.js
+++ b/backend.js
@@ -28,24 +28,22 @@ const fs = require('fs')
 const writeFile = promisify(fs.writeFile)
 const readFile = promisify(fs.readFile)
 
-class Backend extends EventEmitter {
-  constructor() {
+class QueuePlayer extends EventEmitter {
+  constructor({
+    getRecordFor
+  }) {
     super()
 
     this.player = null
     this.playingTrack = null
-    this.recordStore = new RecordStore()
-    this.throttleMetadata = throttlePromise(10)
-    this.metadataDictionary = {}
     this.queueGrouplike = {name: 'Queue', isTheQueue: true, items: []}
     this.pauseNextTrack = false
     this.playedTrackToEnd = false
+    this.timeData = null
 
-    this.rootDirectory = process.env.HOME + '/.mtui'
-    this.metadataPath = this.rootDirectory + '/track-metadata.json'
+    this.getRecordFor = getRecordFor
   }
 
-
   async setup() {
     this.player = await getPlayer()
 
@@ -55,79 +53,16 @@ class Backend extends EventEmitter {
       }
     }
 
-    await this.loadMetadata()
-
     this.player.on('printStatusLine', data => {
       if (this.playingTrack) {
-        this.emit('printStatusLine', data)
+        this.timeData = data
+        this.emit('received time data', data, this)
       }
     })
 
     return true
   }
 
-
-  async readMetadata() {
-    try {
-      return JSON.parse(await readFile(this.metadataPath))
-    } catch (error) {
-      // Just stop. It's okay to fail to load metadata.
-      return null
-    }
-  }
-
-  async loadMetadata() {
-    Object.assign(this.metadataDictionary, await this.readMetadata())
-  }
-
-  async saveMetadata() {
-    const newData = Object.assign({}, await this.readMetadata(), this.metadataDictionary)
-    await writeFile(this.metadataPath, JSON.stringify(newData))
-  }
-
-  getMetadataFor(item) {
-    const key = this.metadataDictionary[item.downloaderArg]
-    return this.metadataDictionary[key] || null
-  }
-
-  async processMetadata(item, reprocess = false, top = true) {
-    let counter = 0
-
-    if (isGroup(item)) {
-      const results = await Promise.all(item.items.map(x => this.processMetadata(x, reprocess, false)))
-      counter += results.reduce((acc, n) => acc + n, 0)
-    } else process: {
-      if (!reprocess && this.getMetadataFor(item)) {
-        break process
-      }
-
-      await this.throttleMetadata(async () => {
-        const filePath = await this.download(item)
-        const metadataReader = getMetadataReaderFor(filePath)
-        const data = await metadataReader(filePath)
-
-        this.metadataDictionary[item.downloaderArg] = filePath
-        this.metadataDictionary[filePath] = data
-      })
-
-      this.emit('processMetadata progress', this.throttleMetadata.queue.length)
-
-      counter++
-    }
-
-    if (top) {
-      await this.saveMetadata()
-    }
-
-    return counter
-  }
-
-
-  getRecordFor(item) {
-    return this.recordStore.getRecord(item)
-  }
-
-
   queue(topItem, afterItem = null, {movePlayingTrack = true} = {}) {
     const { items } = this.queueGrouplike
     const newTrackIndex = items.length
@@ -389,49 +324,9 @@ class Backend extends EventEmitter {
     this.emit('queue updated')
   }
 
-  seekAhead(seconds) {
-    this.player.seekAhead(seconds)
-  }
-
-  seekBack(seconds) {
-    this.player.seekBack(seconds)
-  }
-
-  togglePause() {
-    this.player.togglePause()
-  }
-
-  setPause(value) {
-    this.player.setPause(value)
-  }
-
-  toggleLoop() {
-    this.player.toggleLoop()
-  }
-
-  setLoop(value) {
-    this.player.setLoop(value)
-  }
-
-  volUp(amount = 10) {
-    this.player.volUp(amount)
-  }
-
-  volDown(amount = 10) {
-    this.player.volDown(amount)
-  }
-
-  setVolume(value) {
-    this.player.setVolume(value)
-  }
-
-  setPauseNextTrack(value) {
-    this.pauseNextTrack = !!value
-  }
-
   async stopPlaying() {
-    // We emit this so playTrack doesn't immediately start a new track.
-    // We aren't *actually* about to play a new track.
+    // We emit this so the active play() call doesn't immediately start a new
+    // track. We aren't *actually* about to play a new track.
     this.emit('playing new track')
     await this.player.kill()
     this.clearPlayingTrack()
@@ -559,10 +454,185 @@ class Backend extends EventEmitter {
     if (this.playingTrack !== null) {
       const oldTrack = this.playingTrack
       this.playingTrack = null
+      this.timeData = null
       this.emit('playing', null, oldTrack)
     }
   }
 
+  async download(item) {
+    if (isGroup(item)) {
+      // TODO: Download all children (recursively), show a confirmation prompt
+      // if there are a lot of items (remember to flatten).
+      return
+    }
+
+    // Don't start downloading an item if we're already downloading it!
+    if (this.getRecordFor(item).downloading) {
+      return
+    }
+
+    const arg = item.downloaderArg
+    this.getRecordFor(item).downloading = true
+    try {
+      return await getDownloaderFor(arg)(arg)
+    } finally {
+      this.getRecordFor(item).downloading = false
+    }
+  }
+
+  seekAhead(seconds) {
+    this.player.seekAhead(seconds)
+  }
+
+  seekBack(seconds) {
+    this.player.seekBack(seconds)
+  }
+
+  togglePause() {
+    this.player.togglePause()
+  }
+
+  setPause(value) {
+    this.player.setPause(value)
+  }
+
+  toggleLoop() {
+    this.player.toggleLoop()
+  }
+
+  setLoop(value) {
+    this.player.setLoop(value)
+  }
+
+  volUp(amount = 10) {
+    this.player.volUp(amount)
+  }
+
+  volDown(amount = 10) {
+    this.player.volDown(amount)
+  }
+
+  setVolume(value) {
+    this.player.setVolume(value)
+  }
+
+  setPauseNextTrack(value) {
+    this.pauseNextTrack = !!value
+  }
+
+  get playSymbol() {
+    if (this.player && this.playingTrack) {
+      if (this.player.isPaused) {
+        return '⏸'
+      } else {
+        return '▶'
+      }
+    } else {
+      return '.'
+    }
+  }
+}
+
+class Backend extends EventEmitter {
+  constructor() {
+    super()
+
+    this.queuePlayers = []
+
+    this.recordStore = new RecordStore()
+    this.throttleMetadata = throttlePromise(10)
+    this.metadataDictionary = {}
+
+    this.rootDirectory = process.env.HOME + '/.mtui'
+    this.metadataPath = this.rootDirectory + '/track-metadata.json'
+  }
+
+  async setup() {
+    const error = await this.addQueuePlayer()
+    if (error.error) {
+      return error
+    }
+
+    await this.loadMetadata()
+
+    return true
+  }
+
+  async addQueuePlayer() {
+    const queuePlayer = new QueuePlayer({
+      getRecordFor: item => this.getRecordFor(item)
+    })
+
+    const error = await queuePlayer.setup()
+    if (error.error) {
+      return error
+    }
+
+    this.queuePlayers.push(queuePlayer)
+    this.emit('added queue player', queuePlayer)
+
+    return queuePlayer
+  }
+
+  async readMetadata() {
+    try {
+      return JSON.parse(await readFile(this.metadataPath))
+    } catch (error) {
+      // Just stop. It's okay to fail to load metadata.
+      return null
+    }
+  }
+
+  async loadMetadata() {
+    Object.assign(this.metadataDictionary, await this.readMetadata())
+  }
+
+  async saveMetadata() {
+    const newData = Object.assign({}, await this.readMetadata(), this.metadataDictionary)
+    await writeFile(this.metadataPath, JSON.stringify(newData))
+  }
+
+  getMetadataFor(item) {
+    const key = this.metadataDictionary[item.downloaderArg]
+    return this.metadataDictionary[key] || null
+  }
+
+  async processMetadata(item, reprocess = false, top = true) {
+    let counter = 0
+
+    if (isGroup(item)) {
+      const results = await Promise.all(item.items.map(x => this.processMetadata(x, reprocess, false)))
+      counter += results.reduce((acc, n) => acc + n, 0)
+    } else process: {
+      if (!reprocess && this.getMetadataFor(item)) {
+        break process
+      }
+
+      await this.throttleMetadata(async () => {
+        const filePath = await this.download(item)
+        const metadataReader = getMetadataReaderFor(filePath)
+        const data = await metadataReader(filePath)
+
+        this.metadataDictionary[item.downloaderArg] = filePath
+        this.metadataDictionary[filePath] = data
+      })
+
+      this.emit('processMetadata progress', this.throttleMetadata.queue.length)
+
+      counter++
+    }
+
+    if (top) {
+      await this.saveMetadata()
+    }
+
+    return counter
+  }
+
+  getRecordFor(item) {
+    return this.recordStore.getRecord(item)
+  }
+
   getDuration(item) {
     let noticedMissingMetadata = false
 
@@ -588,24 +658,9 @@ class Backend extends EventEmitter {
     return {seconds, string, noticedMissingMetadata, approxSymbol}
   }
 
-  async download(item) {
-    if (isGroup(item)) {
-      // TODO: Download all children (recursively), show a confirmation prompt
-      // if there are a lot of items (remember to flatten).
-      return
-    }
-
-    // Don't start downloading an item if we're already downloading it!
-    if (this.getRecordFor(item).downloading) {
-      return
-    }
-
-    const arg = item.downloaderArg
-    this.getRecordFor(item).downloading = true
-    try {
-      return await getDownloaderFor(arg)(arg)
-    } finally {
-      this.getRecordFor(item).downloading = false
+  async stopPlayingAll() {
+    for (const queuePlayer of this.queuePlayers) {
+      await queuePlayer.stopPlaying()
     }
   }
 }