music on atproto
plyr.fm
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.