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#### confidential OAuth client (PRs #578, #580-582, Dec 12-13) 51 52**confidential client support** (PR #578): 53- implemented ATProto OAuth confidential client using `private_key_jwt` authentication 54- when `OAUTH_JWK` is configured, plyr.fm authenticates with a cryptographic key 55- confidential clients earn 180-day refresh tokens (vs 2-week for public clients) 56- added `/.well-known/jwks.json` endpoint for public key discovery 57- updated `/oauth-client-metadata.json` with confidential client fields 58 59**bug fixes** (PRs #580-582): 60- fixed client assertion JWT to use Authorization Server's issuer as `aud` claim (not token endpoint URL) 61- fixed JWKS endpoint to preserve `kid` field from original JWK 62- fixed `OAuthClient` to pass `client_secret_kid` for JWT header 63 64**atproto fork updates** (zzstoatzz/atproto#6, #7): 65- added `issuer` parameter to `_make_token_request()` for correct `aud` claim 66- added `client_secret_kid` parameter to include `kid` in client assertion JWT header 67 68**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. 69 70--- 71 72#### pagination & album management (PRs #550-554, Dec 9-10) 73 74**tracks list pagination** (PR #554): 75- cursor-based pagination on `/tracks/` endpoint (default 50 per page) 76- infinite scroll on homepage using native IntersectionObserver 77- zero new dependencies - uses browser APIs only 78- pagination state persisted to localStorage for fast subsequent loads 79 80**album management improvements** (PRs #550-552): 81- album delete and track reorder fixes 82- album page edit mode matching playlist UX (inline title editing, cover upload) 83- optimistic UI updates for album title changes (instant feedback) 84- ATProto record sync when album title changes (updates all track records + list record) 85 86**playlist show on profile** (PR #553): 87- restored "show on profile" toggle that was lost during inline editing refactor 88- users can now control whether playlists appear on their public profile 89 90--- 91 92#### public cost dashboard (PR #548, Dec 9) 93 94- `/costs` page showing live platform infrastructure costs 95- daily export to R2 via GitHub Action, proxied through `/stats/costs` endpoint 96- includes fly.io, neon, cloudflare, and audd API costs 97- ko-fi integration for community support 98 99#### docket background tasks & concurrent exports (PRs #534-546, Dec 9) 100 101**docket integration** (PRs #534, #536, #539): 102- migrated background tasks from inline asyncio to docket (Redis-backed task queue) 103- copyright scanning, media export, ATProto sync, and teal scrobbling now run via docket 104- graceful fallback to asyncio for local development without Redis 105- parallel test execution with xdist template databases (#540) 106 107**concurrent export downloads** (PR #545): 108- exports now download tracks in parallel (up to 4 concurrent) instead of sequentially 109- significantly faster for users with many tracks or large files 110- zip creation remains sequential (zipfile constraint) 111 112**ATProto refactor** (PR #534): 113- reorganized ATProto record code into `_internal/atproto/records/` by lexicon namespace 114- extracted `client.py` for low-level PDS operations 115- cleaner separation between plyr.fm and teal.fm lexicons 116 117**documentation & observability**: 118- AudD API cost tracking dashboard (#546) 119- promoted runbooks from sandbox to `docs/runbooks/` 120- updated CLAUDE.md files across the codebase 121 122--- 123 124#### artist support links & inline playlist editing (PRs #520-532, Dec 8) 125 126**artist support link** (PR #532): 127- artists can set a support URL (Ko-fi, Patreon, etc.) in their portal profile 128- support link displays as a button on artist profile pages next to the share button 129- URLs validated to require https:// prefix 130 131**inline playlist editing** (PR #531): 132- edit playlist name and description directly on playlist detail page 133- click-to-upload cover art replacement without modal 134- cleaner UX - no more edit modal popup 135 136**platform stats enhancements** (PRs #522, #528): 137- total duration displayed in platform stats (e.g., "42h 15m of music") 138- duration shown per artist in analytics section 139- combined stats and search into single centered container for cleaner layout 140 141**navigation & data loading fixes** (PR #527): 142- fixed stale data when navigating between detail pages of the same type 143- e.g., clicking from one artist to another now properly reloads data 144 145**copyright moderation improvements** (PR #480): 146- enhanced moderation workflow for copyright claims 147- improved labeler integration 148 149**letta-backed status maintenance** (PR #529): 150- automated status maintenance using Letta AI agent 151- agent reviews merged PRs and updates STATUS.md narratively 152 153--- 154 155#### playlist fast-follow fixes (PRs #507-519, Dec 7-8) 156 157**public playlist viewing** (PR #519): 158- playlists now publicly viewable without authentication 159- ATProto records are public by design - auth was unnecessary for read access 160- shared playlist URLs no longer redirect unauthenticated users to homepage 161 162**inline playlist creation** (PR #510): 163- clicking "create new playlist" from AddToMenu previously navigated to `/library?create=playlist` 164- this caused SvelteKit to reinitialize the layout, destroying the audio element and stopping playback 165- fix: added inline create form that creates playlist and adds track in one action without navigation 166 167**UI polish** (PRs #507-509, #515): 168- include `image_url` in playlist SSR data for og:image link previews 169- invalidate layout data after token exchange - fixes stale auth state after login 170- fixed stopPropagation blocking "create new playlist" link clicks 171- detail page button layouts: all buttons visible on mobile, centered AddToMenu on track detail 172- AddToMenu smart positioning: menu opens upward when near viewport bottom 173 174**documentation** (PR #514): 175- added lexicons overview documentation at `docs/lexicons/overview.md` 176- covers `fm.plyr.track`, `fm.plyr.like`, `fm.plyr.comment`, `fm.plyr.list`, `fm.plyr.actor.profile` 177 178--- 179 180#### playlists, ATProto sync, and library hub (PR #499, Dec 6-7) 181 182**playlists** (full CRUD): 183- create, rename, delete playlists with cover art upload 184- add/remove/reorder tracks with drag-and-drop 185- playlist detail page with edit modal 186- "add to playlist" menu on tracks with inline create 187- playlist sharing with OpenGraph link previews 188 189**ATProto integration**: 190- `fm.plyr.list` lexicon for syncing playlists/albums to user PDSes 191- `fm.plyr.actor.profile` lexicon for artist profiles 192- automatic sync of albums, liked tracks, profile on login 193 194**library hub** (`/library`): 195- unified page with tabs: liked, playlists, albums 196- nav changed from "liked" → "library" 197 198**related**: scope upgrade OAuth flow (PR #503), settings consolidation (PR #496) 199 200--- 201 202#### sensitive image moderation (PRs #471-488, Dec 5-6) 203 204- `sensitive_images` table flags problematic images 205- `show_sensitive_artwork` user preference 206- flagged images blurred everywhere: track lists, player, artist pages, search, embeds 207- Media Session API respects sensitive preference 208- SSR-safe filtering for og:image link previews 209 210--- 211 212#### teal.fm scrobbling (PR #467, Dec 4) 213 214- native scrobbling to user's PDS using teal's ATProto lexicons 215- scrobble at 30% or 30 seconds (same threshold as play counts) 216- toggle in settings, link to pdsls.dev to view records 217 218--- 219 220### Earlier December / November 2025 221 222See `.status_history/2025-12.md` and `.status_history/2025-11.md` for detailed history including: 223- unified search with Cmd+K (PR #447) 224- light/dark theme system (PR #441) 225- tag filtering and bufo easter egg (PRs #431-438) 226- developer tokens (PR #367) 227- copyright moderation system (PRs #382-395) 228- export & upload reliability (PRs #337-344) 229- transcoder API deployment (PR #156) 230 231## immediate priorities 232 233### known issues 234- playback auto-start on refresh (#225) 235- iOS PWA audio may hang on first play after backgrounding 236 237### immediate focus 238- **moderation cleanup**: consolidate copyright detection, reduce AudD API costs, streamline labeler integration (issues #541-544) 239 240### feature ideas 241- issue #334: add 'share to bluesky' option for tracks 242- issue #373: lyrics field and Genius-style annotations 243 244### backlog 245- audio transcoding pipeline integration (#153) - transcoder service deployed, integration deferred 246 247## technical state 248 249### architecture 250 251**backend** 252- language: Python 3.11+ 253- framework: FastAPI with uvicorn 254- database: Neon PostgreSQL (serverless) 255- storage: Cloudflare R2 (S3-compatible) 256- background tasks: docket (Redis-backed) 257- hosting: Fly.io (2x shared-cpu VMs) 258- observability: Pydantic Logfire 259- auth: ATProto OAuth 2.1 260 261**frontend** 262- framework: SvelteKit (v2.43.2) 263- runtime: Bun 264- hosting: Cloudflare Pages 265- styling: vanilla CSS with lowercase aesthetic 266- state management: Svelte 5 runes 267 268**deployment** 269- ci/cd: GitHub Actions 270- backend: automatic on main branch merge (fly.io) 271- frontend: automatic on every push to main (cloudflare pages) 272- migrations: automated via fly.io release_command 273 274**what's working** 275 276**core functionality** 277- ✅ ATProto OAuth 2.1 authentication 278- ✅ secure session management via HttpOnly cookies 279- ✅ developer tokens with independent OAuth grants 280- ✅ platform stats and Media Session API 281- ✅ timed comments with clickable timestamps 282- ✅ artist profiles synced with Bluesky 283- ✅ track upload with streaming 284- ✅ audio streaming via 307 redirects to R2 CDN 285- ✅ play count tracking, likes, queue management 286- ✅ unified search with Cmd/Ctrl+K 287- ✅ teal.fm scrobbling 288- ✅ copyright moderation with ATProto labeler 289- ✅ docket background tasks (copyright scan, export, atproto sync, scrobble) 290- ✅ media export with concurrent downloads 291 292**albums** 293- ✅ album CRUD with cover art 294- ✅ ATProto list records (auto-synced on login) 295 296**playlists** 297- ✅ full CRUD with drag-and-drop reordering 298- ✅ ATProto list records (synced on create/modify) 299- ✅ "add to playlist" menu, global search results 300 301**deployment URLs** 302- production frontend: https://plyr.fm 303- production backend: https://api.plyr.fm 304- staging: https://stg.plyr.fm / https://api-stg.plyr.fm 305 306### technical decisions 307 308**why Python/FastAPI instead of Rust?** 309- rapid prototyping velocity during MVP phase 310- trade-off: accepting higher latency for faster development 311 312**why Cloudflare R2 instead of S3?** 313- zero egress fees (critical for audio streaming) 314- S3-compatible API, integrated CDN 315 316**why async everywhere?** 317- I/O-bound workload: most time spent waiting on network/disk 318- PRs #149-151 eliminated all blocking operations 319 320## cost structure 321 322current monthly costs: ~$18/month (plyr.fm specific) 323 324see live dashboard: [plyr.fm/costs](https://plyr.fm/costs) 325 326- fly.io (plyr apps only): ~$12/month 327 - relay-api (prod): $5.80 328 - relay-api-staging: $5.60 329 - plyr-moderation: $0.24 330 - plyr-transcoder: $0.02 331- neon postgres: $5/month 332- cloudflare (R2 + pages + domain): ~$1.16/month 333- audd audio fingerprinting: $0-10/month (6000 free/month) 334- logfire: $0 (free tier) 335 336## admin tooling 337 338### content moderation 339script: `scripts/delete_track.py` 340 341usage: 342```bash 343uv run scripts/delete_track.py <track_id> --dry-run 344uv run scripts/delete_track.py <track_id> 345uv run scripts/delete_track.py --url https://plyr.fm/track/34 346``` 347 348## for new contributors 349 350### getting started 3511. clone: `gh repo clone zzstoatzz/plyr.fm` 3522. install dependencies: `uv sync && cd frontend && bun install` 3533. run backend: `uv run uvicorn backend.main:app --reload` 3544. run frontend: `cd frontend && bun run dev` 3555. visit http://localhost:5173 356 357### development workflow 3581. create issue on github 3592. create PR from feature branch 3603. ensure pre-commit hooks pass 3614. merge to main → deploys to staging 3625. create github release → deploys to production 363 364### key principles 365- type hints everywhere 366- lowercase aesthetic 367- ATProto first 368- async everywhere (no blocking I/O) 369- mobile matters 370- cost conscious 371 372### project structure 373``` 374plyr.fm/ 375├── backend/ # FastAPI app & Python tooling 376│ ├── src/backend/ # application code 377│ ├── tests/ # pytest suite 378│ └── alembic/ # database migrations 379├── frontend/ # SvelteKit app 380│ ├── src/lib/ # components & state 381│ └── src/routes/ # pages 382├── moderation/ # Rust moderation service (ATProto labeler) 383├── transcoder/ # Rust audio transcoding service 384├── docs/ # documentation 385└── justfile # task runner 386``` 387 388## documentation 389 390- [docs/README.md](docs/README.md) - documentation index 391- [runbooks](docs/runbooks/) - production incident procedures 392- [background tasks](docs/backend/background-tasks.md) - docket task system 393- [logfire querying](docs/tools/logfire.md) - observability queries 394- [moderation & labeler](docs/moderation/atproto-labeler.md) - copyright, sensitive content 395- [lexicons overview](docs/lexicons/overview.md) - ATProto record schemas 396 397--- 398 399this is a living document. last updated 2025-12-13.