audio streaming app plyr.fm

chore: status maintenance (#981)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

authored by

claude[bot]
claude[bot]
Claude Opus 4.6
and committed by
GitHub
010ad025 4fe2098a

+142 -123
+126
.status_history/2026-02.md
··· 1 + # plyr.fm Status History - February 2026 (Early) 2 + 3 + Archived from STATUS.md — covers Feb 2-12, 2026. 4 + 5 + --- 6 + 7 + #### playlist track recommendations (PRs #895-898, Feb 11) 8 + 9 + **inline recommendations when editing playlists**: shows 3 recommended tracks below the track list based on CLAP audio embeddings in turbopuffer. adaptive algorithm scales with playlist size — direct vector query for 1 track, per-track Reciprocal Rank Fusion for 2-5, k-means clustering into centroids for 6+. results cached in Redis keyed on the playlist's ATProto record CID (auto-invalidates when tracks change). 10 + 11 + **backend**: new `recommendations.py` module with pure-python k-means (no numpy), RRF merge, and `get_vectors()` in the turbopuffer client. new `GET /playlists/{id}/recommendations` endpoint with owner-only auth, Redis caching (24h TTL), and graceful degradation when turbopuffer is disabled. 12 + 13 + **frontend**: recommendation cards match TrackItem geometry exactly — dashed border + reduced opacity (0.7 → 1.0 on hover) distinguishes suggestions from committed playlist tracks. "add tracks" button and recommendations section align with track card width inside edit mode rows. 14 + 15 + no feature flag needed — degrades gracefully when turbopuffer or embeddings are unavailable. 16 + 17 + see `docs/backend/playlist-recommendations.md` for full architecture. 18 + 19 + --- 20 + 21 + #### main.py extraction + bug fixes (PRs #890-894, Feb 10) 22 + 23 + **main.py extraction (PR #890)**: `main.py` shrunk from 372 to 138 lines — now pure orchestration (imports, lifespan, wiring). extracted `SecurityHeadersMiddleware` → `utilities/middleware.py`, logfire/span enrichment → `utilities/observability.py`, and 6 root-level endpoints → `api/meta.py` router. added `__main__.py` for `python -m backend` convenience. 24 + 25 + **notification DM fix (PR #891)**: upload notification DMs were silently dropped because `_send_track_notification` received a `Track` object from a closed session. the `db.refresh()` call hit `DetachedInstanceError`, caught by a blanket `except`. fix: accept `track_id: int` and re-fetch with `joinedload(Track.artist)` from the current session. discovered via Logfire. 26 + 27 + **mobile share fix (PR #892)**: on mobile Safari, `await fetch()` before `navigator.clipboard.writeText()` consumes the transient user activation, breaking clipboard access. fix: eagerly create tracked share links when the menu opens (not on tap). added `navigator.share()` as fallback. 28 + 29 + **developer token scoping docs (PR #893)**: corrected misleading "full account access" language — tokens are actually scoped to `fm.plyr.*` via ATProto OAuth. fixed stale `resolved_scope` examples. 30 + 31 + **artist page track limit (PR #894)**: initial load was showing 50 tracks (backend default), burying albums below the fold. reduced to 5 with "load more" (10 per click). 32 + 33 + --- 34 + 35 + #### OAuth permission set cleanup + docs audit (PR #889, Feb 8) 36 + 37 + **OAuth permission set**: authorization page was showing raw NSID (`fm.plyr.authFullApp`) instead of human-readable description. root cause: ATProto permission sets use `detail` field (not `description`) for the subtitle text. updated lexicon and publish script, republished to PDS. also modernized publish script from raw `os.environ` to pydantic settings. 38 + 39 + **docs audit (PR #888)**: fixed stale/broken documentation across 6 files — wrong table names in copyright docs, outdated pool_recycle values, broken links in docs index, missing tools entries. updated README to reflect full feature set. added semantic search and playlists to search.md. 40 + 41 + --- 42 + 43 + #### auth state refresh + backend refactor (PRs #886-887, Feb 8) 44 + 45 + **auth state refresh (PR #887)**: after account switch or login, stale user data persisted because `AuthManager.initialize()` no-ops once the `initialized` flag is set. added `refresh()` that resets the flag before re-fetching, used in all exchange-token call sites. 46 + 47 + **backend package split (PR #886)**: split three monolith files into focused packages: 48 + - `auth.py` (1,400 lines) → `auth/` package (8 modules) 49 + - `background_tasks.py` (803 lines) → `tasks/` package (5 domain modules: copyright, ml, pds, storage, sync) 50 + - 5 `*_client.py` files → `clients/` package 51 + - extracted upload pipeline into 7 named phase functions, shared tag ops to `utilities/tags.py` 52 + 53 + all public APIs preserved via `__init__.py` re-exports. 424 tests pass. 54 + 55 + --- 56 + 57 + #### portal pagination + perf optimization (PRs #878-879, Feb 8) 58 + 59 + **portal pagination (PR #878)**: `GET /tracks/me` now supports `limit`/`offset` pagination (default 10 per page). portal loads first 10 tracks with a "load more" button. export section uses total count for accurate messaging. 60 + 61 + **GET /tracks/top latency fix (PR #879)**: baseline p95 was 1.2s due to stale connection reconnects and redundant DB queries. 62 + - merged top-track-ids + like-counts into single `get_top_tracks_with_counts()` query (1 fewer round-trip) 63 + - scoped liked-track check to `track_id IN (...)` (10 rows) instead of all user likes 64 + - `pool_recycle` 7200s → 1800s to reduce stale connection spikes 65 + - authenticated requests dropped from 11 DB queries to 7. post-deploy p95: ~550ms 66 + - 14 new regression tests 67 + 68 + --- 69 + 70 + #### repo reorganization (PR #876, Feb 8) 71 + 72 + moved auxiliary services into `services/` (transcoder, moderation, clap) and infrastructure into `infrastructure/` (redis). updated all GitHub Actions workflows, pre-commit config, justfile module paths, and docs. 73 + 74 + --- 75 + 76 + #### auto-tag at upload + ML audit (PRs #870-872, Feb 7) 77 + 78 + **auto-tag on upload (PR #871)**: checkbox on the upload form ("auto-tag with recommended genres") that applies top genre tags after classification completes. ratio-to-top filter (>= 50% of top score, capped at 5), additive with manual tags. flag stored in `track.extra`, cleaned up after use. 79 + 80 + **genre/subgenre split (PR #870)**: compound Discogs labels like "Electronic---Ambient" now produce two separate tags ("electronic", "ambient") instead of one compound tag. 81 + 82 + **ML audit script (PR #872)**: `scripts/ml_audit.py` reports which tracks/artists have been processed by ML features. supports `--verbose` and `--check-embeddings` for privacy/ToS auditing. 83 + 84 + --- 85 + 86 + #### ML genre classification + suggested tags (PRs #864-868, Feb 6-7) 87 + 88 + **genre classification via Replicate**: tracks classified into genre labels using [effnet-discogs](https://replicate.com/mtg/effnet-discogs) on Replicate (EfficientNet trained on Discogs ~400 categories). 89 + 90 + - on upload: classification runs as docket background task if `REPLICATE_ENABLED=true` 91 + - on demand: `GET /tracks/{id}/recommended-tags` classifies on the fly if no cached predictions 92 + - predictions stored in `track.extra["genre_predictions"]` with file_id-based cache invalidation 93 + - raw Discogs labels cleaned to lowercase format. cost: ~$0.00019/run 94 + - Replicate SDK incompatible with Python 3.14 (pydantic v1) — uses httpx directly with `Prefer: wait` header 95 + 96 + **frontend UX (PR #868)**: suggested genre tags appear as clickable dashed-border chips in the portal edit modal. `$derived` reactively hides suggestions matching manually-typed tags. 97 + 98 + --- 99 + 100 + #### mood search (PRs #848-858, Feb 5-6) 101 + 102 + **search by how music sounds** — type "chill lo-fi beats" into the search bar and find tracks that match the vibe, not just the title. 103 + 104 + **architecture**: CLAP (Contrastive Language-Audio Pretraining) model hosted on Modal generates audio embeddings at upload time and text embeddings at search time. vectors stored in turbopuffer. keyword and semantic searches fire in parallel — keyword results appear instantly (~50ms), semantic results append when ready (~1-2s). 105 + 106 + **key design decisions**: 107 + - unified search: no mode toggle. keyword + semantic results merge by score, client-side deduplication removes overlap 108 + - graceful degradation: backend returns `available: false` instead of 502/503 when CLAP/turbopuffer are down 109 + - quality controls: distance threshold, spread check to filter low-signal results, result cap 110 + - gated behind `vibe-search` feature flag with version-aware terms re-acceptance 111 + 112 + **hardening (PRs #849-858)**: m4a support for CLAP, correct R2 URLs, normalize similarity scores, switch from larger_clap_music to clap-htsat-unfused, handle empty turbopuffer namespace, rename "vibe search" → "mood search", concurrent backfill script. 113 + 114 + --- 115 + 116 + #### recommended tags via audio similarity (PR #859, Feb 6) 117 + 118 + `GET /tracks/{track_id}/recommended-tags` finds tracks with similar CLAP embeddings in turbopuffer, aggregates their tags weighted by similarity score. excludes existing tags, normalizes scores to 0-1. replaced by genre classification (PR #864) but the endpoint pattern persisted. 119 + 120 + --- 121 + 122 + #### mobile login UX + misc fixes (PRs #841-845, Feb 2) 123 + 124 + - **handle hint sizing (PRs #843-845)**: iterative fix for login page handle hint wrapping on mobile — final approach: reduced font size, gap, and `nowrap` to keep full text visible 125 + - **PDS backfill gate (PR #842)**: PDS backfill button gated behind `pds-audio-uploads` feature flag 126 + - **share button reuse (PR #841)**: track detail page now uses shared `ShareButton` component
+16 -123
STATUS.md
··· 153 153 154 154 --- 155 155 156 - #### playlist track recommendations (PRs #895-898, Feb 11) 157 - 158 - **inline recommendations when editing playlists**: shows 3 recommended tracks below the track list based on CLAP audio embeddings in turbopuffer. adaptive algorithm scales with playlist size — direct vector query for 1 track, per-track Reciprocal Rank Fusion for 2-5, k-means clustering into centroids for 6+. results cached in Redis keyed on the playlist's ATProto record CID (auto-invalidates when tracks change). 159 - 160 - **backend**: new `recommendations.py` module with pure-python k-means (no numpy), RRF merge, and `get_vectors()` in the turbopuffer client. new `GET /playlists/{id}/recommendations` endpoint with owner-only auth, Redis caching (24h TTL), and graceful degradation when turbopuffer is disabled. 161 - 162 - **frontend**: recommendation cards match TrackItem geometry exactly — dashed border + reduced opacity (0.7 → 1.0 on hover) distinguishes suggestions from committed playlist tracks. "add tracks" button and recommendations section align with track card width inside edit mode rows. 163 - 164 - no feature flag needed — degrades gracefully when turbopuffer or embeddings are unavailable. 165 - 166 - see `docs/backend/playlist-recommendations.md` for full architecture. 167 - 168 - --- 169 - 170 - #### main.py extraction + bug fixes (PRs #890-894, Feb 10) 171 - 172 - **main.py extraction (PR #890)**: `main.py` shrunk from 372 to 138 lines — now pure orchestration (imports, lifespan, wiring). extracted `SecurityHeadersMiddleware` → `utilities/middleware.py`, logfire/span enrichment → `utilities/observability.py`, and 6 root-level endpoints → `api/meta.py` router. added `__main__.py` for `python -m backend` convenience. 173 - 174 - **notification DM fix (PR #891)**: upload notification DMs were silently dropped because `_send_track_notification` received a `Track` object from a closed session. the `db.refresh()` call hit `DetachedInstanceError`, caught by a blanket `except`. fix: accept `track_id: int` and re-fetch with `joinedload(Track.artist)` from the current session. discovered via Logfire. 175 - 176 - **mobile share fix (PR #892)**: on mobile Safari, `await fetch()` before `navigator.clipboard.writeText()` consumes the transient user activation, breaking clipboard access. fix: eagerly create tracked share links when the menu opens (not on tap). added `navigator.share()` as fallback. 177 - 178 - **developer token scoping docs (PR #893)**: corrected misleading "full account access" language — tokens are actually scoped to `fm.plyr.*` via ATProto OAuth. fixed stale `resolved_scope` examples. 179 - 180 - **artist page track limit (PR #894)**: initial load was showing 50 tracks (backend default), burying albums below the fold. reduced to 5 with "load more" (10 per click). 181 - 182 - --- 183 - 184 - #### OAuth permission set cleanup + docs audit (PR #889, Feb 8) 185 - 186 - **OAuth permission set**: authorization page was showing raw NSID (`fm.plyr.authFullApp`) instead of human-readable description. root cause: ATProto permission sets use `detail` field (not `description`) for the subtitle text. updated lexicon and publish script, republished to PDS. also modernized publish script from raw `os.environ` to pydantic settings. 187 - 188 - **docs audit (PR #888)**: fixed stale/broken documentation across 6 files — wrong table names in copyright docs, outdated pool_recycle values, broken links in docs index, missing tools entries. updated README to reflect full feature set. added semantic search and playlists to search.md. 189 - 190 - --- 191 - 192 - #### auth state refresh + backend refactor (PRs #886-887, Feb 8) 193 - 194 - **auth state refresh (PR #887)**: after account switch or login, stale user data persisted because `AuthManager.initialize()` no-ops once the `initialized` flag is set. added `refresh()` that resets the flag before re-fetching, used in all exchange-token call sites. 195 - 196 - **backend package split (PR #886)**: split three monolith files into focused packages: 197 - - `auth.py` (1,400 lines) → `auth/` package (8 modules) 198 - - `background_tasks.py` (803 lines) → `tasks/` package (5 domain modules: copyright, ml, pds, storage, sync) 199 - - 5 `*_client.py` files → `clients/` package 200 - - extracted upload pipeline into 7 named phase functions, shared tag ops to `utilities/tags.py` 201 - 202 - all public APIs preserved via `__init__.py` re-exports. 424 tests pass. 203 - 204 - --- 205 - 206 - #### portal pagination + perf optimization (PRs #878-879, Feb 8) 207 - 208 - **portal pagination (PR #878)**: `GET /tracks/me` now supports `limit`/`offset` pagination (default 10 per page). portal loads first 10 tracks with a "load more" button. export section uses total count for accurate messaging. 209 - 210 - **GET /tracks/top latency fix (PR #879)**: baseline p95 was 1.2s due to stale connection reconnects and redundant DB queries. 211 - - merged top-track-ids + like-counts into single `get_top_tracks_with_counts()` query (1 fewer round-trip) 212 - - scoped liked-track check to `track_id IN (...)` (10 rows) instead of all user likes 213 - - `pool_recycle` 7200s → 1800s to reduce stale connection spikes 214 - - authenticated requests dropped from 11 DB queries to 7. post-deploy p95: ~550ms 215 - - 14 new regression tests 216 - 217 - --- 218 - 219 - #### repo reorganization (PR #876, Feb 8) 220 - 221 - moved auxiliary services into `services/` (transcoder, moderation, clap) and infrastructure into `infrastructure/` (redis). updated all GitHub Actions workflows, pre-commit config, justfile module paths, and docs. 222 - 223 - --- 224 - 225 - #### auto-tag at upload + ML audit (PRs #870-872, Feb 7) 226 - 227 - **auto-tag on upload (PR #871)**: checkbox on the upload form ("auto-tag with recommended genres") that applies top genre tags after classification completes. ratio-to-top filter (>= 50% of top score, capped at 5), additive with manual tags. flag stored in `track.extra`, cleaned up after use. 228 - 229 - **genre/subgenre split (PR #870)**: compound Discogs labels like "Electronic---Ambient" now produce two separate tags ("electronic", "ambient") instead of one compound tag. 230 - 231 - **ML audit script (PR #872)**: `scripts/ml_audit.py` reports which tracks/artists have been processed by ML features. supports `--verbose` and `--check-embeddings` for privacy/ToS auditing. 232 - 233 - --- 234 - 235 - #### ML genre classification + suggested tags (PRs #864-868, Feb 6-7) 236 - 237 - **genre classification via Replicate**: tracks classified into genre labels using [effnet-discogs](https://replicate.com/mtg/effnet-discogs) on Replicate (EfficientNet trained on Discogs ~400 categories). 238 - 239 - - on upload: classification runs as docket background task if `REPLICATE_ENABLED=true` 240 - - on demand: `GET /tracks/{id}/recommended-tags` classifies on the fly if no cached predictions 241 - - predictions stored in `track.extra["genre_predictions"]` with file_id-based cache invalidation 242 - - raw Discogs labels cleaned to lowercase format. cost: ~$0.00019/run 243 - - Replicate SDK incompatible with Python 3.14 (pydantic v1) — uses httpx directly with `Prefer: wait` header 244 - 245 - **frontend UX (PR #868)**: suggested genre tags appear as clickable dashed-border chips in the portal edit modal. `$derived` reactively hides suggestions matching manually-typed tags. 246 - 247 - --- 248 - 249 - #### mood search (PRs #848-858, Feb 5-6) 250 - 251 - **search by how music sounds** — type "chill lo-fi beats" into the search bar and find tracks that match the vibe, not just the title. 252 - 253 - **architecture**: CLAP (Contrastive Language-Audio Pretraining) model hosted on Modal generates audio embeddings at upload time and text embeddings at search time. vectors stored in turbopuffer. keyword and semantic searches fire in parallel — keyword results appear instantly (~50ms), semantic results append when ready (~1-2s). 254 - 255 - **key design decisions**: 256 - - unified search: no mode toggle. keyword + semantic results merge by score, client-side deduplication removes overlap 257 - - graceful degradation: backend returns `available: false` instead of 502/503 when CLAP/turbopuffer are down 258 - - quality controls: distance threshold, spread check to filter low-signal results, result cap 259 - - gated behind `vibe-search` feature flag with version-aware terms re-acceptance 260 - 261 - **hardening (PRs #849-858)**: m4a support for CLAP, correct R2 URLs, normalize similarity scores, switch from larger_clap_music to clap-htsat-unfused, handle empty turbopuffer namespace, rename "vibe search" → "mood search", concurrent backfill script. 262 - 263 - --- 264 - 265 - #### recommended tags via audio similarity (PR #859, Feb 6) 266 - 267 - `GET /tracks/{track_id}/recommended-tags` finds tracks with similar CLAP embeddings in turbopuffer, aggregates their tags weighted by similarity score. excludes existing tags, normalizes scores to 0-1. replaced by genre classification (PR #864) but the endpoint pattern persisted. 268 - 269 - --- 270 - 271 - #### mobile login UX + misc fixes (PRs #841-845, Feb 2) 272 - 273 - - **handle hint sizing (PRs #843-845)**: iterative fix for login page handle hint wrapping on mobile — final approach: reduced font size, gap, and `nowrap` to keep full text visible 274 - - **PDS backfill gate (PR #842)**: PDS backfill button gated behind `pds-audio-uploads` feature flag 275 - - **share button reuse (PR #841)**: track detail page now uses shared `ShareButton` component 156 + See `.status_history/2026-02.md` for Feb 2-12 history including: 157 + - playlist track recommendations via CLAP embeddings (PRs #895-898) 158 + - main.py extraction + bug fixes (PRs #890-894) 159 + - OAuth permission set cleanup + docs audit (PRs #888-889) 160 + - auth state refresh + backend package split (PRs #886-887) 161 + - portal pagination + perf optimization (PRs #878-879) 162 + - repo reorganization (PR #876) 163 + - auto-tag at upload + ML audit (PRs #870-872) 164 + - ML genre classification + suggested tags (PRs #864-868) 165 + - mood search via CLAP + turbopuffer (PRs #848-858) 166 + - recommended tags via audio similarity (PR #859) 167 + - mobile login UX + misc fixes (PRs #841-845) 276 168 277 169 --- 278 170 ··· 323 215 324 216 ### current focus 325 217 326 - image performance and architecture cleanup: 96x96 WebP thumbnails for all artwork (track, album, playlist), storage protocol abstraction, backfill script for existing images. jams and PDS audio uploads shipped to all users (feature flags removed). homepage performance improved with Redis-cached follow graph and parallelized data fetching. 218 + jams shipped to all users — feature flag removed, output device mode (single-speaker) working. image performance: 96x96 WebP thumbnails for all artwork with storage protocol abstraction and backfill script. PDS audio uploads graduated to GA. homepage performance improved with Redis-cached follow graph and parallelized network artists fetch. ATProto scope parsing replaced with spec-compliant SDK implementation. 327 219 328 220 ### known issues 329 221 - iOS PWA audio may hang on first play after backgrounding ··· 387 279 - ✅ media export with concurrent downloads 388 280 - ✅ supporter-gated content via atprotofans 389 281 - ✅ listen receipts (tracked share links with visitor/listener stats) 390 - - ✅ jams — shared listening rooms with real-time sync via Redis Streams + WebSocket (feature-flagged) 282 + - ✅ jams — shared listening rooms with real-time sync via Redis Streams + WebSocket 283 + - ✅ 96x96 WebP thumbnails for artwork (track, album, playlist) 391 284 392 285 **albums** 393 286 - ✅ album CRUD with cover art ··· 498 391 499 392 --- 500 393 501 - this is a living document. last updated 2026-02-27 (image thumbnails, storage protocol, jam GA, network artists perf). 394 + this is a living document. last updated 2026-02-27 (archival of early Feb, jams GA, thumbnails, scope parsing, homepage perf). 502 395
update.wav

This is a binary file and will not be displayed.