* feat: add public liked pages for any user (#498)
likes are stored on users' PDSes as ATProto records, making them public data.
this enables viewing any user's liked tracks without authentication.
backend:
- add GET /users/{handle}/likes endpoint
- add get_optional_session dependency for endpoints that benefit from optional auth
- endpoint returns user info, tracks, and count
frontend:
- add /liked/[handle] route with user header and track list
- add fetchUserLikes API function with UserLikesResponse type
- update LikersTooltip to link to user's liked page instead of profile
tests:
- add test_users.py with 4 tests for the new endpoint
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: consolidate optional auth into get_optional_session dependency
replaces inline session_id extraction pattern with reusable FastAPI dependency:
- listing.py: uses get_optional_session instead of manual cookie/header parsing
- playback.py: uses get_optional_session for get_track and increment_play_count
- removes utilities/auth.py (get_session_id_from_request no longer needed)
- updates test_hidden_tags_filter.py to override dependency instead of patching
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add show_liked_on_profile preference
Adds a new user preference (defaults to false) that will allow users
to display their liked tracks on their artist page.
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add show_liked_on_profile setting with artist page integration
- Add show_liked_on_profile to preferences API (get/update)
- Expose preference in artist API response for public profiles
- Add settings toggle in frontend privacy & display section
- Update artist page to conditionally fetch and display liked tracks
- Update Preferences interface and layout to include new field
When enabled, a user's liked tracks are displayed on their artist page
below albums, allowing others to discover music they enjoy.
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add fm.plyr.list lexicon for playlists and albums
introduces ATProto record type for ordered track collections:
- lexicons/list.json: defines fm.plyr.list with strongRef track references
- purpose field distinguishes "album", "playlist", "collection"
- items array ordering determines display order (reorder via putRecord)
- each item uses strongRef (uri + cid) for content-addressability
backend infrastructure:
- create_list_record/update_list_record in records.py
- list_collection added to OAuth scopes
- exported from _internal.atproto module
design notes:
- strongRef ensures list items point to specific track versions
- when tracks are deleted, list records should be updated to remove refs
- albums can be formalized as list records with purpose="album"
- no records created yet - this is infrastructure for future integration
relates to #221 (ATProto records for albums) and #146 (content-addressable storage)
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: generalize fm.plyr.list to support any record type
- renamed listItem.track to listItem.subject for generic references
- added more purpose knownValues: discography, favorites
- updated descriptions to clarify any ATProto record can be referenced
- subject.uri indicates record type (e.g., fm.plyr.track, fm.plyr.list)
enables:
- lists of tracks (albums, playlists)
- lists of albums (discographies)
- lists of lists (playlist collections)
- lists of artists (following, featured)
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: simplify fm.plyr.list lexicon to minimal fields
stripped to essentials per lexicon style guide:
- required: items, createdAt
- optional: name (display name), listType (semantic category)
- removed: purpose, description, imageUrl, addedAt
listType knownValues: album, playlist, liked
(extensible - any string valid per ATProto knownValues spec)
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add optional updatedAt field to fm.plyr.list
- lexicon: optional updatedAt datetime field
- update_list_record auto-sets updatedAt to now
- create_list_record omits updatedAt (new records)
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add feat/playlists branch status to STATUS.md
documents current state of list infrastructure:
- what's done (lexicon, backend functions, oauth scopes)
- what's NOT done (no UI, no records created, no migrations)
- design decisions and next steps
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add fm.plyr.actor.profile lexicon and upsert function
- create profile.json lexicon with bio, createdAt, updatedAt fields
- add profile_collection to AtprotoSettings config
- add profile collection to OAuth scopes
- implement build_profile_record and upsert_profile_record
- uses putRecord with rkey="self" for upsert semantics
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: wire up profile record upsert to artist bio endpoints
- call upsert_profile_record when bio is set on create/update
- handle ATProto failures gracefully (log but don't fail request)
- add tests for profile record integration
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: sync profile record on login with proper background task handling
- add background sync in GET /artists/me for existing users with bios
- skip write if ATProto record already exists with same bio (no-op)
- use proper task lifecycle management to prevent GC before completion
- return None from upsert_profile_record when skipped
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: simplify profile sync to silent fire-and-forget
remove over-engineered response field for toast notification.
profile record sync happens silently in background on GET /artists/me.
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: remove unused check_profile_record_sync_needed
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: always sync profile record, not just when bio exists
profile record should be created on login regardless of whether
user has a bio set. bio is optional in the lexicon.
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: sync albums and liked tracks as ATProto list records on login
Backend:
- Add upsert_album_list_record() and upsert_liked_list_record() functions
- Wire up fire-and-forget sync on GET /artists/me for all artist albums
- Wire up fire-and-forget sync for user's liked tracks list
- Persist ATProto URIs/CIDs back to database after sync
- Migration: add liked_list_uri and liked_list_cid to user_preferences
Frontend:
- Artist page: replace inline liked tracks with link card to /liked/{handle}
- Add "collections" section header to distinguish from albums
- Liked page: handle link now goes to artist page, not Bluesky
Design decisions:
- Liked list references track records directly (not like records) for simplicity
- Array order = display order (ATProto-native approach)
- Albums ordered by track created_at asc, likes by created_at desc
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add library hub page and update navigation
- create /library route as hub for personal collections
- show liked tracks card with count
- add placeholder for future playlists
- change nav from "liked" โ "library" (heart icon goes to /library)
- keep /liked route for direct access
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update feat/playlists branch status in STATUS.md
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: scope upgrade OAuth flow for teal.fm integration (#503)
* feat: add scope upgrade OAuth flow for teal.fm integration
- Add /auth/scope-upgrade/start endpoint that initiates OAuth flow with
expanded scopes (mirrors developer token pattern)
- Replace passive "please re-login" message with immediate OAuth redirect
when user enables teal.fm scrobbling
- Fix preferences bug where toggling settings reset theme to dark mode
(theme is client-side only, preserved from localStorage on fetch)
- Add PendingScopeUpgrade model to track in-flight scope upgrades
- Handle scope_upgraded callback to replace old session with new one
- Add tests for scope upgrade flow
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update STATUS.md with scope upgrade OAuth flow
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: resolve test failures from connection pool exhaustion
- add DATABASE_POOL_SIZE=2, DATABASE_MAX_OVERFLOW=0 to pytest env vars
- dispose ENGINES cache after each test in conftest to prevent connection accumulation
- fix mock_refresh_session functions to accept `self` parameter (method signature)
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add full-page overlay for developer token display
when users return from OAuth after creating a developer token,
show a prominent overlay so they don't miss it. the token won't
be shown again after dismissing, so this ensures visibility.
- full-page modal with blur backdrop
- copy button with success feedback
- warning text emphasizing save-now urgency
- link to python SDK docs
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* docs: update STATUS.md with completed scope upgrade work
- mark feat/scope-invalidation as merged to feat/playlists
- document developer token overlay feature
- document test fixes for connection pool exhaustion
- note all 281 tests passing
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: remove confusing album migration note from STATUS.md
albums sync as list records on login - no migration needed
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: implement playlist CRUD with ATProto integration
- add playlist endpoints to lists.py (create, list, get, add/remove tracks, delete)
- add Playlist model for database caching of ATProto list records
- add playlist types to frontend (Playlist, PlaylistWithTracks, PlaylistTrack)
- update library page with playlist list and create modal
- fix font inheritance on create button
- filter search results to exclude tracks already in playlist
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use API_URL for playlist endpoints and fix button font inheritance
- fix all fetch calls to use API_URL instead of /api/ relative paths
- add font-family: inherit to all modal buttons
- library page: create playlist modal buttons
- playlist page: add-btn, empty-add-btn, cancel/confirm buttons
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add share button and link previews for playlists
- add font-family: inherit to search input in playlist modal
- add font-family: inherit to create playlist input
- add ShareButton component to playlist page (visible to all users)
- add public /lists/playlists/{id}/meta endpoint (no auth required)
- add +page.server.ts to fetch playlist meta for SSR
- add OG meta tags for link previews (og:type, og:title, og:description, twitter:card)
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: playlist enhancements and UX improvements
- add "add to playlist" menu to track items (AddToMenu component)
- include playlists in global search results
- filter current playlist from add-to-playlist picker on playlist detail page
- add "create new playlist" link in playlist picker menus
- show playlist artwork in library page list
- fix portal empty playlist state link to open create modal
- update edit button tooltip to "edit playlist metadata"
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: portal album/playlist consistency
- use consistent edit icon (document+pencil) for album edit button
- match playlist grid sizing to album grid (200px min, 1.5rem gap)
- match playlist card padding and font sizes to album cards
- update placeholder icon size from 32 to 48
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: standardize liked tracks page buttons to match playlist/album style
* fix: populate is_liked on playlist tracks and resolve svelte-check warnings
* fix: add toast notifications for mutations (playlist CRUD, profile updates)
* feat: graceful degradation for unavailable tracks in playlists
when a track in someone's PDS list no longer exists in the database
(e.g., the track owner deleted it), we now show it grayed out with
"track unavailable" instead of silently hiding it.
- add UnavailableTrack schema to backend
- return unavailable_tracks array from get_playlist endpoint
- render unavailable tracks with muted styling in frontend
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* jpeg fix and a couple other tings
---------
Co-authored-by: Claude <noreply@anthropic.com>