mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
09f24beb32d57b70a393e6e681d16e1790d33bf0
549 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
09f24beb32 | feat(audio): hi-res transition blend rate for crossfade, AutoDJ, and gapless (#1171) | ||
|
|
0b7b5df30c |
test(ui): smoke-test the #1165 hook-guard split and recent-search rename (#1168)
* test(cover): smoke-test CoverArtImage hook-guard split (#1165) * test(search): smoke-test MobileSearchOverlay recent-search (#1165) |
||
|
|
7c724a642f |
chore(eslint): add ESLint toolchain and clean src to strict 0/0 (#1165)
* chore(eslint): add eslint toolchain and configs * fix(eslint): resolve gradual-config errors (rules-of-hooks, no-empty, prefer-const, …) * chore(eslint): clear unused vars in config, contexts, app and test * chore(eslint): clear unused vars in api and music-network * chore(eslint): clear unused vars in utils * chore(eslint): clear unused vars in store * chore(eslint): clear unused vars in cover and hooks * chore(eslint): clear unused vars in components * chore(eslint): clear unused vars in pages * chore(eslint): remove explicit any in src * chore(eslint): align react-hooks exhaustive-deps * chore(eslint): zero gradual config on src * chore(eslint): strict hook rules in store and utils * chore(eslint): strict hook rules in hooks and cover * chore(eslint): strict hook rules in components * chore(eslint): strict hook rules in pages and contexts * chore(eslint): document scripts ignore in eslint config * chore(eslint): add lint script and zero strict findings * chore(eslint): address review round 1 (gradual 0/0, per-site disable reasons) * chore(eslint): tighten two set-state disable comments (review round 2 LOW) * chore(nix): sync npmDepsHash with package-lock.json * docs(changelog): add Under the Hood entry for ESLint setup (PR #1165) --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
c7d76af790 |
feat(themes): add follow-system mode to the theme scheduler (#1163)
* feat(themes): add follow-system mode to the theme scheduler The theme scheduler can now switch the day/night theme pair by the OS light/dark preference instead of a clock schedule. A segmented control picks the trigger; in system mode the time inputs are hidden and the two theme pickers read as Light/Dark. The OS theme is read via the native Tauri window theme API (theme() + onThemeChanged) rather than the Web prefers-color-scheme media query, which is unreliable through WebKitGTK on Linux. Live updates land instantly where the platform forwards them; on Linux setups that don't, a hint notes the change applies after an app restart. * docs(changelog): add follow-system theme mode entry (#1163) CHANGELOG Added entry, contributor credit and What's New highlight for the theme scheduler's new system-theme mode. |
||
|
|
78a177b1bc |
fix(orbit): outbox lost-update recovery and session hardening (#1159)
* fix(orbit): harden the guest outbox — recover lost suggestions, avoid a duplicate outbox Two outbox robustness issues on the poll-based, non-atomic playlist transport: - Lost-update: a track a guest appends to its outbox can be wiped by a concurrent host sweep-clear before the host recorded it, leaving the suggestion lost AND stuck on "waiting on host" forever (it only clears once it reaches the host's play queue). The playlist read-modify-write can't be made atomic, so recover: re-send a pending suggestion the host hasn't recorded (absent from state.queue) past a grace window — the host dedupes by (user, trackId), so it's idempotent — and give up + toast past a 45s window so the row stops hanging. New pendingResend planner module with unit tests; suggest + tick share ensureTrackInOutbox. - Duplicate outbox: a transient getPlaylists failure at join was swallowed to [], so the existing-outbox check missed and a second outbox was created. Distinguish "lookup failed" from "genuinely absent" and retry once before falling back to create. New i18n key for the give-up toast across all locales. * fix(orbit): reject non-http(s) schemes in share invites normalizeShareServerUrl only prepends http:// when the server string doesn't already start with "http", so an invite carrying a scheme like httpx:// or https-phish:// slips through unchanged. Reject anything whose parsed protocol isn't http(s) at parse time, before the known-server match. Add round-trip tests covering the accepted and rejected schemes. * fix(orbit): read exit-modal role fresh to avoid a stale keydown closure The exit modal's Enter/Escape handler binds once per open (deps [isOpen]) and closed over the role prop, so a role change while the modal was up would act on a stale value. Read role from the store inside the action instead, and drop the now-unused reactive subscription. * docs(changelog): consolidate Orbit fixes into one block, add 1159 |
||
|
|
9b03949f34 |
feat(orbit): sync the host's track-transition settings to guests (#1158)
* feat(orbit): mirror host track-transition settings into the session Each client previously used its own local crossfade / gapless / AutoDJ settings, so guests blended tracks differently from the host and drifted out of sync — the exact drift the Catch-Up button exists to paper over. Add an optional OrbitSettings.transitions (crossfade enabled/secs/trim, AutoDJ smooth-skip, gapless), additive on the wire so older sessions simply omit it. The host seeds its own prefs at start and refreshes them every tick, so a mid-session change propagates. Guests snapshot their own prefs on join, adopt the host's for the session (idempotent — no setState churn / audio-sync re-fire when unchanged), and restore their own on leave. Applying via authStore.setState reuses the existing authSyncListener path to the Rust engine, so no new IPC. Without the Hot cache a guest's AutoDJ blend degrades gracefully via the existing byte-preload fallback. Add a bridge module with unit tests for read/apply/save/restore. * feat(orbit): lock guest transition controls during a session Since a guest now mirrors the host's track-transition settings (re-applied every read tick), their own controls would visibly fight the sync. Disable them while a guest is in a live session and explain why. Settings -> Audio -> Track transitions greys out the mode picker, seconds slider and smooth-skip toggle with a "controlled by the Orbit host" note; the queue toolbar's Gapless / Crossfade / AutoDJ quick-toggles get the same disabled state and tooltip. Host controls are untouched. New i18n key added across all locales. * docs(changelog): add Orbit transition sync (1158) |
||
|
|
15cecb5d7d | feat(server): custom HTTP headers for reverse-proxy gates (#1095) (#1156) | ||
|
|
bd43ea5240 |
fix(playlists): wrap header action buttons instead of overflowing (#1153)
* fix(playlists): wrap header action buttons instead of overflowing The Playlists header action row was a fixed flex row, so at narrow widths (e.g. with the queue panel open) the buttons overflowed and were clipped off-screen. Add flex-wrap so they wrap, left-aligned, matching the Albums toolbar behaviour. * docs(changelog): add 1.49.0 entry for Playlists header button wrap (#1153) |
||
|
|
93166c774a |
feat(i18n): add Hungarian (hu) translation (#1149)
* feat(i18n): add Hungarian (hu) translation files * feat(i18n): register Hungarian locale and language option * docs(changelog): Hungarian translation entry and credit (#1149) * fix(i18n): list Romanian in the Login language picker |
||
|
|
ec8cfbc1c8 |
fix(eq): show the active AutoEQ profile name in the preset picker (#1147)
* fix(eq): surface the active AutoEQ profile name in the preset picker * i18n(settings): add AutoEQ preset group label (10 locales) * docs(changelog): AutoEQ active-profile picker fix (#1147) |
||
|
|
887c940e1b |
feat(eq): remember EQ profile per audio output device (#1146)
* feat(eq): add per-device EQ snapshot state to eqStore * feat(eq): wire per-device EQ sync into audio listener setup * feat(settings): add 'remember EQ per device' toggle to audio output section * i18n(settings): add 'remember EQ per device' strings (10 locales) * test(eq): cover per-device EQ snapshot store + device sync orchestration * docs(changelog): per-device EQ entry, credit and what's new (#1146) * i18n(settings): note per-device EQ scope (system default + explicit switch) in toggle description * test(eq): cover system-default bucket and device-reset (null) path |
||
|
|
986550c854 | fix(favorites): bulk add-to-playlist and play/enqueue selected (#1140) | ||
|
|
30ccaf51a8 |
fix(updater): rebuild the update modal on a reusable Modal (#1142)
* feat(ui): add reusable Modal component Portal + dimmed backdrop (no backdrop-filter — on WebKitGTK the blur bleeds onto modal content on some GPU stacks), Escape/backdrop/X to close, header (icon/title/subtitle) + scrollable body + footer slot. First consumer is the update modal; the other hand-rolled modals migrate onto it over time. * fix(updater): rebuild the update modal on the reusable Modal Fixes a user report (Manjaro / AMD / X11): blurry modal text (the eq-popup backdrop-filter bled onto the content), the version arrow sitting between the two header rows, and the unclear Skip / Remind buttons. Now uses the no-blur Modal shell; the header icon aligns with the title; Skip is a clear button and Remind me later is the accent action when there is no in-app install. Drops the now-dead update-modal shell CSS. * docs(changelog): update notification popup fix (PR #1142) |
||
|
|
b950d4704b |
feat(cover): artist artwork from fanart.tv (off by default) (#1137)
* feat(cover): add artist_artwork_lookup table + accessors
Image-scraper P0 (design-review §12): additive library-SQLite migration 013
plus get/upsert/clear-per-server accessors for the external artist-artwork
lookup (fanart.tv). Render never reads it; the on-demand cover ensure path
and the mbid_ambiguous 24h negative cache use it. server_id = serverIndexKey.
* feat(cover): add fanart.tv + getArtistInfo2 provider layer
Image-scraper P0 (§7/§19/§23): new cover_cache/external.rs — Rust-side
getArtistInfo2 tag-MBID resolution plus fanart.tv v3/music URL + first
artistbackground fetch (BYOK client_key sent in addition to the project
api_key per fanart.tv ToS, §22). Extract a shared build_subsonic_url helper
in fetch.rs (cover URL behaviour unchanged). Add a dedicated low-concurrency
fanart_http_sem so external HTTP never starves Navidrome (§26). URL builders
unit-tested; wired into ensure_inner next.
* feat(cover): wire fanart.tv external branch into ensure_inner
Image-scraper P0 (§16): on-demand artist `fanart` ensures try fanart.tv
before the Navidrome fallback. MBID resolved Rust-side via getArtistInfo2
(§23, tag MBID); on a miss it falls through WITHOUT a .fetch-failed marker so
Navidrome stays the display fallback (§28). External tiers are written as
{tier}-fanart.webp in the same entity dir (same cacheKind, §16) — 2000 + 512
(matryoshka §17); peek prefers them for the fanart surface. Dedicated
low-concurrency fanart lane (§26); .miss-fanart ~30min negative marker.
Additive IPC args (externalArtworkEnabled, surfaceKind), off by default and
gated by PSYSONIC_FANART_KEY — inert until a render surface opts in (P1).
Quality gate (§11), name->MusicBrainz (§19), and lookup-table writes are P1.
* feat(cover): fanart-first peek for the fanart surface
Image-scraper P0: for an artist `fanart` ensure, the early peek serves only
the external {tier}-fanart.webp tiers; if none exist yet it returns None so
ensure runs the external branch (fetch fanart) instead of short-circuiting on
a cached Navidrome tier. Realises "fanart prioritised" (§18) for the opt-in
surface; Navidrome stays the fallback inside the branch's miss path.
* chore(cover): dev-only artist-fanart spike helper
DEV-only window.psyFanartSpike(name) — resolves an artist by name and fires
the real cover_cache_ensure with externalArtworkEnabled+surfaceKind=fanart to
verify the P0 pipeline against a live server (with PSYSONIC_FANART_KEY set).
Not wired in production.
* feat(cover): §11 quality gate for the fanart surface
Before an external fanart fetch, check whether a Navidrome tier already on
disk is an HQ ~16:9 image (width >= 1280, aspect 1.6-2.0) and skip the fetch
if so — square artist portraits never satisfy it, so the common case still
fetches. Reads tier dimensions only (no full decode). Rust consts per the
design review. Unit-tested predicate.
* feat(cover): persist fanart resolution in artist_artwork_lookup (§12)
Wire the lookup table as both the MBID resolution cache and the negative
cache: a cached MBID skips the getArtistInfo2 round-trip; no_mbid/mbid_ambiguous
back off 24h and miss 30min from updated_at before re-querying. Writes
hit/miss/no_mbid with mbid/mbid_source/provider; transient network errors are
not cached. All store reads/writes run off the async executor via
spawn_blocking and no-op before login. Store reached via app.try_state::<LibraryRuntime>().
* feat(cover): compile-time fanart key fallback + album/name IPC args
A runtime PSYSONIC_FANART_KEY still wins (dev), else the key baked in at build
time via option_env! (release). Add additive artistName/albumTitle ensure args
as context for the §19 name->MusicBrainz fallback (inert until the render
passes them). Library backfill passes None.
* feat(settings): add External Artwork Scraper toggle under Integrations
Master toggle (themeStore, off by default per §20) in a new Integrations
subsection, alongside the other opt-in third-party categories. Contacts
fanart.tv only when enabled. i18n across all 9 locales.
* feat(cover): wire fanart background into the fullscreen player (§28)
New useArtistFanart hook resolves a fanart.tv 16:9 background via a dedicated
cover_cache_ensure (surfaceKind=fanart) — it bypasses the shared peek/disk-src
cache (the {tier}-fanart.webp surface is keyed differently) and reuses
coverDiskUrl for the asset URL. Fullscreen background priority is now
fanart -> Navidrome artist image (cover pipeline) -> album cover; the live
useFsArtistPortrait probe is deleted (§28). Additive ensure opts
(surfaceKind/artistName/albumTitle); externalArtworkEnabled is derived in
ensureArgsFromRef from the master toggle and restricted to the artist fanart
surface, so plain cover ensures are unaffected.
* feat(cover): generalize external surface to fanart + banner (§13)
surfaceKind='banner' fetches the fanart.tv musicbanner array -> {tier}-banner.webp
in the same entity dir; fanart stays the 16:9 artistbackground. The ensure
branch, peek, lookup rows (per-surface surface_kind), miss marker
(.miss-<surface>) and tier suffix are all surface-parameterised. The §11
quality gate stays fanart-only (the banner strip has its own aspect). Unit
test for the surface->fanart JSON key map.
* feat(artist): fanart banner on the artist-detail header (§13, Option B)
The artist-detail header gets an album-detail-style background layer: fanart.tv
banner (musicbanner) -> the 16:9 fanart background cropped to the strip ->
empty. Both via a shared useArtistExternalImage hook (useArtistBanner /
useArtistFanart); each fetches on demand and shares the Rust cache, so the
header and the fullscreen player warm each other's images. The header is its
own stacking context (isolation) so a z-index:-1 banner clips behind the avatar
+ meta with no content wrapper; the album-style framing (padding/radius/clip)
is applied only when a banner is shown, so the off-by-default case stays
pixel-identical.
* refactor(artist): reuse album-detail header structure for the fanart banner
The artist-detail header now uses the same album-detail-* container classes as
AlbumHeader (header/bg/overlay/content/hero) with the fanart banner as the
background; the Back button moves inside the header. A surgical
`artist-detail-bleed` cancels the artist page's .content-body padding so the
banner is full-bleed to the container edges, matching the album header exactly
instead of the earlier inset card. Reverts the experimental artist-specific bg
CSS.
* feat(cover): drop artist_artwork_lookup rows on clear-cover-cache (§12/B.4)
cover_cache_clear_server already removed the server's whole cover dir (so the
{tier}-fanart.webp / -banner.webp tiers + .miss-* markers go with it); also
clear the artist_artwork_lookup rows for that server (off-thread) so no stale
resolution state lingers. Automatic toggle-off purge deferred — turning the
toggle off already hides external artwork (render is gated), and explicit
cache-clear now cleans external state too.
* feat(cover): name->MusicBrainz album-confirmed MBID resolution (§19)
When getArtistInfo2 has no tag MBID and the ensure carries the artist name +
an album in context (fullscreen), one MusicBrainz release-search query resolves
the artist MBID: the primary artist across score>=90 releases wins, conflicting
ids -> mbid_ambiguous (24h backoff), none -> no_mbid. Sends the required
User-Agent; a single-permit musicbrainz_sem + >=1s spacing holds us under MB's
rate limit. mbid_source=musicbrainz persisted. Banner surface (no album
context) correctly skips this. Pure classify/escape helpers unit-tested.
* fix(cover): enable banner surface in ensureArgsFromRef
externalEnsureFields only set externalArtworkEnabled for surfaceKind 'fanart',
so the 'banner' surface never fired — the artist-detail header always fell back
to the fanart image instead of the fanart.tv musicbanner. Both external artist
surfaces (fanart/banner) now enable the external branch.
* feat(settings): optional BYOK personal fanart key field
Add an optional personal fanart.tv API key field to the External Artwork
Scraper block (shown when the toggle is on): a masked input, a saved/in-use
status line, and the simple note that it is sent in addition to the app key.
Persisted in themeStore and plumbed through cover_cache_ensure
(externalArtworkByok); Rust prefers the settings key, falling back to the
PSYSONIC_FANART_CLIENT_KEY dev env. i18n x9.
* fix(cover): resolve artist-page fanart image collision on navigation
The artist-detail header keyed its fanart/banner hooks on the route `id`,
which flips immediately on navigation while `artist`/`albums` refetch a beat
later. The mismatched ensure wrote the previous artist's image under the new
artist's id (e.g. Sepultura's image under Lordi's id).
- key on the loaded `artist.id`, not the route `id`, so id/name/album always
describe the same artist
- pick the §19 album context from an album that actually belongs to this
artist (`albums.find(a => a.artistId === artist.id)`), so a stale album can't
run a mismatched name→MusicBrainz query or cache a wrong `no_mbid`
- reset `src` on every input change in `useArtistExternalImage` so a previous
artist's image never lingers while the new one resolves
* fix(cover): strip trailing album qualifier before MusicBrainz lookup
Library titles like "Show No Mercy (2004 Remastered)" or "Album [Deluxe
Edition]" failed the §19 MusicBrainz release query, blocking name-confirmed
MBID resolution. `normalize_album_for_mb` strips a single trailing
parenthetical/bracketed qualifier; leading qualifiers (e.g. "(What's the
Story) Morning Glory?") are left intact. Unit-tested.
* fix(cover): don't cache no_mbid when album context is unavailable
The banner ensure could fire before the artist's albums loaded, with no album
in context. The old code cached `no_mbid` there and the 24h backoff then
blocked the later ensure that arrived *with* album context. Could-not-attempt
is not tried-and-failed: the no-album branch now returns without persisting.
* fix(cover): don't emit tier-ready for external fanart/banner surfaces
`try_external_fanart` emitted `cover:tier-ready` with the `{tier}-{surface}.webp`
path. That event is keyed by the canonical cover key (cacheKind/cacheEntityId/
tier, no surface), so the frontend `useCoverArtBridge` listener seeded the
Navidrome artist cover's disk-src cache with the external image — leaking
fanart/banner into the plain artist cover (avatar, fullscreen "navidrome-artist"
fallback) even with the scraper toggled off.
Remove the emit: the fanart/banner hooks read the path from the
`cover_cache_ensure` return value, so no event is needed. (No disk-level
overwrite — the suffixed files are never matched by `tier_exists`; this was
frontend disk-src-cache cross-contamination.)
* fix(cover): wait for the final external background before showing it, with fade-in
The fullscreen player and artist-detail header flashed several backgrounds in
sequence while the fanart resolved (upscaled album cover → Navidrome artist
image → fanart), and the artist header could show the fanart first and then
swap to the banner.
- the album cover is no longer a background source — it only feeds the
foreground thumbnail
- the external-artwork hooks return `{ src, pending }` so callers can tell
"still resolving" (hold back) from "resolved, no image" (fall back now)
- fullscreen background: scraper on → fanart, empty while it resolves, Navidrome
artist image only on a confirmed miss; scraper off → Navidrome artist image
- artist header: the banner is preferred — nothing shows while it resolves
(no fanart flash), fanart is the fallback only once the banner misses
- both backgrounds preload the chosen image and fade it in (`onLoad` plus a
`ref` `complete` check so an already-cached image, whose load event can fire
before React attaches the handler, still appears). The header fade is a
scoped inline opacity so the shared `album-detail-bg` class is untouched.
* ci(release): pass PSYSONIC_FANART_KEY into the macOS + Linux builds
* refactor(cover): extract external-artwork ensure into its own module
Pure code move: the on-demand fanart/banner fetch, the quality gate, the
surface-aware peek and the lookup-table cache move from cover_cache/mod.rs
into cover_cache/external_ensure.rs. Behaviour unchanged; mod.rs 1877 -> 1488.
* chore(cover): remove dev-only fanart spike helper
The real render wiring now exercises the external ensure branch, so the
dev-only window.psyFanartSpike helper is redundant.
* feat(cover): purge external artwork on opt-out (B3)
New cover_cache_purge_external command: when the External Artwork toggle is
turned off, drop every fetched {tier}-{provider}.webp, .miss-{provider}
marker and artist_artwork_lookup row across all configured servers, leaving
the canonical Navidrome covers intact. Opting out now removes the
third-party-sourced data instead of just hiding it (design-review §9/§12/B.4).
* docs: changelog, credits and what's new for artist fanart (PR #1137)
|
||
|
|
d8e5d4eed4 |
feat(i18n): add Japanese translation (#1134)
* feat(i18n): add Japanese translation Signed-off-by: Soli0222 <github@str08.net> * docs: changelog, what's new and credit for Japanese translation --------- Signed-off-by: Soli0222 <github@str08.net> Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> |
||
|
|
c037ab459a | fix(queue): suspend idle pull after local queue edits (#1132) | ||
|
|
955a9fcbd6 | feat(queue): play queue sync — manual pull, idle auto-sync, multi-server push (#1131) | ||
|
|
c428d37e0e |
Settings — own Audio categories + Queue Settings consolidation (#1130)
* refactor(settings): promote Normalization and Track transitions to own Audio categories Pull Normalization and Track transitions out of the combined Playback section into their own top-level SettingsSubSection categories, placed directly under Audio Output Device. Both follow the established reusable pattern (SettingsSubSection header + title-less SettingsGroup inside a single-group settings-card, so the frame-collapse CSS applies). - New TrackTransitionsBlock extracted from PlaybackBehaviorBlock; the latter now holds only the Queue behaviour toggle (slated to move to Personalisation). - NormalizationBlock's SettingsGroup is now title-less; the section header and description name it. - Split the single audio search-index row into three (Normalization / Track transitions / Playback) so crossfade/replaygain/lufs keywords focus the right section. * refactor(settings): consolidate Queue Settings under Personalisation, drop Audio Playback section Combine Queue Display Mode and Queue Toolbar under one 'Queue Settings' category in the Personalisation tab, and move the Queue behaviour toggle (preservePlayNextOrder) there from Audio. The now-empty Audio Playback section is removed. - New 'Queue Settings' SettingsSubSection holds three titled groups: Queue Display Mode, Queue behaviour, and (advanced-only) Queue Toolbar. The toolbar group keeps its reset, now via a new optional 'action' slot on SettingsGroup (+ .settings-group-title-action CSS). - Delete PlaybackBehaviorBlock; its single toggle is inlined. - Search index: drop the audio 'playbackTitle' row; the personalisation queue row now points at 'queueSettingsTitle' with merged keywords. - i18n: add settings.queueSettingsTitle to all 9 locales. * fix(settings): align queue-toolbar separator label and restore Advanced badge - QueueToolbarCustomizer: the separator row rendered a 1px rule where other rows have a 16px icon, so its label sat shifted left. Reserve the full 16px icon column (1px rule centred) so the label lines up. (Preexisting.) - SettingsGroup gains an optional 'advanced' flag rendering the Advanced badge; badge + action now share a right-aligned title-end slot (.settings-group-title-end), so the badge sits just left of the reset button. Restores the Advanced indicator the Queue Toolbar lost when it moved from a SettingsSubSection into a group. * fix(audio): hide the output-device category on macOS instead of showing a notice Playback is pinned to the system default on macOS, so the picker showed a notice explaining it does nothing there. Gate the whole Audio Output Device category out on macOS (`!IS_MACOS` in AudioTab) and drop the now-dead notice branch + the `audioOutputDeviceMacNotice` string from all 9 locales. The device-probe hook already short-circuits on macOS, so no work is wasted. * refactor(settings): box the sidebar customizer groups consistently The sidebar display toggles sat as a bare div and looked unfinished. Box them in a SettingsGroup, with the nav-item drag list in a second group. Render the groups directly in the sub-section content (no settings-card wrapper) — matching the other Personalisation customizers, which use bare SettingsGroups. Also drop the settings-card around Queue Settings for the same reason, so every Personalisation section's boxes share one width and inset instead of the card-wrapped ones sitting narrower/indented. * docs(settings): changelog, what's new, and credits for the settings reorg (#1130) Fold the Audio/Personalisation reorganization into the existing 'Settings — consistent grouped layout' changelog entry (now #1126 + #1130) and the matching What's New highlight, and add a consolidated settings- refactor line to the contributor credits. |
||
|
|
4225146a16 | feat(autodj): smooth skip and interrupt blend transitions (#1128) | ||
|
|
d50c9c444d |
refactor(settings): reusable SettingsGroup/SettingsToggle + boxed sections across tabs (#1126)
* refactor(settings): extract reusable SettingsGroup component
Pull the boxed sub-section pattern (bordered panel + accent uppercase
header) introduced for the Audio tab into a reusable <SettingsGroup
title desc> component, and migrate NormalizationBlock and
PlaybackBehaviorBlock onto it. No visual change.
* feat(settings): box the System behavior section into groups
Split the System -> App Behavior card (which bundled tray toggles, Linux
rendering tweaks and the clock format) into titled SettingsGroup panels
(Tray / Linux rendering / Clock) for a clearer, consistent boxed look.
Adds the group titles across all 9 locales.
* feat(settings): box Appearance visual options; optional SettingsGroup title
Split the Appearance -> Visual options card into a Display group and a
Window group (Linux custom titlebar controls). SettingsGroup now allows an
optional/omitted title for plain boxed panels. Group titles added across
all 9 locales.
* feat(settings): box Discord cover source and templates
Group the Discord cover-source toggles and the activity templates into
separate SettingsGroup panels (templates reuse their existing title/desc),
dropping the manual indent and inline header. No new strings.
* feat(settings): box the Music Network section
Give the scrobble-master toggle, the enrichment-primary picker and the
provider list each a boxed SettingsGroup with an accent header, and switch
the add-a-service provider rows to the boxed panel style for consistent
contrast with the rest of settings.
* feat(settings): titled boxes for Integrations sections + cover source label
Wrap the Discord enable toggle, the Discord cover-source picker (now with a
"Cover art source" title and explainer), Bandsintown and Show-in-Now-Playing
in titled SettingsGroup panels so each single-item section gets the same
accent-headed boxed look. New cover-source strings across all 9 locales.
* feat(settings): box Lyrics tab sections
Wrap the lyrics-sources customizer and the sidebar-style picker in titled
SettingsGroup panels (reusing the existing section titles).
* refactor(settings): consistent content inset in SettingsGroup
Add a body wrapper with a small left inset so every boxed section indents its
controls uniformly (title stays flush at the box edge). Defined once in the
component instead of per section.
* feat(settings): box Storage tab sections incl. cover art cache
Wrap media directory, cover art cache strategy, next-track buffering and
downloads in titled SettingsGroup panels (reusing existing section titles);
align the cover-cache table's first column flush with the new content inset.
* feat(settings): box Library tab, relocate Lucky Mix, add SettingsToggle
- Add a reusable SettingsToggle component for the repeated label/desc/switch row.
- Box the Random Mix blacklist (grouping built-in keywords with the custom
filter), Ratings and the Analytics strategy section.
- Move the "Show Lucky Mix in menu" toggle out of the blacklist and up next to
split-mix navigation and now-playing-at-top in the sidebar customizer.
* feat(settings): box remaining Appearance sections
Wrap library grid, UI scale, font and seekbar style in titled SettingsGroup
panels (reusing the existing section titles).
* feat(settings): box System language, logging and backup sections
Wrap language and logging in titled SettingsGroup panels, and rework the
backup section to drop its redundant inline header (the subsection already
provides it) and box its content. About / Contributors / Licenses stay as
free display content.
* feat(settings): box remaining Audio sections
Wrap Hi-Res, equalizer, playback speed, audio output device and track
previews in titled SettingsGroup panels (reusing the existing section titles).
* feat(settings): box Input tab keybinding sections
Wrap the in-app and global shortcut lists in titled SettingsGroup panels.
* feat(settings): box the Personalisation tab
Convert every customizer (sidebar, home, artist, queue toolbar, playlist,
player bar) to plain inner containers and wrap each in a titled SettingsGroup,
so the boxed look is consistent without nested cards. Box the queue display
mode picker too.
* feat(settings): box the Themes sections above the store
Add a boxed option to the flat Themes sections and apply it to Your Themes,
Auto-Switch Theme and Import (the theme store stays unboxed). Drop the inner
settings-card from InstalledThemes, ThemeImportSection and the scheduler so
the SettingsGroup is the only frame.
* fix(settings): give the Themes boxes a card carrier and accent header
The flat Themes sections had no settings-card behind them, so the bg-app
SettingsGroup had no contrast against the page. Box each above-store section
as settings-card > titled SettingsGroup (accent header) like the rest.
* fix(settings): clean up the server library sync status line
Collapse the offline status to a single icon + phrase ("Server offline —
sync deferred") instead of a redundant "Deferred" badge alongside it, and
drop the now-unused string across all 9 locales.
* fix(queue): keep toolbar toggle buttons coloured active while hovered
The generic .queue-round-btn:hover rule outweighed .active in specificity, so
an active button kept the hover colour until the pointer left — the toggle
only looked on after moving the mouse away. Add an explicit active-hover rule
(mirroring the mini player) so active wins immediately on click.
* fix(settings): fix Lyrics triple title, drop dead Discord cover keys
- Remove LyricsSourcesCustomizer's own section header (the subsection already
titles it) and make its inner cards plain; the Lyrics boxes are now title-less.
- Drop the dead discordCoverSource/discordCoverSourceDesc i18n keys (superseded
by discordCoverTitle/Desc) and un-jam the libraryIndexServer status line in
all 9 locales.
* refactor(settings): drop redundant group titles and double frames
Single-group sections duplicated the subsection title in the group header — make
those groups title-less so the subsection header is the only title. A card whose
sole child is one group now collapses via CSS so single-group sections render a
single frame (matching the card-less Lyrics layout) instead of a card-in-group
double border. Multi-group cards keep their frame as the grouping container.
* fix(settings): keep the flat Themes cards framed when collapsing single-group cards
The single-group card collapse also flattened the Themes section cards, which —
not being inside an accordion — are themselves the contrast surface. Exclude
.themes-section descendants from the collapse.
* refactor(settings): use SettingsToggle for Appearance and System toggle rows
Replace the hand-rolled toggle-row markup in the Appearance visual options and
System tray/Linux/changelog rows with the shared SettingsToggle component, and
extend it with a searchText prop and ReactNode descriptions for the remaining
call sites.
* refactor(settings): roll SettingsToggle out to Integrations, Audio and Storage
Replace hand-rolled toggle rows in the Discord/Bandsintown/Now-Playing,
Hi-Res, hot cache and playback behaviour/rate sections with SettingsToggle.
Make its label optional (desc-only rows whose title is the group header) and
add searchText/id pass-throughs.
* refactor(settings): use SettingsToggle for the remaining standard toggle rows
Track previews master, sidebar lyrics style, YouLyPlus/static-only, queue
display mode and the sidebar split-nav/now-playing/lucky-mix rows now use the
shared component. Genuinely custom rows (drag lists, the skip-star threshold,
the AudioMuse row with its inline link, the fine-step advanced badge) stay
hand-rolled.
* fix(settings): drop duplicate titles on Hi-Res/Bandsintown/Now-Playing, un-jam locale line
These single-toggle sections kept a group title identical to their subsection
header; once the single-group card collapses the title rendered twice stacked.
Make them title-less (the subsection header is the label). Also split the
discordCoverTitle locale entry onto its own line in all 9 locales.
* feat(settings): restore section icons in boxed Themes headers
SettingsGroup gained an optional accent icon slot rendered before the
title; ThemesTab forwards the Palette/Clock/Upload icons the boxed
sections lost when they moved off the bespoke <h2> header.
* style(settings): re-indent SettingsGroup children to nesting level
Pure whitespace: children wrapped in SettingsGroup were left at their
pre-wrap indent. No content change (git diff -w is empty).
* fix(settings): clarify Native Hi-Res Playback description
The old copy led with the disabled-state behaviour ("forces 44.1 kHz"),
which read as if the toggle itself locked output to 44.1 kHz. Describe
what enabling actually does: play each track at its native sample rate
(reconfiguring the output device to match) instead of resampling to
44.1 kHz. Updated across all 9 locales.
* docs: changelog + what's-new for the settings layout refactor (#1126)
|
||
|
|
ee044ece1a | fix(now-playing): poll Live listener count every 30s in the background (#1125) | ||
|
|
fde7ab432f |
feat: split AutoDJ into a standalone playback feature (#1124)
* feat(playback): add transition-mode helper Centralise the crossfade/AutoDJ/gapless mutual exclusivity in one place instead of the scattered setter combinations across the toolbar, mini player and settings. AutoDJ stays encoded as crossfade + trim-silence, so the persisted flags and the audio engine are unchanged. * feat(queue): split AutoDJ into its own toolbar button + playlist submenu - AutoDJ becomes a standalone toolbar button (Blend icon) next to crossfade, driven by the shared transition-mode helper. The crossfade right-click popover drops the mode switch and keeps only the seconds slider. - Save + load playlist collapse into one Playlist button opening a small submenu, freeing up toolbar space. - queueToolbarStore gains a position-preserving rehydrate migration (legacy save/load -> playlist, autodj inserted after crossfade) with unit tests. - Toolbar customizer and all 9 queue locales updated to match. * feat(mini-player): standalone AutoDJ button, shared transition helper Mirror the queue toolbar: AutoDJ gets its own Blend button, the crossfade popover keeps only the seconds slider. The mini player now drives all three transitions through a single additive `mini:set-transition-mode` event handled by the shared helper, replacing the per-flag mini events. Drop the now-dead crossfade-mode CSS. * feat(settings): segmented track-transition picker, regroup playback Replace the crossfade toggle + inner crossfade/AutoDJ switch with a single Off | Gapless | Crossfade | AutoDJ segmented control (mirroring the Normalization picker above), driven by the shared transition helper — the mutual exclusivity now reads at a glance. Crossfade keeps its seconds slider; AutoDJ shows its content-driven explainer. The block is regrouped under "Track transitions" and "Queue behaviour" headings. Drops the now unused crossfade/gapless description and not-available i18n keys across all 9 locales and adds the new transition strings. * fix(queue): open the playlist submenu inward The playlist submenu inherited the crossfade popover's right:0 anchor and, sitting on the left of the toolbar, opened out under the main container. Anchor it left:0 so it stays inside the queue panel. Update the toolbar test for the new playlist submenu (save/load moved off the toolbar). * feat(settings): box playback sub-sections into panels Wrap Normalization, Track transitions and Queue behaviour each in their own bordered panel with an accent uppercase header (new reusable .settings-group classes), so the sections read as distinct blocks instead of one wall of text. Drops the thin divider that separated them. * docs: changelog, credits and what's new for AutoDJ standalone Fold the standalone-AutoDJ changes into the existing AutoDJ entry in the changelog and the in-app What's New, and add the credit (#1124). |
||
|
|
a6ee0668c8 |
feat(crossfade): AutoDJ — content-aware silence-trimming crossfade (#1122)
* feat(crossfade): add "trim silence between tracks" toggle
New persisted setting `crossfadeTrimSilence` (default off; existing
installs rehydrate off via the persist default-merge). Surfaced in
Settings -> Audio and in the crossfade popovers of the queue toolbar
and the mini-player.
Crossfade buttons now separate the two actions: left-click toggles
crossfade on/off, right-click opens the settings popover (seconds +
trim). Shared the mini popover positioning into useMiniAnchoredPopover
(now backing both volume and crossfade). Mini bridge carries
crossfadeSecs/crossfadeTrimSilence and gains mini:set-crossfade-secs /
mini:set-crossfade-trim-silence.
The actual silence-trimming playback behaviour is wired in a follow-up;
this commit only persists the user intent. i18n added across 9 locales.
* feat(crossfade): trim silence between tracks (waveform-driven)
Wire the actual silence-aware crossfade behind the (default-off)
crossfadeTrimSilence toggle. Detection is derived on the fly from the
cached 500-bin waveform + track duration — no new analysis pass or
cache fields.
- waveformSilence.ts: computeWaveformSilence(bins, duration) → lead/trail
silence + content bounds, using the peak curve, a low absolute cut and
a per-side cap. Unit-tested.
- A-tail (JS): handleAudioProgress advances the crossfade early, at
contentEnd - crossfadeSecs, when the current track ends in real
trailing silence, so the fade overlaps music. Guarded once per play
generation.
- B-head: audio_play gains an additive optional start_secs; the freshly
built source is try_seek'd past the next track's leading silence before
append, then seek_offset/samples_played are re-anchored so position is
content-relative. Non-seekable / cold sources degrade to today.
- Pre-buffer: crossfade next-track download + B-head probe moved to
crossfadePreload.ts with a fixed ~30 s budget before the track needs to
play (widened by trailing silence so the early advance keeps the
budget). Also fired right after a seek into the window so jumping near
the end still buffers in time.
Checks: tsc, vitest (store suite + new units), cargo test/clippy for
psysonic-audio.
* feat(crossfade): recommend hot cache for trim; probe B-head regardless
Add a "for reliable results, enable the Hot playback cache" note to the
trim-silence toggle description across all 9 locales — hot cache keeps
the next track on disk so it starts instantly past its lead silence.
Fix: the leading-silence probe (B-head) now runs even when hot cache is
on; only the redundant byte pre-download is gated on !hotCache. Without
this, enabling hot cache (the recommended setting) would have skipped the
probe and disabled leading-silence trimming.
* feat(crossfade): content-driven smart crossfade overlap
Smart crossfade derives the per-transition overlap purely from the
waveform envelopes — max(A outro fade, B intro rise) clamped 0.5–12s —
instead of the fixed crossfadeSecs ("work by fact"). The JS early
advance arms the computed overlap and audio_play applies it through a
new crossfade_secs_override (capping only this swap's fade); plain
loud→loud endings fall back to the engine crossfade at crossfadeSecs.
* feat(crossfade): "Crossfade | Smart crossfade" mode switch in UI
Replace the standalone "trim silence" toggle with a Crossfade / Smart
crossfade segmented control in settings and both crossfade popovers
(queue toolbar + mini-player). Classic Crossfade shows the seconds
slider; Smart crossfade is content-driven with no duration to set.
Adds smartCrossfade / smartCrossfadeDesc strings to all nine locales
and the "smart crossfade" search keyword.
* feat(crossfade): don't double-fade a track that already fades out
Decouple the outgoing track's fade-out from the incoming fade-in. When
A carries its own recorded fade-out (outroFadeA ≥ 1s and ≥ B's intro
rise), planCrossfadeTransition now sets outgoingFadeSec = 0; the engine
then skips A's TriggeredFadeOut so A keeps full gain and its recording
carries it down while B rises underneath — no more double attenuation
that made A vanish early and B blare in. Hard-cut endings still get an
engine fade over the overlap.
audio_play gains outgoing_fade_secs_override (Some(0) = ride A's own
fade), threaded via SinkSwapInputs.outgoing_fade_secs; the JS advance
arms it alongside the overlap.
* feat(crossfade): rename the smart crossfade mode to "AutoDJ"
User-facing rename of the content-driven crossfade mode from "Smart
crossfade" to "AutoDJ" across the settings segmented switch, the queue
and mini-player popovers, all nine locales, and the settings search
keywords. The underlying store flag (crossfadeTrimSilence) is unchanged.
* feat(crossfade): standard ~2s blend for hard loud→loud meetings
When AutoDJ trims a track's protective trailing silence and the loud
ending butts straight into a loud intro, neither edge fades, so the old
0.5s anti-click floor sounded like an abrupt cut. Use a standard ~2s
equal-power crossfade for that case (both edges analysed, nothing
fades); real fade-outs/buildups keep their longer content-driven span,
and the bare floor only survives when an envelope is missing.
* fix(crossfade): keep B's fade-in across the B-head start-offset seek
EqualPowerFadeIn::try_seek jumped straight to unity gain for any seek
≥100ms, which also hit the initial start-offset seek that skips the
incoming track's leading silence — so a crossfaded track with trimmed
lead silence popped in at full gain instead of fading in. Only skip the
fade-in for mid-playback seeks (sample_count > 0); a seek before any
audio has played keeps the fade-in.
* feat(crossfade): gate AutoDJ early fade on next-track readiness
The early, content-driven advance now fires only when the next track's
audio is actually available — in the engine RAM preload slot
(enginePreloadedTrackId) or local on disk (offline library, favourite-auto
or hot-cache ephemeral) — via isCrossfadeNextReady(). Analysis alone is
not enough: a cold, still-buffering stream would fade in over silence.
When B isn't ready the gen guard stays unset so it re-checks on later
ticks; if B never readies, the plain engine crossfade handles the
transition (graceful degrade) instead of a broken fade. A RAM preload
copy suffices — the full track need not be cached to disk.
* feat(crossfade): suppress engine auto-crossfade for AutoDJ + eager preload
With the hot cache off the readiness gate alone wasn't enough: the engine's
progress task autonomously fires its crossfade audio:ended ~crossfadeSecs
before the end, independently of JS, and would start a still-buffering next
track and fade over it — an audible jump.
- Engine: add autodj_suppress_autocrossfade flag (audio_set_autodj_suppress);
the progress task treats it like crossfade-off, so the early timer never
fires and audio:ended only comes from real source exhaustion / watchdog.
- JS drives the transition: set the flag when a content fade is pending
(wantEarly) and clear it for plain loud→loud / non-AutoDJ, so the normal
engine crossfade is preserved there. When the next track never readies, A
plays out and we degrade to a clean sequential start instead of a jump.
- audio_preload gains an `eager` flag; the crossfade/AutoDJ pre-buffer passes
it to skip the 8s start throttle so the RAM slot fills before the fade.
* docs(changelog): AutoDJ content-aware crossfade (PR #1122)
Add the 1.49.0 Added entry and the cucadmuh credits line for AutoDJ.
|
||
|
|
ad74578ef6 |
feat(playlists): local playlist folders (sidebar + page, DnD, view toggle) (#1119)
* feat(playlists): playlist folder model — store + pure grouping core Local, per-server folder layer over the server's flat playlist list (the Subsonic API has no folder concept). Adds: - playlistFolders.ts: shared types + pure groupPlaylistsByFolder (used by every surface), with full unit coverage. - playlistFolderStore.ts: persisted Zustand store (create/rename/delete/ assign/collapse), per-server scoped; deleting a folder drops its assignments so playlists fall back to ungrouped. UI surfaces (sidebar + Playlists page) build on this in following commits. * i18n(playlists): folder strings across all 9 locales Adds the playlists.folders.* namespace (folder names, move/remove, expand/ collapse, group-by-folders toggle, count plurals, and the local-only notice explaining that Navidrome and the Subsonic API have no native folder support). * feat(playlists): folder views, drag-to-folder, move-to-folder menu, view toggle Surfaces the local folder layer on both the Playlists page and the sidebar: - Page: collapsible folder sections + ungrouped remainder, each reusing VirtualCardGrid; flat grid is kept verbatim when no folders exist or the group view is toggled off. - Drag-to-folder via the shared mouse-based psy-drop system (HTML5 DnD is unusable in WebKitGTK); the whole section is the drop zone, and the ungrouped zone appears during a drag so a playlist can always return to root. - "Move to folder" submenu in the playlist context menu (keyboard-accessible path; also creates folders on the fly) — stays available offline. - Header gets a "New folder" action and a "Group by folders" view toggle (both context-aware); a notice surfaces the local-only caveat. - Sidebar renders the same collapsible folder groups. - groupView preference added to the folder store. * docs(changelog): note playlist folders (#1119) |
||
|
|
44d373d7bb |
fix(player): player bar context menu acts on the current song, not its album (#1117)
* fix(player): player bar context menu acts on the current song, not its album Right-clicking the current track in the player bar built an album object from the playing track and opened the album context menu, so "Add to playlist" added the whole album instead of the song. It now opens a song-scoped menu for the current track. Left-click on the title still navigates to the album. * docs(changelog): note player bar add-to-playlist fix (#1117) |
||
|
|
e563749ace |
fix(queue): stop re-walking the whole queue on every track change
QueueHeader aggregated total/future duration in a useMemo keyed on queueIndex,
so every skip ran a synchronous O(n) pass over the entire queue (resolveQueueTrack
per item). On very large queues this blocked the main thread for seconds — the
UI froze on skip and on the device-switch playTrack fallback (#1072; the freeze
half of #1090). QueuePanel is always mounted, so it hit even with the queue
collapsed.
Build a cumulative-duration prefix keyed on queue/resolver-version only; the
future-tracks total is now an O(1) lookup per skip. Display output unchanged.
(cherry picked from commit
|
||
|
|
15fb0f6c56 |
Theme store: version display + animated/static filter (#1104)
* feat(themes): show theme version in the store and installed list * feat(themes): filter the theme store by animated / static * docs(changelog): add 1.49.0 entry for theme store version + animated filter |
||
|
|
07232cea9a |
fix(fullscreen-player): honour the Lyrics scroll style setting (#1089)
The rebuilt fullscreen player hardcoded Apple-style lyrics scrolling (active line ~35% from the top) and ignored Settings -> Lyrics -> Lyrics scroll style, which still drives the sidebar/mobile LyricsPane. FsLyricsApple now reads sidebarLyricsStyle: 'classic' centres the active line, 'apple' keeps the 35% anchor, matching the sidebar lyrics. |
||
|
|
c1403f8bd6 |
feat: now-playing liveness dot + admin-gated radio management (#1086)
* feat(now-playing): liveness indicator dot in the listening popover Replace the raw "Nm ago" line in the "Who is listening?" popover with a derived presence dot (green playing / amber paused / dim idle). The presence is computed in one tested helper that unifies the playbackReport transport state with the legacy getNowPlaying recency, instead of formatting a raw timestamp inline. The dot carries the localized status as an aria-label and tooltip so it is not conveyed by colour alone. * feat(radio): gate station create/edit/delete behind Navidrome admin role Navidrome >= 0.62 restricts internet-radio management to admins (GHSA-jw24-qqrj-633c); non-admin requests fail. Hide Add Station, Search Directory, the per-card edit chip and delete button for confirmed standard Navidrome users via a canManageNavidromeRadio() helper on the existing useNavidromeAdminRole framework. Admins, non-Navidrome servers and transient states stay unrestricted; playback and favourites remain available to all. * docs(changelog): now-playing status dot + admin-gated radio (#1086) |
||
|
|
0b7d9eae2d |
feat(playback): Semitones strategy, 2-decimal speed label, advanced fine steps (#1084)
* feat(playback): varispeed-by-semitones strategy and 2-decimal speed label Adds a fourth playback-rate strategy "Varispeed (semitones)" — a frontend lens over varispeed where the user dials the pitch change directly in semitones (±12 st, 0.1 step) and speed = 2^(st/12). Engine contract is unchanged: the store maps it to the existing "varispeed" Rust strategy via engineStrategy(), and switching between the two varispeed lenses at the same speed no longer restarts the track. Also widens the speed readout to two decimals (formatSpeedLabel → toFixed(2)) so every 0.05 slider step is visible (1.05×, 1.10×, 1.15×), addressing the feedback on issue #531 that the label looked stuck between steps. Includes new i18n keys and updated hint across all 9 locales, plus Vitest coverage for the new helper, label formatting, and the no-restart lens switch. * refactor(playback): shorten Semitones strategy label, add per-strategy tooltips Renames the fourth strategy to a concise "Semitones" (was "Varispeed (semitones)") across all 9 locales, and lets the four strategy buttons share the row width so they fit on one line in Settings while still wrapping in the narrow player popover. Each strategy button now has a short hover tooltip explaining how it is built (incl. the 2^(st/12) and 12 × log2(speed) maths). * feat(playback): advanced fine-step precision for speed/pitch sliders Adds an opt-in "Fine adjustment" toggle in Settings → Audio (visible only in Advanced mode) that shrinks the playback-rate slider steps to 0.01× for speed and 0.01 st for pitch/semitones, with the pitch readout widening to two decimals to match. Default behaviour (0.05× / 0.1 st) is unchanged. The preference is UI-only (persisted, not sent to the engine) and also applies to the player-bar popover and wheel. Addresses the finer-precision request on issue #531 without cluttering the default UX. * docs: changelog and credits for playback speed follow-up (PR #1084) |
||
|
|
028eb65f7d |
feat(titlebar): selectable window button styles + minimize toggle (#1083)
* feat(titlebar): selectable window button styles + minimize toggle Custom title bar (Linux) gains a window-button style picker, mirroring the seekbar style picker pattern. Six form-named styles: dots, dotsGlyph, flat, pill, outline, glyph. All buttons now carry minimize/maximize/close glyphs for clear, colour-blind-friendly iconography; dots reveal glyphs on hover, dotsGlyph always shows them. - New authStore state windowButtonStyle (default dots) + showMinimizeButton, with rehydrate validation falling back to dots on unknown values. - WindowButtonPreview reuses the real .titlebar-btn classes for WYSIWYG tiles. - Picker + minimize toggle render under the Custom title bar setting, gated on the toggle being on. - Monochrome styles use --text-primary glyphs and stronger borders for contrast on dark themes. - Dev-build grey marker scoped to the real title bar so previews show true colours. - i18n keys in all 9 locales; setter and rehydrate tests. * docs(changelog): window button styles (#1083) |
||
|
|
891ab0dd5b |
feat(now-playing): OpenSubsonic playbackReport for live now-playing (#1080)
* feat(now-playing): adopt OpenSubsonic playbackReport for live now-playing Drive a small playback state machine (starting → playing ↔ paused → stopped) on the Subsonic-server channel when the server advertises the OpenSubsonic `playbackReport` extension (Navidrome ≥ 0.62), giving `getNowPlaying` a real transport state and an extrapolated position. Reports send `ignoreScrobble=true` so play counts stay on the existing `scrobble.view` 50% path (no double count), and the effective playback speed is included so the server extrapolates position correctly with the speed feature on. - New `playbackReportSession` FSM mirrors the existing `playListenSession` lifecycle hooks and is wired at the same player call sites (start / gapless switch / queue restore / resume / 15s heartbeat / pause / seek / stop / ended / error / app quit). Servers without the extension degrade to the unchanged legacy `scrobble.view?submission=false` presence call. - Gated through the existing serverCapabilities framework: a new `FEATURE_PLAYBACK_REPORT` (auto, extension-detected). The OpenSubsonic extensions probe now stores the full advertised list once and serves both AudioMuse `sonicSimilarity` and `playbackReport` from it, without disturbing the legacy Instant Mix opt-in on pre-0.62 servers. - Now Playing dropdown shows a live position bar and a paused indicator. - reportPlayback uses the real request params (mediaId / mediaType / positionMs). Tests: FSM transitions + gating + legacy fallback, capability resolution, extension-list probe storage/decoupling. Full suite green; no Tauri-boundary changes. * feat(now-playing): glide the Live position bar between polls Extrapolate the position of `playing` entries locally (elapsed × reported playbackRate from the last 10 s poll) and re-render once a second, so the Live progress bar advances smoothly instead of jumping on each refresh. Paused and position-less entries stay frozen. A linear width transition matched to the tick keeps the fill gliding. Only applies to clients that report a position via the playbackReport extension. * fix(now-playing): tighten Live timer layout and report resume immediately Keep the progress-bar width stable without a wide empty gap before the timer: reserve ~2ch inside the current-time span only (right-aligned), not on the whole clock block. Report `playing` to the server as soon as resume() runs, matching the immediate `paused` report on pause instead of waiting for the Rust `audio:playing` event. * docs: CHANGELOG and credits for playbackReport live now-playing (PR #1080) |
||
|
|
3de7b57cc5 |
Fullscreen player polish + Discord Rich Presence fixes (#1068)
* fix(fullscreen-player): drop track-number prefix from title, stop clipping descenders
The big title showed a zero-padded queue position ("11. ") before the
song name; remove it (the position still lives in the top bar). Also bump
line-height 1.05 -> 1.25 so overflow:hidden no longer crops descenders
(g, j, p, q, y) at 46px.
* feat(settings): clarify built-in Discord RP vs official Navidrome plugin
Add a bold notice atop the Discord Rich Presence block explaining it is
the built-in integration, and that users wanting the official Navidrome
Discord RP plugin should leave it off and enable "Show in Now Playing"
instead. Add a matching bold note to the Now Playing setting. New i18n
keys discordRichPresenceNotice / nowPlayingPluginNote across all 5 locales.
* fix(discord-rp): use HTTPS cover URL instead of local file:// path
Discord Rich Presence images are fetched by Discord's servers, so
large_image must be a key or an https:// URL they can reach. Since the
cover pipeline moved to an on-disk webp cache, coverArtUrlForDiscord
returned a file://.../800.webp path on cache hit, which Discord cannot
load and silently falls back to the app icon. Always hand it the
getCoverArt URL; MPRIS keeps the local file:// path unchanged.
* i18n(settings): fill discordRichPresenceNotice + nowPlayingPluginNote for es, nb, ro, ru
The two new Discord-RP notice keys had only landed in 5 of the 9 locales;
complete the set so no locale falls back to English.
* docs(changelog): fullscreen title + Discord RP fixes (PR #1068)
|
||
|
|
1a82376f8c |
feat(music-network): unified scrobble & enrichment framework (replaces hard-wired Last.fm) (#1066)
* feat(music-network): core domain types and wire contracts
Foundation for the Music Network framework: provider-agnostic domain
types, capability model, typed errors, and account shapes under
src/music-network/core, plus the ScrobbleWire / EnrichmentWire /
PresetManifest / AuthStrategy contracts. No runtime wiring yet.
* feat(music-network): generic audioscrobbler/listenbrainz/maloja transports
Generalize the Rust remote layer for the Music Network framework. Add
provider-agnostic transports parameterized by base_url:
- audioscrobbler_request: Audioscrobbler v2 with caller-supplied endpoint
(Last.fm, Libre.fm, Rocksky, custom GNU FM, Maloja compat share it)
- listenbrainz_request: Token-header JSON (direct + Maloja LB compat)
- maloja_request: native /apis/mlj_1 JSON
lastfm_request stays as a thin transition delegate against the fixed
host; it is removed once the framework owns all call sites. Wiremock
tests cover audioscrobbler_request with a custom base_url and API-error
mapping.
* feat(music-network): audioscrobbler wire with last.fm + libre.fm presets
Add the Audioscrobbler v2 wire, the behavioural successor to the legacy
src/api/lastfm.ts, implementing the full EnrichmentWire surface (scrobble,
now playing, love/unlove, loved sync, similar artists, track/artist stats,
top lists, recent tracks, user profile, urls).
- client.ts: transport wrapper over audioscrobbler_request, classifying
failures into MusicNetworkError without touching any store
- sign.ts: TS mirror of the api_sig base-string ordering rule (unit-tested)
- auth/tokenPoll.ts: browser token-poll connect flow as a reusable strategy
- presets/lastfm.ts, presets/librefm.ts: bundled, enrichment-capable,
token-poll presets (both endpoints verified live)
Extends WireContext with profileBase and ConnectContext with authBase so
URL builders and connect flows need no preset lookup.
* feat(music-network): listenbrainz + maloja-native wires, paste-auth presets
Add the scrobble-destination wires and presets:
- ListenBrainz wire (scrobble + now playing via playing_now), backing both
the direct api.listenbrainz.org preset and the Maloja /apis/listenbrainz
compat preset (one wire, two presets, differing only by base URL)
- Maloja native wire (/apis/mlj_1/newscrobble, scrobble only — Maloja has
no now-playing endpoint)
- Shared api_key_only paste-auth strategy (token/key/session-key paste);
the Audioscrobbler wire now dispatches token-poll vs paste by preset
- Presets: listenbrainz, maloja_listenbrainz, maloja_native, rocksky
(scrobble-only, session-key paste, bundled keys — verified live), and
custom_gnufm (token-poll, user-supplied url/key/secret)
- Contracts: ConnectContext.authStrategy, PresetManifest.selfHostedApiSuffix
maloja_compat (the {url}/apis/audioscrobbler mode) is intentionally omitted:
its protocol cannot be verified and is almost certainly the legacy handshake,
not the 2.0 web API; Maloja is covered by the native and ListenBrainz modes.
* feat(music-network): registry, orchestrator, enrichment router + runtime facade
Wire the framework together behind a single facade:
- registry: wireRegistry (WireId -> wire), presetRegistry (the 7 built-in
presets), registerBuiltinWires (one-time side-effect registration)
- CapabilityProbe: wire probe overlaid by manifest staticCapabilities as the
final authority (lets two presets on one wire diverge, e.g. Rocksky's
nowPlaying:false over the Audioscrobbler wire's optimistic yes)
- ScrobbleOrchestrator: best-effort fan-out; flips the per-account
session-error flag on AUTH_SESSION_INVALID and clears it on next success
- EnrichmentRouter: resolves the single primary to its EnrichmentWire; the
type guard rejects non-enrichment wires (Maloja/ListenBrainz)
- MusicNetworkRuntime: the only app entry point — accounts, roles, fan-out,
enrichment, urls, probe. Reads/writes state through the MusicNetworkStore
port (Phase 5 backs it with the auth store) and a RuntimeHost for side effects
- getMusicNetworkRuntime singleton + index.ts public barrel
Tests cover fan-out, master toggle, capability gating, session-error
flip/clear, primary eligibility, and enrichment routing.
* feat(music-network): auth-store state + lossless legacy migration + runtime bridge
Add the persisted Music Network state to the auth store and wire the runtime,
all additively — nothing existing breaks yet.
- authStoreTypes: musicNetworkAccounts / enrichmentPrimaryId /
scrobblingMasterEnabled + actions; legacy lastfm* fields kept until Phase 6
- authMusicNetworkActions + defaults/wiring (synchronous localStorage)
- accountPersistence: migrateLegacyLastfm (lossless — preserves session key,
username and scrobbling preference; fills bundled Last.fm key from the preset;
sets the migrated account as enrichment primary) + sanitizeAccounts
- authStoreRehydrate: one-shot migration guarded by a sentinel so a later
disconnect cannot resurrect the account from still-present legacy fields
- musicNetworkBridge: backs the MusicNetworkStore port with the auth store and
the RuntimeHost with the Tauri shell; initialized in pre-React bootstrap
nowPlayingEnabled stays a global toggle (not a lastfm* field); the Phase 6
playback call-site will gate dispatchNowPlaying on it, preserving behaviour.
* feat(music-network): route playback, enrichment and love through the runtime
Migrate every Last.fm call-site onto the Music Network runtime, preserving
behaviour:
- playback (audioEventHandlers, playTrackAction): scrobble@50% and now-playing
via dispatchScrobble/dispatchNowPlaying; loved-fetch via isTrackLoved. Now-
playing follows scrobbling (as Last.fm did), Navidrome now-playing keeps the
nowPlayingEnabled gate
- enrichment: useArtistSimilarArtists, useNowPlayingFetchers, Statistics, and
the ArtistDetail similar-artists gate now use the runtime, gated on an
enrichment primary
- love: PlayerBar, PlayerTrackInfo, all context menus, useNowPlayingStarLove and
the startup loved-sync route through setTrackLoved / toggleNetworkLove
- player store: lastfmLoved/lastfmLovedCache -> networkLoved/networkLovedCache;
lastfmActions -> networkLoveActions; loved cache storage renamed with a
lossless legacy-key fallback
- getMusicNetworkRuntimeOrNull() for best-effort callers so they no-op (not
throw) before the runtime is initialized
src/api/lastfm.ts and the Integrations UI still use the legacy path; they are
migrated and removed in the next phase.
* feat(music-network): manifest-driven Integrations UI + scrobble batch format
Replace the Last.fm Integrations card with a manifest-driven Music Network
section, and fix Audioscrobbler scrobbling to the batch/array shape.
- settings/musicNetwork/: MusicNetworkSection (master toggle, destination
cards, enrichment-primary picker, Maloja proxy warning, add-a-service list)
driven entirely off the preset registry; icon map from PresetManifest.icon
- IntegrationsTab delegates to MusicNetworkSection (Discord/Bandsintown/
Navidrome now-playing unchanged)
- i18n: musicNetwork.* across all 9 locales, incl. a per-field help hint for
Rocksky's CLI session-key flow (rocksky login)
- scrobble now uses the documented array form (artist[0]/track[0]/…); the bare
single form is only tolerated by Last.fm, Rocksky requires the indexed form
- auth-error detection keys off the response message (not the ambiguous numeric
code) so a Rocksky server-500 no longer flips the account to a reconnect state
- PresetManifest.PresetField gains an optional helpKey
Rocksky's server rejects some non-ASCII track metadata with a 500 — a Rocksky
backend bug; the client call is correct (verified).
* fix(music-network): clearer Integrations layout + scrobble batch fix
Address UI feedback on the Music Network section:
- per-account scrobble toggle moves inside its account block (was a loose
row between cards — unclear which account it belonged to)
- master toggle and the primary-service picker are now boxed blocks at the top,
not bare rows
- primary-service copy reworked: 'Primary service' + a line spelling out that
liked tracks/similar artists/stats come from it while scrobbling still goes
to all enabled services
- distinct zones separated by dividers (master/primary · connected · add)
Also folds in the verified scrobble fixes: documented array form
(artist[0]/track[0]/…) so Rocksky accepts scrobbles, and auth-error detection
by message rather than the ambiguous numeric code (a Rocksky server-500 no
longer flips the account to a reconnect state). Rocksky session-key field gains
a CLI help hint (rocksky login), all 9 locales.
* feat(music-network): indicator + remove legacy lastfm path
Phase 7b/7c — finish the cutover and delete the old Last.fm path.
- LastfmIndicator -> MusicNetworkIndicator (shows the enrichment primary's
status, click -> Integrations)
- delete src/api/lastfm.ts; remove Rust lastfm_request (remote.rs + lib.rs)
- remove legacy authStore lastfm* fields, actions and types; delete
authLastfmActions.ts; rehydrate migration reads the legacy blob via a cast
- migrate the remaining NowPlaying call-sites (NowPlaying.tsx + the now-playing
fetchers/prewarm/star-love hooks) off lastfmSessionKey/lastfmUsername onto the
enrichment primary (gate + cache key)
- type imports LastfmTrackInfo/LastfmArtistStats -> music-network TrackStats/
ArtistStats; drop 8 stale api/lastfm test mocks and the obsolete Last.fm auth
tests; update settingsTabs + src/CLAUDE.md
No lastfm imports remain outside src/music-network/; lastfm_request removed
(acceptance §12). tsc clean, 1947 frontend tests + remote rust tests green.
* test(music-network): cover scrobble shape + error classification, drop dead i18n keys
Remove the 14 unused legacy scrobble/connection i18n keys across all 9
locales (settings.lfm*/scrobble*, connection.lastfm*); the live love,
profile-link and now-playing keys stay.
Add regression tests for the parity-critical transport logic: the indexed
batch/array scrobble body, the auth-vs-network error classification
(numeric codes collide across providers), and the manifest-overrides-probe
capability merge.
* feat(music-network): provider-agnostic UI + Maloja Audioscrobbler & Koito presets
- de-hardcode the single provider name across every enrichment surface
(love labels, now-playing badge, stats title); derive it from the
enrichment primary and interpolate via {{provider}} i18n params
- surface a toast when a paste-auth connect probe fails — a static
'supported' capability flag no longer masks a runtime probe error
- add the Maloja Audioscrobbler (GNU FM) preset (the third Maloja wire
mode) and a Koito preset (ListenBrainz-compatible), both data-only
- generalise PRIVACY.md and the scrobbling help entry to the framework
- rename residual lfm* identifiers to network*; strip legacy flat
lastfm* fields from the persisted blob; neutral transport error prefix
- tests: error classification, scrobble body, capability probe, registry
* fix(music-network): validate paste-auth keys on connect + UI polish
- AudioscrobblerWire.probe now validates an api_key_only session with a
signed call and reports scrobble:'error' only on a genuine auth failure
(a scrobble-only service that rejects user.getInfo is not a bad key), so
an invalid Maloja Audioscrobbler / Rocksky key surfaces a connect toast
instead of failing silently; WireContext carries the preset authStrategy
- drop the unreachable Statistics empty-state branch and its dead
lfmNotConnected i18n key; use the useEnrichmentPrimaryLabel hook there
- drive the love-button glyph from the enrichment primary's manifest icon;
neutral Music Network section icon; remove a dead LastfmIcon import
- tests: paste-auth vs token-poll probe behaviour
* chore(music-network): rename showLastfmSimilar → showNetworkSimilar, refresh stale comments
Post-parity polish: the similar-artists toggle now sources from the generic
enrichment runtime, so rename the lingering lastfm-flavoured identifier; drop
stale 'Mirrors today's LastfmX' doc comments referencing the removed legacy
types, and generalise the scrobble-point comment.
* docs(changelog): Music Network entry (PR #1066)
Co-Authored-By: cucadmuh <49571317+cucadmuh@users.noreply.github.com>
* fix(music-network): bound request timeout on the provider transports
audioscrobbler_request / listenbrainz_request / maloja_request built a
reqwest client with no timeout, so a hung provider left scrobble / probe /
loved-sync promises unresolved. Add a shared provider_http_client() with a
15s timeout, matching the sibling fetch_* commands. Addresses review C3.
* refactor(music-network): dedupe wire transport + no-enrichment helpers
The three provider clients repeated the same invoke -> classify-error ->
MusicNetworkError boilerplate, and the three probe() bodies repeated the
"mark every enrichment capability no" loop. Extract
wires/shared/invokeTransport() (each wire keeps its own arg shape + auth
rule) and markNoEnrichment() in core/capabilities.ts. Addresses review C4.
* refactor(music-network): drop write-only malojaWireMode dead state
malojaWireMode was written on connect but never read — the wire is resolved
by wireId and the Maloja base URL by the preset's selfHostedApiSuffix.
Remove the field, the MalojaWireMode type, the malojaWireModeFor helper,
the AccountPatch entry, the PresetField union member, and the export.
Addresses review C1.
* refactor(music-network): one useEnrichmentPrimary hook, drop lastfm fallback
The enrichment-primary lookup (accounts.find by enrichmentPrimaryId) was
duplicated across two hooks and inlined in the indicator and both
context-menu builders, two of them with a hardcoded 'lastfm' icon fallback.
Add one music-network/ui/useEnrichmentPrimary() returning
{account,label,icon}|null; useEnrichmentPrimaryLabel/Icon delegate to it and
the indicator + context menus consume it directly. Icon fallback is the
neutral 'custom' glyph, never a provider (provider-agnostic, §7.3).
Addresses review C2.
---------
Co-authored-by: cucadmuh <49571317+cucadmuh@users.noreply.github.com>
|
||
|
|
c7d71ea57c |
feat(whats-new): remote release notes with dev workspace mode (#1058)
* feat(whats-new): remote release notes with dev workspace mode Add WHATS_NEW.md, CI whats-new.md asset upload, and client fetch/cache with embedded fallbacks. Dev and -dev builds read the full file from the repo for debugging; RC/stable download the release asset on first use. * fix(whats-new): render ## headings and add changelog tab Parse h2 sections in release-notes markdown; load changelog alongside highlights and let users switch views on the What's New page. * fix(whats-new): prefetch on startup and fix CI typecheck prebuild Prefetch whats-new asset when the shell loads on RC/stable builds. Run prebuild:release-notes before tsc and coverage jobs so the gitignored generated bundle exists in CI. * docs: CHANGELOG and credits for What's New remote notes (PR #1058) * fix(whats-new): always slice embedded release notes to current line Drop full CHANGELOG embed for -dev bundles; tauri:dev still reads live markdown from the repo. Ignore all of src/generated/ in git. * fix(whats-new): fetch release asset via Rust to bypass CORS Route whats-new.md download through fetch_url_bytes; rename the technical tab label; add fetch unit tests (PR #1058 review). |
||
|
|
fb5a257735 |
fix(library): show album artist correctly in album grids (#1056) (#1057)
* fix(library): show album artist in album grids (#1056) Prefer album-artist tags over track artist when building album rows from the local index, and align grid cards with OpenSubsonic displayArtist. * fix(library): align album artist in FTS, search, and offline paths (#1056) Apply album-artist preference in FTS album dedupe and live search, fix offline pin hydration order, and use albumArtistDisplayName in remaining cheap UI/export/download call sites. * docs(changelog): album artist grid fix for compilations (PR #1057) * fix(library): parity guard and live-search album artist helper (#1056) Align SQL ELSE branch with pick_album_group_artist trimming, add parity test, and use albumArtistDisplayName in LiveSearch and MobileSearchOverlay. |
||
|
|
ae9be74719 |
feat(settings): compact server cards with capability badges (#1054)
* feat(settings): redesign server cards with identity line and capability badges Compact two-line server headers (entry name + user@host), HTTPS lock, and a clickable version info tooltip. Navidrome ≥0.62 shows a green AudioMuse inline badge; older Navidrome keeps the manual toggle row. Adds click-pinned tooltips via data-tooltip-click on TooltipPortal. * feat(settings): unify use/active slot and move delete into edit form Merge Active badge and Use button into one rightmost action; Active uses green styling. Reorder actions to edit, test, use/active. Remove the card delete icon — deletion lives in the server edit form footer. * docs: note compact server cards in CHANGELOG and credits (PR #1054) * fix(credits): move PR #1054 server cards line to cucadmuh block Was appended to Psychotoxical's contributions array by mistake; CHANGELOG already credited cucadmuh (gh pr view author). |
||
|
|
36a0615dcb |
fix(home): stop Most Played load-more from snapping the page upward (#1053)
* fix(home): stop Most Played load-more from snapping the page upward Loading more Most Played albums rebuilds the Because-You-Like anchor pool (it is seeded from mostPlayed), and that rail's fetch effect keyed on the pool array ref — so it re-ran on every append, swapping the row's cards and producing a height blip that scroll anchoring turned into an upward viewport jump. Gate the effect on poolKey (the stable top-anchor identity) instead, matching the sibling reserve effect, so it only re-runs on a real seed change. * docs(changelog): add #1053 Most Played load-more scroll fix |
||
|
|
155ef88cc0 |
fix(themes): serve the Theme Store from GitHub raw, not jsDelivr (#1051)
Theme assets (registry.json, theme CSS, thumbnails) were served from the jsDelivr CDN's mutable @main edge, which caches up to 12h. The registry is purged on every themes push so updates were offered promptly — but the theme CSS could still be served stale, so an update stored pre-update CSS under the new version label and no further update was offered to correct it (a freshly animated theme would install without its @keyframes). Fetch everything from GitHub raw (permissive CORS, ~5-min server cache), which is always current. jsDelivr is dropped entirely: our request volume does not need a CDN, and its only real upside here (high-traffic edge serving) does not apply, while its staleness actively caused the bug. Rename cdnUrl -> assetUrl to reflect the source. |
||
|
|
316c99ba07 |
refactor(themes): drop unreliable popularity/download stats from the store (#1050)
The install/download numbers came from jsDelivr CDN hits, which are structurally unreliable: the stats API caps at the top 100 files, so most themes report 0 and the figures jump from day to day. Showing them — and a popularity bar and a most-popular sort built on them — only misled users. Remove the stats meta box (popularity bar + download count), move the author back under the theme name, and keep the reliable git-based last-changed date inline. Sort is now Newest (default) or Alphabetical. |
||
|
|
ad8e376c9c |
feat(settings): show server software and version on server cards (#1045)
* feat(settings): show server software and version on server cards Render the OpenSubsonic-reported software and version (e.g. "Navidrome 0.62.0") under each server name on Settings -> Servers. The value comes from the existing ping identity (subsonicServerIdentityByServer), so no extra request is made; the line is omitted when the server reports no type (e.g. plain Subsonic without OpenSubsonic). Extract the formatting into a shared formatServerSoftware() helper and reuse it in the PsyLab perf-probe server section in place of its local copy. * docs: add CHANGELOG entry for PR #1045 |
||
|
|
cfc9419de7 |
feat(themes): sidebar notice when an installed theme has an update (#1041)
* feat(themes): sidebar notice when an installed theme has an update Adds a dismissible sidebar pill (sibling of the What's New banner) shown while an installed community theme has a newer version in the store. Clicking opens Settings -> Themes; dismiss hides it until a new update changes the set. The theme registry is now refreshed from source once per app launch instead of only when the Theme Store tab is opened, so newly published themes and updates surface without a manual refresh -- and feed this notice. * docs: add CHANGELOG entry for PR #1041 * feat(themes): in-place update control on installed theme cards Themes with a newer version in the store now show a centered update icon on their card in Settings -> Themes; clicking it fetches and reinstalls in place. Extracts the shared installThemeFromRegistry helper (fetch -> validate -> install) used by both the store list and the card control, and surfaces the full registry entry from useThemeUpdates so the card can update directly. |
||
|
|
6118b3940f |
feat(server): capability framework, AudioMuse sonic routing & PsyLab Connections (#1033)
* feat(server): probe AudioMuse via OpenSubsonic and add PsyLab Connections tab Navidrome ≥0.62: detect sonicSimilarity extension for reliable plugin signal; older servers keep the legacy getSimilarSongs probe. PsyLab gets a Connections tab with session, endpoint, and active-server capability details. * feat(psylab): polish Connections tab, admin role probe, and tab bar layout Status badges and Navidrome admin/user validation in Connections; prevent PsyLab tab row from vertically collapsing under the Logs flex layout. * docs: add CHANGELOG and credits for PR #1032 * feat(settings): auto-enable AudioMuse on Navidrome 0.62+ with status indicator Replace the per-server manual toggle with a probe-driven badge when sonicSimilarity is available; pre-0.62 Navidrome keeps the legacy toggle. * feat(server): capability framework with AudioMuse sonic routing Add a declarative server-capability catalog (src/serverCapabilities/) that picks a feature strategy per server generation, runs only the needed probes, and routes API calls. AudioMuse Instant Mix now prefers the OpenSubsonic sonicSimilarity endpoint (getSonicSimilarTracks) on Navidrome 0.62+ and falls back to legacy getSimilarSongs. - catalog/context/resolve: eligibility, detection, activation, call routing - storeView: read facade over the existing per-server probe maps - getSonicSimilarTracks API client + fetchSimilarTracksRouted router - route Instant Mix and Lucky Mix through the resolver - ServersTab + PsyLab Connections read the resolver (auto status vs toggle) - tests: resolve, storeView, router * docs: update CHANGELOG and credits for PR #1033 Renamed branch supersedes PR #1032: point changelog/credits at #1033 and document the server-capability framework, auto-managed AudioMuse indicator, and sonic Instant Mix routing. * refactor(server): address PR #1033 review — idempotent probe, drop dead code - Make scheduleInstantMixProbeForServer idempotent: skip when a definitive result is cached; re-probe only on force (add/edit/test server), a prior error, or a server version/type change (invalidated in setSubsonicServerIdentity). Removes the steady-state 120 s re-probe, the present→probing→present flicker, and the momentary legacy-fallback routing window. - Remove now-dead identity helpers (showAudiomuseNavidromeServerSetting, isAudiomusePluginAutoManaged, isNavidromeSonicSimilarityEligible, resolveAudiomusePluginProbeUiStatus + type) and the superseded probeAudiomusePluginWithCredentials; the catalog is the single source of truth. - Drop the never-emitted 'unsupported' AudiomusePluginProbeResult variant. - Fill audiomuseStatus* keys in all 8 non-en locales. - Tests: probe idempotency + version-change invalidation; retarget OpenSubsonic test to fetchOpenSubsonicExtensionsWithCredentials. |
||
|
|
0878cbf308 |
feat(themes): Theme Store install counts, downloads & sort (#1036)
* feat(themes): install counts, downloads, popularity & sort in the Theme Store Each registry theme now carries an install count and a last-changed date. Store rows show them in a dedicated meta panel (author, popularity bar, total downloads, last changed), plus a sort dropdown (most popular / newest / name), zebra-striped rows, a numbered pager, and a note that the stats refresh daily. Adds a shared formatRelativeTime helper (formatLastSeen now delegates to it). * test(themes): cover sort modes, pager jump and relative-time formatting * fix(build): gate nvidia_quirk_active to Linux Its only caller is the Linux branch of theme_animation_risk, so on Windows and macOS the function was dead code and tripped clippy's -D warnings. * docs: CHANGELOG + credits for theme store download stats & sort (#1036) |
||
|
|
68f0f09aae |
fix(themes): restyle the animated-theme indicator (#1035)
Replace the raw hazard-triangle icon with a small amber motion chip (shared AnimatedThemeBadge): an Activity glyph on a tinted rounded surface. In the Theme Store it stays inline after the name; on installed theme cards it moves from bottom-right to top-centre, clear of the active indicator (top-right) and the uninstall control (top-left). Tooltip and aria-label are unchanged. |
||
|
|
086c7e43b4 |
feat(psylab): rename probe, Tuning tab, log tools, and safe log sanitization (#1027)
* feat(psylab): rename probe UI, add Tuning tab and log copy/export PsyLab (Ctrl+Shift+D): cover backfill threads move to Tuning; logs gain selectable text, toolbar copy/export, and a selection-only context menu. * fix(logging): redact secrets and mask remote hosts in runtime logs Sanitize lines at append time (buffer, CLI tail, export) and in PsyLab: Subsonic/auth query params, bearer tokens, password fields, URL userinfo; remote hostnames partially starred, LAN/localhost left readable. * fix(logging): UTF-8-safe log sanitization — unbreak playback on em dash Byte-indexed URL scanning panicked on multi-byte chars (e.g. "—" in stream logs), killing tokio workers and aborting playback. Iterate by char boundary; add infallible wrapper on the append hot path. * docs(changelog): PsyLab UI and safe log sanitization (PR #1027) |
||
|
|
e527cfe67f |
fix(fullscreen): respect Settings clock format on wall clock (#1025)
* fix(fullscreen): respect Settings clock format on wall clock The fullscreen player corner clock always used the browser locale default (12-hour). Route it through formatClockTime and authStore.clockFormat so 24-hour matches queue ETA and sleep-timer preview. * docs(changelog): fullscreen clock format fix (PR #1025) |
||
|
|
30e9db1a2b |
fix(artists): per-artist links on song rails and shared OpenSubsonic refs (#1023)
* fix(artists): per-artist links on song rails and shared OpenSubsonic refs Song cards in Random Picks and Discover Songs showed joined artist credits but navigated to a single artistId. Route track surfaces through resolveTrackArtistRefs and coerce single-object Subsonic JSON payloads. * docs(changelog): note song-rail multi-artist link fix (PR #1023) |
||
|
|
49ad3618a8 | fix(themes): show animated-theme warning on the thumbnail in My Themes (#1022) | ||
|
|
4148e063dd |
fix(home): sync Mainstage hero backdrop with album on fast nav (#1021)
* fix(home): sync Mainstage hero backdrop with album on fast nav Bind hero background URL to the current slide and drop stale crossfade layers so rapid carousel clicks no longer show the previous album's art. * docs(changelog): note Mainstage hero backdrop sync fix (PR #1021) |