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