Add YouTube Music now-playing server, OBS overlay, and browser extension
Node.js server on port 9095 receives now-playing data via POST and broadcasts to connected WebSocket clients. Includes a Firefox/Chrome extension that scrapes YouTube Music's player bar every 3s, an OBS-ready transparent overlay with animated equalizer bars, and an image proxy to bypass CDN restrictions in OBS. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
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());
|
||||
|
||||
// Serve static files from the overlay directory
|
||||
app.use(express.static(path.join(__dirname, '..', 'overlay')));
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// 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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user