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