« 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>2024-05-16 22:29:28 -0300
commitbb96788a3ab48776229985ca0a02f9c5c124b65a (patch)
tree7fab8749efb6f00f9448319c5011dd0183f930cc
parent9a1d0c26338a7d0851c60b99296669cdc80673f2 (diff)
WIP socket shenanigans [!!!]
this commit is mostly trash lol

[!!!] Editor's note: Okay, after rebasing this commit is *super*
spooky! It's interacting with a bunch of stuff that was previously
handled in a merge commit and the revised changes may or may not
be totally broken. If in doubt, assume this commit is the root
of all evil (probably).
-rw-r--r--backend.js34
-rwxr-xr-xindex.js88
-rw-r--r--serialized-backend.js2
-rw-r--r--socket.js166
-rw-r--r--todo.txt12
5 files changed, 246 insertions, 56 deletions
diff --git a/backend.js b/backend.js
index 93de487..ffa620a 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 8888d48..1f25b0f 100755
--- a/index.js
+++ b/index.js
@@ -104,33 +104,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', () => {
@@ -152,7 +187,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 {
@@ -181,26 +216,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 7ae5e9d..575ba8f 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 c91a1af..525be82 100644
--- a/socket.js
+++ b/socket.js
@@ -239,7 +239,11 @@ function validateCommand(command) {
               command.sender === 'server'
             ) || !command.startingTrack
           )
-        case 'share-with-party':
+        case 'added queue player':
+          return (
+            typeof command.id === 'string'
+          )
+        case 'share with party':
           return (
             typeof command.item === 'string' ||
             Array.isArray(command.item)
@@ -389,8 +393,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',
@@ -641,6 +643,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) {
@@ -707,11 +712,14 @@ export function attachBackendToSocketClient(backend, client) {
               backend.loadSharedSources(socketId, sharedSources)
             }
             await restoreBackend(backend, command.backend)
-            backend.on('playing', QP => {
-              QP.once('received time data', () => {
-                client.sendCommand({code: 'status', status: 'sync-playback'})
-              })
-            })
+            attachPlaybackBackendListeners()
+            // Commented out as part of a merge commit catching up
+            // socket-mtui with main. Spooky! //
+            // backend.on('playing', QP => {
+            //   QP.once('received time data', () => {
+            //     client.sendCommand({code: 'status', status: 'sync-playback'})
+            //   })
+            // })
             return
         }
         // Again, no break. Client commands can come from the server.
@@ -766,9 +774,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':
@@ -798,6 +806,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
@@ -809,6 +819,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': {
@@ -972,6 +991,131 @@ export function attachBackendToSocketClient(backend, client) {
       })
     }
   })
+
+  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 attachPlaybackBackendListeners() {
+    backend.on('QP: clear queue', queuePlayer => {
+      client.sendCommand({
+        code: 'clear queue',
+        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: clear queue up to', (queuePlayer, track) => {
+      client.sendCommand({
+        code: 'clear queue up to',
+        queuePlayer: queuePlayer.id,
+        track: saveItemReference(track)
+      })
+    })
+
+    backend.on('QP: distribute queue', (queuePlayer, topItem, opts) => {
+      client.sendCommand({
+        code: 'distribute queue',
+        queuePlayer: queuePlayer.id,
+        topItem: saveItemReference(topItem),
+        opts
+      })
+    })
+
+    backend.on('QP: done playing', queuePlayer => {
+      client.sendCommand({
+        code: 'status',
+        status: 'done playing',
+        queuePlayer: queuePlayer.id
+      })
+    })
+
+    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: 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: seek ahead', handleSeek)
+    backend.on('QP: seek back', handleSeek)
+    backend.on('QP: seek to', handleSeek)
+
+    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: toggle pause', queuePlayer => {
+      client.sendCommand({
+        code: 'set pause',
+        queuePlayer: queuePlayer.id,
+        paused: queuePlayer.player.isPaused
+      })
+    })
+
+    backend.on('QP: unqueue', (queuePlayer, topItem) => {
+      client.sendCommand({
+        code: 'unqueue',
+        queuePlayer: queuePlayer.id,
+        topItem: saveItemReference(topItem)
+      })
+    })
+
+    backend.on('added queue player', (queuePlayer) => {
+      client.sendCommand({
+        code: 'added queue player',
+        id: queuePlayer.id,
+      })
+    })
+  }
 }
 
 export function attachSocketServerToBackend(server, backend) {
diff --git a/todo.txt b/todo.txt
index e718ce0..253ea8c 100644
--- a/todo.txt
+++ b/todo.txt
@@ -754,6 +754,18 @@ TODO: There should be a way for the server to handle disputes between two
       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.
+
 TODO: When you're navigating down (or up) a menu, if that menu's got a
       scrollbar *and* is divided into sections, passing a divider line should
       try to scroll the whole newly active section into view! This way you get