Commit Graph

4 Commits

Author SHA1 Message Date
Frank Stellmacher bd56177e2c feat(home): Lossless Albums rail + dedicated page + sidebar nav (#506)
* wip(home): Lossless rail + dedicated /lossless-albums page

Rail under Home > mostPlayed and a dedicated infinite-scroll page that
list albums whose tracks are tagged in lossless containers. Walks
Navidrome's native /api/song?_sort=bit_depth&_order=DESC, dedupes by
albumId on the way down, stops when the song stream crosses into lossy
(bitDepth==0) or the server runs out of rows. _filters has no operators
on quality columns, so a sort + walk is the only path; equality on
sample_rate / bit_depth probes returned empty (verified).

The page paginates through the song-cursor with an in-flight ref so
overlapping IntersectionObserver fires don't double-add albums, plus a
cancelled flag so React StrictMode's double-mount doesn't apply two
parallel result sets in dev.

Suffix allowlist excludes ambiguous wrappers — m4a/m4b can be ALAC
*or* AAC and Navidrome's response carries an empty codec field, so we
can't tell them apart; same story for wma. Allowlist is flac, wav,
aiff/aif, dsf/dff, ape, wv, shn, tta — containers that are only
lossless. ALAC-in-m4a setups will miss out, acceptable trade-off
without a reliable codec field.

Status: WIP. Settings home-customizer label and en.ts strings landed,
no other locales yet, no quality badge on AlbumCards across the rest of
the app, no CHANGELOG. Branch was 'feat/home-hires-rail' during the
earlier hi-res-only iteration before the lossless broadening.

* wip(home): Lossless page header parity + streaming + sidebar nav

Page header now mirrors All Albums: selection mode with three action
buttons (Enqueue Selected, Add Offline, Download ZIPs) wired to the
same handlers Albums.tsx uses, selection-counter title swap, and the
perfFlags.disableMainstageStickyHeader respect path. Filters were
intentionally skipped — the rail is sorted by bit_depth, mixing client-
side filters with server-driven pagination would produce gaps.

Loading feels noticeably faster: ndListLosslessAlbumsPage takes an
optional onProgress callback that fires once per internal fetch with
the entries discovered in that fetch, so the page can stream new
albums into state instead of waiting on the whole loadMore. Page-side
budget dropped from 5×200 to 2×100 songs per loadMore (~1 MB worst
case vs 5 MB before), since the rail's catch-em-all pass is wrong for
infinite-scroll UX. Subtitle under the page title primes the user that
this is slower than other album pages because Psysonic walks the song
catalog by quality (Navidrome ignores _fields, so per-song responses
ship with lyrics + tags + participants whether we want them or not).

Sidebar nav entry registered under 'losslessAlbums' with a Gem icon,
defaults to visible:false (matches composers / folderBrowser /
deviceSync — niche browsing modes). Existing users get the entry
appended at the end of their persisted sidebar list automatically via
the onRehydrateStorage merge that sidebarStore already runs for new
DEFAULT_SIDEBAR_ITEMS.

i18n: full coverage across all 8 locales for sidebar.losslessAlbums,
home.losslessAlbums, losslessAlbums.empty, losslessAlbums.unsupported
and the new losslessAlbums.slowFetchHint subtitle. ru/zh are
machine-translation quality, flagged for a polish pass.

* feat(home): default Lossless sidebar entry to visible

Flips the DEFAULT_SIDEBAR_ITEMS entry for `losslessAlbums` from false
to true. Existing installs keep whatever the user has in persisted
storage; fresh installs see the entry in the sidebar from the start.

Earlier wip commit defaulted it off (matched composers / folderBrowser
/ deviceSync as a niche browse mode), but the rail + page do show
something useful for any library with at least one FLAC/WAV/etc. album,
so off-by-default just hid the feature.

* docs: changelog entry for PR #506

Logs the Lossless Albums rail + page + sidebar entry in v1.46.0
"## Added".

* docs(settings): contributor entry for PR #506

Adds the Lossless Albums rail/page bullet to Psychotoxical's
contributions list.
2026-05-07 20:44:27 +02:00
Frank Stellmacher 59744601d4 feat(composer): Browse by Composer page (issue #465) (#487)
* feat(composer): Browse by Composer page (issue #465)

New library section listing every artist credited as composer on at
least one track, with a detail page showing all works they're credited
on in that role. Targeted at classical-music libraries where the
"recording artist" tag carries the orchestra and the "composer" tag
carries Bach / Mozart / Chopin.

Hits Navidrome's native /api/artist?_filters={"role":"composer"} for
the listing and /api/album?_filters={"role_composer_id":"…"} for the
works grid — Subsonic getArtist only follows AlbumArtist relations and
returns 0 albums for composer-only credits, so the native API is the
only path that works. Requires Navidrome 0.55+ (uses
library_artist.stats role aggregation); on older / pure-Subsonic
servers the page shows a one-line capability banner.

- Two new Tauri commands: nd_list_artists_by_role +
  nd_list_albums_by_artist_role, generic over participant role so
  conductor / lyricist / arranger pages are trivial to add later.
- Composers grid: text-only compact tiles (name + participation count
  pulled from stats[role].albumCount). No avatars — composer libraries
  carry no useful imagery and the listing endpoint exposes no image
  URLs anyway.
- ComposerDetail: hero with Last.fm bio (via getArtistInfo2) plus the
  full work grid, with a graceful fallback when the artist has no
  external info synced.
- Sidebar entry default off (Feather icon) — opt-in for the niche
  classical use case.
- nd_retry backoffs widened from [500] to [300, 800, 1800] — helps
  every nd_* call survive intermittent TLS-handshake-EOF errors that
  some reverse-proxy setups produce when keep-alive pools churn.
- Distinguishes "server can't do this" (HTTP 400/404/422/501) from
  transient errors so the capability banner only fires when the server
  actually rejects the request shape; everything else gets a retry
  button.
- i18n in all 8 supported locales.

* fix(composer): address review feedback on detail page + role queries

- Re-fetch ComposerDetail when music-library scope changes; previously
  the album grid stayed stale until navigation while the list refreshed.
- Thread library_id through nd_list_artists_by_role and
  nd_list_albums_by_artist_role so role queries respect the active
  Navidrome library, matching the Subsonic musicFolderId already piped
  through libraryFilterParams().
- Fix CachedImage cache-key mismatch on ComposerDetail: a Last.fm header
  image was stored under the Subsonic cover-art key, aliasing cache
  entries and risking cross-source pollution.
- Consolidate the two contradictory composer-imagery comments in
  Composers.tsx into a single accurate one (the older one referenced an
  Images toggle that was never implemented).
- Align openLink toast duration with ArtistDetail (1500ms -> 2500ms).

* fix(composer): keep bio across scope changes, add share, degrade gracefully

Three remaining items from the latest review pass on the composer flow.

1. Bio survives a music-library scope change.
   The previous fix added musicLibraryFilterVersion to the load effect,
   but that effect also did setInfo(null) while the getArtistInfo effect
   still depended on [id] alone — so a scope bump on the open page
   wiped the bio without re-fetching it. Move the info reset into the
   bio effect (keyed on id) and out of the load effect: the album grid
   still refreshes on scope change; the Last.fm header image and
   biography survive untouched, since both are library-independent.

2. Composers join the share pipeline as a first-class entity kind.
   Extend EntityShareKind with 'composer' (and isEntityKind), branch
   applySharePastePayload to validate via getArtist (same id pool) and
   navigate to /composer/:id, and wire a Share button into
   ComposerDetail. A pasted composer link now opens the composer view
   instead of the artist view, matching what was copied. i18n added in
   all 8 locales (sharePaste.composerUnavailable, openedComposer;
   composerDetail.shareComposer, unknownComposer).

3. Partial server failure no longer hides the works.
   If getArtist rejects but ndListAlbumsByArtistRole succeeds, the page
   used to show full "not found" despite having data to display. Switch
   the not-found gate to require both empty (`!artist && !albums`) and
   render a degraded header (placeholder name, no Wikipedia / favourite
   / share / Last.fm image) when only metadata is missing.

* fix(composer): right-click share copies a composer link, not an artist link

The context menu opened from a composer card / row uses type='artist'
because every composer-action (radio, favourite, rating, add-to-playlist)
is identical to the artist counterpart — they share an id space and a
backend representation. Sharing was the one exception: the "Share Link"
entry produced a 'psysonic2-' string with k='artist', so a paste opened
/artist/:id even though the user came from /composers.

Add an optional shareKindOverride to openContextMenu (default: undefined,
preserves existing behaviour) and have the artist-typed branch consult
it when calling copyShareLink. Composers.tsx now passes 'composer' on
both right-click sites; nothing else changes downstream because the
override only affects the share kind.

* polish(composer): show Last.fm avatar even without server metadata

Two minor follow-ups from the latest review.

- ComposerDetail: drop the `&& artist` guard on the header-avatar render
  path. info?.largeImageUrl can resolve through getArtistInfo(id) without
  ever needing the SubsonicArtist record, so the previous gate hid a
  perfectly good Last.fm portrait whenever getArtist failed but the
  bio fetch succeeded. Replace artist.name with displayName so the
  alt / aria-label degrade to the localised "Composer" placeholder
  instead of empty strings.
- copyEntityShareLink: doc comment now mentions composer alongside
  track / album / artist.

* fix(composer): derive Last.fm cache key from route id, not from artist record

Follow-up to the previous polish: the avatar render path no longer
requires `artist` to be populated, but the cache-key gate still did. So
when getArtist failed but getArtistInfo returned a Last.fm portrait, the
key fell through to coverKey — which is empty without an artist record,
re-creating the very aliasing bug the earlier Subsonic-vs-Last.fm fix
was meant to close.

Switch the Last.fm branch to the route id (same id namespace as the
SubsonicArtist record), so the key stays stable whenever Last.fm art is
shown, independent of getArtist succeeding.

* docs: CHANGELOG + Contributors entry for composer browsing (PR #487)
2026-05-07 00:36:09 +02:00
Frank Stellmacher 1799e90e04 feat(tracks): Highly Rated rail + per-card star display (#443)
* feat(tracks): Highly Rated rail + per-card star display

Adds a new SongRail above the Random Pick on the Tracks page that
surfaces the user's highly-rated tracks (sorted by rating DESC).
Auto-hides on non-Navidrome servers and when the library has no
rated tracks yet. Reuses the existing SongRail layout, with the
standard reroll button forcing a cache bypass.

Per-card stars: any SongCard whose `userRating > 0` now shows a
small five-star row (filled to the rating value) below the artist
line — visible everywhere SongCard is used, not only in the new
rail. Read-only display; rating is still done via the row's
context menu or the Now Playing star widget.

Cache layer in `ndListSongs`: opt-in `cacheMs` parameter (skipped
by VirtualSongList; used only by the Highly Rated rail with a 60 s
TTL). Cleared on `setRating` mutation so a freshly-rated track
shows up on the next page revisit, and on server switch alongside
the existing token cache. The reroll button explicitly invalidates
before refetching, so a manual refresh always hits the network.

* docs(changelog): add #443 Tracks Highly Rated rail entry

* chore(credits): add #443 to Psychotoxical contributions
2026-05-03 18:54:07 +02:00
Frank Stellmacher e3aabd98b7 feat(tracks): add Tracks library hub page (closes #299) (#300)
New /tracks route with three sections:

- Hero "Track of the moment" — random pick with play / enqueue / reroll
- Random Pick rail — 18 song cards, rerollable; hero song deduped
- Browse all tracks — virtualized list (@tanstack/react-virtual),
  paginated 50 at a time

Browse uses Navidrome's native /api/song?_sort=title&_order=ASC for
proper A-Z order (no Subsonic equivalent), with automatic fallback
to search3 on non-Navidrome servers. Search input drives search3
with 300ms debounce. Bearer token cached module-level, re-auth on 401.

Play button on rows + cards calls a new enqueueAndPlay() helper that
appends to the existing queue (skip if duplicate) and jumps to the
song — different from playSongNow which replaces the queue. Enqueue
button stays opaque (no hover-only).

i18n keys for sidebar.tracks + tracks.* namespace in all 8 locales.
New AudioLines sidebar icon. Sidebar entry inserted between
"All Albums" and "Build a Mix".

Performance: cover thumbnails dropped (uniform layout instead),
RAF-throttled scroll prefetch, hover transforms removed from cards
(WebKitGTK compositing-friendly).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:23:15 +02:00