diff --git a/docs/superpowers/specs/2026-04-26-yt-music-player-design.md b/docs/superpowers/specs/2026-04-26-yt-music-player-design.md new file mode 100644 index 0000000..533bb4c --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-yt-music-player-design.md @@ -0,0 +1,165 @@ +# 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 + +1. App boot → main process starts the in-process Express+WS server, then opens a `BrowserWindow` loading `https://music.youtube.com`. +2. The `BrowserWindow` has a preload script (`yt-scraper.js`) that polls the YT Music DOM at ~1s intervals using the same selectors as today's `extension/content.js`. +3. When the scraped payload changes, the preload calls `ipcRenderer.send('wtm:state', payload)`. +4. Main process listens with `ipcMain.on('wtm:state', ...)`, updates `currentState`, and broadcasts the state over the WS server to all connected clients. +5. Overlay (`overlay/index.html`) is served from the same in-process server. OBS points at `http://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 via `electron-store`). + - `webPreferences.preload` points at `yt-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-playing` route is **removed**. (No external producers.) +- `setState(payload)` is the single update entry point used by both IPC handlers and any internal callers; it merges into `currentState` and broadcasts to WS clients (same merge logic the POST handler had). +- `GET /api/now-playing`, `GET /api/health`, `GET /api/image-proxy`, static serving of `overlay/`, and WS broadcasting are unchanged. +- The path to `overlay/` is resolved relative to `app.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', ... })` with `ipcRenderer.send('wtm:state', payload)`. +- Imports only `ipcRenderer` from `electron`. 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: