Replaces the OS-detection / browser-extension input architecture with a single Electron app that embeds music.youtube.com and pushes now-playing state to an in-process server on localhost:9095. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10 KiB
10 KiB
Embedded YouTube Music Player — Design
Status: Draft for review Date: 2026-04-26
Goal
Replace the current "scrape OS media APIs / browser extension" architecture with a single Electron app that embeds the YouTube Music web app as its player, extracts now-playing state from inside that embedded view, and serves the state to OBS / overlay clients on localhost:9095.
The user uses YouTube Music exclusively. Spotify and other sources are explicitly out of scope.
Non-goals
- Spotify support (deferred; not designed for).
- Custom player UI / controls.
- Code-signed Windows builds (portable zip only).
- Cross-machine server deployment (server is in-process, localhost-only).
- Browser extension (deleted).
- OS-level media detection (deleted).
- Tray-resident background mode (closing the player window quits the app).
Architecture
One Electron process. Inside it:
┌─────────────────────────────────────────────────────────────┐
│ Electron main process │
│ │
│ ┌──────────────────┐ ┌─────────────────────────┐ │
│ │ Express + WS │ │ BrowserWindow │ │
│ │ server module │◀──IPC─│ url: music.youtube.com │ │
│ │ :9095 │ │ preload: yt-scraper.js │ │
│ │ │ └─────────────────────────┘ │
│ │ GET / │ │
│ │ GET /api/health │ │
│ │ GET /api/now- │ │
│ │ playing │ │
│ │ GET /api/image- │ │
│ │ proxy │ │
│ │ WS broadcast │ │
│ └──────────────────┘ │
│ │
│ Tray icon (show/quit only) │
└─────────────────────────────────────────────────────────────┘
│
│ HTTP / WS on localhost:9095
▼
OBS browser source / overlay clients
State flow
- App boot → main process starts the in-process Express+WS server, then opens a
BrowserWindowloadinghttps://music.youtube.com. - The
BrowserWindowhas a preload script (yt-scraper.js) that polls the YT Music DOM at ~1s intervals using the same selectors as today'sextension/content.js. - When the scraped payload changes, the preload calls
ipcRenderer.send('wtm:state', payload). - Main process listens with
ipcMain.on('wtm:state', ...), updatescurrentState, and broadcasts the state over the WS server to all connected clients. - Overlay (
overlay/index.html) is served from the same in-process server. OBS points athttp://localhost:9095/. Behavior identical to today from the consumer side.
Components
desktop-client/index.js (Electron main entry)
Single entry point. Responsibilities, in order:
- Create the Express + WS server module and start it on port 9095.
- Create a
BrowserWindow(default size, e.g. 1280×800; resizable; remembers size between launches viaelectron-store).webPreferences.preloadpoints atyt-scraper.js.webPreferences.contextIsolation: true,nodeIntegration: false. The preload is the only bridge.
- Load
https://music.youtube.com. - Register
ipcMain.on('wtm:state', ...)to feed payloads into the server's state-update function. - Create the tray icon (see Tray UX).
- On
window-all-closed→app.quit(). Closing the player window terminates the app.
desktop-client/server.js (in-process server module)
Lifted from the current standalone server/index.js, with the following changes:
- Exposed as a module:
startServer(): { setState(payload), stop() }rather than a process. - The HTTP
POST /api/now-playingroute is removed. (No external producers.) setState(payload)is the single update entry point used by both IPC handlers and any internal callers; it merges intocurrentStateand broadcasts to WS clients (same merge logic the POST handler had).GET /api/now-playing,GET /api/health,GET /api/image-proxy, static serving ofoverlay/, and WS broadcasting are unchanged.- The path to
overlay/is resolved relative toapp.getAppPath()so it works in both dev and packaged builds.
desktop-client/yt-scraper.js (preload script)
Adapted from extension/content.js. Same DOM selectors, same change-detection logic. Differences:
- No
chrome.storage; no server URL configuration. There is no URL — IPC goes direct to the main process. - Replace
fetch(url, { method: 'POST', ... })withipcRenderer.send('wtm:state', payload). - Imports only
ipcRendererfromelectron. Runs in an isolated world (preload), so no risk of clashing with YT Music's own JS. - Polling interval kept at the existing 3 s for parity. (Can tune later.)
desktop-client/tray.js
Simplified from the current tray. Menu items:
- "What's That Music" (disabled label)
- "Now Playing: