« get me outta code hell

WIP socket shenanigans - mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-05-15 16:06:04 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-05-15 16:06:04 -0300
commitfdc5961a4b8423ffe5e6e4939f9f709f8e7e34c9 (patch)
treed43503ad016eed18319e5ebe2a95fbf8b26fdea5
parent7f2461a60fba35013551fdb27ba0bb8d0720021d (diff)
WIP socket shenanigans
this commit is mostly trash lol
-rw-r--r--backend.js34
-rwxr-xr-xindex.js88
-rw-r--r--serialized-backend.js2
-rw-r--r--socket.js249
-rw-r--r--todo.txt12
5 files changed, 228 insertions, 157 deletions
diff --git a/backend.js b/backend.js
index 232d912..c04f4a9 100644
--- a/backend.js
+++ b/backend.js
@@ -11,7 +11,7 @@ import shortid from 'shortid'
 
 import {getDownloaderFor} from './downloaders.js'
 import {getMetadataReaderFor} from './metadata-readers.js'
-import {getPlayer} from './players.js'
+import {getPlayer, GhostPlayer} from './players.js'
 import RecordStore from './record-store.js'
 
 import {
@@ -381,12 +381,6 @@ class QueuePlayer extends EventEmitter {
       throw new Error('Attempted to play before a player was loaded')
     }
 
-    let playingThisTrack = true
-    this.emit('playing new track')
-    this.once('playing new track', () => {
-      playingThisTrack = false
-    })
-
     // If it's a group, play the first track.
     if (isGroup(item)) {
       item = flattenGrouplike(item).items[0]
@@ -402,13 +396,18 @@ class QueuePlayer extends EventEmitter {
       return
     }
 
-    playTrack: {
+    let playingThisTrack = true
+    this.emit('playing new track')
+    this.once('playing new track', () => {
+      playingThisTrack = false
+    })
+
+    if (this.player instanceof GhostPlayer) {
+      await this.#ghostPlay(item, startTime)
+    } else if (!item.downloaderArg) {
       // No downloader argument? That's no good - stop here.
       // TODO: An error icon on this item, or something???
-      if (!item.downloaderArg) {
-        break playTrack
-      }
-
+    } else {
       // If, by the time the track is downloaded, we're playing something
       // different from when the download started, assume that we just want to
       // keep listening to whatever new thing we started.
@@ -452,6 +451,17 @@ class QueuePlayer extends EventEmitter {
     }
   }
 
+  async #ghostPlay(item, startTime) {
+    // If we're playing off a GhostPlayer, strip down the whole process.
+    // Downloading is totally unnecessary, for example.
+
+    this.timeData = null
+    this.time = null
+    this.playingTrack = item
+    this.emit('playing', this.playingTrack)
+    await this.player.playFile('-', startTime)
+  }
+
   playNext(track, automaticallyQueueNextTrack = false) {
     if (!track) return false
 
diff --git a/index.js b/index.js
index fea9222..6aad592 100755
--- a/index.js
+++ b/index.js
@@ -100,33 +100,68 @@ async function main() {
     process.exit(1)
   }
 
-  const backend = new Backend({
-    playerName: options['player'],
-    playerOptions: options['player-options']
-  })
+  const backendConfig =
+    (options['socket-server']
+      ? {
+          playerName: 'ghost',
+        }
+      : {
+          playerName: options['player'],
+          playerOptions: options['player-options'],
+        })
+
+  const appConfig =
+    (options['socket-server']
+      ? {
+          showPartyControls: true,
+          canControlPlayback: false,
+          canControlQueue: false,
+          canControlQueuePlayers: false,
+          canProcessMetadata: false,
+        }
+   : options['socket-client']
+      ? {
+          showPartyControls: true,
+        }
+      : {})
 
-  const result = await backend.setup()
-  if (result.error) {
-    console.error(result.error)
+  const backend = new Backend(backendConfig)
+
+  const setupResult = await backend.setup()
+  if (setupResult.error) {
+    console.error(setupResult.error)
     process.exit(1)
   }
 
-  backend.on('playing', track => {
-    if (track) {
-      writeFile(backend.rootDirectory + '/current-track.txt',
-        getItemPathString(track))
-      writeFile(backend.rootDirectory + '/current-track.json',
-        JSON.stringify(track, null, 2))
-    }
-  })
+  if (options['socket-server']) {
+    const socketServer = makeSocketServer()
+    attachSocketServerToBackend(socketServer, backend)
+    socketServer.listen(options['socket-server'])
+
+    const socketClient = makeSocketClient()
+    attachBackendToSocketClient(backend, socketClient)
+    socketClient.socket.connect(options['socket-server'])
+
+    backend.setPartyNickname('Internal Client')
+    backend.announceJoinParty()
+  }
+
+  if (!options['socket-server']) {
+    backend.on('playing', track => {
+      if (track) {
+        writeFile(backend.rootDirectory + '/current-track.txt',
+          getItemPathString(track))
+        writeFile(backend.rootDirectory + '/current-track.json',
+          JSON.stringify(track, null, 2))
+      }
+    })
+  }
 
   const { appElement, dirtyTerminal, flushable, root } = await setupClient({
     backend,
     screenInterface: new CommandLineInterface(),
     writable: process.stdout,
-    appConfig: {
-      showPartyControls: !!(options['socket-server'] || options['socket-client'])
-    },
+    appConfig,
   })
 
   appElement.on('quitRequested', () => {
@@ -148,7 +183,7 @@ async function main() {
     root.renderNow()
   })
 
-  if (playlistSources.length === 0) {
+  if (!options['socket-server'] && playlistSources.length === 0) {
     if (jsonConfig.defaultPlaylists) {
       playlistSources.push(...jsonConfig.defaultPlaylists)
     } else {
@@ -177,26 +212,13 @@ async function main() {
     appElement.attachAsServerHost(telnetServer)
   }
 
-  let socketClient
-  let socketServer
-  if (options['socket-server']) {
-    socketServer = makeSocketServer()
-    attachSocketServerToBackend(socketServer, backend)
-    socketServer.listen(options['socket-server'])
-
-    socketClient = makeSocketClient()
-    socketClient.socket.connect(options['socket-server'])
-  }
-
   if (options['socket-client']) {
-    socketClient = makeSocketClient()
+    const socketClient = makeSocketClient()
     const [ p1, p2 ] = options['socket-client'].split(':')
     const host = p2 && p1
     const port = p2 ? p2 : p1
     socketClient.socket.connect(port, host)
-  }
 
-  if (socketClient) {
     attachBackendToSocketClient(backend, socketClient)
 
     let nickname = process.env.USER
diff --git a/serialized-backend.js b/serialized-backend.js
index bdac54c..4b3f845 100644
--- a/serialized-backend.js
+++ b/serialized-backend.js
@@ -56,6 +56,8 @@ export function saveBackend(backend) {
 }
 
 export async function restoreBackend(backend, data) {
+  // console.log('restoring backend:', data)
+
   if (data.queuePlayers) {
     if (data.queuePlayers.length === 0) {
       return
diff --git a/socket.js b/socket.js
index 59f70d9..5c54bbc 100644
--- a/socket.js
+++ b/socket.js
@@ -247,6 +247,10 @@ function validateCommand(command) {
               command.sender === 'server'
             ) || !command.startingTrack
           )
+        case 'added queue player':
+          return (
+            typeof command.id === 'string'
+          )
         case 'share with party':
           return (
             typeof command.item === 'string' ||
@@ -397,8 +401,6 @@ export function makeSocketServer() {
             if (readySockets && !readySockets.includes(socketId)) {
               readySockets.push(socketId)
               if (readySockets.length === Object.keys(socketMap).length) {
-                // const QP = server.canonicalBackend.queuePlayers.find(QP => QP.id === command.queuePlayer)
-                // silenceEvents(QP, ['set pause'], () => QP.setPause(false))
                 for (const socket of Object.values(socketMap)) {
                   socket.write(serializeCommandToData({
                     sender: 'server',
@@ -649,6 +651,9 @@ export function attachBackendToSocketClient(backend, client) {
       case 'unqueue':
         actionmsg = `removed ${itemToMessage(command.topItem)} from the queue`
         break
+      case 'added queue player':
+        actionmsg = `created a new playback queue`
+        break
       case 'status':
         isVerbose = true
         switch (command.status) {
@@ -715,6 +720,7 @@ export function attachBackendToSocketClient(backend, client) {
               backend.loadSharedSources(socketId, sharedSources)
             }
             await restoreBackend(backend, command.backend)
+            attachPlaybackBackendListeners()
             // backend.on('QP: playing', QP => {
             //   QP.once('received time data', () => {
             //     client.sendCommand({code: 'status', status: 'sync playback'})
@@ -774,9 +780,9 @@ export function attachBackendToSocketClient(backend, client) {
                   queuePlayer: QP.id
                 })
               })
-              silenceEvents(QP, ['playing'], () => QP.play(
-                restoreNewItem(command.track, getPlaylistSources())
-              ))
+              silenceEvents(QP, ['playing'], () => {
+                QP.play(restoreNewItem(command.track, getPlaylistSources()))
+              })
             }
             return
           case 'queue':
@@ -806,6 +812,8 @@ export function attachBackendToSocketClient(backend, client) {
             return
           }
           case 'set pause': {
+            // All this code looks very scary???
+            /*
             // TODO: there's an event leak here when toggling pause while
             // nothing is playing
             let playingThisTrack = true
@@ -817,6 +825,15 @@ export function attachBackendToSocketClient(backend, client) {
                 if (QP) silenceEvents(QP, ['set pause'], () => QP.setPause(command.paused))
               }
             }, command.startingTrack ? 500 : 0)
+            */
+            silenceEvents(QP, ['set pause'], () => QP.setPause(command.paused))
+            return
+          }
+          case 'added queue player': {
+            silenceEvents(backend, ['added queue player'], () => {
+              const QP = backend.addQueuePlayer()
+              QP.id = command.id
+            })
             return
           }
           case 'share with party': {
@@ -839,148 +856,156 @@ export function attachBackendToSocketClient(backend, client) {
     }
   })
 
-  backend.on('QP: clear queue', queuePlayer => {
+  backend.on('announce join party', () => {
     client.sendCommand({
-      code: 'clear queue',
-      queuePlayer: queuePlayer.id
+      code: 'announce join'
     })
   })
 
-  backend.on('QP: clear queue past', (queuePlayer, track) => {
-    client.sendCommand({
-      code: 'clear queue past',
-      queuePlayer: queuePlayer.id,
-      track: saveItemReference(track)
-    })
+  backend.on('share with party', item => {
+    if (sharedSources.items.every(x => x[originalSymbol] !== item)) {
+      const serialized = serializePartySource(item)
+      const deserialized = deserializePartySource(serialized)
+
+      deserialized[parentSymbol] = sharedSources
+      deserialized[originalSymbol] = item
+
+      sharedSources.items.push(deserialized)
+      backend.sharedSourcesUpdated(client.socketId, sharedSources)
+
+      updateRestoredTracksUsingPlaylists(backend, getPlaylistSources())
+
+      client.sendCommand({
+        code: 'share with party',
+        item: serialized
+      })
+    }
   })
 
-  backend.on('QP: clear queue up to', (queuePlayer, track) => {
-    client.sendCommand({
-      code: 'clear queue up to',
-      queuePlayer: queuePlayer.id,
-      track: saveItemReference(track)
-    })
+  backend.on('set party nickname', nickname => {
+    let oldNickname = client.nickname
+    sharedSources.name = namePartySources(nickname)
+    client.nickname = nickname
+    client.sendCommand({code: 'set nickname', nickname, oldNickname})
   })
 
-  backend.on('QP: distribute queue', (queuePlayer, topItem, opts) => {
-    client.sendCommand({
-      code: 'distribute queue',
-      queuePlayer: queuePlayer.id,
-      topItem: saveItemReference(topItem),
-      opts
+  function attachPlaybackBackendListeners() {
+    backend.on('QP: clear queue', queuePlayer => {
+      client.sendCommand({
+        code: 'clear queue',
+        queuePlayer: queuePlayer.id
+      })
     })
-  })
 
-  backend.on('QP: done playing', queuePlayer => {
-    client.sendCommand({
-      code: 'status',
-      status: 'done playing',
-      queuePlayer: queuePlayer.id
+    backend.on('QP: clear queue past', (queuePlayer, track) => {
+      client.sendCommand({
+        code: 'clear queue past',
+        queuePlayer: queuePlayer.id,
+        track: saveItemReference(track)
+      })
     })
-  })
 
-  backend.on('QP: playing', (queuePlayer, track) => {
-    if (track) {
+    backend.on('QP: clear queue up to', (queuePlayer, track) => {
       client.sendCommand({
-        code: 'play',
+        code: 'clear queue up to',
         queuePlayer: queuePlayer.id,
         track: saveItemReference(track)
       })
-      queuePlayer.once('received time data', data => {
-        client.sendCommand({
-          code: 'status',
-          status: 'ready to resume',
-          queuePlayer: queuePlayer.id
-        })
+    })
+
+    backend.on('QP: distribute queue', (queuePlayer, topItem, opts) => {
+      client.sendCommand({
+        code: 'distribute queue',
+        queuePlayer: queuePlayer.id,
+        topItem: saveItemReference(topItem),
+        opts
       })
-    } else {
+    })
+
+    backend.on('QP: done playing', queuePlayer => {
       client.sendCommand({
-        code: 'stop playing',
+        code: 'status',
+        status: 'done playing',
         queuePlayer: queuePlayer.id
       })
-    }
-  })
-
-  let n = 0
-  backend.on('QP: queue', (queuePlayer, topItem, afterItem, opts) => {
-    client.sendCommand({
-      code: 'queue',
-      queuePlayer: queuePlayer.id,
-      topItem: saveItemReference(topItem),
-      afterItem: saveItemReference(afterItem),
-      opts
     })
-  })
 
-  function handleSeek(queuePlayer) {
-    client.sendCommand({
-      code: 'seek to',
-      queuePlayer: queuePlayer.id,
-      time: queuePlayer.time
+    backend.on('QP: playing', (queuePlayer, track) => {
+      if (track) {
+        client.sendCommand({
+          code: 'play',
+          queuePlayer: queuePlayer.id,
+          track: saveItemReference(track)
+        })
+        queuePlayer.once('received time data', data => {
+          client.sendCommand({
+            code: 'status',
+            status: 'ready to resume',
+            queuePlayer: queuePlayer.id
+          })
+        })
+      } else {
+        client.sendCommand({
+          code: 'stop playing',
+          queuePlayer: queuePlayer.id
+        })
+      }
     })
-  }
 
-  backend.on('QP: seek ahead', handleSeek)
-  backend.on('QP: seek back', handleSeek)
-  backend.on('QP: seek to', handleSeek)
+    backend.on('QP: queue', (queuePlayer, topItem, afterItem, opts) => {
+      client.sendCommand({
+        code: 'queue',
+        queuePlayer: queuePlayer.id,
+        topItem: saveItemReference(topItem),
+        afterItem: saveItemReference(afterItem),
+        opts
+      })
+    })
 
-  backend.on('set party nickname', nickname => {
-    let oldNickname = client.nickname
-    sharedSources.name = namePartySources(nickname)
-    client.nickname = nickname
-    client.sendCommand({code: 'set nickname', nickname, oldNickname})
-  })
+    function handleSeek(queuePlayer) {
+      client.sendCommand({
+        code: 'seek to',
+        queuePlayer: queuePlayer.id,
+        time: queuePlayer.time
+      })
+    }
 
-  backend.on('QP: shuffle queue', queuePlayer => {
-    client.sendCommand({
-      code: 'restore queue',
-      why: 'shuffle',
-      queuePlayer: queuePlayer.id,
-      tracks: queuePlayer.queueGrouplike.items.map(saveItemReference)
-    })
-  })
+    backend.on('QP: seek ahead', handleSeek)
+    backend.on('QP: seek back', handleSeek)
+    backend.on('QP: seek to', handleSeek)
 
-  backend.on('QP: toggle pause', queuePlayer => {
-    client.sendCommand({
-      code: 'set pause',
-      queuePlayer: queuePlayer.id,
-      paused: queuePlayer.player.isPaused
+    backend.on('QP: shuffle queue', queuePlayer => {
+      client.sendCommand({
+        code: 'restore queue',
+        why: 'shuffle',
+        queuePlayer: queuePlayer.id,
+        tracks: queuePlayer.queueGrouplike.items.map(saveItemReference)
+      })
     })
-  })
 
-  backend.on('QP: unqueue', (queuePlayer, topItem) => {
-    client.sendCommand({
-      code: 'unqueue',
-      queuePlayer: queuePlayer.id,
-      topItem: saveItemReference(topItem)
+    backend.on('QP: toggle pause', queuePlayer => {
+      client.sendCommand({
+        code: 'set pause',
+        queuePlayer: queuePlayer.id,
+        paused: queuePlayer.player.isPaused
+      })
     })
-  })
 
-  backend.on('announce join party', () => {
-    client.sendCommand({
-      code: 'announce join'
+    backend.on('QP: unqueue', (queuePlayer, topItem) => {
+      client.sendCommand({
+        code: 'unqueue',
+        queuePlayer: queuePlayer.id,
+        topItem: saveItemReference(topItem)
+      })
     })
-  })
-
-  backend.on('share with party', item => {
-    if (sharedSources.items.every(x => x[originalSymbol] !== item)) {
-      const serialized = serializePartySource(item)
-      const deserialized = deserializePartySource(serialized)
-
-      deserialized[parentSymbol] = sharedSources
-      deserialized[originalSymbol] = item
-
-      sharedSources.items.push(deserialized)
-      backend.sharedSourcesUpdated(client.socketId, sharedSources)
-
-      updateRestoredTracksUsingPlaylists(backend, getPlaylistSources())
 
+    backend.on('added queue player', (queuePlayer) => {
       client.sendCommand({
-        code: 'share with party',
-        item: serialized
+        code: 'added queue player',
+        id: queuePlayer.id,
       })
-    }
-  })
+    })
+  }
 }
 
 export function attachSocketServerToBackend(server, backend) {
diff --git a/todo.txt b/todo.txt
index 59a9f87..63ba2e2 100644
--- a/todo.txt
+++ b/todo.txt
@@ -753,3 +753,15 @@ TODO: There should be a way for the server to handle disputes between two
       send out events to start the next track, and in reaction only to the
       clients' own "done playing" events (or the GHOST PLAYER reaching the
       playback time provided when the track was first shared).
+
+TODO: Implement a waaaay better socat system, particularly one which waits for
+      feedback when a command is sent and returns that. This has to be special-
+      coded for mpv since there isn't a generalized standard, so it should make
+      use of the existing Socat class, not replace it outright.
+
+TODO: Use above socat system to keep "pinging" the socket until a response is
+      received - mpv doesn't make the socket immediately available. I think if
+      we wait for a pong response before allowing any actual commands to go
+      through, we can avoid weirdness with commands being dropped beacuse they
+      were sent too early. For now we just use a time-based delay on the base
+      Socat class, which is a hack.