Skip to content

feat(recap): a year summarised from play events (ADR-0156, ADR-0157) - #421

Merged
jeffcrouse merged 1 commit into
mainfrom
feature/recap
Oct 30, 2026
Merged

jeffcrouse merged 1 commit into
mainfrom
feature/recap

Conversation

@jeffcrouse

Copy link
Copy Markdown
Member

Summary

The server half of a Wrapped-style year in review. ADR-0156 and ADR-0157 are both accepted in this PR.

  • GET /api/v1/recap/{year}?tz=… (tag plays, profile required) summarises one profile's play events for the year in their time zone.
    • Clipped to FEEDBACK_TRUSTWORTHY_SINCE (2026-08-01) and to now.
    • Returns window_start/window_end, so the 2026 recap is labelled "since August 1" from the data rather than from a hard-coded date.
  • What's in it:
    • Top 5 tracks and artists (canonical), and the top album.
    • Minutes listened. Skips count as time listened; errored plays count for nothing.
    • The year's sound from TrackAnalysis: medians weighted by play, the most common key, and the top three mood tags.
    • A 24-hour listening clock and peak hour, and the top month.
    • First listens and favourites added.
  • Fewer than 50 completed plays returns insufficient. Clients then show no recap at all (ADR-0022 point 3).
  • ProfilePlayHistory is never read. It has no dates, so it cannot be scoped to a year.
  • ADR-0157 decides the video is rendered on the device, in FamiliarKit, for both the Mac and the phone, with a clip of the feat: add context menu button for mobile in FullPlayer #1 track. That half is in familiar-apple, after this merges and scripts/vendor-schema.sh --fetch has run.

The change is additive: the contract stays at version 1, and openapi.json, contract.lock.json and packages/api-client are regenerated.

Test plan

  • tests/test_recap.py: 18 tests, covering:
    • the window and time zones
    • the cutoff
    • errored and skipped plays
    • profile isolation
    • the threshold
    • tie-breaking
    • canonical artist grouping
    • play-weighted sound
    • first listens and favourites
    • the endpoint's 400 and 401 responses
  • Removing the cutoff or the errored filter fails 7 of the 18 tests.
  • Full backend suite: 2435 passed, 18 skipped.
  • ruff, mypy, lint-contracts, dump_openapi --check, check_contract_bump --check, check_docs.py, api-client check
  • After merge: make deploy-dev, then check the endpoint's numbers against a SQL count of play_events on the NAS.

🤖 Generated with Claude Code

GET /api/v1/recap/{year}?tz= summarises one profile's play events for the
year in their time zone, clipped to FEEDBACK_TRUSTWORTHY_SINCE and to now,
and states the window it read so a client labels the 2026 recap "since
August 1" from the data. Top tracks, artists and album; minutes listened;
the sound of the year from TrackAnalysis, weighted by play; a 24-hour
listening clock; first listens and favourites added. Fewer than 50
completed plays is `insufficient`, and clients show no recap.

ProfilePlayHistory is never read: it has no dates.

ADR-0157 puts the video on the device, in FamiliarKit, for both the Mac
and the phone; that half lives in familiar-apple.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jeffcrouse
jeffcrouse merged commit a5f3a11 into main Oct 30, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant