chore: status maintenance - playlists and ATProto sync polish (#524)

๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>

authored by claude[bot] claude[bot] Claude and committed by GitHub a5a6cf3a 7d553af8

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

This is a binary file and will not be displayed.