play_history is written and never read — recency-weighted play stats #185

Open
opened 2026-08-21 14:22:50 +00:00 by yonlu · 0 comments
Owner

Report

play_history records one row per play (audio_file_id, played_at)
and nothing reads it for recency. Every "what do I listen to"
surface in the app uses audio_files.play_count, which is a lifetime
counter: an album played fifty times in 2023 and never since outranks
one played daily this month, permanently.

Findings

  • Schema: backend/database/sql/schemas/play_history.sql, indexed on
    audio_file_id. There is no index on played_at, which a windowed
    query would want.
  • home.sql's shelves use SUM(af.play_count)
    (HomeMostPlayedAlbums) and MAX(af.last_played)
    (HomeRecentlyPlayedAlbums) — the two things a history table exists
    to do better than.
  • A lifetime counter also cannot answer "played 3 times last week"
    versus "played 3 times ever", which is the distinction that makes a
    recency shelf worth having.

Direction

A windowed aggregate — plays per entity in the last N days — behind one
query, plus the played_at index. Then decide what consumes it. Three
candidates, and they are not the same piece of work:

  1. A home shelf ("On repeat lately"), which is where this most obviously
    belongs and would want home's existing duplicate guard.
  2. Fuel for #45 (auto-DJ), whose whole problem is choosing what to
    queue next.
  3. A sort key, which is the weakest of the three — it is a question
    about a period, and a column header cannot say which period.

Deliberately not folded into #32/#46: those are a projection widening
plus comparators, and this is a new query with a windowing decision and
a retention question (does play_history grow forever? nothing prunes
it today).

**Report** `play_history` records one row per play (`audio_file_id`, `played_at`) and **nothing reads it for recency**. Every "what do I listen to" surface in the app uses `audio_files.play_count`, which is a lifetime counter: an album played fifty times in 2023 and never since outranks one played daily this month, permanently. **Findings** - Schema: `backend/database/sql/schemas/play_history.sql`, indexed on `audio_file_id`. There is no index on `played_at`, which a windowed query would want. - `home.sql`'s shelves use `SUM(af.play_count)` (`HomeMostPlayedAlbums`) and `MAX(af.last_played)` (`HomeRecentlyPlayedAlbums`) — the two things a history table exists to do better than. - A lifetime counter also cannot answer "played 3 times last week" versus "played 3 times ever", which is the distinction that makes a recency shelf worth having. **Direction** A windowed aggregate — plays per entity in the last N days — behind one query, plus the `played_at` index. Then decide what consumes it. Three candidates, and they are not the same piece of work: 1. A home shelf ("On repeat lately"), which is where this most obviously belongs and would want `home`'s existing duplicate guard. 2. Fuel for #45 (auto-DJ), whose whole problem is choosing what to queue next. 3. A sort key, which is the weakest of the three — it is a question about a period, and a column header cannot say which period. Deliberately **not** folded into #32/#46: those are a projection widening plus comparators, and this is a new query with a windowing decision and a retention question (does `play_history` grow forever? nothing prunes it today).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yonlu/yellowjacket#185