« 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.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()
     }
   }
 }