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