From 3445ef89f03c4d55b3243f0e66cfed8a82d6526d Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 26 Apr 2026 09:38:11 -0700 Subject: [PATCH] Add in-process server module with setState entry point Co-Authored-By: Claude Sonnet 4.6 --- desktop-client/server.js | 82 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 desktop-client/server.js diff --git a/desktop-client/server.js b/desktop-client/server.js new file mode 100644 index 0000000..1cbde08 --- /dev/null +++ b/desktop-client/server.js @@ -0,0 +1,82 @@ +const express = require('express'); +const http = require('http'); +const { WebSocketServer } = require('ws'); +const { initialState, mergeState } = require('./state'); + +/** + * Start the now-playing server. + * + * @param {Object} opts + * @param {number} opts.port Port to listen on (e.g. 9095). + * @param {string} [opts.overlayDir] Absolute path to the overlay/ directory to serve as static files. Optional (omit for tests). + * @returns {Promise<{ setState: (update: object) => object, stop: () => Promise }>} + */ +function startServer({ port, overlayDir }) { + let currentState = { ...initialState }; + + const app = express(); + const server = http.createServer(app); + const wss = new WebSocketServer({ server }); + + // CORS: overlay/OBS browser sources may be loaded from arbitrary origins. + app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type'); + if (req.method === 'OPTIONS') return res.sendStatus(204); + next(); + }); + + app.get('/api/health', (req, res) => res.json({ status: 'ok' })); + + app.get('/api/now-playing', (req, res) => res.json(currentState)); + + // Image proxy so overlay clients can fetch album art that may have CORS / referrer restrictions. + app.get('/api/image-proxy', async (req, res) => { + const url = req.query.url; + if (!url) return res.status(400).send('Missing url parameter'); + try { + const response = await fetch(url); + if (!response.ok) return res.status(response.status).send('Upstream error'); + res.set('Content-Type', response.headers.get('content-type') || 'image/jpeg'); + res.set('Cache-Control', 'public, max-age=3600'); + const buffer = await response.arrayBuffer(); + res.send(Buffer.from(buffer)); + } catch (err) { + res.status(502).send('Failed to fetch image'); + } + }); + + if (overlayDir) { + app.use(express.static(overlayDir)); + } + + wss.on('connection', (ws) => { + ws.send(JSON.stringify(currentState)); + }); + + function setState(update) { + currentState = mergeState(currentState, update || {}); + const message = JSON.stringify(currentState); + wss.clients.forEach((client) => { + if (client.readyState === 1) client.send(message); + }); + return currentState; + } + + function stop() { + return new Promise((resolve) => { + wss.close(() => server.close(() => resolve())); + }); + } + + return new Promise((resolve, reject) => { + server.listen(port, '127.0.0.1', () => { + console.log(`[wtm] server listening on http://127.0.0.1:${port}`); + resolve({ setState, stop }); + }); + server.on('error', reject); + }); +} + +module.exports = { startServer };