« get me outta code hell

basic command relay across socket clients - mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2020-07-10 20:52:13 -0300
committerFlorrie <towerofnix@gmail.com>2020-07-10 20:52:13 -0300
commitd00b26b23d9b3fc1e54a4d117366f0f22e664135 (patch)
tree7bc214a0ca10eec5ea6ae5d641481409aa00cc6d
parent503a37ba4d7550f9c2ed1602e589a0142a20d10d (diff)
basic command relay across socket clients
-rw-r--r--backend.js47
-rwxr-xr-xindex.js14
-rw-r--r--package-lock.json13
-rw-r--r--package.json1
-rw-r--r--players.js10
-rw-r--r--serialized-backend.js7
-rw-r--r--socket.js130
7 files changed, 193 insertions, 29 deletions
diff --git a/backend.js b/backend.js
index d2d0138..f3d8cfd 100644
--- a/backend.js
+++ b/backend.js
@@ -8,6 +8,7 @@ const { getMetadataReaderFor } = require('./metadata-readers')
 const { getPlayer } = require('./players')
 const RecordStore = require('./record-store')
 const os = require('os')
+const shortid = require('shortid')
 
 const {
   getTimeStringsFromSec,
@@ -62,6 +63,8 @@ class QueuePlayer extends EventEmitter {
   }) {
     super()
 
+    this.id = shortid.generate()
+
     this.player = null
     this.playingTrack = null
     this.queueGrouplike = {name: 'Queue', isTheQueue: true, items: []}
@@ -69,6 +72,7 @@ class QueuePlayer extends EventEmitter {
 
     this.playedTrackToEnd = false
     this.timeData = null
+    this.time = null
 
     this.getPlayer = getPlayer
     this.getRecordFor = getRecordFor
@@ -86,6 +90,7 @@ class QueuePlayer extends EventEmitter {
     this.player.on('printStatusLine', data => {
       if (this.playingTrack) {
         this.timeData = data
+        this.time = data.curSecTotal
         this.emit('received time data', data, this)
       }
     })
@@ -414,6 +419,7 @@ class QueuePlayer extends EventEmitter {
       }
 
       this.timeData = null
+      this.time = null
       this.playingTrack = item
       this.emit('playing', this.playingTrack, oldTrack, this)
 
@@ -504,6 +510,7 @@ class QueuePlayer extends EventEmitter {
       const oldTrack = this.playingTrack
       this.playingTrack = null
       this.timeData = null
+      this.time = null
       this.emit('playing', null, oldTrack, this)
     }
   }
@@ -513,51 +520,69 @@ class QueuePlayer extends EventEmitter {
   }
 
   seekAhead(seconds) {
+    this.time += seconds
     this.player.seekAhead(seconds)
+    this.emit('seek-ahead', +seconds)
   }
 
   seekBack(seconds) {
+    this.time -= seconds
     this.player.seekBack(seconds)
+    this.emit('seek-back', +seconds)
+  }
+
+  seekTo(timeInSecs) {
+    this.time = timeInSecs
+    this.player.seekTo(timeInSecs)
+    this.emit('seek-to', +timeInSecs)
   }
 
   togglePause() {
     this.player.togglePause()
+    this.emit('toggle-pause')
   }
 
   setPause(value) {
     this.player.setPause(value)
+    this.emit('set-pause', !!value)
   }
 
   toggleLoop() {
     this.player.toggleLoop()
+    this.emit('toggle-loop')
   }
 
   setLoop(value) {
     this.player.setLoop(value)
+    this.emit('set-loop', !!value)
   }
 
   volUp(amount = 10) {
     this.player.volUp(amount)
+    this.emit('vol-up', +amount)
   }
 
   volDown(amount = 10) {
     this.player.volDown(amount)
+    this.emit('vol-down', +amount)
   }
 
   setVolume(value) {
     this.player.setVolume(value)
+    this.emit('set-volume', +value)
   }
 
   setVolumeMultiplier(value) {
-    this.player.setVolumeMultiplier(value);
+    this.player.setVolumeMultiplier(value)
   }
 
   fadeIn() {
-    return this.player.fadeIn();
+    return this.player.fadeIn()
   }
 
   setPauseNextTrack(value) {
     this.pauseNextTrack = !!value
+    this.emit('set-pause-next-track', !!value)
   }
 
   get remainingTracks() {
@@ -632,6 +657,24 @@ class Backend extends EventEmitter {
     this.queuePlayers.push(queuePlayer)
     this.emit('added queue player', queuePlayer)
 
+    for (const event of [
+      'playing',
+      'seek-ahead',
+      'seek-back',
+      'toggle-pause',
+      'set-pause',
+      'toggle-loop',
+      'set-loop',
+      'vol-up',
+      'vol-down',
+      'set-volume',
+      'set-pause-next-track'
+    ]) {
+      queuePlayer.on(event, (...data) => {
+        this.emit(event, queuePlayer, ...data)
+      })
+    }
+
     return queuePlayer
   }
 
diff --git a/index.js b/index.js
index b0db6cd..03b5bb1 100755
--- a/index.js
+++ b/index.js
@@ -75,7 +75,7 @@ async function main() {
     'player-options': {type: 'series'},
     'stress-test': {type: 'flag'},
     'socket-client': {type: 'value'},
-    'socket-server': {type: 'flag'},
+    'socket-server': {type: 'value'},
     'telnet-server': {type: 'flag'},
     [parseOptions.handleDashless](option) {
       playlistSources.push(option)
@@ -147,20 +147,26 @@ async function main() {
     appElement.attachAsServerHost(telnetServer)
   }
 
+  let socketClient
   let socketServer
   if (options['socket-server']) {
     socketServer = makeSocketServer()
     attachSocketServerToBackend(socketServer, backend)
-    socketServer.listen(1255)
+    socketServer.listen(options['socket-server'])
+
+    socketClient = makeSocketClient()
+    socketClient.socket.connect(options['socket-server'])
   }
 
-  let socketClient
   if (options['socket-client']) {
     socketClient = makeSocketClient()
+    socketClient.socket.connect(options['socket-client'])
+  }
+
+  if (socketClient) {
     attachBackendToSocketClient(backend, socketClient, {
       getPlaylistSources: () => appElement.playlistSources
     })
-    socketClient.socket.connect(1255)
   }
 
   if (options['stress-test']) {
diff --git a/package-lock.json b/package-lock.json
index a4ec0d1..f984dce 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -55,6 +55,11 @@
         "minimist": "^1.2.5"
       }
     },
+    "nanoid": {
+      "version": "2.1.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz",
+      "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA=="
+    },
     "node-fetch": {
       "version": "2.6.0",
       "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
@@ -82,6 +87,14 @@
         "truncate-utf8-bytes": "^1.0.0"
       }
     },
+    "shortid": {
+      "version": "2.2.15",
+      "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.15.tgz",
+      "integrity": "sha512-5EaCy2mx2Jgc/Fdn9uuDuNIIfWBpzY4XIlhoqtXF6qsf+/+SGZ+FxDdX/ZsMZiWupIWNqAEmiNY4RC+LSmCeOw==",
+      "requires": {
+        "nanoid": "^2.1.0"
+      }
+    },
     "temp-dir": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz",
diff --git a/package.json b/package.json
index 7e33ce0..094f101 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
     "node-natural-sort": "^0.8.7",
     "open": "^7.0.3",
     "sanitize-filename": "^1.6.3",
+    "shortid": "^2.2.15",
     "tempy": "^0.2.1",
     "tui-lib": "^0.2.1",
     "tui-text-editor": "^0.3.1",
diff --git a/players.js b/players.js
index 6624f0f..2056ed7 100644
--- a/players.js
+++ b/players.js
@@ -224,13 +224,15 @@ module.exports.ControllableMPVPlayer = class extends module.exports.MPVPlayer {
   }
 
   setPause(val) {
-    this.isPaused = !!val
-    this.sendCommand('set', 'pause', this.isPaused)
+    if (!!val !== this.isPaused) {
+      this.togglePause()
+    }
   }
 
   setLoop(val) {
-    this.isLooping = !!val
-    this.sendCommand('set', 'loop', this.isLooping)
+    if (!!val !== this.isLooping) {
+      this.toggleLoop()
+    }
   }
 
   async kill() {
diff --git a/serialized-backend.js b/serialized-backend.js
index 13bb2b9..041f668 100644
--- a/serialized-backend.js
+++ b/serialized-backend.js
@@ -30,9 +30,9 @@ const {
 const referenceDataSymbol = Symbol('Restored reference data')
 
 function getPlayerInfo(queuePlayer) {
-  const { player, timeData } = queuePlayer
+  const { player } = queuePlayer
   return {
-    time: timeData && timeData.curSecTotal,
+    time: queuePlayer.time,
     isLooping: player.isLooping,
     isPaused: player.isPaused,
     volume: player.volume
@@ -56,6 +56,7 @@ function saveBackend(backend) {
 
   return {
     queuePlayers: backend.queuePlayers.map(QP => ({
+      id: QP.id,
       playingTrack: referenceTrack(QP.playingTrack),
       queuedTracks: QP.queueGrouplike.items.map(referenceTrack),
       pauseNextTrack: QP.pauseNextTrack,
@@ -74,6 +75,8 @@ async function restoreBackend(backend, data) {
       const QP = await backend.addQueuePlayer()
       QP[referenceDataSymbol] = qpData
 
+      QP.id = qpData.id
+
       QP.queueGrouplike.items = qpData.queuedTracks.map(refData => ({
         [referenceDataSymbol]: refData,
         name: refData.name,
diff --git a/socket.js b/socket.js
index 5bfe80a..ae8ef87 100644
--- a/socket.js
+++ b/socket.js
@@ -48,8 +48,23 @@ function validateCommand(command) {
         case 'initialize-backend':
           return typeof command.backend === 'object'
       }
-      break
+      // No break here; servers can send commands which typically come from
+      // clients too.
     case 'client':
+      switch (command.code) {
+        case 'seek-to':
+          return (
+            typeof command.queuePlayer === 'string' &&
+            typeof command.time === 'number'
+          )
+        case 'set-pause':
+          return (
+            typeof command.queuePlayer === 'string' &&
+            typeof command.paused === 'boolean'
+          )
+        case 'status':
+          return typeof command.status === 'string'
+      }
       break
   }
 
@@ -70,7 +85,7 @@ function makeSocketServer() {
 
   server.canonicalBackend = null
 
-  function receivedData(data) {
+  function receivedData(socket, data) {
     // Parse data as a command and validate it. If invalid, drop this data.
 
     let command
@@ -80,14 +95,45 @@ function makeSocketServer() {
       return
     }
 
+    command.sender = 'client'
+
     if (!validateCommand(command)) {
       return
     }
 
+    // If it's a status command, respond appropriately, and return so that it
+    // is not relayed.
+
+    if (command.code === 'status') {
+      switch (command.status) {
+        case 'sync-playback':
+          for (const QP of server.canonicalBackend.queuePlayers) {
+            if (QP.timeData) {
+              socket.write(JSON.stringify({
+                sender: 'server',
+                code: 'seek-to',
+                queuePlayer: QP.id,
+                time: QP.timeData.curSecTotal
+              }) + '\n')
+              socket.write(JSON.stringify({
+                sender: 'server',
+                code: 'set-pause',
+                queuePlayer: QP.id,
+                paused: false
+              }))
+            }
+          }
+
+          break
+      }
+
+      return
+    }
+
     // Relay the data to client sockets.
 
     for (const socket of sockets) {
-      socket.write(command)
+      socket.write(JSON.stringify(command))
     }
   }
 
@@ -100,12 +146,20 @@ function makeSocketServer() {
       }
     })
 
-    socket.on('data', receivedData)
+    socket.on('data', data => receivedData(socket, data))
+
+    const savedBackend = saveBackend(server.canonicalBackend)
+
+    for (const qpData of savedBackend.queuePlayers) {
+      if (qpData.playerInfo) {
+        qpData.playerInfo.isPaused = true
+      }
+    }
 
     socket.write(JSON.stringify({
       sender: 'server',
       code: 'initialize-backend',
-      backend: saveBackend(server.canonicalBackend)
+      backend: savedBackend
     }))
   })
 
@@ -129,18 +183,20 @@ function makeSocketClient() {
     // Same sort of "guarding" deserialization/validation as in the server
     // code, because it's possible the client and server backends mismatch.
 
-    let command
-    try {
-      command = deserializeDataToCommand(data)
-    } catch (error) {
-      return
-    }
+    for (const line of data.toString().split('\n')) {
+      let command
+      try {
+        command = deserializeDataToCommand(line)
+      } catch (error) {
+        return
+      }
 
-    if (!validateCommand(command)) {
-      return
-    }
+      if (!validateCommand(command)) {
+        return
+      }
 
-    client.emit('command', command)
+      client.emit('command', command)
+    }
   })
 
   return client
@@ -160,11 +216,51 @@ function attachBackendToSocketClient(backend, client, {
             await restoreBackend(backend, command.backend)
             // TODO: does this need to be called here?
             updateRestoredTracksUsingPlaylists(backend, getPlaylistSources())
-            break
+            backend.on('playing', QP => {
+              QP.once('received time data', () => {
+                client.sendCommand({code: 'status', status: 'sync-playback'})
+              })
+            })
+            return
+        }
+        // Again, no pause. Client commands can come from the server.
+      case 'client': {
+        let QP = (
+          command.queuePlayer &&
+          backend.queuePlayers.find(QP => QP.id === command.queuePlayer)
+        )
+
+        switch (command.code) {
+          case 'seek-to':
+            if (QP) QP.seekTo(command.time)
+            return
+          case 'set-pause':
+            if (QP) QP.setPause(command.paused)
+            return
         }
-        break
+      }
     }
   })
+
+  backend.on('toggle-pause', queuePlayer => {
+    client.sendCommand({
+      code: 'set-pause',
+      queuePlayer: queuePlayer.id,
+      paused: queuePlayer.player.isPaused
+    })
+  })
+
+  function handleSeek(queuePlayer) {
+    client.sendCommand({
+      code: 'seek-to',
+      queuePlayer: queuePlayer.id,
+      time: queuePlayer.time
+    })
+  }
+
+  backend.on('seek-ahead', handleSeek)
+  backend.on('seek-back', handleSeek)
+  backend.on('seek-to', handleSeek)
 }
 
 function attachSocketServerToBackend(server, backend) {