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>
This commit is contained in:
@@ -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: <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.
|
||||
Reference in New Issue
Block a user