import { createServer } from "node:http"; import { readFileSync } from "node:fs"; import { Socket } from "node:net"; import { encode, decode } from "@msgpack/msgpack"; import CRC32 from "crc-32"; import { WebSocketServer } from "ws"; const SOVA_HOST = process.env.SOVA_HOST || "localhost"; const SOVA_PORT = parseInt(process.env.SOVA_PORT || "8080"); const SOVA_PASSWORD = process.env.SOVA_PASSWORD || null; const WEB_PORT = parseInt(process.env.WEB_PORT || "3000"); const PROTOCOL_VERSION = 0x02; const indexHtml = readFileSync(new URL("./index.html", import.meta.url)); let state = { peers: [], tempo: 120, isPlaying: false, connected: false, }; // --- Wire protocol --- function buildFrame(msg) { const payload = encode(msg); const crc = CRC32.buf(payload) >>> 0; const len = payload.length; const frame = Buffer.alloc(8 + len); frame[0] = PROTOCOL_VERSION; frame[1] = (len >> 16) & 0xff; frame[2] = (len >> 8) & 0xff; frame[3] = len & 0xff; frame.writeUInt32BE(crc, 4); Buffer.from(payload).copy(frame, 8); return frame; } function parseFrames(buffer) { const messages = []; let offset = 0; while (offset + 8 <= buffer.length) { if (buffer[offset] !== PROTOCOL_VERSION) { offset++; continue; } const len = (buffer[offset + 1] << 16) | (buffer[offset + 2] << 8) | buffer[offset + 3]; if (len > 10 * 1024 * 1024) { offset++; continue; } if (offset + 8 + len > buffer.length) break; const payload = buffer.subarray(offset + 8, offset + 8 + len); const expectedCrc = buffer.readUInt32BE(offset + 4); const actualCrc = CRC32.buf(payload) >>> 0; if (expectedCrc !== actualCrc) { offset++; continue; } try { messages.push(decode(payload)); } catch { // skip malformed msgpack } offset += 8 + len; } return { messages, remaining: buffer.subarray(offset) }; } // --- TCP client to sova-server --- let tcp = null; let reconnectTimer = null; function connectToSova() { if (tcp) return; console.log(`Connecting to sova-server at ${SOVA_HOST}:${SOVA_PORT}`); const sock = new Socket(); let recvBuf = Buffer.alloc(0); sock.connect(SOVA_PORT, SOVA_HOST, () => { console.log("Connected to sova-server"); const handshake = { SetName: { name: "Web Monitor", password: SOVA_PASSWORD } }; sock.write(buildFrame(handshake)); }); sock.on("data", (chunk) => { recvBuf = Buffer.concat([recvBuf, chunk]); const { messages, remaining } = parseFrames(recvBuf); recvBuf = remaining; for (const msg of messages) handleMessage(msg); }); sock.on("close", () => { console.log("Disconnected from sova-server"); tcp = null; state.connected = false; broadcast(); scheduleReconnect(); }); sock.on("error", (err) => { console.error("TCP error:", err.message); sock.destroy(); }); tcp = sock; } function scheduleReconnect() { if (reconnectTimer) return; reconnectTimer = setTimeout(() => { reconnectTimer = null; connectToSova(); }, 3000); } function handleMessage(msg) { if (msg.Hello) { state.connected = true; state.peers = msg.Hello.peers || []; state.isPlaying = msg.Hello.is_playing || false; if (msg.Hello.link_state) state.tempo = msg.Hello.link_state[0]; broadcast(); } else if (msg.PeersUpdated) { state.peers = msg.PeersUpdated; broadcast(); } else if (msg.PlaybackStateChanged != null) { state.isPlaying = !!msg.PlaybackStateChanged; broadcast(); } else if (msg.ClockState) { state.tempo = msg.ClockState[0]; broadcast(); } else if (msg.ConnectionRefused) { console.error("Connection refused:", msg.ConnectionRefused); } } // --- HTTP + WebSocket --- const server = createServer((req, res) => { if (req.url === "/" || req.url === "/index.html") { res.writeHead(200, { "Content-Type": "text/html" }); res.end(indexHtml); } else { res.writeHead(404); res.end(); } }); const wss = new WebSocketServer({ server }); const clients = new Set(); wss.on("connection", (ws) => { clients.add(ws); ws.send(JSON.stringify(state)); ws.on("close", () => clients.delete(ws)); }); function broadcast() { const data = JSON.stringify(state); for (const ws of clients) ws.send(data); } server.listen(WEB_PORT, () => { console.log(`Web server listening on port ${WEB_PORT}`); connectToSova(); });