audio streaming app plyr.fm
1# plyr.fm Status History - February 2026 (Early) 2 3Archived 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 15no feature flag needed — degrades gracefully when turbopuffer or embeddings are unavailable. 16 17see `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 53all 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 72moved 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