feat: Spotify listening analytics - #8
Merged
Merged
Conversation
Spotify's API cannot answer most of what a listening dashboard wants. It exposes top artists/tracks over three fixed ranges and the most recent 50 plays, and nothing else historical: no listening time, no per-hour or per-day breakdown, no monthly trend. audio-features and recommendations now return 403 and 404 respectively for newer apps, so audio-derived analysis is not available either. This service therefore builds its own history. It polls recently-played every 20 minutes and stores plays in SQLite keyed on (track_id, played_at), which is what the overlapping response windows dedupe against. Genres come from the artists endpoint and are cached, being effectively static. Aggregates are computed in SQL rather than in the dashboard. Details worth noting: - Bucketing honours SPOTIFY_UTC_OFFSET_HOURS. Spotify timestamps are UTC, and "when do I listen" is meaningless in anything but local time. A fixed offset suits zones without DST. - Hour and weekday endpoints emit every slot including empty ones, so the chart axis stays stable as data accumulates. - time_range is validated against an allowlist in both the service and the orchestrator proxy rather than being forwarded. - The service key is compared in constant time. - Granted scopes are logged on each token refresh; a silently narrowed scope is what left the portfolio's Spotify panel showing "Offline". The orchestrator proxies /api/spotify/** so the dashboard keeps using the existing JWT boundary, with an allowlist of proxied resources. Play history lives on a named volume so it survives redeploys. Verified against a live account: 50 plays captured, genre breakdown, discovery ratio and hourly buckets all returning sensible data.
Two panels in one row, matching the footprint of the deployments row. The first draft ran to four rows, which put a music panel above deployments in the visual hierarchy of a DevOps console; tabs carry the detail instead. - Top listening: artists, tracks or genres, fixed to Spotify's four week window. The six month and all time rankings barely differed from it. - Listening patterns: plays by hour or weekday, with a compact stat line and a note that history only accumulates from when tracking started, since Spotify exposes just the last 50 plays. Charts are single series throughout, so they use one hue and carry no legend: the bars encode magnitude, not identity. The palette validator reports the two design tokens --accent-primary and --accent-secondary as 6.3 apart for normal vision and 0.8 under protanopia, so they must never encode different things in the same chart. The peak bar uses the lighter step with a hover label, which is the same series under secondary encoding rather than a second category. The dev server can also proxy /api/spotify straight at a local spotify-service via SPOTIFY_PROXY_TARGET, bypassing the orchestrator.
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.
Adds a listening-analytics service and a dashboard panel for it.
What Spotify can and cannot answer
Verified against the live API before building, because it shapes the whole design:
audio-featuresrecommendationsSo half of a "Spotify Wrapped" cannot be computed from the API.
apps/spotify-servicetherefore builds its own history: it polls recently-played every 20 minutes and stores plays in SQLite keyed on(track_id, played_at), which is what the overlapping response windows dedupe against. Genres come from the artists endpoint and are cached, being effectively static.The UI says this plainly rather than showing an empty chart — the patterns panel carries a note that history accumulates from when tracking started.
Backend
SPOTIFY_UTC_OFFSET_HOURS. Spotify timestamps are UTC and "when do I listen" is meaningless in anything else. A fixed offset suits zones without DST.time_rangechecked against an allowlist in both the service and the orchestrator proxy rather than forwarded.The orchestrator proxies
/api/spotify/**behind an allowlist, so the dashboard keeps using the existing JWT boundary. Four new security tests cover unknown resources, invalid range, invalid kind, and unauthenticated access.Frontend
Two panels in one row — the same footprint as the deployments row. The first draft was four rows, which put a music panel above deployments in the hierarchy of a DevOps console; tabs carry the detail instead.
Charts are single-series, so one hue and no legend: the bars encode magnitude, not identity.
Worth recording from the palette validator:
The two design-system accents are effectively the same colour and must never encode different things in one chart. They already appear side by side elsewhere on the dashboard as decoration, which is fine — but it rules them out as a categorical pair.
The peak bar uses the lighter step plus a hover label: same series under secondary encoding, not a second category.
Resource cost
48 MiB limit, bringing declared limits to 624 MiB on an 892 MiB host with 4 GiB of swap. Measured at ~25 MiB in use.
Deploying
Needs four values in the server
.env—SPOTIFY_CLIENT_ID,SPOTIFY_CLIENT_SECRET,SPOTIFY_REFRESH_TOKEN,SPOTIFY_SERVICE_KEY, plusSPOTIFY_UTC_OFFSET_HOURS=1..env.exampledocuments them andscripts/authorize.pyobtains a refresh token with the required scopes.The service will not start without them, so do not merge until they are on the host, or the deploy's health check will fail.
Verified against a live account: 50 plays captured, genres, discovery ratio and hourly buckets all returning real data.