audio streaming app plyr.fm
1# plyr.fm Status History - November 2025 2 3## November 2025 Work 4 5### ATProto labeler and admin UI (PRs #385-395, Nov 29-Dec 1) 6 7**motivation**: integrate with ATProto labeling protocol for proper copyright violation signaling, and improve admin tooling for reviewing flagged content. 8 9**what shipped**: 10- **ATProto labeler implementation** (PRs #385, #391): 11 - standalone labeler service integrated into moderation Rust service 12 - implements `com.atproto.label.queryLabels` and `subscribeLabels` XRPC endpoints 13 - k256 ECDSA signing for cryptographic label verification 14 - SQLite storage for labels with sequence numbers 15 - labels emitted when copyright violations detected 16 - negation labels for false positive resolution 17- **admin UI** (PRs #390, #392, #395): 18 - web interface at `/admin` for reviewing copyright flags 19 - htmx for server-rendered interactivity (no inline JS bloat) 20 - static files extracted to `moderation/static/` for proper syntax highlighting 21 - plyr.fm design tokens for brand consistency 22 - shows track title, artist handle, match scores, and potential matches 23 - "mark false positive" button emits negation label 24- **label context enrichment** (PR #392): 25 - labels now include track_title, artist_handle, artist_did, highest_score, matches 26 - backfill script (`scripts/backfill_label_context.py`) populated 25 existing flags 27 - admin UI displays rich context instead of just ATProto URIs 28- **copyright flag visibility** (PRs #387, #389): 29 - artist portal shows copyright flag indicator on flagged tracks 30 - tooltip shows primary match (artist - title) for quick context 31- **documentation** (PR #386): 32 - comprehensive docs at `docs/moderation/atproto-labeler.md` 33 - covers architecture, label schema, XRPC protocol, signing keys 34 35**admin UI architecture**: 36- `moderation/static/admin.html` - page structure 37- `moderation/static/admin.css` - plyr.fm design tokens 38- `moderation/static/admin.js` - auth handling (~40 lines) 39- htmx endpoints: `/admin/flags-html`, `/admin/resolve-htmx` 40- server-rendered HTML partials for flag cards 41 42--- 43 44### copyright moderation system (PRs #382, #384, Nov 29-30) 45 46**motivation**: detect potential copyright violations in uploaded tracks to avoid DMCA issues and protect the platform. 47 48**what shipped**: 49- **moderation service** (Rust/Axum on Fly.io): 50 - standalone service at `plyr-moderation.fly.dev` 51 - integrates with AuDD enterprise API for audio fingerprinting 52 - scans audio URLs and returns matches with metadata (artist, title, album, ISRC, timecode) 53 - auth via `X-Moderation-Key` header 54- **backend integration** (PR #382): 55 - `ModerationSettings` in config (service URL, auth token, timeout) 56 - moderation client module (`backend/_internal/moderation.py`) 57 - fire-and-forget background task on track upload 58 - stores results in `copyright_scans` table 59 - scan errors stored as "clear" so tracks aren't stuck unscanned 60- **flagging fix** (PR #384): 61 - AuDD enterprise API returns no confidence scores (all 0) 62 - changed from score threshold to presence-based flagging: `is_flagged = !matches.is_empty()` 63 - removed unused `score_threshold` config 64- **backfill script** (`scripts/scan_tracks_copyright.py`): 65 - scans existing tracks that haven't been checked 66 - `--max-duration` flag to skip long DJ sets (estimated from file size) 67 - `--dry-run` mode to preview what would be scanned 68 - supports dev/staging/prod environments 69- **review workflow**: 70 - `copyright_scans` table has `resolution`, `reviewed_at`, `reviewed_by`, `review_notes` columns 71 - resolution values: `violation`, `false_positive`, `original_artist` 72 73**initial review results** (25 flagged tracks): 74- 8 violations (actual copyright issues) 75- 11 false positives (fingerprint noise) 76- 6 original artists (people uploading their own distributed music) 77 78--- 79 80### developer tokens with independent OAuth grants (PR #367, Nov 28) 81 82**motivation**: programmatic API access (scripts, CLIs, automation) needed tokens that survive browser logout and don't become stale when browser sessions refresh. 83 84**what shipped**: 85- **OAuth-based dev tokens**: each developer token gets its own OAuth authorization flow 86 - user clicks "create token" → redirected to PDS for authorization → token created with independent credentials 87 - tokens have their own DPoP keypair, access/refresh tokens - completely separate from browser session 88- **cookie isolation**: dev token exchange doesn't set browser cookie 89 - added `is_dev_token` flag to ExchangeToken model 90 - /auth/exchange skips Set-Cookie for dev token flows 91 - prevents logout from deleting dev tokens (critical bug fixed during implementation) 92- **token management UI**: portal → "your data" → "developer tokens" 93 - create with optional name and expiration (30/90/180/365 days or never) 94 - list active tokens with creation/expiration dates 95 - revoke individual tokens 96- **API endpoints**: 97 - `POST /auth/developer-token/start` - initiates OAuth flow, returns auth_url 98 - `GET /auth/developer-tokens` - list user's tokens 99 - `DELETE /auth/developer-tokens/{prefix}` - revoke by 8-char prefix 100 101**security properties**: 102- tokens are full sessions with encrypted OAuth credentials (Fernet) 103- each token refreshes independently (no staleness from browser session refresh) 104- revokable individually without affecting browser or other tokens 105- explicit OAuth consent required at PDS for each token created 106 107**documentation**: see `docs/authentication.md` "developer tokens" section 108 109--- 110 111### platform stats and media session integration (PRs #359-379, Nov 27-29) 112 113**motivation**: show platform activity at a glance, improve playback experience across devices, and give users control over their data. 114 115**what shipped**: 116- **platform stats endpoint and UI** (PRs #376, #378, #379): 117 - `GET /stats` returns total plays, tracks, and artists 118 - stats bar displays in homepage header (e.g., "1,691 plays • 55 tracks • 8 artists") 119 - skeleton loading animation while fetching 120 - responsive layout: visible in header on wide screens, collapses to menu on narrow 121 - end-of-list animation on homepage 122- **Media Session API** (PR #371): 123 - provides track metadata to CarPlay, lock screens, Bluetooth devices, macOS control center 124 - artwork display with fallback to artist avatar 125 - play/pause, prev/next, seek controls all work from system UI 126 - position state syncs scrubbers on external interfaces 127- **browser tab title** (PR #374): 128 - shows "track - artist • plyr.fm" while playing 129 - persists across page navigation 130 - reverts to page title when playback stops 131- **timed comments** (PR #359): 132 - comments capture timestamp when added during playback 133 - clickable timestamp buttons seek to that moment 134 - compact scrollable comments section on track pages 135- **constellation integration** (PR #360): 136 - queries constellation.microcosm.blue backlink index 137 - enables network-wide like counts (not just plyr.fm internal) 138 - environment-aware namespace handling 139- **account deletion** (PR #363): 140 - explicit confirmation flow (type handle to confirm) 141 - deletes all plyr.fm data (tracks, albums, likes, comments, preferences) 142 - optional ATProto record cleanup with clear warnings about orphaned references 143 144--- 145 146### oEmbed endpoint for Leaflet.pub embeds (PRs #355-358, Nov 25) 147 148**motivation**: plyr.fm tracks embedded in Leaflet.pub (via iframely) showed a black HTML5 audio box instead of our custom embed player. 149 150**what shipped**: 151- **oEmbed endpoint** (PR #355): `/oembed` returns proper embed HTML with iframe 152 - follows oEmbed spec with `type: "rich"` and iframe in `html` field 153 - discovery link in track page `<head>` for automatic detection 154- **iframely domain registration**: registered plyr.fm on iframely.com (free tier) 155 - this was the key fix - iframely now returns our embed iframe as `links.player[0]` 156 157**debugging journey** (PRs #356-358): 158- initially tried `og:video` meta tags to hint iframe embed - didn't work 159- tried removing `og:audio` to force oEmbed fallback - resulted in no player link 160- discovered iframely requires domain registration to trust oEmbed providers 161- after registration, iframely correctly returns embed iframe URL 162 163--- 164 165### export & upload reliability (PRs #337-344, Nov 24) 166 167**motivation**: exports were failing silently on large files (OOM), uploads showed incorrect progress, and SSE connections triggered false error toasts. 168 169**what shipped**: 170- **database-backed jobs** (PR #337): moved upload/export tracking from in-memory to postgres 171 - jobs table persists state across server restarts 172 - enables reliable progress tracking via SSE polling 173- **streaming exports** (PR #343): fixed OOM on large file exports 174 - previously loaded entire files into memory via `response["Body"].read()` 175 - now streams to temp files, adds to zip from disk (constant memory) 176 - 90-minute WAV files now export successfully on 1GB VM 177- **progress tracking fix** (PR #340): upload progress was receiving bytes but treating as percentage 178 - `UploadProgressTracker` now properly converts bytes to percentage 179 - upload progress bar works correctly again 180- **UX improvements** (PRs #338-339, #341-342, #344): 181 - export filename now includes date (`plyr-tracks-2025-11-24.zip`) 182 - toast notification on track deletion 183 - fixed false "lost connection" error when SSE completes normally 184 - progress now shows "downloading track X of Y" instead of confusing count 185 186--- 187 188### queue hydration + ATProto token hardening (Nov 12) 189 190**why**: queue endpoints were occasionally taking 2s+ and restore operations could 401 191when multiple requests refreshed an expired ATProto token simultaneously. 192 193**what shipped**: 194- added persistent `image_url` on `Track` rows so queue hydration no longer probes R2 195 for every track. Queue payloads now pull art directly from Postgres, with a one-time 196 fallback for legacy rows. 197- updated `_internal/queue.py` to backfill any missing URLs once (with caching) instead 198 of per-request GETs. 199- introduced per-session locks in `_refresh_session_tokens` so only one coroutine hits 200 `oauth_client.refresh_session` at a time; others reuse the refreshed tokens. This 201 removes the race that caused the batch restore flow to intermittently 500/401. 202 203**impact**: queue tail latency dropped back under 500 ms in staging tests, ATProto restore flows are now reliable under concurrent use, and Logfire no longer shows 500s from the PDS. 204 205--- 206 207### performance optimization session (Nov 12) 208 209**issue: slow /tracks/liked endpoint** 210 211**symptoms**: 212- `/tracks/liked` taking 600-900ms consistently 213- only ~25ms spent in database queries 214- mysterious 575ms gap with no spans in Logfire traces 215 216**root cause**: 217- PR #184 added `image_url` column to tracks table to eliminate N+1 R2 API calls 218- legacy tracks (15 tracks uploaded before PR) had `image_url = NULL` 219- fallback code called `track.get_image_url()` which makes uninstrumented R2 `head_object` API calls 220- 5 tracks × 120ms = ~600ms of uninstrumented latency 221 222**solution**: created `scripts/backfill_image_urls.py` to populate missing `image_url` values 223 224**results**: 225- `/tracks/liked` now sub-200ms (down from 600-900ms) 226- all endpoints now consistently sub-second response times 227 228**database cleanup**: 229- discovered `queue_state` had 265% bloat (53 dead rows, 20 live rows) 230- ran `VACUUM (FULL, ANALYZE) queue_state` against production 231 232--- 233 234### track detail pages (PR #164, Nov 12) 235 236- ✅ dedicated track detail pages with large cover art 237- ✅ play button updates queue state correctly (#169) 238- ✅ liked state loaded efficiently via server-side fetch 239- ✅ mobile-optimized layouts with proper scrolling constraints 240- ✅ origin validation for image URLs (#168) 241 242--- 243 244### liked tracks feature (PR #157, Nov 11) 245 246- ✅ server-side persistent collections 247- ✅ ATProto record publication for cross-platform visibility 248- ✅ UI for adding/removing tracks from liked collection 249- ✅ like counts displayed in track responses and analytics (#170) 250- ✅ analytics cards now clickable links to track detail pages (#171) 251- ✅ liked state shown on artist page tracks (#163) 252 253**status**: COMPLETE (issue #144 closed) 254 255--- 256 257### upload streaming + progress UX (PR #182, Nov 11) 258 259- Frontend switched from `fetch` to `XMLHttpRequest` so we can display upload progress 260 toasts (critical for >50 MB mixes on mobile). 261- Upload form now clears only after the request succeeds; failed attempts leave the 262 form intact so users don't lose metadata. 263- Backend writes uploads/images to temp files in 8 MB chunks before handing them to the 264 storage layer, eliminating whole-file buffering and iOS crashes for hour-long mixes. 265- Deployment verified locally and by rerunning the exact repro Stella hit (85 minute 266 mix from mobile). 267 268--- 269 270### transcoder API deployment (PR #156, Nov 11) 271 272**standalone Rust transcoding service** 🎉 273- **deployed**: https://plyr-transcoder.fly.dev/ 274- **purpose**: convert AIFF/FLAC/etc. to MP3 for browser compatibility 275- **technology**: Axum + ffmpeg + Docker 276- **security**: `X-Transcoder-Key` header authentication (shared secret) 277- **capacity**: handles 1GB uploads, tested with 85-minute AIFF files (~858MB → 195MB MP3 in 32 seconds) 278- **architecture**: 279 - 2 Fly machines for high availability 280 - auto-stop/start for cost efficiency 281 - stateless design (no R2 integration yet) 282 - 320kbps MP3 output with proper ID3 tags 283- **status**: deployed and tested, ready for integration into plyr.fm upload pipeline 284- **next steps**: wire into backend with R2 integration and job queue (see issue #153) 285 286--- 287 288### AIFF/AIF browser compatibility fix (PR #152, Nov 11) 289 290**format validation improvements** 291- **problem discovered**: AIFF/AIF files only work in Safari, not Chrome/Firefox 292 - browsers throw `MediaError code 4: MEDIA_ERR_SRC_NOT_SUPPORTED` 293 - users could upload files but they wouldn't play in most browsers 294- **immediate solution**: reject AIFF/AIF uploads at both backend and frontend 295 - removed AIFF/AIF from AudioFormat enum 296 - added format hints to upload UI: "supported: mp3, wav, m4a" 297 - client-side validation with helpful error messages 298- **long-term solution**: deployed standalone transcoder service (see above) 299 - separate Rust/Axum service with ffmpeg 300 - accepts all formats, converts to browser-compatible MP3 301 - integration into upload pipeline pending (issue #153) 302 303**observability improvements**: 304- added logfire instrumentation to upload background tasks 305- added logfire spans to R2 storage operations 306- documented logfire querying patterns in `docs/logfire-querying.md` 307 308--- 309 310### async I/O performance fixes (PRs #149-151, Nov 10-11) 311 312Eliminated event loop blocking across backend with three critical PRs: 313 3141. **PR #149: async R2 reads** - converted R2 `head_object` operations from sync boto3 to async aioboto3 315 - portal page load time: 2+ seconds → ~200ms 316 - root cause: `track.image_url` was blocking on serial R2 HEAD requests 317 3182. **PR #150: concurrent PDS resolution** - parallelized ATProto PDS URL lookups 319 - homepage load time: 2-6 seconds → 200-400ms 320 - root cause: serial `resolve_atproto_data()` calls (8 artists × 200-300ms each) 321 - fix: `asyncio.gather()` for batch resolution, database caching for subsequent loads 322 3233. **PR #151: async storage writes/deletes** - made save/delete operations non-blocking 324 - R2: switched to `aioboto3` for uploads/deletes (async S3 operations) 325 - filesystem: used `anyio.Path` and `anyio.open_file()` for chunked async I/O (64KB chunks) 326 - impact: multi-MB uploads no longer monopolize worker thread, constant memory usage 327 328--- 329 330### mobile UI improvements (PRs #159-185, Nov 11-12) 331 332- ✅ compact action menus and better navigation (#161) 333- ✅ improved mobile responsiveness (#159) 334- ✅ consistent button layouts across mobile/desktop (#176-181, #185) 335- ✅ always show play count and like count on mobile (#177) 336- ✅ login page UX improvements (#174-175) 337- ✅ liked page UX improvements (#173) 338- ✅ accent color for liked tracks (#160)