1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018 |
- // Tools for hosting an MTUI party over a socket server. Comparable in idea to
- // telnet.js, but for interfacing over commands rather than hosting all client
- // UIs on one server. The intent of the code in this file is to allow clients
- // to connect and interface with each other, while still running all processes
- // involved in mtui on their own machines -- so mtui will download and play
- // music using each connected machine's own internet connection and speakers.
- // TODO: Option to display listing items which aren't available on all
- // connected devices.
- //
- // TODO: While having a canonical backend is useful for maintaining a baseline
- // playback position and queue/library with which to sync clients, it probably
- // shouldn't be necessary to have an actual JS reference to that backend.
- // Making communication with the canonical backend work over socket (in as much
- // as possible the same way we do current socket communication) means the
- // server can be run on a remote host without requiring access to the music
- // library from there. This would be handy for people with a VPN with its own
- // hostname and firewall protections!
- // single quotes & no semicolons time babey
- import EventEmitter from 'node:events'
- import net from 'node:net'
- import shortid from 'shortid'
- import {
- getTimeStringsFromSec,
- parseWithoutPrototype,
- silenceEvents,
- } from './general-util.js'
- import {
- parentSymbol,
- updateGroupFormat,
- updateTrackFormat,
- isTrack,
- 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
- // and other sockets in the party.
- //
- // TODO: We'll probably need to assign a unique ID to the root item, since
- // otherwise we don't have a way to target it to un-share it.
- if (isGroup(item)) {
- return [item.name, ...item.items.map(serializePartySource).filter(Boolean)]
- } else if (isTrack(item)) {
- return item.name
- } else {
- return null
- }
- }
- function deserializePartySource(source, parent = null) {
- // Reconstruct a party source into the ordinary group/track format.
- const recursive = source => {
- if (Array.isArray(source)) {
- return {name: source[0], items: source.slice(1).map(recursive).filter(Boolean)}
- } else if (typeof source === 'string') {
- return {name: source, downloaderArg: '-'}
- } else {
- return null
- }
- }
- const top = recursive(source)
- const item = (isGroup(top)
- ? updateGroupFormat(top)
- : updateTrackFormat(top))
- if (parent) {
- item[parentSymbol] = parent
- }
- return item
- }
- function serializeCommandToData(command) {
- // Turn a command into a string/buffer that can be sent over a socket.
- return JSON.stringify(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 parseWithoutPrototype(data)
- }
- function namePartySources(nickname) {
- return `Party Sources - ${nickname}`
- }
- 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.
- if (typeof command !== 'object') {
- return false
- }
- if (!['server', 'client'].includes(command.sender)) {
- return false
- }
- switch (command.sender) {
- case 'server':
- switch (command.code) {
- 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':
- return true
- case 'clear queue':
- return typeof command.queuePlayer === 'string'
- case 'clear queue past':
- case 'clear queue up to':
- return (
- typeof command.queuePlayer === 'string' &&
- isItemRef(command.track)
- )
- 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 'play':
- return (
- typeof command.queuePlayer === 'string' &&
- isItemRef(command.track)
- )
- 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)) &&
- ['shuffle'].includes(command.why)
- )
- case 'seek to':
- return (
- typeof command.queuePlayer === 'string' &&
- typeof command.time === 'number'
- )
- case 'set nickname':
- return (
- typeof command.nickname === 'string' &&
- typeof command.oldNickname === 'string' &&
- command.nickname.length >= 1 &&
- command.nickname.length <= 12
- )
- case 'set pause':
- return (
- typeof command.queuePlayer === 'string' &&
- typeof command.paused === 'boolean' &&
- (
- typeof command.startingTrack === 'boolean' &&
- command.sender === 'server'
- ) || !command.startingTrack
- )
- 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 === 'ready to resume' &&
- typeof command.queuePlayer === 'string'
- ) ||
- command.status === 'sync playback'
- )
- case 'stop playing':
- return typeof command.queuePlayer === 'string'
- case 'unqueue':
- return (
- typeof command.queuePlayer === 'string' &&
- isItemRef(command.topItem)
- )
- }
- break
- }
- return false
- }
- function perLine(handleLine) {
- // Wrapper function to run a callback for each line provided to the wrapped
- // callback. Maintains a "partial" variable so that a line may be broken up
- // into multiple chunks before it is sent. Also supports handling multiple
- // lines (including the conclusion to a previously received partial line)
- // being received at once.
- let partial = ''
- return data => {
- const text = data.toString()
- const lines = text.split('\n')
- if (lines.length === 1) {
- partial += text
- } else {
- handleLine(partial + lines[0])
- for (const line of lines.slice(1, -1)) {
- handleLine(line)
- }
- partial = lines[lines.length - 1]
- }
- }
- }
- 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
- // other clients.
- //
- // makeSocketServer doesn't actually start the server listening on a port;
- // that's the responsibility of the caller (use server.listen()).
- 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 = Object.create(null)
- const donePlaying = Object.create(null)
- server.on('connection', socket => {
- const socketId = shortid.generate()
- 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: []
- }
- socketMap[socketId] = socket
- socketInfoMap[socketId] = socketInfo
- socket.on('close', () => {
- if (socketId in socketMap) {
- delete socketMap[socketId]
- delete socketInfoMap[socketId]
- }
- })
- socket.on('data', perLine(line => {
- // Parse data as a command and validate it. If invalid, drop this data.
- let command
- try {
- command = deserializeDataToCommand(line)
- } catch (error) {
- return
- }
- command.sender = 'client'
- command.senderSocketId = socketId
- command.senderNickname = socketInfo.nickname
- if (!validateCommand(command)) {
- return
- }
- // If the socket hasn't announced its joining yet, it only has access to
- // a few commands.
- if (!socketInfo.hasAnnouncedJoin) {
- if (![
- 'announce join',
- 'set nickname'
- ].includes(command.code)) {
- 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 'done playing': {
- const doneSockets = donePlaying[command.queuePlayer]
- if (doneSockets && !doneSockets.includes(socketId)) {
- doneSockets.push(socketId)
- if (doneSockets.length === Object.keys(socketMap).length) {
- // determine next track
- for (const socket of Object.values(socketMap)) {
- // play next track
- }
- delete donePlaying[command.queuePlayer]
- }
- }
- break
- }
- case 'ready to resume': {
- const readySockets = readyToResume[command.queuePlayer]
- if (readySockets && !readySockets.includes(socketId)) {
- readySockets.push(socketId)
- if (readySockets.length === Object.keys(socketMap).length) {
- for (const socket of Object.values(socketMap)) {
- socket.write(serializeCommandToData({
- sender: 'server',
- code: 'set pause',
- queuePlayer: command.queuePlayer,
- startingTrack: true,
- paused: false
- }) + '\n')
- donePlaying[command.queuePlayer] = []
- }
- delete readyToResume[command.queuePlayer]
- }
- }
- break
- }
- case 'sync playback':
- for (const QP of server.canonicalBackend.queuePlayers) {
- if (QP.timeData) {
- socket.write(serializeCommandToData({
- sender: 'server',
- code: 'seek to',
- queuePlayer: QP.id,
- time: QP.timeData.curSecTotal
- }) + '\n')
- socket.write(serializeCommandToData({
- sender: 'server',
- code: 'set pause',
- queuePlayer: QP.id,
- startingTrack: true,
- paused: QP.player.isPaused
- }) + '\n')
- }
- }
- break
- }
- return
- }
- // If it's a 'play' command, set up a new readyToResume array.
- if (command.code === 'play') {
- readyToResume[command.queuePlayer] = []
- }
- // 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 = 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 (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 pass this condition.)
- if (!socketInfo.hasAnnouncedJoin) {
- return
- }
- // Relay the command to client sockets besides the sender.
- const otherSockets = Object.values(socketMap).filter(s => s !== socket)
- for (const socket of otherSockets) {
- socket.write(serializeCommandToData(command) + '\n')
- }
- }))
- const savedBackend = saveBackend(server.canonicalBackend)
- for (const qpData of savedBackend.queuePlayers) {
- if (qpData.playerInfo) {
- qpData.playerInfo.isPaused = true
- }
- }
- socket.write(serializeCommandToData({
- sender: 'server',
- code: 'set socket id',
- socketId
- }) + '\n')
- socket.write(serializeCommandToData({
- sender: 'server',
- code: 'initialize party',
- backend: savedBackend,
- socketInfo: socketInfoMap
- }) + '\n')
- })
- return server
- }
- 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()).
- const client = new EventEmitter()
- client.socket = new net.Socket()
- client.nickname = DEFAULT_NICKNAME
- client.socketId = null // Will be received from server.
- client.sendCommand = function(command) {
- const data = serializeCommandToData(command)
- client.socket.write(data + '\n')
- client.emit('sent command', command)
- }
- client.socket.on('data', perLine(line => {
- // 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(line)
- } catch (error) {
- return
- }
- if (!validateCommand(command)) {
- return
- }
- client.emit('command', command)
- }))
- return 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 sharedSources = {
- name: namePartySources(client.nickname),
- isPartySources: true,
- items: []
- }
- const socketInfoMap = Object.create(null)
- const getPlaylistSources = () =>
- sharedSources.items.map(item => item[originalSymbol])
- backend.setHasAnnouncedJoin(false)
- backend.setAlwaysStartPaused(true)
- backend.setWaitWhenDonePlaying(true)
- function logCommand(command) {
- const nickToMessage = nickname => `\x1b[32;1m${nickname}\x1b[0m`
- const itemToMessage = item => `\x1b[32m"${item.name}"\x1b[0m`
- let senderNickname = command.sender === 'server' ? 'the server' : command.senderNickname
- // TODO: This should use a unique sender ID, provided by the server and
- // corresponding to the socket. This could be implemented into the UI!
- // But also, right now users can totally pretend to be the server by...
- // setting their nickname to "the server", which is silly.
- const sender = senderNickname
- let actionmsg = `sent ${command.code} (no action message specified)`
- let code = command.code
- let mayCombine = false
- let isVerbose = false
- switch (command.code) {
- case 'announce join':
- actionmsg = `joined the party`
- break
- case 'clear queue':
- actionmsg = 'cleared the queue'
- break
- case 'clear queue past':
- actionmsg = `cleared the queue past ${itemToMessage(command.track)}`
- break
- case 'clear queue up to':
- actionmsg = `cleared the queue up to ${itemToMessage(command.track)}`
- break
- case 'distribute queue':
- actionmsg = `distributed ${itemToMessage(command.topItem)} across the queue ${command.opts.how}`
- break
- case 'initialize party':
- return
- case 'play':
- actionmsg = `started playing ${itemToMessage(command.track)}`
- break
- case 'queue': {
- let afterMessage = ''
- if (isItemRef(command.afterItem)) {
- afterMessage = ` after ${itemToMessage(command.afterItem)}`
- } else if (command.afterItem === 'FRONT') {
- afterMessage = ` at the front of the queue`
- }
- actionmsg = `queued ${itemToMessage(command.topItem)}` + afterMessage
- break
- }
- case 'restore queue':
- if (command.why === 'shuffle') {
- actionmsg = 'shuffled the queue'
- }
- break
- 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':
- // 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':
- actionmsg = `updated their nickname (from ${nickToMessage(command.oldNickname)})`
- senderNickname = command.nickname
- break
- case 'set socket id':
- return
- case 'set pause':
- if (command.paused) {
- actionmsg = 'paused the player'
- } else {
- actionmsg = 'resumed the player'
- }
- break
- 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':
- actionmsg = `is ready to play!`
- break
- case 'done playing':
- actionmsg = `has finished playing`
- break
- case 'sync playback':
- actionmsg = `synced playback with the server`
- break
- default:
- actionmsg = `sent status "${command.status}"`
- break
- }
- break
- }
- const text = `${nickToMessage(senderNickname)} ${actionmsg}`
- backend.showLogMessage({
- text,
- code,
- sender,
- mayCombine,
- isVerbose
- })
- }
- client.on('sent command', command => {
- command.senderNickname = client.nickname
- logCommand(command)
- })
- client.on('command', async command => {
- logCommand(command)
- switch (command.sender) {
- case 'server':
- switch (command.code) {
- case 'set socket id':
- client.socketId = command.socketId
- socketInfoMap[command.socketId] = {
- nickname: client.nickname,
- sharedSources
- }
- backend.loadSharedSources(command.socketId, sharedSources)
- return
- 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)
- 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.
- case 'client': {
- let QP = (
- command.queuePlayer &&
- backend.queuePlayers.find(QP => QP.id === command.queuePlayer)
- )
- switch (command.code) {
- case 'announce join': {
- const sharedSources = {
- name: namePartySources(command.senderNickname),
- isPartySources: true,
- items: []
- }
- socketInfoMap[command.senderSocketId] = {
- nickname: command.senderNickname,
- sharedSources
- }
- backend.loadSharedSources(command.senderSocketId, sharedSources)
- return
- }
- 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.track, getPlaylistSources())
- ))
- return
- 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(
- restoreNewItem(command.topItem),
- {
- how: command.opts.how,
- rangeEnd: command.opts.rangeEnd
- }
- ))
- return
- case 'play':
- if (QP) {
- QP.once('received time data', data => {
- client.sendCommand({
- code: 'status',
- status: 'ready to resume',
- queuePlayer: QP.id
- })
- })
- silenceEvents(QP, ['playing'], () => {
- QP.play(restoreNewItem(command.track, getPlaylistSources()))
- })
- }
- 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.replaceAllItems(command.tracks.map(
- refData => restoreNewItem(refData, getPlaylistSources())
- ))
- }
- return
- case 'seek to':
- if (QP) silenceEvents(QP, ['seek to'], () => QP.seekTo(command.time))
- return
- 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': {
- // All this code looks very scary???
- /*
- // TODO: there's an event leak here when toggling pause while
- // nothing is playing
- let playingThisTrack = true
- QP.once('playing new track', () => {
- playingThisTrack = false
- })
- setTimeout(() => {
- if (playingThisTrack) {
- 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': {
- const { sharedSources } = socketInfoMap[command.senderSocketId]
- const deserialized = deserializePartySource(command.item, sharedSources)
- sharedSources.items.push(deserialized)
- backend.sharedSourcesUpdated(command.senderSocketId, sharedSources)
- return
- }
- case 'stop playing':
- if (QP) silenceEvents(QP, ['playing'], () => QP.stopPlaying())
- return
- case 'unqueue':
- if (QP) silenceEvents(QP, ['unqueue'], () => QP.unqueue(
- restoreNewItem(command.topItem, getPlaylistSources())
- ))
- return
- }
- }
- }
- })
- backend.on('announce join party', () => {
- client.sendCommand({
- code: 'announce join'
- })
- })
- 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('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) {
- // 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
- }
|