1# plyr.fm - status 2 3## long-term vision 4 5### the problem 6 7today's music streaming is fundamentally broken: 8- spotify and apple music trap your data in proprietary silos 9- artists pay distribution fees and streaming cuts to multiple gatekeepers 10- listeners can't own their music collections - they rent them 11- switching platforms means losing everything: playlists, play history, social connections 12 13### the atproto solution 14 15plyr.fm is built on the AT Protocol (the protocol powering Bluesky) and enables: 16- **portable identity**: your music collection, playlists, and listening history belong to you, stored in your personal data server (PDS) 17- **decentralized distribution**: artists publish directly to the network without platform gatekeepers 18- **interoperable data**: any client can read your music records - you're not locked into plyr.fm 19- **authentic social**: artist profiles are real ATProto identities with verifiable handles (@artist.bsky.social) 20 21### the dream state 22 23plyr.fm should become: 24 251. **for artists**: the easiest way to publish music to the decentralized web 26 - upload once, available everywhere in the ATProto network 27 - direct connection to listeners without platform intermediaries 28 - real ownership of audience relationships 29 302. **for listeners**: a streaming platform where you actually own your data 31 - your collection lives in your PDS, playable by any ATProto music client 32 - switch between plyr.fm and other clients freely - your data travels with you 33 - share tracks as native ATProto posts to Bluesky 34 353. **for developers**: a reference implementation showing how to build on ATProto 36 - open source end-to-end example of ATProto integration 37 - demonstrates OAuth, record creation, federation patterns 38 - proves decentralized music streaming is viable 39 40--- 41 42**started**: October 28, 2025 (first commit: `454e9bc` - relay MVP with ATProto authentication) 43 44--- 45 46## recent work 47 48### December 2025 49 50#### rate limit moderation endpoint (PR #629, Dec 21) 51 52**incident response**: detected suspicious activity - 72 requests in 17 seconds from a single IP targeting `/moderation/sensitive-images`. investigation via Logfire showed: 53- single IP generating all traffic with no User-Agent header 54- requests spaced ~230ms apart (too consistent for human browsing) 55- no corresponding user activity (page loads, audio streams) 56 57**fix**: added `10/minute` rate limit to the endpoint using existing slowapi infrastructure. verified rate limiting works correctly post-deployment. 58 59--- 60 61#### end-of-year sprint (Dec 20-31) 62 63**focus**: two foundational systems need solid experimental implementations by 2026. 64 65**track 1: moderation architecture overhaul** 66- consolidate sensitive images into moderation service 67- add event-sourced audit trail 68- implement configurable rules (replace hard-coded thresholds) 69- informed by [Roost Osprey](https://github.com/roostorg/osprey) patterns and [Bluesky Ozone](https://github.com/bluesky-social/ozone) workflows 70 71**track 2: atprotofans paywall integration** 72- phase 1: read-only supporter validation (show badges) 73- phase 2: platform registration (artists create support tiers) 74- phase 3: content gating (track-level access control) 75 76**research docs**: 77- [moderation architecture overhaul](docs/research/2025-12-20-moderation-architecture-overhaul.md) 78- [atprotofans paywall integration](docs/research/2025-12-20-atprotofans-paywall-integration.md) 79 80**tracking**: issue #625 81 82--- 83 84#### beartype + moderation cleanup (PRs #617-619, Dec 19) 85 86**runtime type checking** (PR #619): 87- enabled beartype runtime type validation across the backend 88- catches type errors at runtime instead of silently passing bad data 89- test infrastructure improvements: session-scoped TestClient fixture (5x faster tests) 90- disabled automatic perpetual task scheduling in tests 91 92**moderation cleanup** (PRs #617-618): 93- consolidated moderation code, addressing issues #541-543 94- `sync_copyright_resolutions` now runs automatically via docket Perpetual task 95- removed `init_db()` from lifespan (handled by alembic migrations) 96 97--- 98 99#### UX polish (PRs #604-607, #613, #615, Dec 16-18) 100 101**login improvements** (PRs #604, #613): 102- login page now uses "internet handle" terminology for clarity 103- input normalization: strips `@` and `at://` prefixes automatically 104 105**artist page fixes** (PR #615): 106- track pagination on artist pages now works correctly 107- fixed mobile album card overflow 108 109**mobile + metadata** (PRs #605-607): 110- Open Graph tags added to tag detail pages for link previews 111- mobile modals now use full screen positioning 112- fixed `/tag/` routes in hasPageMetadata check 113 114**misc** (PRs #598-601): 115- upload button added to desktop header nav 116- background settings UX improvements 117- switched support link to atprotofans 118- AudD costs now derived from track duration for accurate billing 119 120--- 121 122#### offline mode foundation (PRs #610-611, Dec 17) 123 124**experimental offline playback**: 125- new storage layer using Cache API for audio bytes + IndexedDB for metadata 126- `GET /audio/{file_id}/url` backend endpoint returns direct R2 URLs for client-side caching 127- "auto-download liked" toggle in experimental settings section 128- when enabled, bulk-downloads all liked tracks and auto-downloads future likes 129- Player checks for cached audio before streaming from R2 130- works offline once tracks are downloaded 131 132**robustness improvements**: 133- IndexedDB connections properly closed after each operation 134- concurrent downloads deduplicated via in-flight promise tracking 135- stale metadata cleanup when cache entries are missing 136 137--- 138 139#### visual customization (PRs #595-596, Dec 16) 140 141**custom backgrounds** (PR #595): 142- users can set a custom background image URL in settings with optional tiling 143- new "playing artwork as background" toggle - uses current track's artwork as blurred page background 144- glass effect styling for track items (translucent backgrounds, subtle shadows) 145- new `ui_settings` JSONB column in preferences for extensible UI settings 146 147**bug fix** (PR #596): 148- removed 3D wheel scroll effect that was blocking like/share button clicks 149- root cause: `translateZ` transforms created z-index stacking that intercepted pointer events 150 151--- 152 153#### performance & UX polish (PRs #586-593, Dec 14-15) 154 155**performance improvements** (PRs #590-591): 156- removed moderation service call from `/tracks/` listing endpoint 157- removed copyright check from tag listing endpoint 158- faster page loads for track feeds 159 160**moderation agent** (PRs #586, #588): 161- added moderation agent script with audit trail support 162- improved moderation prompt and UI layout 163 164**bug fixes** (PRs #589, #592, #593): 165- fixed liked state display on playlist detail page 166- preserved album track order during ATProto sync 167- made header sticky on scroll for better mobile navigation 168 169**iOS Safari fixes** (PRs #573-576): 170- fixed AddToMenu visibility issue on iOS Safari 171- menu now correctly opens upward when near viewport bottom 172 173--- 174 175#### mobile UI polish & background task expansion (PRs #558-572, Dec 10-12) 176 177**background task expansion** (PRs #558, #561): 178- moved like/unlike and comment PDS writes to docket background tasks 179- API responses now immediate; PDS sync happens asynchronously 180- added targeted album list sync background task for ATProto record updates 181 182**performance caching** (PR #566): 183- added Redis cache for copyright label lookups (5-minute TTL) 184- fixed 2-3s latency spikes on `/tracks/` endpoint 185- batch operations via `mget`/pipeline for efficiency 186 187**mobile UX improvements** (PRs #569, #572): 188- mobile action menus now open from top with all actions visible 189- UI polish for album and artist pages on small screens 190 191**misc** (PRs #559, #562, #563, #570): 192- reduced docket Redis polling from 250ms to 5s (lower resource usage) 193- added atprotofans support link mode for ko-fi integration 194- added alpha badge to header branding 195- fixed web manifest ID for PWA stability 196 197--- 198 199#### confidential OAuth client (PRs #578, #580-582, Dec 12-13) 200 201**confidential client support** (PR #578): 202- implemented ATProto OAuth confidential client using `private_key_jwt` authentication 203- when `OAUTH_JWK` is configured, plyr.fm authenticates with a cryptographic key 204- confidential clients earn 180-day refresh tokens (vs 2-week for public clients) 205- added `/.well-known/jwks.json` endpoint for public key discovery 206- updated `/oauth-client-metadata.json` with confidential client fields 207 208**bug fixes** (PRs #580-582): 209- fixed client assertion JWT to use Authorization Server's issuer as `aud` claim (not token endpoint URL) 210- fixed JWKS endpoint to preserve `kid` field from original JWK 211- fixed `OAuthClient` to pass `client_secret_kid` for JWT header 212 213**atproto fork updates** (zzstoatzz/atproto#6, #7): 214- added `issuer` parameter to `_make_token_request()` for correct `aud` claim 215- added `client_secret_kid` parameter to include `kid` in client assertion JWT header 216 217**outcome**: users now get 180-day refresh tokens, and "remember this account" on the PDS authorization page works (auto-approves subsequent logins). see #583 for future work on account switching via OAuth `prompt` parameter. 218 219--- 220 221#### pagination & album management (PRs #550-554, Dec 9-10) 222 223**tracks list pagination** (PR #554): 224- cursor-based pagination on `/tracks/` endpoint (default 50 per page) 225- infinite scroll on homepage using native IntersectionObserver 226- zero new dependencies - uses browser APIs only 227- pagination state persisted to localStorage for fast subsequent loads 228 229**album management improvements** (PRs #550-552, #557): 230- album delete and track reorder fixes 231- album page edit mode matching playlist UX (inline title editing, cover upload) 232- optimistic UI updates for album title changes (instant feedback) 233- ATProto record sync when album title changes (updates all track records + list record) 234- fixed album slug sync on rename (prevented duplicate albums when adding tracks) 235 236**playlist show on profile** (PR #553): 237- restored "show on profile" toggle that was lost during inline editing refactor 238- users can now control whether playlists appear on their public profile 239 240--- 241 242#### public cost dashboard (PRs #548-549, Dec 9) 243 244- `/costs` page showing live platform infrastructure costs 245- daily export to R2 via GitHub Action, proxied through `/stats/costs` endpoint 246- dedicated `plyr-stats` R2 bucket with public access (shared across environments) 247- includes fly.io, neon, cloudflare, and audd API costs 248- ko-fi integration for community support 249 250#### docket background tasks & concurrent exports (PRs #534-546, Dec 9) 251 252**docket integration** (PRs #534, #536, #539): 253- migrated background tasks from inline asyncio to docket (Redis-backed task queue) 254- copyright scanning, media export, ATProto sync, and teal scrobbling now run via docket 255- graceful fallback to asyncio for local development without Redis 256- parallel test execution with xdist template databases (#540) 257 258**concurrent export downloads** (PR #545): 259- exports now download tracks in parallel (up to 4 concurrent) instead of sequentially 260- significantly faster for users with many tracks or large files 261- zip creation remains sequential (zipfile constraint) 262 263**ATProto refactor** (PR #534): 264- reorganized ATProto record code into `_internal/atproto/records/` by lexicon namespace 265- extracted `client.py` for low-level PDS operations 266- cleaner separation between plyr.fm and teal.fm lexicons 267 268**documentation & observability**: 269- AudD API cost tracking dashboard (#546) 270- promoted runbooks from sandbox to `docs/runbooks/` 271- updated CLAUDE.md files across the codebase 272 273--- 274 275#### artist support links & inline playlist editing (PRs #520-532, Dec 8) 276 277**artist support link** (PR #532): 278- artists can set a support URL (Ko-fi, Patreon, etc.) in their portal profile 279- support link displays as a button on artist profile pages next to the share button 280- URLs validated to require https:// prefix 281 282**inline playlist editing** (PR #531): 283- edit playlist name and description directly on playlist detail page 284- click-to-upload cover art replacement without modal 285- cleaner UX - no more edit modal popup 286 287**platform stats enhancements** (PRs #522, #528): 288- total duration displayed in platform stats (e.g., "42h 15m of music") 289- duration shown per artist in analytics section 290- combined stats and search into single centered container for cleaner layout 291 292**navigation & data loading fixes** (PR #527): 293- fixed stale data when navigating between detail pages of the same type 294- e.g., clicking from one artist to another now properly reloads data 295 296**copyright moderation improvements** (PR #480): 297- enhanced moderation workflow for copyright claims 298- improved labeler integration 299 300**status maintenance workflow** (PR #529): 301- automated status maintenance using claude-code-action 302- reviews merged PRs and updates STATUS.md narratively 303 304--- 305 306#### playlist fast-follow fixes (PRs #507-519, Dec 7-8) 307 308**public playlist viewing** (PR #519): 309- playlists now publicly viewable without authentication 310- ATProto records are public by design - auth was unnecessary for read access 311- shared playlist URLs no longer redirect unauthenticated users to homepage 312 313**inline playlist creation** (PR #510): 314- clicking "create new playlist" from AddToMenu previously navigated to `/library?create=playlist` 315- this caused SvelteKit to reinitialize the layout, destroying the audio element and stopping playback 316- fix: added inline create form that creates playlist and adds track in one action without navigation 317 318**UI polish** (PRs #507-509, #515): 319- include `image_url` in playlist SSR data for og:image link previews 320- invalidate layout data after token exchange - fixes stale auth state after login 321- fixed stopPropagation blocking "create new playlist" link clicks 322- detail page button layouts: all buttons visible on mobile, centered AddToMenu on track detail 323- AddToMenu smart positioning: menu opens upward when near viewport bottom 324 325**documentation** (PR #514): 326- added lexicons overview documentation at `docs/lexicons/overview.md` 327- covers `fm.plyr.track`, `fm.plyr.like`, `fm.plyr.comment`, `fm.plyr.list`, `fm.plyr.actor.profile` 328 329--- 330 331#### playlists, ATProto sync, and library hub (PR #499, Dec 6-7) 332 333**playlists** (full CRUD): 334- create, rename, delete playlists with cover art upload 335- add/remove/reorder tracks with drag-and-drop 336- playlist detail page with edit modal 337- "add to playlist" menu on tracks with inline create 338- playlist sharing with OpenGraph link previews 339 340**ATProto integration**: 341- `fm.plyr.list` lexicon for syncing playlists/albums to user PDSes 342- `fm.plyr.actor.profile` lexicon for artist profiles 343- automatic sync of albums, liked tracks, profile on login 344 345**library hub** (`/library`): 346- unified page with tabs: liked, playlists, albums 347- nav changed from "liked" → "library" 348 349**related**: scope upgrade OAuth flow (PR #503), settings consolidation (PR #496) 350 351--- 352 353#### sensitive image moderation (PRs #471-488, Dec 5-6) 354 355- `sensitive_images` table flags problematic images 356- `show_sensitive_artwork` user preference 357- flagged images blurred everywhere: track lists, player, artist pages, search, embeds 358- Media Session API respects sensitive preference 359- SSR-safe filtering for og:image link previews 360 361--- 362 363#### teal.fm scrobbling (PR #467, Dec 4) 364 365- native scrobbling to user's PDS using teal's ATProto lexicons 366- scrobble at 30% or 30 seconds (same threshold as play counts) 367- toggle in settings, link to pdsls.dev to view records 368 369--- 370 371### Earlier December / November 2025 372 373See `.status_history/2025-12.md` and `.status_history/2025-11.md` for detailed history including: 374- unified search with Cmd+K (PR #447) 375- light/dark theme system (PR #441) 376- tag filtering and bufo easter egg (PRs #431-438) 377- developer tokens (PR #367) 378- copyright moderation system (PRs #382-395) 379- export & upload reliability (PRs #337-344) 380- transcoder API deployment (PR #156) 381 382## immediate priorities 383 384### end-of-year sprint (Dec 20-31) 385 386see [sprint tracking issue #625](https://github.com/zzstoatzz/plyr.fm/issues/625) for details. 387 388| track | focus | status | 389|-------|-------|--------| 390| moderation | consolidate architecture, add rules engine | planning | 391| atprotofans | supporter validation, content gating | planning | 392 393### known issues 394- playback auto-start on refresh (#225) 395- iOS PWA audio may hang on first play after backgrounding 396 397### backlog 398- audio transcoding pipeline integration (#153) - transcoder service deployed, integration deferred 399- share to bluesky (#334) 400- lyrics and annotations (#373) 401 402## technical state 403 404### architecture 405 406**backend** 407- language: Python 3.11+ 408- framework: FastAPI with uvicorn 409- database: Neon PostgreSQL (serverless) 410- storage: Cloudflare R2 (S3-compatible) 411- background tasks: docket (Redis-backed) 412- hosting: Fly.io (2x shared-cpu VMs) 413- observability: Pydantic Logfire 414- auth: ATProto OAuth 2.1 415 416**frontend** 417- framework: SvelteKit (v2.43.2) 418- runtime: Bun 419- hosting: Cloudflare Pages 420- styling: vanilla CSS with lowercase aesthetic 421- state management: Svelte 5 runes 422 423**deployment** 424- ci/cd: GitHub Actions 425- backend: automatic on main branch merge (fly.io) 426- frontend: automatic on every push to main (cloudflare pages) 427- migrations: automated via fly.io release_command 428 429**what's working** 430 431**core functionality** 432- ✅ ATProto OAuth 2.1 authentication 433- ✅ secure session management via HttpOnly cookies 434- ✅ developer tokens with independent OAuth grants 435- ✅ platform stats and Media Session API 436- ✅ timed comments with clickable timestamps 437- ✅ artist profiles synced with Bluesky 438- ✅ track upload with streaming 439- ✅ audio streaming via 307 redirects to R2 CDN 440- ✅ play count tracking, likes, queue management 441- ✅ unified search with Cmd/Ctrl+K 442- ✅ teal.fm scrobbling 443- ✅ copyright moderation with ATProto labeler 444- ✅ docket background tasks (copyright scan, export, atproto sync, scrobble) 445- ✅ media export with concurrent downloads 446 447**albums** 448- ✅ album CRUD with cover art 449- ✅ ATProto list records (auto-synced on login) 450 451**playlists** 452- ✅ full CRUD with drag-and-drop reordering 453- ✅ ATProto list records (synced on create/modify) 454- ✅ "add to playlist" menu, global search results 455 456**deployment URLs** 457- production frontend: https://plyr.fm 458- production backend: https://api.plyr.fm 459- staging: https://stg.plyr.fm / https://api-stg.plyr.fm 460 461### technical decisions 462 463**why Python/FastAPI instead of Rust?** 464- rapid prototyping velocity during MVP phase 465- trade-off: accepting higher latency for faster development 466 467**why Cloudflare R2 instead of S3?** 468- zero egress fees (critical for audio streaming) 469- S3-compatible API, integrated CDN 470 471**why async everywhere?** 472- I/O-bound workload: most time spent waiting on network/disk 473- PRs #149-151 eliminated all blocking operations 474 475## cost structure 476 477current monthly costs: ~$18/month (plyr.fm specific) 478 479see live dashboard: [plyr.fm/costs](https://plyr.fm/costs) 480 481- fly.io (plyr apps only): ~$12/month 482 - relay-api (prod): $5.80 483 - relay-api-staging: $5.60 484 - plyr-moderation: $0.24 485 - plyr-transcoder: $0.02 486- neon postgres: $5/month 487- cloudflare (R2 + pages + domain): ~$1.16/month 488- audd audio fingerprinting: $0-10/month (6000 free/month) 489- logfire: $0 (free tier) 490 491## admin tooling 492 493### content moderation 494script: `scripts/delete_track.py` 495 496usage: 497```bash 498uv run scripts/delete_track.py <track_id> --dry-run 499uv run scripts/delete_track.py <track_id> 500uv run scripts/delete_track.py --url https://plyr.fm/track/34 501``` 502 503## for new contributors 504 505### getting started 5061. clone: `gh repo clone zzstoatzz/plyr.fm` 5072. install dependencies: `uv sync && cd frontend && bun install` 5083. run backend: `uv run uvicorn backend.main:app --reload` 5094. run frontend: `cd frontend && bun run dev` 5105. visit http://localhost:5173 511 512### development workflow 5131. create issue on github 5142. create PR from feature branch 5153. ensure pre-commit hooks pass 5164. merge to main → deploys to staging 5175. create github release → deploys to production 518 519### key principles 520- type hints everywhere 521- lowercase aesthetic 522- ATProto first 523- async everywhere (no blocking I/O) 524- mobile matters 525- cost conscious 526 527### project structure 528``` 529plyr.fm/ 530├── backend/ # FastAPI app & Python tooling 531│ ├── src/backend/ # application code 532│ ├── tests/ # pytest suite 533│ └── alembic/ # database migrations 534├── frontend/ # SvelteKit app 535│ ├── src/lib/ # components & state 536│ └── src/routes/ # pages 537├── moderation/ # Rust moderation service (ATProto labeler) 538├── transcoder/ # Rust audio transcoding service 539├── docs/ # documentation 540└── justfile # task runner 541``` 542 543## documentation 544 545- [docs/README.md](docs/README.md) - documentation index 546- [runbooks](docs/runbooks/) - production incident procedures 547- [background tasks](docs/backend/background-tasks.md) - docket task system 548- [logfire querying](docs/tools/logfire.md) - observability queries 549- [moderation & labeler](docs/moderation/atproto-labeler.md) - copyright, sensitive content 550- [lexicons overview](docs/lexicons/overview.md) - ATProto record schemas 551 552--- 553 554this is a living document. last updated 2025-12-21.