The Playback quality setting now drives streaming: getStreamUrl appends
?quality= and useAudioPlayer feeds the server-synced streamQuality, so the
backend transcodes on demand (miss → original). Adds an optimizeTrack mutation
and an Optimize action in the metadata editor to warm a track's rendition.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full-screen overlay expanded from the persistent player: big artwork or a
Lyrics tab. Synced (LRC) lyrics highlight the active line by playback position,
auto-scroll, and seek on tap; plain text is the fallback; missing lyrics offers
a re-fetch. Adds lyrics endpoint + LRC parser + isNowPlayingOpen player state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AdminInstance shows backing-service health (db/redis/ml), read-only source
status (env-configured), and a library reindex with graceful handling when no
indexable source exists. InstanceSettings shows the active instance and offers
switch/sign-out to /connect. Adds getServices/getSources/reindex endpoints.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Duplicates / failed / missing-metadata tabs (paginated, reusing TrackRow) plus
admin orphan-file cleanup. Adds getDuplicates/getBrokenFiles/getMissingMetadata/
runCleanup endpoints and a DuplicateGroup type; drops the dead scanStorage/
deleteTrackFile stubs that had no backend route.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
User list with create dialog, role toggle, (de)activate, and reset-password;
self-lockout guard. Adds getUser/resetPassword endpoints, an isActive field on
the User type, and en/ru strings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds src/api/endpoints/settings.ts (getSettings/updateSettings,
getScrobbling/updateScrobbling) plus AppSettings/ScrobblingConfig types
and a Settings cache tag. Replaces the "coming soon" scaffolds:
- Playback: stream-quality preference (SegmentedControl), server-synced.
- Scrobbling: enable + provider (Last.fm/ListenBrainz) + username +
write-only session key, with a linked/not-linked badge and save states.
Built with modern-sk (Window/SegmentedControl/TextField/Button/Callout/
Badge) + the shared Loading/Error states; en + ru strings added. Server
state flows through RTK Query hooks only (no fetch in components).
Also drops a pre-existing unused TextField import in LibraryPage that was
failing `tsc --noEmit`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Tier-2 rehydrated cache seeded fulfilled entries at startup, so RTKQ served stale data and never hit the network (manual metadata edits, etc. only appeared after clearing the cache). Enable refetchOnMountOrArgChange + refetchOnReconnect + refetchOnFocus and wire setupListeners, so the cached snapshot still shows instantly but the server silently revalidates it whenever reachable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The album cards always fell back to the 💿 placeholder: the mapper dropped
the backend's `has_cover` and no album cover URL was ever built. Carry
`hasCover` through `RawAlbum`/`Album` and add `getAlbumCoverUrl` (GET
/albums/{id}/cover, token in the query like the track/stream URLs). The
Library Albums grid and the artist-detail discography now render real
covers, same source the album-detail page already used.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The transient client-side upload queue vanished on refresh, so a just-
uploaded track seemed to disappear. Add a server-backed "Recently
uploaded" section (source=upload, newest first) that survives refresh and
auto-refreshes after each upload (the upload mutation already invalidates
the `Track` tag this query provides).
- api: `source` filter on `LibraryFilters` → `GET /tracks?source=`
- i18n: `upload.recent.*` (en + ru); loading/error/empty states
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the "coming soon" stub with a real dashboard wired to
`GET /storage`. modern-sk visuals: a layered disk-capacity gauge (library
share vs other-used vs free), stat tiles (tracks/artists/albums/playtime/
footprint/avg size), per-format size bars, metadata-health badges, source
breakdown, a popularity-weighted top-genres cloud, and playful fun facts.
- types: full `StorageStats` shape + `toStorageStats` snake→camel mapper
- endpoint: re-point `getStorageStats` to `GET /storage` with transform
- lib: `formatLongDuration` for big playtime spans
- i18n: `storage.*` keys (en + ru)
- three list states (loading / error / empty) per the UI invariant
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the placeholder with a controlled form for title/artist/album/
year/genre/track number, an AcoustID "find matches" action showing
ranked candidates with confidence, a diff/apply picker, a re-enrich
button, and save via PUT /metadata. Adds matches/apply API endpoints,
mappers, types, and en/ru i18n strings. Batch editor remains a
placeholder (deferred).
Add a right-side track info drawer that sits to the right of the queue
panel when both are open. Shows a large cover, title/artist/album links,
a Play/Queue/Edit actions row, and Status/General/File/Identifiers
sections (empty rows omitted). Opens from the track context menu, the
player now-playing tile, and the queue now-playing card.
- ui slice: trackInfoId + open/closeTrackInfo
- TrackInfoDrawer rendered after QueuePanel in AppShell; overlays content
on narrow viewports
- map source/createdAt/enrichedAt from the wire (were unmapped)
- formatDateTime helper, info icon, i18n (en/ru)
- drop orphaned toggleNowPlaying/isNowPlayingOpen from player slice
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mapper dropped metadata_status and hardcoded availability, so enrichment
state was invisible and a just-uploaded track never appeared to change. Map
metadata_status/metadata_error/has_cover onto Track; add MetadataStatusBadge
(pending spinner / enriched / failed-with-reason / manual) shown in TrackRow,
and serve token-bearing track covers via getTrackCoverUrl.
UploadPage now polls each uploaded track (stops once enrichment settles) so the
resolved title/artist — or a failure reason — appears live. i18n in en + ru.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RTK Query 2.12's invalidation slice reads `provided.tags` during cache
rehydration (`Object.entries(provided.tags ?? {})`). Our persisted
snapshot only carried `{ queries, mutations }`, so `provided` was
undefined and `.tags` threw on every startup with a cached snapshot —
crashing the app inside the rehydrate reducer / immer produce.
Snapshot now carries the real `provided` (so invalidation tags
rehydrate), and `load()` defaults it to `{ tags: {}, keys: {} }` so
snapshots persisted before this field existed recover without a manual
localStorage clear.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a login/register toggle to ConnectPage backed by a new
useRegisterMutation (register -> /auth/me, mirroring login). The toggle
is shown only when REGISTRATION_ENABLED, resolved with the same
precedence as the API base URL: runtime window.__APP_CONFIG__ >
PUBLIC_ENABLE_REGISTRATION env > default true. The prod runtime-config
script injects the runtime flag. The backend's ALLOW_REGISTRATION stays
the real authority; this only gates the UI. EN/RU strings added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the faked ConnectPage login with a real /auth/login -> /auth/me
flow, including loading/error states. Add a backend-contract adapter layer
(api/mappers.ts) translating the backend's snake_case, lean *Out schemas and
{items,total,limit,offset} paging into the UI's camelCase domain types, so
swapping backends only touches the mappers.
- auth: chained login (tokens) + /auth/me (user); refresh on snake_case;
expiresIn optional (reauth is 401-driven, backend sends no TTL)
- streaming: GET /stream/{id}?token= (token query param for <audio>); SW
audio cache route + tests follow the path change (token stays cache-stable)
- library/playlists/likes/admin: correct paths (/tracks not /library/tracks),
page/pageSize<->limit/offset, duration_seconds->durationMs, likes as
append-only POST /likes event-log, admin is_superuser<->role
- downloads/storage: marked provisional (backend routes still stubs)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three tiers of offline support, all scoped to the active backend's
localStorage namespace (mirroring the auth slice):
Tier 1 — persist client state. queue + player slices are saved (queue
entries/index/source; player track/position/volume/repeat/shuffle) and
rehydrated on load, so a reload with no backend restores where the user
left off. Playback never auto-resumes (browsers block autoplay). Retires
the DEMO_QUEUE and isQueueOpen:true stubs.
Tier 2 — persist the RTK Query cache. Last-seen library/albums/artists
are snapshotted (fulfilled queries only) and replayed via RTKQ's
extractRehydrationInfo at startup, so the library renders read-only when
the backend is down. ConnectionStatus tooltip flags cached data offline.
No server data is copied into a slice — the cache feeds itself back.
Tier 3 — service worker audio + cover cache (PWA). Audio streams are
cached keyed by content id (token stripped), range-aware (synthetic 206
slicing), with a 500MB LRU cap, so already-played tracks play fully
offline. Cover art uses stale-while-revalidate in its own bounded cache.
Module worker (ESM); pure helpers split into sw-core.js and unit-tested.
Web app manifest enables "Install app". Player source badge now reflects
real cached state.
tsc clean, lint clean, 19 new tests pass, production build verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement the A8 upload screen against the existing /upload contract:
- UploadResponse type ({track_id, title, already_exists}) + mutation typed to it
- buildUploadFormData helper (single file under field `file`, per FastAPI)
- UploadPage: drag-and-drop + file picker, client-side queue with
concurrency cap (3), per-file status badges, retry on error,
already_exists -> "Already in library", deep-link to A7 metadata editor
- i18n upload.* section (en/ru) incl. "metadata pending" hint
Indeterminate spinner per file; percent progress is a follow-up
(needs an XHR baseQuery — fetchBaseQuery gives no upload progress).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>