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#### playlists, ATProto sync, and library hub (feat/playlists branch, PR #499, Dec 6-7) 51 52**status**: feature-complete, ready for final review. ~8k lines changed. 53 54**playlists** (full CRUD): 55- `playlists` and `playlist_tracks` tables with Alembic migration 56- `POST /lists/playlists` - create playlist 57- `PUT /lists/playlists/{id}` - rename playlist 58- `DELETE /lists/playlists/{id}` - delete playlist 59- `POST /lists/playlists/{id}/tracks` - add track to playlist 60- `DELETE /lists/playlists/{id}/tracks/{track_id}` - remove track 61- `PUT /lists/playlists/{id}/tracks/reorder` - reorder tracks 62- `POST /lists/playlists/{id}/cover` - upload cover art 63- playlist detail page (`/playlist/[id]`) with edit modal, drag-and-drop reordering 64- playlists in global search results 65- "add to playlist" menu on tracks (filters out current playlist when on playlist page) 66- "create new playlist" link in add-to menu → `/library?create=playlist` 67- playlist sharing with OpenGraph link previews 68 69**ATProto integration**: 70- `fm.plyr.list` lexicon for syncing playlists and albums to user PDSes 71- `fm.plyr.actor.profile` lexicon for syncing artist profiles 72- automatic sync of albums, liked tracks, and profile on login (fire-and-forget) 73- scope upgrade OAuth flow for teal.fm integration (#503) 74 75**library hub** (`/library`): 76- unified page with tabs: liked, playlists, albums 77- create playlist modal (accessible via `/library?create=playlist` deep link) 78- consistent card layouts across sections 79- nav changed from "liked" → "library" 80 81**user experience**: 82- public liked pages for any user (`/liked/[handle]`) 83- `show_liked_on_profile` preference 84- portal album/playlist section visual consistency 85- toast notifications for all mutations (playlist CRUD, profile updates) 86- z-index fixes for dropdown menus 87 88**accessibility fixes**: 89- fixed 32 svelte-check warnings (ARIA roles, button nesting, unused CSS) 90- proper roles on modals, menus, and drag-drop elements 91 92**design decisions**: 93- lists are generic ordered collections of any ATProto records 94- `listType` semantically categorizes (album, playlist, liked) but doesn't restrict content 95- array order = display order, reorder via `putRecord` 96- strongRef (uri + cid) for content-addressable item references 97- "library" = umbrella term for personal collections 98 99**sync architecture**: 100- **profile, albums, liked tracks**: synced on login via `GET /artists/me` (fire-and-forget background tasks) 101- **playlists**: synced on create/modify (not at login) - avoids N playlist syncs on every login 102- sync tasks don't block the response (~300-500ms for the endpoint, PDS calls happen in background) 103- putRecord calls take ~50-100ms each, with automatic DPoP nonce retry on 401 104 105**file size audit** (candidates for future modularization): 106- `portal/+page.svelte`: 2,436 lines (58% CSS) 107- `playlist/[id]/+page.svelte`: 1,644 lines (48% CSS) 108- `api/lists.py`: 855 lines 109- CSS-heavy files could benefit from shared style extraction in future 110 111**related issues**: #221, #146, #498 112 113--- 114 115#### list reordering UI (feat/playlists branch, Dec 7) 116 117**what's done**: 118- `PUT /lists/liked/reorder` endpoint - reorder user's liked tracks list 119- `PUT /lists/{rkey}/reorder` endpoint - reorder any list by ATProto rkey 120- both endpoints take `items` array of strongRefs (uri + cid) in desired order 121- liked tracks page (`/liked`) now has "reorder" button for authenticated users 122- album page has "reorder" button for album owner (if album has ATProto list record) 123- drag-and-drop reordering on desktop (HTML5 drag API) 124- touch reordering on mobile (6-dot grip handle, same pattern as queue) 125- visual feedback during drag: `.drag-over` and `.is-dragging` states 126- saves order to ATProto via `putRecord` when user clicks "done" 127- added `atproto_record_cid` to TrackResponse schema (needed for strongRefs) 128- added `artist_did` and `list_uri` to AlbumMetadata response 129 130**UX design**: 131- button toggles between "reorder" and "done" states 132- in edit mode, drag handles appear next to each track 133- saving shows spinner, success/error toast on completion 134- only owners can see/use reorder button (liked list = current user, album = artist) 135 136--- 137 138#### scope upgrade OAuth flow (feat/scope-invalidation branch, Dec 7) - merged to feat/playlists 139 140**problem**: when users enabled teal.fm scrobbling, the app showed a passive "please log out and back in" message because the session lacked the required OAuth scopes. this was confusing UX. 141 142**solution**: immediate OAuth handshake when enabling features that require new scopes (same pattern as developer tokens). 143 144**what's done**: 145- `POST /auth/scope-upgrade/start` endpoint initiates OAuth with expanded scopes 146- `pending_scope_upgrades` table tracks in-flight upgrades (10min TTL) 147- callback replaces old session with new one, redirects to `/settings?scope_upgraded=true` 148- frontend shows spinner during redirect, success toast on return 149- fixed preferences bug where toggling settings reset theme to dark mode 150 151**code quality**: 152- eliminated bifurcated OAuth clients (`oauth_client` vs `oauth_client_with_teal`) 153- replaced with `get_oauth_client(include_teal=False)` factory function 154- at ~17 OAuth flows/day, instantiation cost is negligible 155- explicit scope selection at call site instead of module-level state 156 157**developer token UX**: 158- full-page overlay when returning from OAuth after creating a developer token 159- token displayed prominently with warning that it won't be shown again 160- copy button with success feedback, link to python SDK docs 161- prevents users from missing their token (was buried at bottom of page) 162 163**test fixes**: 164- fixed connection pool exhaustion in tests (was hitting Neon's connection limit) 165- added `DATABASE_POOL_SIZE=2`, `DATABASE_MAX_OVERFLOW=0` to pytest env vars 166- dispose cached engines after each test to prevent connection accumulation 167- fixed mock function signatures for `refresh_session` tests 168 169**tests**: 4 new tests for scope upgrade flow, all 281 tests passing 170 171--- 172 173#### settings consolidation (PR #496, Dec 6) 174 175**problem**: user preferences were scattered across multiple locations with confusing terminology: 176- SensitiveImage tooltip said "enable in portal" but mobile menu said "profile" 177- clicking gear icon (SettingsMenu) only showed appearance/playback, not all settings 178- portal mixed content management with preferences 179 180**solution**: clear separation between **settings** (preferences) and **portal** (content & data): 181 182| page | purpose | 183|------|---------| 184| `/settings` | preferences: theme, accent color, auto-advance, sensitive artwork, timed comments, teal.fm, developer tokens | 185| `/portal` | your content & data: profile, tracks, albums, export, delete account | 186 187**changes**: 188- created dedicated `/settings` route consolidating all user preferences 189- slimmed portal to focus on content management 190- added "all settings →" link to SettingsMenu and ProfileMenu 191- renamed mobile menu "profile" → "portal" to match route 192- moved delete account to portal's "your data" section (it's about data, not preferences) 193- fixed `font-family: inherit` on all settings page buttons 194- updated SensitiveImage tooltip: "enable in settings" 195 196--- 197 198#### bufo easter egg improvements (PRs #491-492, Dec 6) 199 200**what shipped**: 201- configurable exclude/include patterns via env vars for bufo easter egg 202- `BUFO_EXCLUDE_PATTERNS`: regex patterns to filter out (default: `^bigbufo_`) 203- `BUFO_INCLUDE_PATTERNS`: allowlist that overrides exclude (default: `bigbufo_0_0`, `bigbufo_2_1`) 204- cache key now includes patterns so config changes take effect immediately 205 206**reusable type**: 207- added `CommaSeparatedStringSet` type for parsing comma-delimited env vars into sets 208- uses pydantic `BeforeValidator` with `Annotated` pattern (not class-coupled validators) 209- handles: `VAR=a,b,c``{"a", "b", "c"}` 210 211**context**: bigbufo tiles are 4x4 grid fragments that looked weird floating individually. now excluded by default, with two specific tiles allowed through. 212 213**thread**: https://bsky.app/profile/zzstoatzzdevlog.bsky.social/post/3m7e3ndmgwl2m 214 215--- 216 217#### mobile artwork upload fix (PR #489, Dec 6) 218 219**problem**: artwork uploads from iOS Photos library silently failed - track uploaded successfully but without artwork. 220 221**root cause**: iOS stores photos in HEIC format. when selected, iOS converts content to JPEG but often keeps the `.heic` filename. backend validated format using only filename extension → rejected as "unsupported format". 222 223**fix**: 224- backend now prefers MIME content_type over filename extension for format detection 225- added `ImageFormat.from_content_type()` method 226- frontend uses `accept="image/*"` for broader iOS compatibility 227 228#### sensitive image moderation (PRs #471-488, Dec 5-6) 229 230**what shipped**: 231- `sensitive_images` table to flag problematic images by R2 `image_id` or external URL 232- `show_sensitive_artwork` user preference (default: hidden, toggle in portal → "your data") 233- flagged images blurred everywhere: track lists, player, artist pages, likers tooltip, search results, embeds 234- Media Session API (CarPlay, lock screen, control center) respects sensitive preference 235- SSR-safe filtering: link previews (og:image) exclude sensitive images on track, artist, and album pages 236- likers tooltip UX: max-height with scroll, hover interaction fix, viewport-aware flip positioning 237- likers tooltip z-index: elevates entire track-container when tooltip open (prevents sibling tracks bleeding through) 238 239**how it works**: 240- frontend fetches `/moderation/sensitive-images` and stores flagged IDs/URLs 241- `SensitiveImage` component wraps images and checks against flagged list 242- server-side check via `+layout.server.ts` for meta tag filtering 243- users can opt-in to view sensitive artwork via portal toggle 244 245**coverage** (PR #488): 246 247| context | approach | 248|---------|----------| 249| DOM images needing blur | `SensitiveImage` component | 250| small avatars in lists | `SensitiveImage` with `compact` prop | 251| SSR meta tags (og:image) | `checkImageSensitive()` function | 252| non-DOM APIs (media session) | direct `isSensitive()` + `showSensitiveArtwork` check | 253 254**moderation workflow**: 255- admin adds row to `sensitive_images` with `image_id` (R2) or `url` (external) 256- images are blurred immediately for all users 257- users who enable `show_sensitive_artwork` see unblurred images 258 259--- 260 261#### teal.fm scrobbling integration (PR #467, Dec 4) 262 263**what shipped**: 264- native teal.fm scrobbling: when users enable the toggle, plays are recorded to their PDS using teal's ATProto lexicons 265- scrobble triggers at 30% or 30 seconds (whichever comes first) - same threshold as play counts 266- user preference stored in database, toggleable from portal → "your data" 267- settings link to pdsls.dev so users can view their scrobble records 268 269**lexicons used**: 270- `fm.teal.alpha.feed.play` - individual play records (scrobbles) 271- `fm.teal.alpha.actor.status` - now-playing status updates 272 273**configuration** (all optional, sensible defaults): 274- `TEAL_ENABLED` (default: `true`) - feature flag for entire integration 275- `TEAL_PLAY_COLLECTION` (default: `fm.teal.alpha.feed.play`) 276- `TEAL_STATUS_COLLECTION` (default: `fm.teal.alpha.actor.status`) 277 278**code quality improvements** (same PR): 279- added `settings.frontend.domain` computed property for environment-aware URLs 280- extracted `get_session_id_from_request()` utility for bearer token parsing 281- added field validator on `DeveloperTokenInfo.session_id` for auto-truncation 282- applied walrus operators throughout auth and playback code 283- fixed now-playing endpoint firing every 1 second (fingerprint update bug in scheduled reports) 284 285**documentation**: `backend/src/backend/_internal/atproto/teal.py` contains inline docs on the scrobbling flow 286 287--- 288 289#### unified search (PR #447, Dec 3) 290 291**what shipped**: 292- `Cmd+K` (mac) / `Ctrl+K` (windows/linux) opens search modal from anywhere 293- fuzzy matching across tracks, artists, albums, and tags using PostgreSQL `pg_trgm` 294- results grouped by type with relevance scores (0.0-1.0) 295- keyboard navigation (arrow keys, enter, esc) 296- artwork/avatars displayed with lazy loading and fallback icons 297- glassmorphism modal styling with backdrop blur 298- debounced input (150ms) with client-side validation 299 300**database**: 301- enabled `pg_trgm` extension for trigram-based similarity search 302- GIN indexes on `tracks.title`, `artists.handle`, `artists.display_name`, `albums.title`, `tags.name` 303 304**documentation**: `docs/frontend/search.md`, `docs/frontend/keyboard-shortcuts.md` 305 306**follow-up polish** (PRs #449-463): 307- mobile search icon in header (PRs #455-456) 308- theme-aware modal styling with styled scrollbar (#450) 309- ILIKE fallback for substring matches when trigram fails (#452) 310- tag collapse with +N button (#453) 311- input focus fix: removed `visibility: hidden` so focus works on open (#457, #463) 312- album artwork fallback in player when track has no image (#458) 313- rate limiting exemption for now-playing endpoints (#460) 314- `--no-dev` flag for release command to prevent dev dep installation (#461) 315 316--- 317 318#### light/dark theme and mobile UX overhaul (Dec 2-3) 319 320**theme system** (PR #441): 321- replaced hardcoded colors across 35 files with CSS custom properties 322- semantic tokens: `--bg-primary`, `--text-secondary`, `--accent`, etc. 323- theme switcher in settings: dark / light / system (follows OS preference) 324- removed zen mode feature (superseded by proper theme support) 325 326**mobile UX improvements** (PR #443): 327- new `ProfileMenu` component — collapses profile, upload, settings, logout into touch-optimized menu (44px tap targets) 328- dedicated `/upload` page — extracted from portal for cleaner mobile flow 329- portal overhaul — tighter forms, track detail links under artwork, fixed icon alignment 330- standardized section headers across home and liked tracks pages 331 332**player scroll timing fix** (PR #445): 333- reduced title scroll cycle from 10s → 8s, artist/album from 15s → 10s 334- eliminated 1.5s invisible pause at end of scroll animation 335- fixed duplicate upload toast (was firing twice on success) 336- upload success toast now includes "view track" link 337 338**CI optimization** (PR #444): 339- pre-commit hooks now skip based on changed paths 340- result: ~10s for most PRs instead of ~1m20s 341- only installs tooling (uv, bun) needed for changed directories 342 343--- 344 345#### tag filtering system and SDK tag support (Dec 2) 346 347**tag filtering** (PRs #431-434): 348- users can now hide tracks by tag via eye icon filter in discovery feed 349- preferences centralized in root layout (fetched once, shared across app) 350- `HiddenTagsFilter` component with expandable UI for managing hidden tags 351- default hidden tags: `["ai"]` for new users 352- tag detail pages at `/tag/[name]` with all tracks for that tag 353- clickable tag badges on tracks navigate to tag pages 354 355**navigation fix** (PR #435): 356- fixed tag links interrupting audio playback 357- root cause: `stopPropagation()` on links breaks SvelteKit's client-side router 358- documented pattern in `docs/frontend/navigation.md` to prevent recurrence 359 360**SDK tag support** (plyr-python-client v0.0.1-alpha.10): 361- added `tags: set[str]` parameter to `upload()` in SDK 362- added `-t/--tag` CLI option (can be used multiple times) 363- updated MCP `upload_guide` prompt with tag examples 364- status maintenance workflow now tags AI-generated podcasts with `ai` (#436) 365 366**tags in detail pages** (PR #437): 367- track detail endpoint (`/tracks/{id}`) now returns tags 368- album detail endpoint (`/albums/{handle}/{slug}`) now returns tags for all tracks 369- track detail page displays clickable tag badges 370 371**bufo easter egg** (PR #438, improved in #491-492): 372- tracks tagged with `bufo` trigger animated toad GIFs on the detail page 373- uses track title as semantic search query against [find-bufo API](https://find-bufo.fly.dev/) 374- toads are semantically matched to the song's vibe (e.g., "Happy Vibes" gets happy toads) 375- results cached in localStorage (1 week TTL) to minimize API calls 376- `TagEffects` wrapper component provides extensibility for future tag-based plugins 377- respects `prefers-reduced-motion`; fails gracefully if API unavailable 378- configurable exclude/include patterns via env vars (see Dec 6 entry above) 379 380--- 381 382#### queue touch reordering and header stats fix (Dec 2) 383 384**queue mobile UX** (PR #428): 385- added 6-dot drag handle to queue items for touch-friendly reordering 386- implemented touch event handlers for mobile drag-and-drop 387- track follows finger during drag with smooth translateY transform 388- drop target highlights while dragging over other tracks 389 390**header stats positioning** (PR #426): 391- fixed platform stats not adjusting when queue sidebar opens/closes 392- added `--queue-width` CSS custom property updated dynamically 393- stats now shift left with smooth transition when queue opens 394 395--- 396 397#### connection pool resilience for Neon cold starts (Dec 2) 398 399**incident**: ~5 minute API outage (01:55-02:00 UTC) - all requests returned 500 errors 400 401**root cause**: Neon serverless cold start after 5 minutes of idle traffic 402- queue listener heartbeat detected dead connection, began reconnection 403- first 5 user requests each held a connection waiting for Neon to wake up (3-5 min each) 404- with pool_size=5 and max_overflow=0, pool exhausted immediately 405- all subsequent requests got `QueuePool limit of size 5 overflow 0 reached` 406 407**fix**: 408- increased `pool_size` from 5 → 10 (handle more concurrent cold start requests) 409- increased `max_overflow` from 0 → 5 (allow burst to 15 connections) 410- increased `connection_timeout` from 3s → 10s (wait for Neon wake-up) 411 412**related**: this is a recurrence of the Nov 17 incident. that fix addressed the queue listener's asyncpg connection but not the SQLAlchemy pool connections. 413 414--- 415 416#### now-playing API (PR #416, Dec 1) 417 418**what shipped**: 419- `GET /now-playing/{did}` and `GET /now-playing/by-handle/{handle}` endpoints 420- returns track metadata, playback position, timestamp 421- 204 when nothing playing, 200 with track data otherwise 422 423**teal.fm integration**: 424- native scrobbling shipped in PR #467 (Dec 4) - plyr.fm writes directly to user's PDS 425- Piper integration (external polling) still open: https://github.com/teal-fm/piper/pull/27 426 427--- 428 429#### admin UI improvements for moderation (PRs #408-414, Dec 1) 430 431**what shipped**: 432- dropdown menu for false positive reasons (fingerprint noise, original artist, fair use, other) 433- artist/track links open in new tabs for verification 434- AuDD score normalization (scores shown as 0-100 range) 435- filter controls to show only high-confidence matches 436- form submission fixes for htmx POST requests 437 438--- 439 440#### ATProto labeler and copyright moderation (PRs #382-395, Nov 29-Dec 1) 441 442**what shipped**: 443- standalone labeler service integrated into moderation Rust service 444- implements `com.atproto.label.queryLabels` and `subscribeLabels` XRPC endpoints 445- k256 ECDSA signing for cryptographic label verification 446- web interface at `/admin` for reviewing copyright flags 447- htmx for server-rendered interactivity 448- integrates with AuDD enterprise API for audio fingerprinting 449- fire-and-forget background task on track upload 450- review workflow with resolution tracking (violation, false_positive, original_artist) 451 452**initial review results** (25 flagged tracks): 453- 8 violations (actual copyright issues) 454- 11 false positives (fingerprint noise) 455- 6 original artists (people uploading their own distributed music) 456 457**documentation**: see `docs/moderation/atproto-labeler.md` 458 459--- 460 461#### developer tokens with independent OAuth grants (PR #367, Nov 28) 462 463**what shipped**: 464- each developer token gets its own OAuth authorization flow 465- tokens have their own DPoP keypair, access/refresh tokens - completely separate from browser session 466- cookie isolation: dev token exchange doesn't set browser cookie 467- token management UI: portal → "your data" → "developer tokens" 468- create with optional name and expiration (30/90/180/365 days or never) 469 470**security properties**: 471- tokens are full sessions with encrypted OAuth credentials (Fernet) 472- each token refreshes independently 473- revokable individually without affecting browser or other tokens 474 475--- 476 477#### platform stats and media session integration (PRs #359-379, Nov 27-29) 478 479**what shipped**: 480- `GET /stats` returns total plays, tracks, and artists 481- stats bar displays in homepage header (e.g., "1,691 plays • 55 tracks • 8 artists") 482- Media Session API for CarPlay, lock screens, Bluetooth devices 483- browser tab title shows "track - artist • plyr.fm" while playing 484- timed comments with clickable timestamps 485- constellation integration for network-wide like counts 486- account deletion with explicit confirmation 487 488--- 489 490#### export & upload reliability (PRs #337-344, Nov 24) 491 492**what shipped**: 493- database-backed jobs (moved tracking from in-memory to postgres) 494- streaming exports (fixed OOM on large file exports) 495- 90-minute WAV files now export successfully on 1GB VM 496- upload progress bar fixes 497- export filename now includes date 498 499--- 500 501### October-November 2025 502 503See `.status_history/2025-11.md` for detailed November development history including: 504- async I/O performance fixes (PRs #149-151) 505- transcoder API deployment (PR #156) 506- upload streaming + progress UX (PR #182) 507- liked tracks feature (PR #157) 508- track detail pages (PR #164) 509- mobile UI improvements (PRs #159-185) 510- oEmbed endpoint for Leaflet.pub embeds (PRs #355-358) 511 512## immediate priorities 513 514### high priority features 5151. **audio transcoding pipeline integration** (issue #153) 516 - ✅ standalone transcoder service deployed at https://plyr-transcoder.fly.dev/ 517 - ⏳ next: integrate into plyr.fm upload pipeline 518 - backend calls transcoder API for unsupported formats 519 - queue-based job system for async processing 520 - R2 integration (fetch original, store MP3) 521 522### known issues 523- playback auto-start on refresh (#225) - investigating localStorage/queue state persistence 524- no AIFF/AIF transcoding support (#153) 525- iOS PWA audio may hang on first play after backgrounding - service worker caching interacts poorly with 307 redirects to R2 CDN. PR #466 added `NetworkOnly` for audio routes which should fix this, but iOS PWAs are slow to update service workers. workaround: delete home screen bookmark and re-add. may need further investigation if issue persists after SW propagates. 526 527### new features 528- issue #146: content-addressable storage (hash-based deduplication) 529- issue #155: add track metadata (genres, tags, descriptions) 530- issue #334: add 'share to bluesky' option for tracks 531- issue #373: lyrics field and Genius-style annotations 532- issue #393: moderation - represent confirmed takedown state in labeler 533 534## technical state 535 536### architecture 537 538**backend** 539- language: Python 3.11+ 540- framework: FastAPI with uvicorn 541- database: Neon PostgreSQL (serverless) 542- storage: Cloudflare R2 (S3-compatible) 543- hosting: Fly.io (2x shared-cpu VMs) 544- observability: Pydantic Logfire 545- auth: ATProto OAuth 2.1 546 547**frontend** 548- framework: SvelteKit (v2.43.2) 549- runtime: Bun 550- hosting: Cloudflare Pages 551- styling: vanilla CSS with lowercase aesthetic 552- state management: Svelte 5 runes 553 554**deployment** 555- ci/cd: GitHub Actions 556- backend: automatic on main branch merge (fly.io) 557- frontend: automatic on every push to main (cloudflare pages) 558- migrations: automated via fly.io release_command 559 560**what's working** 561 562**core functionality** 563- ✅ ATProto OAuth 2.1 authentication with encrypted state 564- ✅ secure session management via HttpOnly cookies 565- ✅ developer tokens with independent OAuth grants 566- ✅ platform stats endpoint and homepage display 567- ✅ Media Session API for CarPlay, lock screens, control center 568- ✅ timed comments on tracks with clickable timestamps 569- ✅ account deletion with explicit confirmation 570- ✅ artist profiles synced with Bluesky 571- ✅ track upload with streaming to prevent OOM 572- ✅ track edit/deletion with cascade cleanup 573- ✅ audio streaming via HTML5 player with 307 redirects to R2 CDN 574- ✅ track metadata published as ATProto records 575- ✅ play count tracking (30% or 30s threshold) 576- ✅ like functionality with counts 577- ✅ queue management (shuffle, auto-advance, reorder) 578- ✅ mobile-optimized responsive UI 579- ✅ cross-tab queue synchronization via BroadcastChannel 580- ✅ share tracks via URL with Open Graph previews 581- ✅ copyright moderation system with admin UI 582- ✅ ATProto labeler for copyright violations 583- ✅ unified search with Cmd/Ctrl+K (fuzzy matching via pg_trgm) 584- ✅ teal.fm scrobbling (records plays to user's PDS) 585 586**albums** 587- ✅ album database schema with track relationships 588- ✅ album browsing and detail pages 589- ✅ album cover art upload and display 590- ✅ server-side rendering for SEO 591- ✅ ATProto list records for albums (auto-synced on login) 592 593**playlists** 594- ✅ full CRUD (create, rename, delete, reorder tracks) 595- ✅ playlist detail pages with drag-and-drop reordering 596- ✅ playlist cover art upload 597- ✅ ATProto list records (synced on create/modify) 598- ✅ "add to playlist" menu on tracks 599- ✅ playlists in global search results 600 601**deployment (fully automated)** 602- **production**: 603 - frontend: https://plyr.fm 604 - backend: https://relay-api.fly.dev → https://api.plyr.fm 605 - database: neon postgresql 606 - storage: cloudflare R2 (audio-prod and images-prod buckets) 607 608- **staging**: 609 - backend: https://api-stg.plyr.fm 610 - frontend: https://stg.plyr.fm 611 - database: neon postgresql (relay-staging) 612 - storage: cloudflare R2 (audio-stg bucket) 613 614### technical decisions 615 616**why Python/FastAPI instead of Rust?** 617- rapid prototyping velocity during MVP phase 618- rich ecosystem for web APIs 619- excellent async support with asyncio 620- trade-off: accepting higher latency for faster development 621 622**why Cloudflare R2 instead of S3?** 623- zero egress fees (critical for audio streaming) 624- S3-compatible API (easy migration if needed) 625- integrated CDN for fast delivery 626 627**why forked atproto SDK?** 628- upstream SDK lacked OAuth 2.1 support 629- needed custom record management patterns 630- maintains compatibility with ATProto spec 631 632**why async everywhere?** 633- event loop performance: single-threaded async handles high concurrency 634- I/O-bound workload: most time spent waiting on network/disk 635- PRs #149-151 eliminated all blocking operations 636 637## cost structure 638 639current monthly costs: ~$35-40/month 640 641- fly.io backend (production): ~$5/month 642- fly.io backend (staging): ~$5/month 643- fly.io transcoder: ~$0-5/month (auto-scales to zero) 644- neon postgres: $5/month 645- audd audio fingerprinting: ~$10/month 646- cloudflare pages: $0 (free tier) 647- cloudflare R2: ~$0.16/month 648- logfire: $0 (free tier) 649- domain: $12/year (~$1/month) 650 651## deployment URLs 652 653- **production frontend**: https://plyr.fm 654- **production backend**: https://api.plyr.fm 655- **staging backend**: https://api-stg.plyr.fm 656- **staging frontend**: https://stg.plyr.fm 657- **repository**: https://github.com/zzstoatzz/plyr.fm (private) 658- **monitoring**: https://logfire-us.pydantic.dev/zzstoatzz/relay 659- **bluesky**: https://bsky.app/profile/plyr.fm 660 661## admin tooling 662 663### content moderation 664script: `scripts/delete_track.py` 665- requires `ADMIN_*` prefixed environment variables 666- deletes audio file, cover image, database record 667- notes ATProto records for manual cleanup 668 669usage: 670```bash 671uv run scripts/delete_track.py <track_id> --dry-run 672uv run scripts/delete_track.py <track_id> 673uv run scripts/delete_track.py --url https://plyr.fm/track/34 674``` 675 676## for new contributors 677 678### getting started 6791. clone: `gh repo clone zzstoatzz/plyr.fm` 6802. install dependencies: `uv sync && cd frontend && bun install` 6813. run backend: `uv run uvicorn backend.main:app --reload` 6824. run frontend: `cd frontend && bun run dev` 6835. visit http://localhost:5173 684 685### development workflow 6861. create issue on github 6872. create PR from feature branch 6883. ensure pre-commit hooks pass 6894. merge to main → deploys to staging automatically 6905. verify on staging 6916. create github release → deploys to production automatically 692 693### key principles 694- type hints everywhere 695- lowercase aesthetic 696- ATProto first 697- async everywhere (no blocking I/O) 698- mobile matters 699- cost conscious 700 701### project structure 702``` 703plyr.fm/ 704├── backend/ # FastAPI app & Python tooling 705│ ├── src/backend/ # application code 706│ │ ├── api/ # public endpoints 707│ │ ├── _internal/ # internal services 708│ │ ├── models/ # database schemas 709│ │ └── storage/ # storage adapters 710│ ├── tests/ # pytest suite 711│ └── alembic/ # database migrations 712├── frontend/ # SvelteKit app 713│ ├── src/lib/ # components & state 714│ └── src/routes/ # pages 715├── moderation/ # Rust moderation service (ATProto labeler) 716│ ├── src/ # Axum handlers, AuDD client, label signing 717│ └── static/ # admin UI (html/css/js) 718├── transcoder/ # Rust audio transcoding service 719├── docs/ # documentation 720└── justfile # task runner 721``` 722 723## documentation 724 725- [deployment overview](docs/deployment/overview.md) 726- [configuration guide](docs/configuration.md) 727- [queue design](docs/queue-design.md) 728- [logfire querying](docs/logfire-querying.md) 729- [moderation & labeler](docs/moderation/atproto-labeler.md) 730- [unified search](docs/frontend/search.md) 731- [keyboard shortcuts](docs/frontend/keyboard-shortcuts.md) 732 733--- 734 735this is a living document. last updated 2025-12-07.