'use strict'; const { resolveEmbyUser } = require('./embyAuth'); const { PartyManager } = require('./partyManager'); /** * Realtime side of a party: a client connects, authenticates with its Emby * token (as the FIRST message, not a query string - see client/js/party.js), * and from then on either drives playback (if it's the host) or receives it. * * Protocol (all messages are JSON): * -> {type:"auth", token, userId, partyId} * <- {type:"members", members:[...]} (on any join/leave) * -> {type:"playstate", isPaused, positionTicks} (host only - ignored otherwise) * <- {type:"playstate", isPaused, positionTicks} (relayed to everyone but the host) * -> {type:"leave"} * <- {type:"error", message} */ function attachWebSocketServer(wss) { wss.on('connection', (ws) => { let party = null; let userId = null; ws.on('message', async (raw) => { let msg; try { msg = JSON.parse(raw.toString()); } catch (err) { return; } if (msg.type === 'auth') { const user = await resolveEmbyUser(msg.token, msg.userId); if (!user) { send(ws, { type: 'error', message: 'Session Emby invalide ou expirée.' }); ws.close(); return; } const p = PartyManager.getById(msg.partyId); const member = p && p.members.get(user.id); if (!p || !member) { send(ws, { type: 'error', message: "Tu n'es pas membre de cette party." }); ws.close(); return; } party = p; userId = user.id; member.connection = ws; broadcastMembers(party); return; } if (!party || !userId) { return; // ignore anything before a successful auth } if (msg.type === 'playstate') { if (userId !== party.hostUserId) { return; // only the host drives playback - see docs/architecture.md } const member = party.members.get(userId); member.isPaused = !!msg.isPaused; member.positionTicks = Number(msg.positionTicks) || 0; member.lastUpdatedUtc = Date.now(); party.lastActivityUtc = Date.now(); broadcast(party, { type: 'playstate', isPaused: member.isPaused, positionTicks: member.positionTicks }, ws); return; } if (msg.type === 'leave') { PartyManager.leave(party, userId); broadcastMembers(party); ws.close(); } }); ws.on('close', () => { if (party && userId) { PartyManager.leave(party, userId); broadcastMembers(party); } }); }); } function send(ws, message) { if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify(message)); } } function broadcast(party, message, exceptWs) { for (const member of party.members.values()) { if (member.connection && member.connection !== exceptWs) { send(member.connection, message); } } } function broadcastMembers(party) { const members = Array.from(party.members.values()).map((m) => ({ userId: m.userId, userName: m.userName, isHost: m.userId === party.hostUserId, connected: !!m.connection })); broadcast(party, { type: 'members', members }, null); } module.exports = { attachWebSocketServer };