Add in-process server module with setState entry point
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<void> }>}
|
||||
*/
|
||||
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 };
|
||||
Reference in New Issue
Block a user