« 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.js600
1 files changed, 341 insertions, 259 deletions
diff --git a/socket.js b/socket.js
index bc35c76..5c54bbc 100644
--- a/socket.js
+++ b/socket.js
@@ -17,39 +17,41 @@
 // library from there. This would be handy for people with a VPN with its own
 // hostname and firewall protections!
 
-'use strict' // single quotes & no semicolons time babey
+// single quotes & no semicolons time babey
 
-// This is expected to be the same across both the client and the server.
-// There will probably be inconsistencies between sender clients and receiving
-// clients / the server otherwise.
-const DEFAULT_NICKNAME = '(Unnamed)'
-
-const originalSymbol = Symbol('Original item')
+import EventEmitter from 'node:events'
+import net from 'node:net'
 
-const EventEmitter = require('events')
-const net = require('net')
-const shortid = require('shortid')
-
-const {
-  saveBackend,
-  restoreBackend,
-  saveItemReference,
-  restoreNewItem,
-  updateRestoredTracksUsingPlaylists
-} = require('./serialized-backend')
+import shortid from 'shortid'
 
-const {
+import {
   getTimeStringsFromSec,
-  silenceEvents
-} = require('./general-util')
+  parseWithoutPrototype,
+  silenceEvents,
+} from './general-util.js'
 
-const {
+import {
   parentSymbol,
   updateGroupFormat,
   updateTrackFormat,
   isTrack,
-  isGroup
-} = require('./playlist-utils')
+  isGroup,
+} from './playlist-utils.js'
+
+import {
+  restoreBackend,
+  restoreNewItem,
+  saveBackend,
+  saveItemReference,
+  updateRestoredTracksUsingPlaylists,
+} from './serialized-backend.js'
+
+// This is expected to be the same across both the client and the server.
+// There will probably be inconsistencies between sender clients and receiving
+// clients / the server otherwise.
+const DEFAULT_NICKNAME = '(Unnamed)'
+
+export const originalSymbol = Symbol('Original item')
 
 function serializePartySource(item) {
   // Turn an item into a sanitized, compact format for sharing with the server
@@ -67,7 +69,7 @@ function serializePartySource(item) {
   }
 }
 
-function deserializePartySource(source) {
+function deserializePartySource(source, parent = null) {
   // Reconstruct a party source into the ordinary group/track format.
 
   const recursive = source => {
@@ -81,9 +83,16 @@ function deserializePartySource(source) {
   }
 
   const top = recursive(source)
-  return (isGroup(top)
+
+  const item = (isGroup(top)
     ? updateGroupFormat(top)
     : updateTrackFormat(top))
+
+  if (parent) {
+    item[parentSymbol] = parent
+  }
+
+  return item
 }
 
 function serializeCommandToData(command) {
@@ -94,7 +103,11 @@ function serializeCommandToData(command) {
 function deserializeDataToCommand(data) {
   // Turn data received from a socket into a command that can be processed as
   // an action to apply to the mtui backend.
-  return JSON.parse(data)
+  return parseWithoutPrototype(data)
+}
+
+function namePartySources(nickname) {
+  return `Party Sources - ${nickname}`
 }
 
 function isItemRef(ref) {
@@ -144,26 +157,33 @@ function validateCommand(command) {
   switch (command.sender) {
     case 'server':
       switch (command.code) {
-        case 'initialize-backend':
-          return typeof command.backend === 'object'
-        case 'set-socket-id':
+        case 'initialize party':
+          return (
+            typeof command.backend === 'object' &&
+            typeof command.socketInfo === 'object' &&
+            Object.values(command.socketInfo).every(info => (
+              typeof info.nickname === 'string' &&
+              Array.isArray(info.sharedSources)
+            ))
+          )
+        case 'set socket id':
           return typeof command.socketId === 'string'
       }
       // No break here; servers can send commands which typically come from
       // clients too.
     case 'client':
       switch (command.code) {
-        case 'announce-join':
+        case 'announce join':
           return true
-        case 'clear-queue':
+        case 'clear queue':
           return typeof command.queuePlayer === 'string'
-        case 'clear-queue-past':
-        case 'clear-queue-up-to':
+        case 'clear queue past':
+        case 'clear queue up to':
           return (
             typeof command.queuePlayer === 'string' &&
             isItemRef(command.track)
           )
-        case 'distribute-queue':
+        case 'distribute queue':
           return (
             typeof command.queuePlayer === 'string' &&
             isItemRef(command.topItem) &&
@@ -199,26 +219,26 @@ function validateCommand(command) {
               )
             ))
           )
-        case 'restore-queue':
+        case 'restore queue':
           return (
             typeof command.queuePlayer === 'string' &&
             Array.isArray(command.tracks) &&
             command.tracks.every(track => isItemRef(track)) &&
             ['shuffle'].includes(command.why)
           )
-        case 'seek-to':
+        case 'seek to':
           return (
             typeof command.queuePlayer === 'string' &&
             typeof command.time === 'number'
           )
-        case 'set-nickname':
+        case 'set nickname':
           return (
             typeof command.nickname === 'string' &&
             typeof command.oldNickname === 'string' &&
             command.nickname.length >= 1 &&
             command.nickname.length <= 12
           )
-        case 'set-pause':
+        case 'set pause':
           return (
             typeof command.queuePlayer === 'string' &&
             typeof command.paused === 'boolean' &&
@@ -227,21 +247,25 @@ 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)
           )
         case 'status':
           return (
-            command.status === 'done-playing' ||
+            command.status === 'done playing' ||
             (
-              command.status === 'ready-to-resume' &&
+              command.status === 'ready to resume' &&
               typeof command.queuePlayer === 'string'
             ) ||
-            command.status === 'sync-playback'
+            command.status === 'sync playback'
           )
-        case 'stop-playing':
+        case 'stop playing':
           return typeof command.queuePlayer === 'string'
         case 'unqueue':
           return (
@@ -278,7 +302,7 @@ function perLine(handleLine) {
   }
 }
 
-function makeSocketServer() {
+export function makeSocketServer() {
   // The socket server has two functions: to maintain a "canonical" backend
   // and synchronize newly connected clients with the relevent data in this
   // backend, and to receive command data from clients and relay this to
@@ -290,23 +314,36 @@ function makeSocketServer() {
   const server = new net.Server()
   const socketMap = Object.create(null)
 
+  // Keeps track of details to share with newly joining sockets for
+  // synchronization.
+  const socketInfoMap = Object.create(null)
+
   server.canonicalBackend = null
 
   // <variable> -> queue player id -> array: socket
-  const readyToResume = {}
-  const donePlaying = {}
+  const readyToResume = Object.create(null)
+  const donePlaying = Object.create(null)
 
   server.on('connection', socket => {
     const socketId = shortid.generate()
 
-    socketMap[socketId] = socket
+    const socketInfo = {
+      hasAnnouncedJoin: false,
+      nickname: DEFAULT_NICKNAME,
+
+      // Unlike in client code, this isn't an array of actual playlist items;
+      // rather, it's the intermediary format used when transferring between
+      // client and server.
+      sharedSources: []
+    }
 
-    let hasAnnouncedJoin = false
-    let nickname = DEFAULT_NICKNAME
+    socketMap[socketId] = socket
+    socketInfoMap[socketId] = socketInfo
 
     socket.on('close', () => {
       if (socketId in socketMap) {
         delete socketMap[socketId]
+        delete socketInfoMap[socketId]
       }
     })
 
@@ -322,7 +359,7 @@ function makeSocketServer() {
 
       command.sender = 'client'
       command.senderSocketId = socketId
-      command.senderNickname = nickname
+      command.senderNickname = socketInfo.nickname
 
       if (!validateCommand(command)) {
         return
@@ -331,10 +368,10 @@ function makeSocketServer() {
       // If the socket hasn't announced its joining yet, it only has access to
       // a few commands.
 
-      if (!hasAnnouncedJoin) {
+      if (!socketInfo.hasAnnouncedJoin) {
         if (![
-          'announce-join',
-          'set-nickname'
+          'announce join',
+          'set nickname'
         ].includes(command.code)) {
           return
         }
@@ -345,7 +382,7 @@ function makeSocketServer() {
 
       if (command.code === 'status') {
         switch (command.status) {
-          case 'done-playing': {
+          case 'done playing': {
             const doneSockets = donePlaying[command.queuePlayer]
             if (doneSockets && !doneSockets.includes(socketId)) {
               doneSockets.push(socketId)
@@ -359,17 +396,15 @@ function makeSocketServer() {
             }
             break
           }
-          case 'ready-to-resume': {
+          case 'ready to resume': {
             const readySockets = readyToResume[command.queuePlayer]
             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',
-                    code: 'set-pause',
+                    code: 'set pause',
                     queuePlayer: command.queuePlayer,
                     startingTrack: true,
                     paused: false
@@ -381,18 +416,18 @@ function makeSocketServer() {
             }
             break
           }
-          case 'sync-playback':
+          case 'sync playback':
             for (const QP of server.canonicalBackend.queuePlayers) {
               if (QP.timeData) {
                 socket.write(serializeCommandToData({
                   sender: 'server',
-                  code: 'seek-to',
+                  code: 'seek to',
                   queuePlayer: QP.id,
                   time: QP.timeData.curSecTotal
                 }) + '\n')
                 socket.write(serializeCommandToData({
                   sender: 'server',
-                  code: 'set-pause',
+                  code: 'set pause',
                   queuePlayer: QP.id,
                   startingTrack: true,
                   paused: QP.player.isPaused
@@ -410,26 +445,34 @@ function makeSocketServer() {
         readyToResume[command.queuePlayer] = []
       }
 
-      // If it's a 'set-nickname' command, save the nickname.
+      // If it's a 'set nickname' command, save the nickname.
       // Also attach the old nickname for display in log messages.
 
-      if (command.code === 'set-nickname') {
-        command.oldNickname = nickname
-        command.senderNickname = nickname
-        nickname = command.nickname
+      if (command.code === 'set nickname') {
+        command.oldNickname = socketInfo.nickname
+        command.senderNickname = socketInfo.nickname
+        socketInfo.nickname = command.nickname
+      }
+
+      // If it's a 'share with party' command, keep track of the item being
+      // shared, so we can synchronize newly joining sockets with it.
+
+      if (command.code === 'share with party') {
+        const { sharedSources } = socketInfoMap[socketId]
+        sharedSources.push(command.item)
       }
 
-      // If it's an 'announce-join' command, mark the variable for this!
+      // If it's an 'announce join' command, mark the variable for this!
 
-      if (command.code === 'announce-join') {
-        hasAnnouncedJoin = true;
+      if (command.code === 'announce join') {
+        socketInfo.hasAnnouncedJoin = true;
       }
 
       // If the socket hasn't announced its joining yet, don't relay the
-      // command. (Since hasAnnouncedJoin gets set above, 'announce-join'
-      // will meet this condition.)
+      // command. (Since hasAnnouncedJoin gets set above, 'announce join'
+      // will pass this condition.)
 
-      if (!hasAnnouncedJoin) {
+      if (!socketInfo.hasAnnouncedJoin) {
         return
       }
 
@@ -452,21 +495,22 @@ function makeSocketServer() {
 
     socket.write(serializeCommandToData({
       sender: 'server',
-      code: 'set-socket-id',
+      code: 'set socket id',
       socketId
     }) + '\n')
 
     socket.write(serializeCommandToData({
       sender: 'server',
-      code: 'initialize-backend',
-      backend: savedBackend
+      code: 'initialize party',
+      backend: savedBackend,
+      socketInfo: socketInfoMap
     }) + '\n')
   })
 
   return server
 }
 
-function makeSocketClient() {
+export function makeSocketClient() {
   // The socket client connects to a server and sends/receives commands to/from
   // that server. This doesn't actually connect the socket to a port/host; that
   // is the caller's responsibility (use client.socket.connect()).
@@ -479,7 +523,7 @@ function makeSocketClient() {
   client.sendCommand = function(command) {
     const data = serializeCommandToData(command)
     client.socket.write(data + '\n')
-    client.emit('sent-command', command)
+    client.emit('sent command', command)
   }
 
   client.socket.on('data', perLine(line => {
@@ -503,22 +547,22 @@ function makeSocketClient() {
   return client
 }
 
-function attachBackendToSocketClient(backend, client) {
+export function attachBackendToSocketClient(backend, client) {
   // All actual logic for instances of the mtui backend interacting with each
   // other through commands lives here.
 
   let hasAnnouncedJoin = false
 
-  const partyGrouplike = {
-    name: `Party Sources - ${client.nickname}`,
+  const sharedSources = {
+    name: namePartySources(client.nickname),
     isPartySources: true,
     items: []
   }
 
-  const partyGrouplikeMap = Object.create(null)
+  const socketInfoMap = Object.create(null)
 
   const getPlaylistSources = () =>
-    partyGrouplike.items.map(item => item[originalSymbol])
+    sharedSources.items.map(item => item[originalSymbol])
 
   backend.setHasAnnouncedJoin(false)
   backend.setAlwaysStartPaused(true)
@@ -541,22 +585,22 @@ function attachBackendToSocketClient(backend, client) {
     let isVerbose = false
 
     switch (command.code) {
-      case 'announce-join':
+      case 'announce join':
         actionmsg = `joined the party`
         break
-      case 'clear-queue':
+      case 'clear queue':
         actionmsg = 'cleared the queue'
         break
-      case 'clear-queue-past':
+      case 'clear queue past':
         actionmsg = `cleared the queue past ${itemToMessage(command.track)}`
         break
-      case 'clear-queue-up-to':
+      case 'clear queue up to':
         actionmsg = `cleared the queue up to ${itemToMessage(command.track)}`
         break
-      case 'distribute-queue':
+      case 'distribute queue':
         actionmsg = `distributed ${itemToMessage(command.topItem)} across the queue ${command.opts.how}`
         break
-      case 'initialize-backend':
+      case 'initialize party':
         return
       case 'play':
         actionmsg = `started playing ${itemToMessage(command.track)}`
@@ -571,52 +615,55 @@ function attachBackendToSocketClient(backend, client) {
         actionmsg = `queued ${itemToMessage(command.topItem)}` + afterMessage
         break
       }
-      case 'restore-queue':
+      case 'restore queue':
         if (command.why === 'shuffle') {
           actionmsg = 'shuffled the queue'
         }
         break
-      case 'share-with-party':
+      case 'share with party':
         // TODO: This isn't an outrageously expensive operation, but it still
         // seems a little unnecessary to deserialize it here if we also do that
         // when actually processing the source?
         actionmsg = `shared ${itemToMessage(deserializePartySource(command.item))} with the party`
         break
-      case 'seek-to':
+      case 'seek to':
         // TODO: the second value here should be the duration of the track
         // (this will make values like 0:0x:yy / 1:xx:yy appear correctly)
         actionmsg = `seeked to ${getTimeStringsFromSec(command.time, command.time).timeDone}`
         mayCombine = true
         break
-      case 'set-nickname':
+      case 'set nickname':
         actionmsg = `updated their nickname (from ${nickToMessage(command.oldNickname)})`
         senderNickname = command.nickname
         break
-      case 'set-socket-id':
+      case 'set socket id':
         return
-      case 'set-pause':
+      case 'set pause':
         if (command.paused) {
           actionmsg = 'paused the player'
         } else {
           actionmsg = 'resumed the player'
         }
         break
-      case 'stop-playing':
+      case 'stop playing':
         actionmsg = 'stopped the player'
         break
       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) {
-          case 'ready-to-resume':
+          case 'ready to resume':
             actionmsg = `is ready to play!`
             break
-          case 'done-playing':
+          case 'done playing':
             actionmsg = `has finished playing`
             break
-          case 'sync-playback':
+          case 'sync playback':
             actionmsg = `synced playback with the server`
             break
           default:
@@ -635,7 +682,7 @@ function attachBackendToSocketClient(backend, client) {
     })
   }
 
-  client.on('sent-command', command => {
+  client.on('sent command', command => {
     command.senderNickname = client.nickname
     logCommand(command)
   })
@@ -645,18 +692,40 @@ function attachBackendToSocketClient(backend, client) {
     switch (command.sender) {
       case 'server':
         switch (command.code) {
-          case 'set-socket-id':
+          case 'set socket id':
             client.socketId = command.socketId
-            partyGrouplikeMap[command.socketId] = partyGrouplike
-            backend.loadPartyGrouplike(client.socketId, partyGrouplike)
+            socketInfoMap[command.socketId] = {
+              nickname: client.nickname,
+              sharedSources
+            }
+            backend.loadSharedSources(command.socketId, sharedSources)
             return
-          case 'initialize-backend':
+          case 'initialize party':
+            for (const [ socketId, info ] of Object.entries(command.socketInfo)) {
+              const nickname = info.nickname
+
+              const sharedSources = {
+                name: namePartySources(nickname),
+                isPartySources: true
+              }
+
+              sharedSources.items = info.sharedSources.map(
+                item => deserializePartySource(item, sharedSources))
+
+              socketInfoMap[socketId] = {
+                nickname,
+                sharedSources
+              }
+
+              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()
+            // backend.on('QP: 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.
@@ -667,31 +736,34 @@ function attachBackendToSocketClient(backend, client) {
         )
 
         switch (command.code) {
-          case 'announce-join': {
-            const partyGrouplike = {
-              name: `Party Sources - ${command.senderNickname}`,
+          case 'announce join': {
+            const sharedSources = {
+              name: namePartySources(command.senderNickname),
               isPartySources: true,
               items: []
             }
-            partyGrouplikeMap[command.senderSocketId] = partyGrouplike
-            backend.loadPartyGrouplike(command.senderSocketId, partyGrouplike)
+            socketInfoMap[command.senderSocketId] = {
+              nickname: command.senderNickname,
+              sharedSources
+            }
+            backend.loadSharedSources(command.senderSocketId, sharedSources)
             return
           }
-          case 'clear-queue':
-            if (QP) silenceEvents(QP, ['clear-queue'], () => QP.clearQueue())
+          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(
+          case 'clear queue past':
+            if (QP) silenceEvents(QP, ['clear queue past'], () => QP.clearQueuePast(
               restoreNewItem(command.track, getPlaylistSources())
             ))
             return
-          case 'clear-queue-up-to':
-            if (QP) silenceEvents(QP, ['clear-queue-up-to'], () => QP.clearQueueUpTo(
+          case 'clear queue up to':
+            if (QP) silenceEvents(QP, ['clear queue up to'], () => QP.clearQueueUpTo(
               restoreNewItem(command.track, getPlaylistSources())
             ))
             return
-          case 'distribute-queue':
-            if (QP) silenceEvents(QP, ['distribute-queue'], () => QP.distributeQueue(
+          case 'distribute queue':
+            if (QP) silenceEvents(QP, ['distribute queue'], () => QP.distributeQueue(
               restoreNewItem(command.topItem),
               {
                 how: command.opts.how,
@@ -704,13 +776,13 @@ function attachBackendToSocketClient(backend, client) {
               QP.once('received time data', data => {
                 client.sendCommand({
                   code: 'status',
-                  status: 'ready-to-resume',
+                  status: 'ready to resume',
                   queuePlayer: QP.id
                 })
               })
-              silenceEvents(QP, ['playing'], () => QP.play(
-                restoreNewItem(command.track, getPlaylistSources())
-              ))
+              silenceEvents(QP, ['playing'], () => {
+                QP.play(restoreNewItem(command.track, getPlaylistSources()))
+              })
             }
             return
           case 'queue':
@@ -722,25 +794,26 @@ function attachBackendToSocketClient(backend, client) {
               }
             ))
             return
-          case 'restore-queue':
+          case 'restore queue':
             if (QP) {
               QP.replaceAllItems(command.tracks.map(
                 refData => restoreNewItem(refData, getPlaylistSources())
               ))
             }
             return
-          case 'seek-to':
-            if (QP) silenceEvents(QP, ['seek-to'], () => QP.seekTo(command.time))
+          case 'seek to':
+            if (QP) silenceEvents(QP, ['seek to'], () => QP.seekTo(command.time))
             return
-          case 'set-nickname': {
-            const partyGrouplike = partyGrouplikeMap[command.senderSocketId]
-            if (partyGrouplike) {
-              partyGrouplike.name = `Party Sources - ${command.senderNickname}`
-              backend.partyGrouplikeUpdated(client.socketId, partyGrouplike)
-            }
+          case 'set nickname': {
+            const info = socketInfoMap[command.senderSocketId]
+            info.nickname = command.senderNickname
+            info.sharedSources.name = namePartySources(command.senderNickname)
+            backend.sharedSourcesUpdated(client.socketId, info.sharedSources)
             return
           }
-          case 'set-pause': {
+          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
@@ -749,20 +822,28 @@ function attachBackendToSocketClient(backend, client) {
             })
             setTimeout(() => {
               if (playingThisTrack) {
-                if (QP) silenceEvents(QP, ['set-pause'], () => QP.setPause(command.paused))
+                if (QP) silenceEvents(QP, ['set pause'], () => QP.setPause(command.paused))
               }
             }, command.startingTrack ? 500 : 0)
+            */
+            silenceEvents(QP, ['set pause'], () => QP.setPause(command.paused))
             return
           }
-          case 'share-with-party': {
-            const deserialized = deserializePartySource(command.item)
-            const partyGrouplike = partyGrouplikeMap[command.senderSocketId]
-            deserialized[parentSymbol] = partyGrouplike
-            partyGrouplike.items.push(deserialized)
-            backend.partyGrouplikeUpdated(command.senderSocketId, partyGrouplike)
+          case 'added queue player': {
+            silenceEvents(backend, ['added queue player'], () => {
+              const QP = backend.addQueuePlayer()
+              QP.id = command.id
+            })
+            return
+          }
+          case 'share with party': {
+            const { sharedSources } = socketInfoMap[command.senderSocketId]
+            const deserialized = deserializePartySource(command.item, sharedSources)
+            sharedSources.items.push(deserialized)
+            backend.sharedSourcesUpdated(command.senderSocketId, sharedSources)
             return
           }
-          case 'stop-playing':
+          case 'stop playing':
             if (QP) silenceEvents(QP, ['playing'], () => QP.stopPlaying())
             return
           case 'unqueue':
@@ -775,161 +856,162 @@ function attachBackendToSocketClient(backend, client) {
     }
   })
 
-  backend.on('clear-queue', queuePlayer => {
+  backend.on('announce join party', () => {
     client.sendCommand({
-      code: 'clear-queue',
-      queuePlayer: queuePlayer.id
+      code: 'announce join'
     })
   })
 
-  backend.on('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('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('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('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('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
       })
-    }
-  })
-
-  backend.on('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('seek-ahead', handleSeek)
-  backend.on('seek-back', handleSeek)
-  backend.on('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
-    partyGrouplike.name = `Party Sources - ${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('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('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('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 (partyGrouplike.items.every(x => x[originalSymbol] !== item)) {
-      const serialized = serializePartySource(item)
-      const deserialized = deserializePartySource(serialized)
-
-      deserialized[parentSymbol] = partyGrouplike
-      deserialized[originalSymbol] = item
-
-      partyGrouplike.items.push(deserialized)
-      backend.partyGrouplikeUpdated(client.socketId, partyGrouplike)
-
-      updateRestoredTracksUsingPlaylists(backend, getPlaylistSources())
 
+    backend.on('added queue player', (queuePlayer) => {
       client.sendCommand({
-        code: 'share-with-party',
-        item: serialized
+        code: 'added queue player',
+        id: queuePlayer.id,
       })
-    }
-  })
+    })
+  }
 }
 
-function attachSocketServerToBackend(server, backend) {
+export function attachSocketServerToBackend(server, backend) {
   // Unlike the function for attaching a backend to follow commands from a
   // client (attachBackendToSocketClient), this function is minimalistic.
   // It just sets the associated "canonical" backend. Actual logic for
   // de/serialization lives in serialized-backend.js.
   server.canonicalBackend = backend
 }
-
-Object.assign(module.exports, {
-  originalSymbol,
-  makeSocketServer,
-  makeSocketClient,
-  attachBackendToSocketClient,
-  attachSocketServerToBackend
-})