commits
* feat: add now-playing API for teal.fm/Piper integration
exposes real-time playback state for external scrobbler services.
backend:
- NowPlayingService with TTL cache (5 min expiry) for ephemeral state
- POST /now-playing/ - frontend reports playback state
- DELETE /now-playing/ - clear state on stop
- GET /now-playing/by-handle/{handle} - public endpoint for Piper
- GET /now-playing/by-did/{did} - alternative by DID
- returns 204 when nothing playing (matches Spotify pattern)
- response format compatible with Piper's expectations
frontend:
- now-playing.svelte.ts service with throttling/debouncing
- reports every 10s during playback, debounced for seeks
- integrated into Player.svelte effects
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use cookie-based auth check in now-playing service
auth is cookie-based (HttpOnly), not localStorage. the localStorage.getItem('session_id')
check was leftover dead code that always failed, preventing now-playing from reporting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use latest state in debounced now-playing reports
when pause events were debounced, the scheduled report used stale values
captured at schedule time instead of the latest state. now stores pending
state separately so the timer always fires with current values.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: admin UI styling + add artist/track links
- Fix badge layout (stack vertically, align right)
- Change matches badge to blue (distinct from pending yellow)
- Add link to artist profile (plyr.fm/u/{handle})
- Add link to track page when track_id is available
- Store track_id in label context for future flags
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: update test to include track_id parameter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: normalize AuDD scores + add filter to admin UI
- normalize scores from integer (0-100) to float (0.0-1.0) when storing
context, fixing the 0% display issue for potential matches
- add filter controls to admin UI: pending (default), resolved, all
- preserve current filter when resolving flags or refreshing
- add unit test for score normalization
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: show match count instead of misleading 0% scores
AuDD doesn't return confidence scores when using accurate_offsets mode
(which we use for timecode data). All scores were 0, showing as "0% match".
Changed to show "{N} matches" badge instead, which is accurate and useful.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
axum's Form extractor expects application/x-www-form-urlencoded,
but FormData sends multipart/form-data, causing 415 Unsupported Media Type.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the savedToken check was calling showMain() before the function was
defined, causing a ReferenceError that stopped script execution and
prevented all other functions (including showReasonSelect) from being
defined.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
replaces the always-visible dropdown with a proper multi-step flow:
1. [mark false positive] button
2. click -> inline reason buttons appear with cancel (✕)
3. click reason -> confirmation prompt with confirm/cancel
4. click confirm -> actually resolves
prevents accidental one-click resolutions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add reason selection for false positive resolution in moderation UI
when marking a copyright flag as a false positive, admins must now select
a reason from preset options:
- original_artist: artist uploaded their own distributed music
- licensed: has licensing/permission rights
- fingerprint_noise: matcher produced false match
- cover_version: legal cover or remix
- other: free text in notes
the reason is stored in label_context table (resolution_reason, resolution_notes)
for audit purposes. the ATProto negation label stays minimal per protocol spec.
resolved flags now display the reason instead of just "resolved".
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* rm random file
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Restore long-term vision section (silo problem, ATProto solution, dream state)
- Add "## recent work" header before detailed updates
- Update workflow to delete podcast_script.txt after TTS generation
- Explicitly note not to commit the script file
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- archived older sections (Nov 10-30) to .status_history/2025-11.md
- kept recent work (Nov 29 - Dec 1) in STATUS.md
- generated podcast script covering recent copyright moderation work
- created audio overview (update.wav) via TTS
🤖 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>
- Split workflow into two phases:
1. workflow_dispatch: archive, generate audio, open PR (audio in PR)
2. on PR merge: upload audio to plyr.fm
- Add temporal awareness instructions:
- Check git history for actual commit dates
- Reference project start (november 2025)
- Don't present old work as "just shipped"
- Remove show_full_output (no longer debugging)
- Remove MCP config (not needed for upload-on-merge flow)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Gemini TTS returns raw PCM (audio/L16;rate=24000), not WAV
- Add pcm_to_wav() to wrap audio in proper RIFF/WAVE header
- Add tone guidelines for podcast scripts: accessible, user-focused,
intuitive analogies, matter-of-fact delivery
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- enable show_full_output for debugging
- add plyrfm MCP server config with PLYR_TOKEN
- allow mcp__plyrfm__* tools
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add status maintenance workflow (claude code)
automated weekly maintenance of STATUS.md:
- archives old sections to .status_history/YYYY-MM.md when over 500 lines
- optionally generates audio overview via gemini TTS
- uploads to plyr.fm using bot account
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use claude_args instead of allowed_tools, remove PR trigger
- claude-code-action uses claude_args with --allowedTools flag
- PR trigger won't work for new workflow files (security feature)
- will test via workflow_dispatch after merge
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use scripts/generate_tts.py instead of inline python
- added scripts/generate_tts.py for gemini TTS generation
- workflow tells claude to run the script, not recreate it
- upload via plyrfm CLI with PLYR_TOKEN env var
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: remove STATUS.md from gitignore
STATUS.md contains no secrets and needs to be tracked for the
status maintenance workflow to work in CI.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: track STATUS.md in version control
~1200 lines of project status history. will be archived down to ~500
lines by the status maintenance workflow.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: create PR instead of pushing directly to main
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- add favicon (shield emoji)
- fix flags list to load automatically on page load with saved token
- fix refresh button indicator styling (hide when not loading)
- add environment badges (dev/stg) for non-production namespace flags
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the auth middleware was blocking /static/* paths, causing 401 on CSS/JS
files needed for the admin UI.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* chore: add rust CI and pre-commit checks
- Rename Justfile → justfile for Linux case sensitivity
- Add cargo check pre-commit hooks for moderation + transcoder
- Add check-rust.yml CI workflow (cargo check + docker build on PRs)
- Include moderation/static/** in deploy trigger paths
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: correct rust-toolchain action name
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Move HTML/CSS/JS from inline const to static/admin.{html,css,js}
- Serve static files via tower-http ServeDir
- Add Io error variant for file read errors
- Update Dockerfile to copy static directory
admin.rs: 730 → 310 lines
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add label context for enriched admin UI
adds track metadata (title, artist, matches) alongside copyright labels
so the admin UI can show meaningful info for reviewing flags.
changes:
- add label_context table to moderation service DB
- update /emit-label to accept optional context payload
- add /admin/context endpoint for backfilling context
- update admin UI with card-based layout showing track info and matches
- update backend to send context when emitting labels
- add backfill scripts for labels and context
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use --with-editable instead of sys.path hack
properly install backend package via uv's --with-editable flag
instead of manipulating sys.path
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: update test to expect full label context parameters
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- make /admin public so HTML page loads (auth handled client-side for API calls)
- reject protected requests if MODERATION_AUTH_TOKEN not set (security fix)
- add CI workflow to auto-deploy on push to main when moderation/** changes
requires FLY_API_TOKEN_MODERATION secret to be set in GitHub
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add admin UI for reviewing copyright flags
adds /admin endpoint to moderation service with:
- simple HTML/JS UI for reviewing flagged tracks
- /admin/flags API to list copyright-violation labels
- /admin/resolve API to create negation labels (false positives)
- auth via existing X-Moderation-Key header
resolves #388
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: modularize moderation service
breaks main.rs into focused modules:
- config.rs: environment config loading
- state.rs: AppState and AppError types
- auth.rs: authentication middleware
- audd.rs: AuDD fingerprinting types and handler
- xrpc.rs: ATProto labeler endpoints
- handlers.rs: health, landing, emit_label
main.rs now only composes routes (~100 lines vs ~700)
also fixes clippy warning in labels.rs (redundant closure)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- add CopyrightInfo dataclass with is_flagged and primary_match
- extract most frequent match from copyright scan results
- add copyright_match field to TrackResponse API schema
- update tooltip to show "potential copyright violation: [match]"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add `copyright_flagged` field to TrackResponse API schema
- batch fetch copyright flags using `get_copyright_flags` aggregation
- display warning icon next to flagged tracks in portal
- style flagged tracks with amber tint background/border
- make warning icon clickable to view ATProto record when available
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Update overview.md with current architecture diagram and admin UI options
- Update copyright-detection.md with actual schemas and label querying
- Add atproto-labeler.md with full labeler service documentation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: implement ATProto labeler for copyright violations
adds a full ATProto-compliant labeler service that emits signed
`copyright-violation` labels when tracks are flagged by copyright scans.
moderation service (rust):
- add Label struct with DAG-CBOR serialization and secp256k1 signing
- add Postgres database for label storage with sequence numbers
- implement com.atproto.label.queryLabels XRPC endpoint
- implement com.atproto.label.subscribeLabels WebSocket with backfill
- add /emit-label endpoint for backend integration
- add landing page at root URL
- fix: split migrations into separate statements (postgres requirement)
- fix: enable tls-rustls for Neon database connections
backend (python):
- add _emit_copyright_label() to POST labels when tracks flagged
- add labeler_url config setting
- emit label only when track has atproto_record_uri
includes regression tests for label emission behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update neon project names
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
AuDD enterprise API doesn't return confidence scores (all scores are 0),
so we now flag tracks if any matches are found instead of comparing against
a score threshold.
Also adds --max-duration flag to scan script to skip long DJ sets.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: wire up copyright scan integration with moderation service
- add ModerationSettings to config (service URL, auth token, timeout)
- create moderation client module to call /scan endpoint
- hook into upload flow with fire-and-forget background task
- scan runs after successful track commit, doesn't affect upload status
- add 8 tests covering success, timeout, disabled, and error scenarios
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: add missing logfire[sqlalchemy] dep to copyright scan script
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: store scan errors as clear results instead of leaving tracks unscanned
when moderation service can't process a file (too short, bad format, etc.),
we now store a clear result with error info in raw_response so the track
isn't stuck in limbo forever.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
The animation created 576 DOM elements (3 waves × 16 dots × 12 ghosts)
each with CSS animations and blur filters running continuously.
This caused severe performance issues on mobile devices.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add request_attributes_mapper to parse User-Agent headers
- enrich spans with client_type (sdk/mcp/browser) and client_version
- enables filtering/clustering traffic by source in Logfire UI
- fix stats positioning on album detail page (move Header outside container)
- fix pre-existing lint warning in test_auth.py
closes #377
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Show whimsical wave animation when scrolling to end of track list
- Three waves with trailing ghost effects, 120° phase offset each
- Colors derived from user's accent color using color-mix()
- Delicate, ephemeral aesthetic inspired by points of light
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: improve stats sidebar design and positioning
- Center sidebar in left margin properly
- Add icons back for context (play, track, artist)
- Smaller, cleaner design without card/box
- Horizontal layout: icon + value + label
- Fix loading lurch by matching skeleton structure
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: move platform stats from sidebar to header
- Move stats to header nav, positioned in left margin on desktop
- Stats appear inline with icons (plays, tracks, artists)
- Collapses into LinksMenu on narrow screens (<1300px)
- Remove floating sidebar approach for cleaner layout
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add platform stats display on homepage
- add /stats API endpoint returning total_plays, total_tracks, total_artists
- add PlatformStats component with skeleton loading
- display stats bar at top of homepage
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: improve platform stats presentation
- add plural handling (1 artist vs 2 artists)
- move stats to desktop sidebar (left of main content)
- integrate stats into LinksMenu popover for mobile
- add 'menu' variant with vertical grid layout
- make 'bar' variant vertical for sidebar display
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* style: add icons and improve stats placement
- add play, track, artist icons to both variants
- use fixed positioning for desktop sidebar
- position stats card relative to main content area
- anchors to left edge of content on wide screens (1200px+)
- cleaner card design with header/content separation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: stats placement and breakpoint gap
- show stats inline (above tracks) on medium screens (769-1099px)
- only hide when LinksMenu becomes visible (768px)
- align sidebar top with content area (top: 140px)
- use max() for left position to prevent going off-screen
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: align LinksMenu breakpoint with sidebar
Stats are now always visible:
- 1100px+: sidebar on left
- <1100px: LinksMenu (info icon) with stats
No gap where stats are invisible.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* style: glass effect on stats sidebar
Match Toast design language:
- semi-transparent background rgba(10, 10, 10, 0.6)
- backdrop-filter blur(12px)
- subtle border rgba(255, 255, 255, 0.06)
- soft shadow
Also bump breakpoint to 1300px to prevent overlap.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- add skeleton loading for comments section (matching artist analytics pattern)
- use fade transitions between loading/loaded states
- add dark scrollbar styling to comments list
- center comments section on mobile
- prevent layout shift with min-height container
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- override document title with playing track across all pages
- only show playing track when actively playing (not paused)
- restore page title when paused
- track detail page defers to player when different track is playing
- fix hasPageMetadata to include artist and liked pages
- use music note icon for player artwork fallback (matches track detail page)
- remove status page subtitle in links menu
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- fix navigation to track detail page not interrupting playback
(removed stopPropagation that was blocking SvelteKit's router)
- render URLs in comments as clickable hyperlinks
- add handle autocomplete to login page for easier sign-in
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
integrate with navigator.mediaSession to provide track metadata (title,
artist, album, artwork) and action handlers (play/pause, prev/next,
seek) to system media interfaces like CarPlay, lock screens, and
Bluetooth devices.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- remove scripts/plyr.py (now redundant with plyrfm on PyPI)
- add docs/tools/plyrfm.md documenting CLI and SDK usage
users can now `uv tool install plyrfm` instead of running the script
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: developer token management with listing and revocation
adds full lifecycle management for developer tokens:
**backend**
- add `is_developer_token` and `token_name` fields to UserSession model
- add GET /auth/developer-tokens endpoint to list user's tokens
- add DELETE /auth/developer-tokens/{prefix} to revoke tokens
- update POST /auth/developer-token to accept optional name parameter
- add AuthSettings config for token expiration limits
- fix connection pool leak in test fixtures (was causing TooManyConnectionsError)
**frontend**
- show existing tokens in portal with names, creation/expiration dates
- add token name input field when creating new tokens
- add revoke button for each active token
- add loading state indicator while fetching tokens
**tests**
- add 9 tests for developer token API (create, list, revoke)
- add integration test for upload/delete flow
- add `just backend integration` recipe
**docs**
- update authentication.md with token management section
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add cyclopts CLI for upload/download/list/delete
- PEP 723 inline script deps (just uv run it)
- pydantic-settings for PLYR_TOKEN/PLYR_API_URL config
- rich tables and status spinners
* refactor: rename RelaySettingsSection to AppSettingsSection
relay is the old project name
* refactor: use separate OAuth grants for developer tokens
Developer tokens now get their own OAuth credentials via a fresh
authorization flow, rather than copying credentials from the browser
session. This prevents tokens from going stale when browser sessions
refresh their tokens independently.
Changes:
- Add PendingDevToken model to store OAuth flow metadata by state
- Add POST /auth/developer-token/start to initiate OAuth for dev tokens
- Modify /auth/callback to detect dev token flows and create independent sessions
- Update frontend to redirect to PDS authorization and handle callback
- Remove old credential-copying endpoint
- Rewrite tests for new OAuth-based flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update authentication.md for OAuth-based dev tokens
- Update creating token flow to mention PDS authorization step
- Update API example to show /auth/developer-token/start endpoint
- Explain that tokens have independent OAuth grants, not inherited credentials
- Add PR #367 reference
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: default CLI to localhost, document env overrides
- Default PLYR_API_URL to http://localhost:8001 (local dev)
- Add docstring examples for staging and production overrides
- Prevents accidental production API calls during development
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: prevent dev token exchange from overwriting browser cookie
When creating a dev token via OAuth, the exchange endpoint was setting
a cookie with the dev token's session_id, overwriting the browser's
session cookie. This caused logout to delete the dev token instead of
the browser session.
Fix:
- Add is_dev_token flag to ExchangeToken model
- Pass is_dev_token=True when creating exchange token for dev tokens
- Skip cookie setting in /auth/exchange for dev token exchanges
Now dev tokens are fully independent of browser sessions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update dev token section with CLI usage and cookie isolation
- Replace sandbox script reference with scripts/plyr.py CLI
- Add CLI examples for list/upload/download/delete
- Mention cookie isolation in how it works section
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Set ORJSONResponse as the default response class for all FastAPI
endpoints. orjson is ~6x faster than stdlib json for encoding.
This is a drop-in replacement that applies globally without
per-endpoint changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: sync comment edits to ATProto record
comment edits were only updating the database, leaving ATProto records
stale. now updates ATProto record with new text and updatedAt timestamp
before persisting to database.
- add update_comment_record function to records.py
- update_comment endpoint now syncs to ATProto first
- ATProto record includes updatedAt field for edit tracking
- add regression test verifying ATProto sync on edit
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix atproto sync for comments
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add account deletion with explicit confirmation
users can now permanently delete all their data from plyr.fm:
- DELETE /account/ endpoint with handle confirmation
- deletes tracks, albums, likes, comments, preferences, sessions, queue
- deletes R2 objects (audio files, images)
- optional: delete ATProto records from user's PDS with separate consent
- clear warning about orphaned references when deleting ATProto records
docs: updated offboarding.md with full account deletion documentation
tests: comprehensive regression tests for all deletion scenarios
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: correct test model fields and use client.request for DELETE with body
- use client.request("DELETE", ..., json=...) since AsyncClient.delete() doesn't support json=
- use oauth_session_data (string) not oauth_data (bytes) for UserSession
- use state (dict) not track_ids/current_index for QueueState
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add pdsls.dev link for self-service PDS management
users can now click through to pdsls.dev to manage their ATProto
records directly, or let us clean them up via the checkbox option.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
the /preferences endpoint has a trailing slash, so requests without it
get 307 redirected. behind fly's proxy, this redirect was generating
http:// URLs causing mixed content errors. matching the pattern already
used in SettingsMenu.svelte.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: detect stale OAuth scopes and prompt re-login
when new features require additional scopes (like comments), sessions
with old scopes now get a friendly prompt to log in again instead of
cryptic errors.
backend:
- add scope validation helpers to parse and compare OAuth scopes
- require_auth now checks if session has all required scopes
- returns 403 with "scope_upgrade_required" when scopes are stale
frontend:
- detect scope_upgrade_required in auth initialization
- show friendly toast: "we added new features! log in again to use them"
- clears stale session so user sees logged-out state
includes 12 unit tests for scope validation logic.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: improve scope change message and add release link
- neutral wording: "permissions have changed" (not "new capabilities")
- adds clickable "see changes" link to latest release
- extends toast system to support action links
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add timed comments on tracks
- add TrackComment model with timestamp_ms for positioning
- add allow_comments toggle to UserPreferences (off by default)
- add ATProto fm.plyr.comment record support
- add GET/POST/DELETE endpoints for comments API
- add frontend UI for viewing comments on track page
- add toggle in portal settings for artists
- limit to 20 comments per track
- comments ordered by timestamp for playback sync
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: play button resume and clickable comment timestamps
- fix play button not resuming after pause (check if track is loaded, not playing state)
- make comment timestamps clickable to seek to that point in the song
- fix layout overlap with share button (add flex-direction: column to main)
- make comments section more compact with scroll overflow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: extract _make_pds_request utility for ATProto record operations
consolidates duplicated OAuth session handling and token refresh logic
from 5 separate functions into a single private utility:
- create_track_record
- update_record
- create_like_record
- delete_record_by_uri
- create_comment_record
also adds _parse_at_uri helper for AT URI parsing.
net reduction of ~147 lines.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: address PR review feedback
- add db.rollback() before cleanup on comment creation failure
- add aria-label for accessibility on comment input and toggle
- fix seekToTimestamp race condition by waiting for loadedmetadata
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: address copilot review feedback
- fix Index syntax in track_comment.py (can't use .desc() in constructor)
- make migration idempotent for allow_comments column
- add console.error for failed comment loads
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add constellation client for network-wide like counts
integrates with constellation.microcosm.blue backlink index to query
like counts across the entire atproto network, not just plyr.fm.
- adds get_like_count() for raw queries
- exports get_like_count_safe() from _internal (fallback on error)
- uses settings.atproto.like_collection for environment awareness
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: add user-agent header to constellation requests
per fig's request in constellation readme - be a good citizen
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Reverts the og:video experiment. iframely doesn't create links.player
from og:video for unknown providers. Restoring og:audio for platforms
that use it directly.
The oEmbed endpoint remains - Leaflet would need to check the oEmbed
html field as a fallback when links.player is missing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
iframely prefers og:audio over og:video, causing it to return the raw
MP3 as the player link instead of our embed iframe. Removing og:audio
should make iframely use og:video with type="text/html" for the player.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
iframely was using og:audio to return the raw MP3 as the player link,
causing Leaflet.pub to show a black HTML5 audio box instead of our
embed iframe. Adding og:video with type="text/html" pointing to the
embed URL should make iframely return the iframe as the player.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Enables services like Leaflet.pub (via iframely) to discover and use
our embed player instead of falling back to raw HTML5 audio.
- Add /oembed backend endpoint that returns oEmbed JSON for track URLs
- Add oEmbed discovery link to track page head
- Add comprehensive tests for oEmbed endpoint
The oEmbed response includes:
- iframe HTML pointing to /embed/track/{id}
- Track title, artist name, and artist URL
- Thumbnail when track has cover art
- Standard width (400) and height (165) matching our embed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Let more of the album art colors show through the blurred background.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: make mobile embed background more transparent
reduces overlay opacity from 0.65 to 0.35 so album art shows through
better, and increases logo visibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: add pointer-events: none to mobile background layers
The bg-image and bg-overlay elements were intercepting clicks
on mobile, preventing buttons and links from being clickable.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
on narrow screens (≤400px), switches to a blurred album art
background instead of showing a square image on the side.
this gives the full width for controls while maintaining the
visual identity of the track.
implements feedback from bluesky maintainers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
makes the artist name in the embed clickable, linking to their
profile page on plyr.fm
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
adds padding-right to meta container to leave room for the
absolutely-positioned plyr.fm logo
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
adds media queries to handle bluesky embeds on mobile:
- ≤400px: smaller album art (100px), tighter spacing
- ≤280px: hide album art entirely
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add script to scan tracks for copyright
adds a UV script that:
- connects to any environment (dev/staging/prod)
- queries tracks without copyright scans
- calls moderation service for each track
- stores results in copyright_scans table
tested on staging - successfully scanned 3 tracks.
related to #166, #167
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: handle enterprise audd endpoint response format
- use enterprise.audd.io by default for large file support
- parse offset as string ("MM:SS") or integer (ms)
- add enterprise-only response fields (release_date, label, song_link)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add now-playing API for teal.fm/Piper integration
exposes real-time playback state for external scrobbler services.
backend:
- NowPlayingService with TTL cache (5 min expiry) for ephemeral state
- POST /now-playing/ - frontend reports playback state
- DELETE /now-playing/ - clear state on stop
- GET /now-playing/by-handle/{handle} - public endpoint for Piper
- GET /now-playing/by-did/{did} - alternative by DID
- returns 204 when nothing playing (matches Spotify pattern)
- response format compatible with Piper's expectations
frontend:
- now-playing.svelte.ts service with throttling/debouncing
- reports every 10s during playback, debounced for seeks
- integrated into Player.svelte effects
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use cookie-based auth check in now-playing service
auth is cookie-based (HttpOnly), not localStorage. the localStorage.getItem('session_id')
check was leftover dead code that always failed, preventing now-playing from reporting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use latest state in debounced now-playing reports
when pause events were debounced, the scheduled report used stale values
captured at schedule time instead of the latest state. now stores pending
state separately so the timer always fires with current values.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: admin UI styling + add artist/track links
- Fix badge layout (stack vertically, align right)
- Change matches badge to blue (distinct from pending yellow)
- Add link to artist profile (plyr.fm/u/{handle})
- Add link to track page when track_id is available
- Store track_id in label context for future flags
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: update test to include track_id parameter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: normalize AuDD scores + add filter to admin UI
- normalize scores from integer (0-100) to float (0.0-1.0) when storing
context, fixing the 0% display issue for potential matches
- add filter controls to admin UI: pending (default), resolved, all
- preserve current filter when resolving flags or refreshing
- add unit test for score normalization
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: show match count instead of misleading 0% scores
AuDD doesn't return confidence scores when using accurate_offsets mode
(which we use for timecode data). All scores were 0, showing as "0% match".
Changed to show "{N} matches" badge instead, which is accurate and useful.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
the savedToken check was calling showMain() before the function was
defined, causing a ReferenceError that stopped script execution and
prevented all other functions (including showReasonSelect) from being
defined.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
replaces the always-visible dropdown with a proper multi-step flow:
1. [mark false positive] button
2. click -> inline reason buttons appear with cancel (✕)
3. click reason -> confirmation prompt with confirm/cancel
4. click confirm -> actually resolves
prevents accidental one-click resolutions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add reason selection for false positive resolution in moderation UI
when marking a copyright flag as a false positive, admins must now select
a reason from preset options:
- original_artist: artist uploaded their own distributed music
- licensed: has licensing/permission rights
- fingerprint_noise: matcher produced false match
- cover_version: legal cover or remix
- other: free text in notes
the reason is stored in label_context table (resolution_reason, resolution_notes)
for audit purposes. the ATProto negation label stays minimal per protocol spec.
resolved flags now display the reason instead of just "resolved".
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* rm random file
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Restore long-term vision section (silo problem, ATProto solution, dream state)
- Add "## recent work" header before detailed updates
- Update workflow to delete podcast_script.txt after TTS generation
- Explicitly note not to commit the script file
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- archived older sections (Nov 10-30) to .status_history/2025-11.md
- kept recent work (Nov 29 - Dec 1) in STATUS.md
- generated podcast script covering recent copyright moderation work
- created audio overview (update.wav) via TTS
🤖 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>
- Split workflow into two phases:
1. workflow_dispatch: archive, generate audio, open PR (audio in PR)
2. on PR merge: upload audio to plyr.fm
- Add temporal awareness instructions:
- Check git history for actual commit dates
- Reference project start (november 2025)
- Don't present old work as "just shipped"
- Remove show_full_output (no longer debugging)
- Remove MCP config (not needed for upload-on-merge flow)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Gemini TTS returns raw PCM (audio/L16;rate=24000), not WAV
- Add pcm_to_wav() to wrap audio in proper RIFF/WAVE header
- Add tone guidelines for podcast scripts: accessible, user-focused,
intuitive analogies, matter-of-fact delivery
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add status maintenance workflow (claude code)
automated weekly maintenance of STATUS.md:
- archives old sections to .status_history/YYYY-MM.md when over 500 lines
- optionally generates audio overview via gemini TTS
- uploads to plyr.fm using bot account
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use claude_args instead of allowed_tools, remove PR trigger
- claude-code-action uses claude_args with --allowedTools flag
- PR trigger won't work for new workflow files (security feature)
- will test via workflow_dispatch after merge
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use scripts/generate_tts.py instead of inline python
- added scripts/generate_tts.py for gemini TTS generation
- workflow tells claude to run the script, not recreate it
- upload via plyrfm CLI with PLYR_TOKEN env var
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: remove STATUS.md from gitignore
STATUS.md contains no secrets and needs to be tracked for the
status maintenance workflow to work in CI.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: track STATUS.md in version control
~1200 lines of project status history. will be archived down to ~500
lines by the status maintenance workflow.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: create PR instead of pushing directly to main
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- add favicon (shield emoji)
- fix flags list to load automatically on page load with saved token
- fix refresh button indicator styling (hide when not loading)
- add environment badges (dev/stg) for non-production namespace flags
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* chore: add rust CI and pre-commit checks
- Rename Justfile → justfile for Linux case sensitivity
- Add cargo check pre-commit hooks for moderation + transcoder
- Add check-rust.yml CI workflow (cargo check + docker build on PRs)
- Include moderation/static/** in deploy trigger paths
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: correct rust-toolchain action name
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Move HTML/CSS/JS from inline const to static/admin.{html,css,js}
- Serve static files via tower-http ServeDir
- Add Io error variant for file read errors
- Update Dockerfile to copy static directory
admin.rs: 730 → 310 lines
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add label context for enriched admin UI
adds track metadata (title, artist, matches) alongside copyright labels
so the admin UI can show meaningful info for reviewing flags.
changes:
- add label_context table to moderation service DB
- update /emit-label to accept optional context payload
- add /admin/context endpoint for backfilling context
- update admin UI with card-based layout showing track info and matches
- update backend to send context when emitting labels
- add backfill scripts for labels and context
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use --with-editable instead of sys.path hack
properly install backend package via uv's --with-editable flag
instead of manipulating sys.path
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: update test to expect full label context parameters
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- make /admin public so HTML page loads (auth handled client-side for API calls)
- reject protected requests if MODERATION_AUTH_TOKEN not set (security fix)
- add CI workflow to auto-deploy on push to main when moderation/** changes
requires FLY_API_TOKEN_MODERATION secret to be set in GitHub
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add admin UI for reviewing copyright flags
adds /admin endpoint to moderation service with:
- simple HTML/JS UI for reviewing flagged tracks
- /admin/flags API to list copyright-violation labels
- /admin/resolve API to create negation labels (false positives)
- auth via existing X-Moderation-Key header
resolves #388
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: modularize moderation service
breaks main.rs into focused modules:
- config.rs: environment config loading
- state.rs: AppState and AppError types
- auth.rs: authentication middleware
- audd.rs: AuDD fingerprinting types and handler
- xrpc.rs: ATProto labeler endpoints
- handlers.rs: health, landing, emit_label
main.rs now only composes routes (~100 lines vs ~700)
also fixes clippy warning in labels.rs (redundant closure)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- add CopyrightInfo dataclass with is_flagged and primary_match
- extract most frequent match from copyright scan results
- add copyright_match field to TrackResponse API schema
- update tooltip to show "potential copyright violation: [match]"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add `copyright_flagged` field to TrackResponse API schema
- batch fetch copyright flags using `get_copyright_flags` aggregation
- display warning icon next to flagged tracks in portal
- style flagged tracks with amber tint background/border
- make warning icon clickable to view ATProto record when available
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Update overview.md with current architecture diagram and admin UI options
- Update copyright-detection.md with actual schemas and label querying
- Add atproto-labeler.md with full labeler service documentation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: implement ATProto labeler for copyright violations
adds a full ATProto-compliant labeler service that emits signed
`copyright-violation` labels when tracks are flagged by copyright scans.
moderation service (rust):
- add Label struct with DAG-CBOR serialization and secp256k1 signing
- add Postgres database for label storage with sequence numbers
- implement com.atproto.label.queryLabels XRPC endpoint
- implement com.atproto.label.subscribeLabels WebSocket with backfill
- add /emit-label endpoint for backend integration
- add landing page at root URL
- fix: split migrations into separate statements (postgres requirement)
- fix: enable tls-rustls for Neon database connections
backend (python):
- add _emit_copyright_label() to POST labels when tracks flagged
- add labeler_url config setting
- emit label only when track has atproto_record_uri
includes regression tests for label emission behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update neon project names
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
AuDD enterprise API doesn't return confidence scores (all scores are 0),
so we now flag tracks if any matches are found instead of comparing against
a score threshold.
Also adds --max-duration flag to scan script to skip long DJ sets.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: wire up copyright scan integration with moderation service
- add ModerationSettings to config (service URL, auth token, timeout)
- create moderation client module to call /scan endpoint
- hook into upload flow with fire-and-forget background task
- scan runs after successful track commit, doesn't affect upload status
- add 8 tests covering success, timeout, disabled, and error scenarios
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: add missing logfire[sqlalchemy] dep to copyright scan script
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: store scan errors as clear results instead of leaving tracks unscanned
when moderation service can't process a file (too short, bad format, etc.),
we now store a clear result with error info in raw_response so the track
isn't stuck in limbo forever.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- add request_attributes_mapper to parse User-Agent headers
- enrich spans with client_type (sdk/mcp/browser) and client_version
- enables filtering/clustering traffic by source in Logfire UI
- fix stats positioning on album detail page (move Header outside container)
- fix pre-existing lint warning in test_auth.py
closes #377
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Show whimsical wave animation when scrolling to end of track list
- Three waves with trailing ghost effects, 120° phase offset each
- Colors derived from user's accent color using color-mix()
- Delicate, ephemeral aesthetic inspired by points of light
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: improve stats sidebar design and positioning
- Center sidebar in left margin properly
- Add icons back for context (play, track, artist)
- Smaller, cleaner design without card/box
- Horizontal layout: icon + value + label
- Fix loading lurch by matching skeleton structure
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: move platform stats from sidebar to header
- Move stats to header nav, positioned in left margin on desktop
- Stats appear inline with icons (plays, tracks, artists)
- Collapses into LinksMenu on narrow screens (<1300px)
- Remove floating sidebar approach for cleaner layout
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add platform stats display on homepage
- add /stats API endpoint returning total_plays, total_tracks, total_artists
- add PlatformStats component with skeleton loading
- display stats bar at top of homepage
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: improve platform stats presentation
- add plural handling (1 artist vs 2 artists)
- move stats to desktop sidebar (left of main content)
- integrate stats into LinksMenu popover for mobile
- add 'menu' variant with vertical grid layout
- make 'bar' variant vertical for sidebar display
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* style: add icons and improve stats placement
- add play, track, artist icons to both variants
- use fixed positioning for desktop sidebar
- position stats card relative to main content area
- anchors to left edge of content on wide screens (1200px+)
- cleaner card design with header/content separation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: stats placement and breakpoint gap
- show stats inline (above tracks) on medium screens (769-1099px)
- only hide when LinksMenu becomes visible (768px)
- align sidebar top with content area (top: 140px)
- use max() for left position to prevent going off-screen
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: align LinksMenu breakpoint with sidebar
Stats are now always visible:
- 1100px+: sidebar on left
- <1100px: LinksMenu (info icon) with stats
No gap where stats are invisible.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* style: glass effect on stats sidebar
Match Toast design language:
- semi-transparent background rgba(10, 10, 10, 0.6)
- backdrop-filter blur(12px)
- subtle border rgba(255, 255, 255, 0.06)
- soft shadow
Also bump breakpoint to 1300px to prevent overlap.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- add skeleton loading for comments section (matching artist analytics pattern)
- use fade transitions between loading/loaded states
- add dark scrollbar styling to comments list
- center comments section on mobile
- prevent layout shift with min-height container
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- override document title with playing track across all pages
- only show playing track when actively playing (not paused)
- restore page title when paused
- track detail page defers to player when different track is playing
- fix hasPageMetadata to include artist and liked pages
- use music note icon for player artwork fallback (matches track detail page)
- remove status page subtitle in links menu
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- fix navigation to track detail page not interrupting playback
(removed stopPropagation that was blocking SvelteKit's router)
- render URLs in comments as clickable hyperlinks
- add handle autocomplete to login page for easier sign-in
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
integrate with navigator.mediaSession to provide track metadata (title,
artist, album, artwork) and action handlers (play/pause, prev/next,
seek) to system media interfaces like CarPlay, lock screens, and
Bluetooth devices.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: developer token management with listing and revocation
adds full lifecycle management for developer tokens:
**backend**
- add `is_developer_token` and `token_name` fields to UserSession model
- add GET /auth/developer-tokens endpoint to list user's tokens
- add DELETE /auth/developer-tokens/{prefix} to revoke tokens
- update POST /auth/developer-token to accept optional name parameter
- add AuthSettings config for token expiration limits
- fix connection pool leak in test fixtures (was causing TooManyConnectionsError)
**frontend**
- show existing tokens in portal with names, creation/expiration dates
- add token name input field when creating new tokens
- add revoke button for each active token
- add loading state indicator while fetching tokens
**tests**
- add 9 tests for developer token API (create, list, revoke)
- add integration test for upload/delete flow
- add `just backend integration` recipe
**docs**
- update authentication.md with token management section
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add cyclopts CLI for upload/download/list/delete
- PEP 723 inline script deps (just uv run it)
- pydantic-settings for PLYR_TOKEN/PLYR_API_URL config
- rich tables and status spinners
* refactor: rename RelaySettingsSection to AppSettingsSection
relay is the old project name
* refactor: use separate OAuth grants for developer tokens
Developer tokens now get their own OAuth credentials via a fresh
authorization flow, rather than copying credentials from the browser
session. This prevents tokens from going stale when browser sessions
refresh their tokens independently.
Changes:
- Add PendingDevToken model to store OAuth flow metadata by state
- Add POST /auth/developer-token/start to initiate OAuth for dev tokens
- Modify /auth/callback to detect dev token flows and create independent sessions
- Update frontend to redirect to PDS authorization and handle callback
- Remove old credential-copying endpoint
- Rewrite tests for new OAuth-based flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update authentication.md for OAuth-based dev tokens
- Update creating token flow to mention PDS authorization step
- Update API example to show /auth/developer-token/start endpoint
- Explain that tokens have independent OAuth grants, not inherited credentials
- Add PR #367 reference
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: default CLI to localhost, document env overrides
- Default PLYR_API_URL to http://localhost:8001 (local dev)
- Add docstring examples for staging and production overrides
- Prevents accidental production API calls during development
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: prevent dev token exchange from overwriting browser cookie
When creating a dev token via OAuth, the exchange endpoint was setting
a cookie with the dev token's session_id, overwriting the browser's
session cookie. This caused logout to delete the dev token instead of
the browser session.
Fix:
- Add is_dev_token flag to ExchangeToken model
- Pass is_dev_token=True when creating exchange token for dev tokens
- Skip cookie setting in /auth/exchange for dev token exchanges
Now dev tokens are fully independent of browser sessions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update dev token section with CLI usage and cookie isolation
- Replace sandbox script reference with scripts/plyr.py CLI
- Add CLI examples for list/upload/download/delete
- Mention cookie isolation in how it works section
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Set ORJSONResponse as the default response class for all FastAPI
endpoints. orjson is ~6x faster than stdlib json for encoding.
This is a drop-in replacement that applies globally without
per-endpoint changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: sync comment edits to ATProto record
comment edits were only updating the database, leaving ATProto records
stale. now updates ATProto record with new text and updatedAt timestamp
before persisting to database.
- add update_comment_record function to records.py
- update_comment endpoint now syncs to ATProto first
- ATProto record includes updatedAt field for edit tracking
- add regression test verifying ATProto sync on edit
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix atproto sync for comments
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add account deletion with explicit confirmation
users can now permanently delete all their data from plyr.fm:
- DELETE /account/ endpoint with handle confirmation
- deletes tracks, albums, likes, comments, preferences, sessions, queue
- deletes R2 objects (audio files, images)
- optional: delete ATProto records from user's PDS with separate consent
- clear warning about orphaned references when deleting ATProto records
docs: updated offboarding.md with full account deletion documentation
tests: comprehensive regression tests for all deletion scenarios
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: correct test model fields and use client.request for DELETE with body
- use client.request("DELETE", ..., json=...) since AsyncClient.delete() doesn't support json=
- use oauth_session_data (string) not oauth_data (bytes) for UserSession
- use state (dict) not track_ids/current_index for QueueState
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add pdsls.dev link for self-service PDS management
users can now click through to pdsls.dev to manage their ATProto
records directly, or let us clean them up via the checkbox option.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
the /preferences endpoint has a trailing slash, so requests without it
get 307 redirected. behind fly's proxy, this redirect was generating
http:// URLs causing mixed content errors. matching the pattern already
used in SettingsMenu.svelte.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: detect stale OAuth scopes and prompt re-login
when new features require additional scopes (like comments), sessions
with old scopes now get a friendly prompt to log in again instead of
cryptic errors.
backend:
- add scope validation helpers to parse and compare OAuth scopes
- require_auth now checks if session has all required scopes
- returns 403 with "scope_upgrade_required" when scopes are stale
frontend:
- detect scope_upgrade_required in auth initialization
- show friendly toast: "we added new features! log in again to use them"
- clears stale session so user sees logged-out state
includes 12 unit tests for scope validation logic.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: improve scope change message and add release link
- neutral wording: "permissions have changed" (not "new capabilities")
- adds clickable "see changes" link to latest release
- extends toast system to support action links
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add timed comments on tracks
- add TrackComment model with timestamp_ms for positioning
- add allow_comments toggle to UserPreferences (off by default)
- add ATProto fm.plyr.comment record support
- add GET/POST/DELETE endpoints for comments API
- add frontend UI for viewing comments on track page
- add toggle in portal settings for artists
- limit to 20 comments per track
- comments ordered by timestamp for playback sync
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: play button resume and clickable comment timestamps
- fix play button not resuming after pause (check if track is loaded, not playing state)
- make comment timestamps clickable to seek to that point in the song
- fix layout overlap with share button (add flex-direction: column to main)
- make comments section more compact with scroll overflow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: extract _make_pds_request utility for ATProto record operations
consolidates duplicated OAuth session handling and token refresh logic
from 5 separate functions into a single private utility:
- create_track_record
- update_record
- create_like_record
- delete_record_by_uri
- create_comment_record
also adds _parse_at_uri helper for AT URI parsing.
net reduction of ~147 lines.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: address PR review feedback
- add db.rollback() before cleanup on comment creation failure
- add aria-label for accessibility on comment input and toggle
- fix seekToTimestamp race condition by waiting for loadedmetadata
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: address copilot review feedback
- fix Index syntax in track_comment.py (can't use .desc() in constructor)
- make migration idempotent for allow_comments column
- add console.error for failed comment loads
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add constellation client for network-wide like counts
integrates with constellation.microcosm.blue backlink index to query
like counts across the entire atproto network, not just plyr.fm.
- adds get_like_count() for raw queries
- exports get_like_count_safe() from _internal (fallback on error)
- uses settings.atproto.like_collection for environment awareness
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: add user-agent header to constellation requests
per fig's request in constellation readme - be a good citizen
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Reverts the og:video experiment. iframely doesn't create links.player
from og:video for unknown providers. Restoring og:audio for platforms
that use it directly.
The oEmbed endpoint remains - Leaflet would need to check the oEmbed
html field as a fallback when links.player is missing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
iframely prefers og:audio over og:video, causing it to return the raw
MP3 as the player link instead of our embed iframe. Removing og:audio
should make iframely use og:video with type="text/html" for the player.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
iframely was using og:audio to return the raw MP3 as the player link,
causing Leaflet.pub to show a black HTML5 audio box instead of our
embed iframe. Adding og:video with type="text/html" pointing to the
embed URL should make iframely return the iframe as the player.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Enables services like Leaflet.pub (via iframely) to discover and use
our embed player instead of falling back to raw HTML5 audio.
- Add /oembed backend endpoint that returns oEmbed JSON for track URLs
- Add oEmbed discovery link to track page head
- Add comprehensive tests for oEmbed endpoint
The oEmbed response includes:
- iframe HTML pointing to /embed/track/{id}
- Track title, artist name, and artist URL
- Thumbnail when track has cover art
- Standard width (400) and height (165) matching our embed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: make mobile embed background more transparent
reduces overlay opacity from 0.65 to 0.35 so album art shows through
better, and increases logo visibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: add pointer-events: none to mobile background layers
The bg-image and bg-overlay elements were intercepting clicks
on mobile, preventing buttons and links from being clickable.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
on narrow screens (≤400px), switches to a blurred album art
background instead of showing a square image on the side.
this gives the full width for controls while maintaining the
visual identity of the track.
implements feedback from bluesky maintainers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add script to scan tracks for copyright
adds a UV script that:
- connects to any environment (dev/staging/prod)
- queries tracks without copyright scans
- calls moderation service for each track
- stores results in copyright_scans table
tested on staging - successfully scanned 3 tracks.
related to #166, #167
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: handle enterprise audd endpoint response format
- use enterprise.audd.io by default for large file support
- parse offset as string ("MM:SS") or integer (ms)
- add enterprise-only response fields (release_date, label, song_link)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>