« 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/socket.js
diff options
context:
space:
mode:
Diffstat (limited to 'socket.js')
-rw-r--r--socket.js206
1 files changed, 198 insertions, 8 deletions
diff --git a/socket.js b/socket.js
index ae8ef87..a972f0c 100644
--- a/socket.js
+++ b/socket.js
@@ -16,9 +16,15 @@ const net = require('net')
 const {
   saveBackend,
   restoreBackend,
+  saveItemReference,
+  restoreNewItem,
   updateRestoredTracksUsingPlaylists
 } = require('./serialized-backend')
 
+const {
+  silenceEvents
+} = require('./general-util')
+
 function serializeCommandToData(command) {
   // Turn a command into a string/buffer that can be sent over a socket.
   return JSON.stringify(command)
@@ -30,6 +36,38 @@ function deserializeDataToCommand(data) {
   return JSON.parse(data)
 }
 
+function isItemRef(ref) {
+  if (ref === null || typeof ref !== 'object') {
+    return false
+  }
+
+  // List of true/false/null. False means *invalid* reference data; null
+  // means *nonpresent* reference data. True means present and valid.
+  const conditionChecks = [
+    'name' in ref ? typeof ref.name === 'string' : null,
+    'path' in ref ? Array.isArray(ref.path) && ref.path.every(n => typeof n === 'string') : null,
+    'downloaderArg' in ref ? (
+      !('items' in ref) &&
+      typeof ref.downloaderArg === 'string'
+    ) : null,
+    'items' in ref ? (
+      !('downloaderArg' in ref) &&
+      Array.isArray(ref.items) &&
+      ref.items.every(isItemRef)
+    ) : null
+  ]
+
+  if (conditionChecks.includes(false)) {
+    return false
+  }
+
+  if (!conditionChecks.includes(true)) {
+    return false
+  }
+
+  return true
+}
+
 function validateCommand(command) {
   // TODO: Could be used to validate "against" a backend, but for now it just
   // checks data types.
@@ -52,6 +90,51 @@ function validateCommand(command) {
       // clients too.
     case 'client':
       switch (command.code) {
+        case 'clear-queue':
+          return typeof command.queuePlayer === 'string'
+        case 'clear-queue-past':
+        case 'clear-queue-up-to':
+          return (
+            typeof command.queuePlayer === 'string' &&
+            isItemRef(command.topItem)
+          )
+        case 'distribute-queue':
+          return (
+            typeof command.queuePlayer === 'string' &&
+            isItemRef(command.topItem) &&
+            (!command.opts || typeof command.opts === 'object' && (
+              (
+                !command.opts.how ||
+                ['evenly', 'randomly'].includes(command.opts.how)
+              ) &&
+              (
+                !command.opts.rangeEnd ||
+                ['end-of-queue'].includes(command.opts.rangeEnd) ||
+                typeof command.opts.rangeEnd === 'number'
+              )
+            ))
+          )
+        case 'queue':
+          return (
+            typeof command.queuePlayer === 'string' &&
+            isItemRef(command.topItem) &&
+            (
+              isItemRef(command.afterItem) ||
+              [null, 'FRONT'].includes(command.afterItem)
+            ) &&
+            (!command.opts || typeof command.opts === 'object' && (
+              (
+                !command.opts.movePlayingTrack ||
+                typeof command.opts.movePlayingTrack === 'boolean'
+              )
+            ))
+          )
+        case 'restore-queue':
+          return (
+            typeof command.queuePlayer === 'string' &&
+            Array.isArray(command.tracks) &&
+            command.tracks.every(track => isItemRef(track))
+          )
         case 'seek-to':
           return (
             typeof command.queuePlayer === 'string' &&
@@ -64,6 +147,11 @@ function validateCommand(command) {
           )
         case 'status':
           return typeof command.status === 'string'
+        case 'unqueue':
+          return (
+            typeof command.queuePlayer === 'string' &&
+            isItemRef(command.topItem)
+          )
       }
       break
   }
@@ -119,7 +207,7 @@ function makeSocketServer() {
                 sender: 'server',
                 code: 'set-pause',
                 queuePlayer: QP.id,
-                paused: false
+                paused: QP.player.isPaused
               }))
             }
           }
@@ -130,9 +218,11 @@ function makeSocketServer() {
       return
     }
 
-    // Relay the data to client sockets.
+    // Relay the command to client sockets besides the sender.
+
+    const otherSockets = sockets.filter(s => s !== socket)
 
-    for (const socket of sockets) {
+    for (const socket of otherSockets) {
       socket.write(JSON.stringify(command))
     }
   }
@@ -231,22 +321,98 @@ function attachBackendToSocketClient(backend, client, {
         )
 
         switch (command.code) {
+          case 'clear-queue':
+            if (QP) silenceEvents(QP, ['clear-queue'], () => QP.clearQueue())
+            return
+          case 'clear-queue-past':
+            if (QP) silenceEvents(QP, ['clear-queue-past'], () => QP.clearQueuePast(
+              restoreNewItem(command.topItem, getPlaylistSources())
+            ))
+            return
+          case 'clear-queue-up-to':
+            if (QP) silenceEvents(QP, ['clear-queue-up-to'], () => QP.clearQueueUpTo(
+              restoreNewItem(command.topItem, getPlaylistSources())
+            ))
+            return
+          case 'distribute-queue':
+            if (QP) silenceEvents(QP, ['distribute-queue'], () => QP.distributeQueue(
+              restoreNewItem(command.topItem, getPlaylistSources()),
+              {
+                how: command.opts.how,
+                rangeEnd: command.opts.rangeEnd
+              }
+            ))
+            return
+          case 'queue':
+            if (QP) silenceEvents(QP, ['queue'], () => QP.queue(
+              restoreNewItem(command.topItem, getPlaylistSources()),
+              isItemRef(command.afterItem) ? restoreNewItem(command.afterItem, getPlaylistSources()) : command.afterItem,
+              {
+                movePlayingTrack: command.opts.movePlayingTrack
+              }
+            ))
+            return
+          case 'restore-queue':
+            if (QP) {
+              QP.queueGrouplike.items = command.tracks.map(refData => restoreNewItem(refData))
+              // TODO: target just the one queue player. hacks = illegal
+              updateRestoredTracksUsingPlaylists(backend, getPlaylistSources())
+            }
           case 'seek-to':
-            if (QP) QP.seekTo(command.time)
+            if (QP) silenceEvents(QP, ['seek-to'], () => QP.seekTo(command.time))
             return
           case 'set-pause':
-            if (QP) QP.setPause(command.paused)
+            if (QP) silenceEvents(QP, ['set-pause'], () => QP.setPause(command.paused))
+            return
+          case 'unqueue':
+            if (QP) silenceEvents(QP, ['unqueue'], () => QP.unqueue(
+              restoreNewItem(command.topItem, getPlaylistSources())
+            ))
             return
         }
       }
     }
   })
 
-  backend.on('toggle-pause', queuePlayer => {
+  backend.on('clear-queue', queuePlayer => {
     client.sendCommand({
-      code: 'set-pause',
+      code: 'clear-queue',
+      queuePlayer: queuePlayer.id
+    })
+  })
+
+  backend.on('clear-queue-past', (queuePlayer, topItem) => {
+    client.sendCommand({
+      code: 'clear-queue-past',
       queuePlayer: queuePlayer.id,
-      paused: queuePlayer.player.isPaused
+      topItem: saveItemReference(topItem)
+    })
+  })
+
+  backend.on('clear-queue-up-to', (queuePlayer, topItem) => {
+    client.sendCommand({
+      code: 'clear-queue-up-to',
+      queuePlayer: queuePlayer.id,
+      topItem: saveItemReference(topItem)
+    })
+  })
+
+  backend.on('distribute-queue', (queuePlayer, topItem, opts) => {
+    client.sendCommand({
+      code: 'distribute-queue',
+      queuePlayer: queuePlayer.id,
+      topItem: saveItemReference(topItem),
+      opts
+    })
+  })
+
+  backend.on('queue', (queuePlayer, topItem, afterItem, opts) => {
+    client.sendCommand({
+      code: 'queue',
+      queuePlayer: queuePlayer.id,
+      topItem: saveItemReference(topItem),
+      afterItem: saveItemReference(afterItem),
+      opts
     })
   })
 
@@ -261,6 +427,30 @@ function attachBackendToSocketClient(backend, client, {
   backend.on('seek-ahead', handleSeek)
   backend.on('seek-back', handleSeek)
   backend.on('seek-to', handleSeek)
+
+  backend.on('shuffle-queue', queuePlayer => {
+    client.sendCommand({
+      code: 'restore-queue',
+      queuePlayer: queuePlayer.id,
+      tracks: queuePlayer.queueGrouplike.items.map(saveItemReference)
+    })
+  })
+
+  backend.on('toggle-pause', queuePlayer => {
+    client.sendCommand({
+      code: 'set-pause',
+      queuePlayer: queuePlayer.id,
+      paused: queuePlayer.player.isPaused
+    })
+  })
+
+  backend.on('unqueue', (queuePlayer, topItem) => {
+    client.sendCommand({
+      code: 'unqueue',
+      queuePlayer: queuePlayer.id,
+      topItem: saveItemReference(topItem)
+    })
+  })
 }
 
 function attachSocketServerToBackend(server, backend) {