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

10 KiB
Raw Blame History

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: