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### January 2026 49 50#### copyright moderation improvements (PRs #703-704, Jan 2) 51 52**per legal advice**, redesigned copyright handling to reduce liability exposure: 53- **disabled auto-labeling** (PR #703): labels are no longer automatically emitted when copyright matches are detected. the system now only flags and notifies, leaving takedown decisions to humans 54- **raised threshold** (PR #703): copyright flag threshold increased from "any match" to configurable score (default 85%). controlled via `MODERATION_COPYRIGHT_SCORE_THRESHOLD` env var 55- **DM notifications** (PR #704): when a track is flagged, both the artist and admin receive BlueSky DMs with details. includes structured error handling for when users have DMs disabled 56- **observability** (PR #704): Logfire spans added to all notification paths (`send_dm`, `copyright_notification`) with error categorization (`dm_blocked`, `network`, `auth`, `unknown`) 57- **notification tracking**: `notified_at` field added to `copyright_scans` table to track which flags have been communicated 58 59**why this matters**: DMCA safe harbor requires taking action on notices, not proactively policing. auto-labeling was creating liability by making assertions about copyright status. human review is now required before any takedown action. 60 61--- 62 63#### ATProto OAuth permission sets (PRs #697-698, Jan 1-2) 64 65**permission sets enabled** - OAuth now uses `include:fm.plyr.authFullApp` instead of listing individual `repo:` scopes: 66- users see clean "plyr.fm" permission title instead of raw collection names 67- permission set lexicon published to `com.atproto.lexicon.schema` on plyr.fm authority repo 68- DNS TXT records at `_lexicon.plyr.fm` and `_lexicon.stg.plyr.fm` link namespaces to authority DID 69- fixed scope validation in atproto SDK fork to handle PDS permission expansion (`include:``repo?collection=`) 70 71**why this matters**: permission sets are ATProto's mechanism for defining platform access tiers. enables future third-party integrations (mobile apps, read-only stats dashboards) to request semantic permission bundles instead of raw collection lists. 72 73**docs**: [lexicons/overview.md](docs/lexicons/overview.md), [research/2026-01-01-atproto-oauth-permission-sets.md](docs/research/2026-01-01-atproto-oauth-permission-sets.md) 74 75--- 76 77#### atprotofans supporters display (PRs #695-696, Jan 1) 78 79**supporters now visible on artist pages** - artists using atprotofans can show their supporters: 80- compact overlapping avatar circles (GitHub sponsors style) with "+N" overflow badge 81- clicks link to supporter's plyr.fm artist page (keeps users in-app) 82- `POST /artists/batch` endpoint enriches supporter DIDs with avatar_url from our Artist table 83- frontend fetches from atprotofans, enriches via backend, renders with consistent avatar pattern 84 85**route ordering fix** (PR #696): FastAPI was matching `/artists/batch` as `/{did}` with did="batch". moved POST route before the catchall GET route. 86 87--- 88 89#### UI polish (PRs #692-694, Dec 31 - Jan 1) 90 91- **feed/library toggle** (PR #692): consistent header layout with toggle between feed and library views 92- **shuffle button moved** (PR #693): shuffle now in queue component instead of player controls 93- **justfile consistency** (PR #694): standardized `just run` across frontend/backend modules 94 95--- 96 97### December 2025 98 99#### avatar sync on login (PR #685, Dec 31) 100 101**avatars now stay fresh** - previously set once at artist creation, causing stale/broken avatars throughout the app: 102- on login, avatar is refreshed from Bluesky and synced to both postgres and ATProto profile record 103- added `avatar` field to `fm.plyr.actor.profile` lexicon (optional, URI format) 104- one-time backfill script (`scripts/backfill_avatars.py`) refreshed 28 stale avatars in production 105 106--- 107 108#### self-hosted redis (PR #674-675, Dec 30) 109 110**replaced Upstash with self-hosted Redis on Fly.io** - ~$75/month → ~$4/month: 111- Upstash pay-as-you-go was charging per command (37M commands = $75) 112- self-hosted Redis on 256MB Fly VMs costs fixed ~$2/month per environment 113- deployed `plyr-redis` (prod) and `plyr-redis-stg` (staging) 114- added CI workflow for redis deployments on merge 115 116**no state migration needed** - docket stores ephemeral task queue data, job progress lives in postgres. 117 118--- 119 120#### supporter-gated content (PR #637, Dec 22-23) 121 122**atprotofans paywall integration** - artists can now mark tracks as "supporters only": 123- tracks with `support_gate` require atprotofans validation before playback 124- non-supporters see lock icon and "become a supporter" CTA linking to atprotofans 125- artists can always play their own gated tracks 126 127**backend architecture**: 128- audio endpoint validates supporter status via atprotofans API before serving gated content 129- HEAD requests return 200/401/402 for pre-flight auth checks (avoids CORS issues) 130- `R2Storage.move_audio()` moves files between public/private buckets when toggling gate 131- background task handles bucket migration asynchronously 132- ATProto record syncs when toggling gate (updates `supportGate` field and `audioUrl`) 133 134**frontend**: 135- `playback.svelte.ts` guards queue operations with gated checks BEFORE modifying state 136- clicking locked track shows toast with CTA - does NOT interrupt current playback 137- portal shows support gate toggle in track edit UI 138 139--- 140 141#### supporter badges (PR #627, Dec 21-22) 142 143**phase 1 of atprotofans integration**: 144- supporter badge displays on artist pages when logged-in viewer supports the artist 145- calls atprotofans `validateSupporter` API directly from frontend (public endpoint) 146- badge only shows when viewer is authenticated and not viewing their own profile 147 148--- 149 150#### rate limit moderation endpoint (PR #629, Dec 21) 151 152**incident response**: detected suspicious activity - 72 requests in 17 seconds from a single IP targeting `/moderation/sensitive-images`. added `10/minute` rate limit using existing slowapi infrastructure. 153 154--- 155 156#### end-of-year sprint planning (PR #626, Dec 20) 157 158**focus**: two foundational systems need solid experimental implementations by 2026. 159 160| track | focus | status | 161|-------|-------|--------| 162| moderation | consolidate architecture, add rules engine | in progress | 163| atprotofans | supporter validation, content gating | shipped (phase 1-3) | 164 165**research docs**: 166- [moderation architecture overhaul](docs/research/2025-12-20-moderation-architecture-overhaul.md) 167- [atprotofans paywall integration](docs/research/2025-12-20-atprotofans-paywall-integration.md) 168 169--- 170 171#### beartype + moderation cleanup (PRs #617-619, Dec 19) 172 173**runtime type checking** (PR #619): 174- enabled beartype runtime type validation across the backend 175- catches type errors at runtime instead of silently passing bad data 176- test infrastructure improvements: session-scoped TestClient fixture (5x faster tests) 177 178**moderation cleanup** (PRs #617-618): 179- consolidated moderation code, addressing issues #541-543 180- `sync_copyright_resolutions` now runs automatically via docket Perpetual task 181- removed dead `init_db()` from lifespan (handled by alembic migrations) 182 183--- 184 185#### UX polish (PRs #604-607, #613, #615, Dec 16-18) 186 187**login improvements** (PRs #604, #613): 188- login page now uses "internet handle" terminology for clarity 189- input normalization: strips `@` and `at://` prefixes automatically 190 191**artist page fixes** (PR #615): 192- track pagination on artist pages now works correctly 193- fixed mobile album card overflow 194 195**mobile + metadata** (PRs #605-607): 196- Open Graph tags added to tag detail pages for link previews 197- mobile modals now use full screen positioning 198- fixed `/tag/` routes in hasPageMetadata check 199 200--- 201 202#### offline mode foundation (PRs #610-611, Dec 17) 203 204**experimental offline playback**: 205- storage layer using Cache API for audio bytes + IndexedDB for metadata 206- `GET /audio/{file_id}/url` backend endpoint returns direct R2 URLs for client-side caching 207- "auto-download liked" toggle in experimental settings section 208- Player checks for cached audio before streaming from R2 209 210--- 211 212### Earlier December 2025 213 214See `.status_history/2025-12.md` for detailed history including: 215- visual customization with custom backgrounds (PRs #595-596, Dec 16) 216- performance & moderation polish (PRs #586-593, Dec 14-15) 217- mobile UI polish & background task expansion (PRs #558-572, Dec 10-12) 218- confidential OAuth client for 180-day sessions (PRs #578-582, Dec 12-13) 219- pagination & album management (PRs #550-554, Dec 9-10) 220- public cost dashboard (PRs #548-549, Dec 9) 221- docket background tasks & concurrent exports (PRs #534-546, Dec 9) 222- artist support links & inline playlist editing (PRs #520-532, Dec 8) 223- playlist fast-follow fixes (PRs #507-519, Dec 7-8) 224- playlists, ATProto sync, and library hub (PR #499, Dec 6-7) 225- sensitive image moderation (PRs #471-488, Dec 5-6) 226- teal.fm scrobbling (PR #467, Dec 4) 227- unified search with Cmd+K (PR #447, Dec 3) 228- light/dark theme system (PR #441, Dec 2-3) 229- tag filtering and bufo easter egg (PRs #431-438, Dec 2) 230 231### November 2025 232 233See `.status_history/2025-11.md` for detailed history including: 234- developer tokens (PR #367) 235- copyright moderation system (PRs #382-395) 236- export & upload reliability (PRs #337-344) 237- transcoder API deployment (PR #156) 238 239## immediate priorities 240 241### quality of life mode (Dec 29-31) 242 243end-of-year sprint [#625](https://github.com/zzstoatzz/plyr.fm/issues/625) complete. remaining days before 2026 are for minor polish and bug fixes as they arise. 244 245**what shipped in the sprint:** 246- moderation consolidation: sensitive images moved to moderation service (#644) 247- atprotofans: supporter badges (#627) and content gating (#637) 248 249**aspirational (deferred until scale justifies):** 250- configurable rules engine for moderation 251- time-release gating (#642) 252 253### known issues 254- playback auto-start on refresh (#225) 255- iOS PWA audio may hang on first play after backgrounding 256 257### backlog 258- audio transcoding pipeline integration (#153) - transcoder service deployed, integration deferred 259- share to bluesky (#334) 260- lyrics and annotations (#373) 261 262## technical state 263 264### architecture 265 266**backend** 267- language: Python 3.11+ 268- framework: FastAPI with uvicorn 269- database: Neon PostgreSQL (serverless) 270- storage: Cloudflare R2 (S3-compatible) 271- background tasks: docket (Redis-backed) 272- hosting: Fly.io (2x shared-cpu VMs) 273- observability: Pydantic Logfire 274- auth: ATProto OAuth 2.1 275 276**frontend** 277- framework: SvelteKit (v2.43.2) 278- runtime: Bun 279- hosting: Cloudflare Pages 280- styling: vanilla CSS with lowercase aesthetic 281- state management: Svelte 5 runes 282 283**deployment** 284- ci/cd: GitHub Actions 285- backend: automatic on main branch merge (fly.io) 286- frontend: automatic on every push to main (cloudflare pages) 287- migrations: automated via fly.io release_command 288 289**what's working** 290 291**core functionality** 292- ✅ ATProto OAuth 2.1 authentication 293- ✅ secure session management via HttpOnly cookies 294- ✅ developer tokens with independent OAuth grants 295- ✅ platform stats and Media Session API 296- ✅ timed comments with clickable timestamps 297- ✅ artist profiles synced with Bluesky 298- ✅ track upload with streaming 299- ✅ audio streaming via 307 redirects to R2 CDN 300- ✅ play count tracking, likes, queue management 301- ✅ unified search with Cmd/Ctrl+K 302- ✅ teal.fm scrobbling 303- ✅ copyright moderation with ATProto labeler 304- ✅ docket background tasks (copyright scan, export, atproto sync, scrobble) 305- ✅ media export with concurrent downloads 306- ✅ supporter-gated content via atprotofans 307 308**albums** 309- ✅ album CRUD with cover art 310- ✅ ATProto list records (auto-synced on login) 311 312**playlists** 313- ✅ full CRUD with drag-and-drop reordering 314- ✅ ATProto list records (synced on create/modify) 315- ✅ "add to playlist" menu, global search results 316 317**deployment URLs** 318- production frontend: https://plyr.fm 319- production backend: https://api.plyr.fm 320- staging: https://stg.plyr.fm / https://api-stg.plyr.fm 321 322### technical decisions 323 324**why Python/FastAPI instead of Rust?** 325- rapid prototyping velocity during MVP phase 326- trade-off: accepting higher latency for faster development 327 328**why Cloudflare R2 instead of S3?** 329- zero egress fees (critical for audio streaming) 330- S3-compatible API, integrated CDN 331 332**why async everywhere?** 333- I/O-bound workload: most time spent waiting on network/disk 334- PRs #149-151 eliminated all blocking operations 335 336## cost structure 337 338current monthly costs: ~$20/month (plyr.fm specific) 339 340see live dashboard: [plyr.fm/costs](https://plyr.fm/costs) 341 342- fly.io (backend + redis + moderation): ~$14/month 343- neon postgres: $5/month 344- cloudflare (R2 + pages + domain): ~$1/month 345- audd audio fingerprinting: $5-10/month (usage-based) 346- logfire: $0 (free tier) 347 348## admin tooling 349 350### content moderation 351script: `scripts/delete_track.py` 352 353usage: 354```bash 355uv run scripts/delete_track.py <track_id> --dry-run 356uv run scripts/delete_track.py <track_id> 357uv run scripts/delete_track.py --url https://plyr.fm/track/34 358``` 359 360## for new contributors 361 362### getting started 3631. clone: `gh repo clone zzstoatzz/plyr.fm` 3642. install dependencies: `uv sync && cd frontend && bun install` 3653. run backend: `uv run uvicorn backend.main:app --reload` 3664. run frontend: `cd frontend && bun run dev` 3675. visit http://localhost:5173 368 369### development workflow 3701. create issue on github 3712. create PR from feature branch 3723. ensure pre-commit hooks pass 3734. merge to main → deploys to staging 3745. create github release → deploys to production 375 376### key principles 377- type hints everywhere 378- lowercase aesthetic 379- ATProto first 380- async everywhere (no blocking I/O) 381- mobile matters 382- cost conscious 383 384### project structure 385``` 386plyr.fm/ 387├── backend/ # FastAPI app & Python tooling 388│ ├── src/backend/ # application code 389│ ├── tests/ # pytest suite 390│ └── alembic/ # database migrations 391├── frontend/ # SvelteKit app 392│ ├── src/lib/ # components & state 393│ └── src/routes/ # pages 394├── moderation/ # Rust moderation service (ATProto labeler) 395├── transcoder/ # Rust audio transcoding service 396├── redis/ # self-hosted Redis config 397├── docs/ # documentation 398└── justfile # task runner 399``` 400 401## documentation 402 403- [docs/README.md](docs/README.md) - documentation index 404- [runbooks](docs/runbooks/) - production incident procedures 405- [background tasks](docs/backend/background-tasks.md) - docket task system 406- [logfire querying](docs/tools/logfire.md) - observability queries 407- [moderation & labeler](docs/moderation/atproto-labeler.md) - copyright, sensitive content 408- [lexicons overview](docs/lexicons/overview.md) - ATProto record schemas 409 410--- 411 412this is a living document. last updated 2026-01-02.