Commit graph

2 commits

Author SHA1 Message Date
BoulderBadgeDad
c3aea58b03 Player revamp Phase 2: smart radio ranking (play-count + popularity)
Replaces radio's pure ORDER BY RANDOM() with weighted ranking. Each tier now
fetches a generous random POOL (4x the needed count, floored) and
core/radio/selection ranks it before the collector keeps the best:

  score_candidate = play_count(log-damped, w=1.0)
                  + lastfm_playcount(log-damped, w=0.5)
                  - recently_played penalty(w=2.0)
                  + stable per-id jitter(w=1.0, hash-derived so runs vary but
                    tests stay reproducible)

Modest weights so popularity guides without burying lesser-played tracks, and
jitter keeps radio from being identical every run. All intelligence is in pure
functions (rank_candidates / score_candidate) so it's tunable + unit-testable
without SQL.

Defensive: the DB method probes PRAGMA table_info(tracks) and omits
play_count/lastfm_playcount from the SELECT when absent (older DBs predating
the listening-history migration) — the scorer treats missing signals as 0, so
radio degrades to jitter-only instead of crashing on 'no such column'.

Tests (tests/radio/, 43 total):
  - score_candidate / rank_candidates: deterministic unit coverage (popularity
    ordering, lastfm contribution, recency penalty, garbage→0, stable jitter).
    These CANNOT pass against pre-Phase-2 code.
  - DB end-to-end: ranking surfaces the heavily-played track first out of a
    decoy pool (wiring proof — probabilistic vs old random, documented honestly);
    plus a no-rank-columns DB proving the defensive degrade path.
  - All Phase-0a behavioral/refactor-equivalence tests still green.
60 radio + adjacent-DB tests pass; ruff clean.
2026-05-30 08:47:18 -07:00
BoulderBadgeDad
cbc001e283 Player revamp Phase 0a: extract radio selection into testable core/radio/
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.
2026-05-30 08:34:27 -07:00