You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
SoulSync/revamp_plan.md

4.0 KiB

Stream / Player / Radio Revamp — Plan

Goal: bring the audio stream + media-player + radio system to Spotify/Apple-level polish and feature set. Target stack: plain JS (webui/static/media-player.js), not the React migration. Intended architecture direction: multi-listener (final call deferred to Phase 3; Phases 02 stay compatible either way).

Rule for every phase: kettui standard — importable/testable logic, seam-level + differential tests, break nothing, ship one reviewable phase at a time.


Phase 0 — Make it provable (foundation, no user-visible change)

  • 0a. Extract radio selection logic into testable core/radio/. DONE (commit cbc001e2). core/radio/selection.py owns parse_tags/merge_tags/same_artist_cap/build_like_conditions/RadioCollector; DB method delegates. 29 tests, refactor-equivalence proven (behavioral tests pass against old AND new).
  • 0b. Centralize frontend player state. ~10 scattered np* globals in media-player.js → one PlayerState object. Seam for every later frontend phase. No behavior change.

Phase 1 — Polish / feel (frontend)

  • Persistent queue across refresh (localStorage) — commit a8985b31
  • Drag-to-reorder queue; duration + art per queue item — ffbe669c + 3461d923
  • Seek tooltip (hover timestamp) — 112ecbb2
  • Crossfade via dual-<audio> swap (library tracks, experimental) — ccfb3fb0 + 592b68c1
  • Full Media Session API (lockscreen / hardware transport keys + position) — 866f2e4a
  • Fuller keyboard bindings (N/P/M/space/seek/vol) — 65f49cce
  • Keyboard shortcut OVERLAY/cheatsheet (bindings done; no discoverability UI yet)
  • Click-art visualizer, sleep timer, up-next, click-to-seek lyrics, vibrant art-color glow, crafted entrance, mini-player shuffle/repeat parity, 'Play next' + queue buttons, 'Playing from' context, web-player play logging
  • (Optional) Sleep timer 'end of track' mode; waveform seek bar (flourish)

Phase 2 — Smart radio (backend algorithm)

  • Weighted ranking DONE. Each tier now fetches a random POOL (4x, floored) and core/radio/selection.rank_candidates orders it by score_candidate: play_count + lastfm_playcount (log-damped), recently-played penalty, stable per-id jitter for run variety. Defensive column-probe → still works on a DB predating the play_count/lastfm migration. 43 radio tests; ranking math is deterministic-unit-proven; DB wiring shown via decoy-pool test (probabilistic by nature — documented).
  • Future (optional deepening): wire _recently_played from listening_history (column + scorer support already exist; not yet populated in the query), genre-adjacency graph (currently exact-genre LIKE only).

Phase 3 — Architecture (deepest, riskiest — multi-listener)

  • 3a. Stream-state store extracted + wired (foundation). DONE. core/streaming/state.py: StreamSession (dict-compatible, own RLock) + StreamStateStore (named-session registry, lazy create, race-safe). web_server.py now binds stream_state to the store's DEFAULT session — behavior identical to the old single global (proven by call-site-compat + real-session worker tests). 33 streaming tests. This is the provable foundation multi-listener needs.
  • 3b. Per-listener session id. DONE (commit f6174589). _stream_session_id() from the Flask cookie; all 5 stream routes route to the caller's session + lock; per-session background tasks (stream_tasks[sid]); per-session Stream/ staging; executor 1→4 workers. Single-user behavior unchanged. EXPERIMENTAL — route-level two-client no-collision needs Boulder's live multi-client verification (can't boot Flask + 2 cookies in tests). Isolation invariant covered by test_stream_state_store.py.
  • Server-side persistent queue (resume across devices/refresh).

Order of execution

0a (radio extraction) → 2 (smart radio) first: highest visible upgrade, backend-only, cleanest to prove, zero playback risk. Then 0b → 1 (polish). Then 3 (architecture) last.