S10.6 — Recently Played (frecency)#57
Merged
Merged
Conversation
…gate (chunk 1) The Recently-Played frecency data layer + its proof. Schema v4 adds tracks.frecency_score (decayed-play accumulator) + tracks.frecency_rank (indexed projected read key) + idx_tracks_frecency_rank. Additive migration; counts reset via delete-rebuild (founder D9); nullable rank sorts NULLs last (plain indexed DESC — no COALESCE — keeps the filesort gone). - LibraryStore.frecencyAfterPlay (pure, public): score = prev·2^(−max(0,now−lp)/H)+1 (first play → 1; backward-clock clamp), rank = now + (H/ln2)·ln(score). H = 7 days. - incrementPlayCount(id:/url:) → read-modify-write in one dbWriter.write txn (Swift-computed score/rank; silent no-op if absent). - frecencyTracksDisplay(limit:offset:): WHERE play_count>0 ORDER BY frecency_rank DESC, id DESC — index-driven, no now, no temp b-tree. - frecencyState(id:) + explainFrecencyTracksDisplayPlan verification hooks. Gate: VerifyLibraryStore 114/114 (106 prior + FR1–FR8). FR6 verifies the projected- rank order ≡ current frecency 2^((rank−t)/H) end-to-end; FR3 proves burst-vs-spread; FR8 asserts index-used + no temp b-tree. Existing 106 pass at schema v4. Design + expert/Fool review + fixes: recently-played-frecency-design.md §8.
…ring (chunk 2) Replaces the natural-completion play-count rule with the founder's ≥60%-heard rule. - PlaybackQueueKit.PlayThroughTracker (pure, Sendable): counts once per play-through when heard ≥ min(60%·duration, 240s), with a ≥30s track floor. accrue(delta, duration) fires on the crossing; naturalEnd(duration) is the same-gate fallback (FIX-1 — a scrub-to-end / sub-30s / unresolved-duration track does NOT count); reset() re-arms (repeat-one recounts). maxPlausibleDelta clamps a pathological single delta while still accruing real UI-tick stalls (FIX-3). - AudioViewModel: accruePlayThrough() runs every transport tick — advances a suspend-stopping monotonic reference (DispatchTime.uptimeNanoseconds, excludes system sleep, FIX-2) unconditionally, accrues only while isPlaying (so pause/ stall/seek never mis-count, FIX-3), and records the current play on the crossing. The four completion sites now route through countPlayIfNaturalEndQualifies() (gated) instead of an unconditional count. resetPlayTracking() at the two new-play-through points (fresh startPlayback; gapless/repeat-one transition). - writePlayCount bumps playCountRevision on the main actor AFTER the store write commits (R4), for the Recently-Played refresh. Tests: 12 PlayThroughTracker cases (PT-01..12) — below/at/past threshold, once-per- play-through, reset/repeat, 30s floor + boundary, 240s cap, stall-accrues, pathological-clamp, seek-back/no-motion, duration-0, scrub-to-end. Build clean.
The History tab becomes an all-time, per-track, frecency-ranked "Recently Played"
view backed by the persistent store read — the duplicate-row-on-replay bug is gone
by construction (each track appears once).
- LibraryBrowseModel.loadHistory() (+History extension): frecencyTracksDisplay read,
loadSongs-style epoch/isStoreReady guard; history/historyState internal state.
- RecentlyPlayedRow: LibraryTrackDisplay-bound row — leading artwork (now-playing ▶
cue), title + FormatBadge, and a "N plays · «relative last-played»" cue replacing
the blank subtitle so the frecency order reads as intentional. No rank/duration.
Design-system tokens throughout; a11y label = title · count · last-played.
- QueueHistoryList: rebinds to LibraryBrowseModel.history via RecentlyPlayedRow; tap
→ playTrackNextNow (non-destructive); .task loads on appear + reloads on
playCountRevision (only while the tab is on screen → no background thrash). Empty
state reworded ("Tracks you finish will appear here.").
- PlaylistView: tab renamed "Recently Played" (header) / "Recent" (segmented picker,
avoids truncation); subtitle count from the browse model.
- DELETED the in-memory session log: AudioViewModel+History.swift, Models/HistoryItem
.swift, the sessionHistory property, and the recordPlayStart call sites (replaced by
resetPlayTracking in chunk 2).
make strict-gate PASSED: 0 lint, clang-tidy + periphery clean, 144 tests
(incl. PlayThroughTracker), VerifyLibraryStore 114/114, leak-check clean.
…-it) qa-expert break-it pass on the real impl found 1 high + 1 low (seam ordering it flagged as top-risk was confirmed CORRECT). - HIGH: startPlayback reset the PlayThroughTracker INSIDE its async Task (after await engine.startAudio). On a manual switch (next/prev/playTrack/Pure reconfigure) selectedTrackIndex is already the new track, and the 20Hz tick fires while the Task is parked — so the OUTGOING track's accrual could cross the threshold and count against the NEWLY-selected track (and the real ≥60% track miss its count). Reset now runs SYNCHRONOUSLY before the Task (resumeFrom == nil), closing the window. - LOW: RecentlyPlayedRow rendered a future/clock-skewed last_played as 'in N minutes' — clamp the display string to 'just now' when the stamp is not in the past. Verified-safe by the pass (no change): migration legacy-row path, NULL-rank ordering, double-load/refresh race, now-playing cue, repeat-one, idempotency, Swift-6 isolation, pause/stall accrual. make strict-gate PASSED (144 tests, 114/114, periphery clean).
ramith
added a commit
that referenced
this pull request
Jul 15, 2026
Vetted design for S10.4 (last R1 gate): media keys + Now Playing/Control Center (MediaPlayer) + Stop/Jump shortcuts, folding in the on-screen footer/mini-player metadata fix (same resolver). architect + swift-expert (SDK-grounded) + Fool + founder brainstorm. Locked: no entitlement/Info.plist change needed; explicit macOS playbackState; event-driven pushes (not per-tick); NowPlayingController peer via the onNowPlayingRefresh hook; pure NowPlayingSnapshot/RemoteCommandIntent in PlaybackQueueKit for tests; fix the latent ⌘←/⌘→ text-focus bug in passing. sprint-plan §Status + tables: S10.6 ✅ (shipped #57), S10.4 in progress → then S10.3.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
S10.6 — Recently Played (frecency)
Reworks the S10.2 History tab into an all-time, per-track, frecency-ranked "Recently Played" view (US-PLAY-10). Each track appears once with a play count, ordered so recency outweighs raw count — the duplicate-row-on-replay bug is gone by construction.
Design + full expert/Fool review trail: recently-played-frecency-design.md (merged in #56; §8 carries the research-grounded resolutions).
What shipped (3 chunks)
frecency_score(decayed-play accumulator) +frecency_rank(indexed projected read key) +idx_tracks_frecency_rank. Write is a read-modify-write in one txn:score = prev·2^(−max(0,now−lp)/H) + 1;rank = now + (H/ln2)·ln(score); H = 7 days. Read:WHERE play_count>0 ORDER BY frecency_rank DESC, id DESC— index-driven, nonow, no temp b-tree.PlaybackQueueKit.PlayThroughTracker— counts once per play-through atmin(60%·duration, 240s)with a ≥30s floor. Accrues suspend-stopping monotonic playback time every tick (immune to seeks/stalls/sleep/clock-skew); the four completion sites are gated (no unconditional count).RecentlyPlayedRow(artwork + title + "N plays · «time ago»" cue + now-playing ▶), tap = play-now; refreshes on a post-commitplayCountRevision. The in-memory session log is deleted.Research-grounded design (Last.fm scrobble rule · Firefox/Mozilla-Places frecency ·
fre)2^((rank−t)/H)analytically, numerically (0 mismatches / 200now× 400 rows), and byEXPLAINon 10k rows.Review & QA (the bar, not just the gate)
make strict-gatePASSED — 0 lint, clang-tidy + periphery clean, 144 swift tests (incl. 12PlayThroughTrackercases), VerifyLibraryStore 114/114 (+8 new FR checks: first-play, decay, burst-vs-spread, recency-outweighs-count, never-played-excluded, rank≡current-frecency equivalence, backward-clock-clamp, indexed-read), leak-check clean. Founder by-ear ✓.Notes
🤖 Generated with Claude Code