First step of the stream/player/radio revamp (see revamp_plan.md). The radio
algorithm lived inline inside database.music_database.get_radio_tracks as raw
SQL tangled with selection logic — untestable without a live DB (which also
throws in the dev sandbox). Lifted the pure DECISIONS into core/radio/selection.py:
- parse_tags / merge_tags — JSON-or-CSV tag fields → ordered deduped list
- same_artist_cap — tier-1 30%-floored-at-5 cap
- build_like_conditions — OR-of-LIKEs SQL fragment + params per tier
- RadioCollector — dedup + cap + exclude-set + NOT-IN placeholder/value tracking
The DB method keeps the cursor work and now delegates every decision to these
helpers. Faithful extraction, not a rewrite — behavior unchanged.
This is the kettui foundation move: radio is now unit-testable, so Phase 2
(smart ranking — play-count / recency / feature seeding) becomes 'evolve a
tested function' instead of 'rewrite SQL and pray'.
Tests (tests/radio/):
- test_selection.py (22): unit coverage of every extracted helper
- test_get_radio_tracks_db.py (7): drive the REAL get_radio_tracks against
in-memory sqlite — tier fallback, dedup, exclude, file_path filter.
Behavior-pinned: these 7 pass against BOTH old inline and new extracted
code (refactor-equivalence proof). 52 adjacent DB+radio tests green.
2.6 KiB
2.6 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 0–2 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/. The algorithm (tier orchestration, cap math, dedup, tag parsing, SQL-condition building) is currently tangled withcursor.executeinsidedatabase/music_database.py:get_radio_tracks(~12756) — untestable without a live DB. Pull the pure decisions intocore/radio/selection.py; the DB method keeps SQL execution but delegates the decisions. Differential-test: same inputs → same output as today. - 0b. Centralize frontend player state. ~10 scattered
np*globals inmedia-player.js→ onePlayerStateobject. Seam for every later frontend phase. No behavior change.
Phase 1 — Polish / feel (frontend)
- Persistent queue across refresh (localStorage first; server-side in P3)
- Drag-to-reorder queue; duration + art per queue item
- Seek tooltip (hover timestamp); smoother progress
- Crossfade via dual-
<audio>swap (honest approximation of gapless — true gapless impossible w/ single element) - Full Media Session API (lockscreen / hardware transport keys)
- Keyboard shortcut overlay + fuller bindings
Phase 2 — Smart radio (backend algorithm)
- Replace
ORDER BY RANDOM()with real seeding: play-count + recency weighting, genre-adjacency, recently-played memory. Slots into the Phase-0a pure module → fully unit-testable (seed → expected ordering). Both radio buttons benefit (shared function).
Phase 3 — Architecture (deepest, riskiest — listener decision lands here)
- Per-session (or multi-tenant) stream state — replaces the single global
stream_state+ 1-worker executor + singleStream/staging file (web_server.py:747). - Server-side persistent queue (resume across devices/refresh).
- Final multi-listener vs single-listener scope decided here, with real usage in hand.
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.