Files
whats-that-music/docs/superpowers/specs/2026-04-26-yt-music-player-design.md
T
shadowdaoandClaude Opus 4.7 24054a29bb Add design spec for embedded YouTube Music player
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>
2026-04-26 09:18:27 -07:00

166 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: <title> — <artist>" (disabled, updates as state changes)
- separator
- "Show Player" — focuses the YT Music window
- "Open Overlay in Browser" — opens `http://localhost:9095/` in the default browser
- "Copy OBS URL" — copies `http://localhost:9095/` to clipboard
- separator
- "Quit"
Removed: "Server URL" display, "Change Server URL..." prompt, all `electron-store` config related to server URL. Window-size persistence is the only remaining `electron-store` use.
## Files to delete
The following are removed wholesale; they have no consumers in the new design:
- `extension/` — entire directory.
- `desktop-client/backends/` — entire directory (Windows / macOS / Linux OS detection).
- `desktop-client/config.js` — server URL config, no longer needed.
- `server/` — directory removed; its logic moves into `desktop-client/server.js`.
- Top-level `package.json` and `node_modules/` at the project root — the project root is no longer a Node project. The Electron app's own `desktop-client/package.json` is the only `package.json`.
- Top-level `README.md` will be updated, not deleted.
The `overlay/` directory is kept; it's served by the in-process server.
## Files to modify
- `desktop-client/package.json`:
- Add deps: `express`, `ws`. Keep `electron-store`.
- Set `build.win.target: "zip"` (replacing the existing `nsis`). The `zip` target produces a `.zip` of the unpacked Windows app — user unzips and runs `WhatsThatMusic.exe`. No Wine required on the build host. Note: `electron-builder`'s `portable` target name is reserved for single-file portable `.exe` builds (which do require NSIS / Wine), so we use `zip` instead. Mac and Linux build configs are left alone; they're inert for this scope.
- Add `build.files` entries to include `../overlay/**/*` so the overlay is bundled into the packaged app.
- `README.md`: rewrite to describe the new "single app" model and how to point OBS at it.
## Build / packaging
- `npm start` (in `desktop-client/`) → runs `electron .` against the source.
- `npm run build` → produces a Windows `.zip` via `electron-builder`. The user unzips it and runs `WhatsThatMusic.exe` directly. No installer, no Wine, no signing.
- The Linux→Windows `zip` build needs no extra system packages beyond Node + the npm deps (`electron-builder` downloads Windows Electron binaries on demand).
## Testing strategy
This is small, mostly-glue code with a hard-to-mock browser dependency (YT Music DOM). Pragmatic plan:
- **Unit:** the server's `setState` merge logic — deterministic, easy to cover. Move the merge into a small pure function so it's testable without binding sockets.
- **Integration (manual):** run `npm start`, sign in to YT Music in the embedded window, play tracks, watch the overlay at `http://localhost:9095/` update in another browser window. Verify play/pause and track changes.
- **Smoke (manual):** build the portable zip, copy to a Windows machine, run, repeat the manual integration check.
No automated end-to-end tests. The DOM selectors are inherently fragile and Google-controlled — automated checks would mostly test our own mock, not reality.
## Risks
- **YT Music DOM changes** break scraping. Mitigation: same selectors as today's working extension; if they break, fix in one place (`yt-scraper.js`). No worse than the status quo.
- **Single-source-of-failure window:** if YT Music's web app fails to load (network, Google outage), the app shows a blank or error page. Acceptable — there's nothing to overlay anyway. We do not implement fallback retry logic in v1; the user can close and reopen the app.
- **Login persistence:** Electron's session storage handles this by default. The user signs in once; cookies persist in the userData directory. Not designed beyond default behavior.
## Branching
This work supersedes the in-progress `feature/os-media-detection` branch. Plan:
- Cut a new branch `feature/embedded-yt-music-player` from `main`.
- Abandon `feature/os-media-detection` (do not merge). The architecture-decision memory referencing it should be updated/removed once this design lands.
## Open questions
None blocking. Items deferred to implementation discretion:
- Default window size and "remember size" persistence — implementation choice.
- Whether to ship a custom app icon vs. default Electron icon — implementation choice.
- Tray icon visual — current minimal green-circle PNG is fine.