2026-04-12 11:55:04 -07:00
|
|
|
const express = require('express');
|
|
|
|
|
const http = require('http');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
const { WebSocketServer } = require('ws');
|
|
|
|
|
|
|
|
|
|
const app = express();
|
|
|
|
|
const server = http.createServer(app);
|
|
|
|
|
const wss = new WebSocketServer({ server });
|
|
|
|
|
|
|
|
|
|
const PORT = 9095;
|
|
|
|
|
|
|
|
|
|
// Current now-playing state
|
|
|
|
|
let currentState = {
|
|
|
|
|
title: '',
|
|
|
|
|
artist: '',
|
|
|
|
|
album: '',
|
|
|
|
|
albumArt: '',
|
|
|
|
|
isPlaying: false,
|
|
|
|
|
progress: 0,
|
|
|
|
|
duration: 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// CORS middleware
|
|
|
|
|
app.use((req, res, next) => {
|
|
|
|
|
res.header('Access-Control-Allow-Origin', '*');
|
|
|
|
|
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
|
|
|
res.header('Access-Control-Allow-Headers', 'Content-Type');
|
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
|
|
|
return res.sendStatus(204);
|
|
|
|
|
}
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Parse JSON bodies
|
|
|
|
|
app.use(express.json());
|
|
|
|
|
|
|
|
|
|
// Image proxy — fetches album art server-side so OBS can display it
|
|
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Health check
|
|
|
|
|
app.get('/api/health', (req, res) => {
|
|
|
|
|
res.json({ status: 'ok' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Get current now-playing state
|
|
|
|
|
app.get('/api/now-playing', (req, res) => {
|
|
|
|
|
res.json(currentState);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update now-playing state
|
|
|
|
|
app.post('/api/now-playing', (req, res) => {
|
|
|
|
|
const { title, artist, album, albumArt, isPlaying, progress, duration } = req.body;
|
|
|
|
|
|
|
|
|
|
currentState = {
|
|
|
|
|
title: title ?? currentState.title,
|
|
|
|
|
artist: artist ?? currentState.artist,
|
|
|
|
|
album: album ?? currentState.album,
|
|
|
|
|
albumArt: albumArt ?? currentState.albumArt,
|
|
|
|
|
isPlaying: isPlaying ?? currentState.isPlaying,
|
|
|
|
|
progress: progress ?? currentState.progress,
|
|
|
|
|
duration: duration ?? currentState.duration,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Broadcast to all connected WebSocket clients
|
|
|
|
|
const message = JSON.stringify(currentState);
|
|
|
|
|
wss.clients.forEach((client) => {
|
|
|
|
|
if (client.readyState === 1) { // WebSocket.OPEN
|
|
|
|
|
client.send(message);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.json(currentState);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-12 11:58:12 -07:00
|
|
|
// Serve static files from the overlay directory (after API routes)
|
|
|
|
|
app.use(express.static(path.join(__dirname, '..', 'overlay')));
|
|
|
|
|
|
2026-04-12 11:55:04 -07:00
|
|
|
// Send current state to newly connected WebSocket clients
|
|
|
|
|
wss.on('connection', (ws) => {
|
|
|
|
|
ws.send(JSON.stringify(currentState));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
server.listen(PORT, () => {
|
|
|
|
|
console.log(`Server running on http://localhost:${PORT}`);
|
|
|
|
|
});
|