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