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#### 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.