* 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)
* 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)
* fix(music-network): keep API suffix for self-hosted paste-token providers
The api_key_only connect strategy persisted the raw origin from the baseUrl
field instead of the resolved API base, dropping the preset's
selfHostedApiSuffix (e.g. /apis/listenbrainz). Scrobbles and now-playing then
hit <origin>/1/submit-listens (404/405, silently unlogged) instead of
<origin>/apis/listenbrainz/1/submit-listens, so nothing was recorded.
Return the runtime-resolved ctx.baseUrl (origin + suffix) and fall back to the
field only when it is absent. Fixes Koito and both Maloja compat surfaces
(ListenBrainz and Audioscrobbler). Existing accounts must reconnect to
re-persist the corrected base.
* docs(changelog): self-hosted scrobble URL fix (#1085)
* 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)
* docs(whats-new): refresh 1.48.0 highlights for release screen
Reorder and expand user-facing copy to match CHANGELOG Added order,
add Music Network and Live, drop stale About/licenses line, replace
the vague Fixed placeholder with grouped fixes, and add a short Under
the hood section for significant non-UI work.
* docs(whats-new): order 1.48 sections by user impact
Sort Highlights, Fixed groups, and bullets within each block so the
most noticeable changes for everyday use appear first.
* docs: CHANGELOG 1.48 strict PR order; clarify WHATS_NEW sorting
Reorder Added/Changed/Fixed entries in CHANGELOG [1.48.0] by ascending PR
number. WHATS_NEW keeps impact-based order; add a maintainer note that
the two files intentionally differ.
* chore(deps): bump esbuild to 0.28.1 for GHSA-g7r4-m6w7-qqqr
Closes Dependabot alert #17 — path traversal in esbuild dev server
(--servedir) on Windows; patched in 0.28.1.
* chore(nix): sync npmDepsHash with package-lock.json
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(installer): write logs to stderr so the download URL isn't polluted
* fix(installer): read the reinstall prompt from /dev/tty
* fix(installer): add --fail --globoff to the package download
* fix(installer): degrade gracefully when no controlling terminal is available
* 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)
Move the source-building pipeline out of play_input.rs into a focused
source_build.rs: BuildSourceArgs/PlaybackSource, the ranged-stream probe
fallback (build_playback_source_with_probe_fallback) and its private helpers
(build_source_from_play_input, wait_or_fetch_bytes_for_stream_fallback, etc.).
play_input.rs now only handles source *selection* and drops from 799 to 420
lines; helpers that became module-internal lose their pub(super) visibility.
No behavior change.
* fix(audio): release idle output stream after 60s (#1071)
Lazy-open CPAL on first playback and close the device handle after one
minute without active audio so Windows can sleep; emit output-released
for cold resume and skip post-wake reopen when idle.
* docs: CHANGELOG and credits for idle audio stream fix (PR #1073)
* fix(audio): satisfy clippy if-same-then-else in idle watcher
* fix(audio): silence rodio DeviceSink drop unless logging is debug
Gate log_on_drop(false) on runtime should_log_debug() so normal/off
logging modes avoid stderr noise from intentional idle stream release.
* feat(audio): cold-start paused restore and silent engine prepare
After getPlayQueue on startup, apply saved seek position to the UI,
prefetch the current track to hot cache, and load the engine paused via
new audio_play startPaused so playback does not audibly start before
pause. Shared engineLoadTrackAtPosition with queue-undo restore.
* fix(audio): satisfy clippy too_many_arguments on stream arm helper
Bundle spawn_legacy_stream_start_when_armed parameters into
LegacyStreamStartWhenArmed so workspace clippy passes.
* fix(audio): release output stream immediately on stop (#1071)
Stop and natural queue end call audio_stop; close the CPAL device right
away instead of waiting for the 60s idle timer. Pause keeps the grace
period for warm resume.
* fix(audio): keep waveform mounted after stop (#1071)
Stop preserves currentTrack, so its cached analysis waveform stays valid.
Stop no longer nulls waveformBins for the still-shown track and re-hydrates
them from the analysis DB, instead of dropping to flat placeholder bars.
* test(audio): cover output_stream_is_needed branches; harden audio_play arg (#1071)
- Add unit tests for the idle-keepalive decision: empty/playing/paused main
sink, preview and fading-out sinks, and radio playing/paused. Players are
built device-less via rodio's Player::new + a Zero source, so empty()/state
are exercised without an audio device.
- Make audio_play's `start_paused` an Option<bool> defaulting to false, so the
new field is strictly additive (omitting startPaused no longer fails serde).
- Drop the unused `_engine` parameter from start_stream_idle_watcher; it
resolves the engine from the AppHandle each poll.
* refactor(audio): extract sink-swap lifecycle into sink_swap module (#1071)
Move SinkSwapInputs/swap_in_new_sink and the legacy stream-arm helper
(LegacyStreamStartWhenArmed/spawn_legacy_stream_start_when_armed) out of
play_input.rs into a focused sink_swap.rs, so source selection and source
building stay separate from sink lifecycle. play_input.rs drops from 953 to
799 lines. No behavior change.
Release CSP blocked runtime <style> injection for community themes; add
explicit style-src/style-src-elem and font-src. Derive the effective theme
synchronously in useThemeScheduler so data-theme updates on the same commit.
* fix(mpris): disable WebKit media session so radio doesn't duplicate the player
Internet radio plays through an HTML <audio> element, for which WebKitGTK
auto-registers its own MPRIS player (org.webkit.*) alongside the app's
souvlaki one. On Linux desktops that list every player, now-playing then
showed twice during radio (issue #1048: one "psysonic", one "Psysonic").
Disable the WebKit media session at main-window setup (enable-media-session,
set by GObject property name since the pinned binding has no typed setter,
guarded by find_property) so souvlaki stays the single now-playing source;
radio metadata still reaches it via mpris_set_metadata. The navigator.media
Session push in useRadioMprisSync is kept as a fallback in case a WebKitGTK
version still registers the player, so issue #816 cannot regress.
* docs(changelog): MPRIS radio duplication fix (PR #1069)
* 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)
* 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>
* fix(favorites): reflect player-bar song star in track lists
Liking a song from the player bar / fullscreen / shortcuts wrote only to
the session `starredOverrides` map, which onStarSuccess deleted once the
server sync resolved. List views (AlbumDetail, Favorites, RandomMix,
playlists) seed their starred state from a one-shot fetch and reflect
later changes only by merging that override, so the row reverted the
instant the sync completed — the like never stuck in the list. Toggling
from a list row also updated the row's own local state, which is why that
direction already worked.
Keep the star override as the durable session source of truth (stop
deleting it on success); the in-memory Track / queue-cache patches stay.
The override is unpersisted and superseded by the next toggle, so it never
diverges from the server. Ratings keep their existing clear-on-success
behavior.
* fix(whats-new): allow plugin-fs write into AppData cache dir
mkdir for release-notes/ succeeded but write_text_file to nested
paths was denied without fs:allow-app-write-recursive — RC/stable
re-fetched whats-new.md on every launch with an empty cache folder.
* docs: CHANGELOG PR #1062 for release-notes cache write
runGenreTagsPhase entered the blocking 'inspecting' phase before awaiting
the inspect IPC, so the migration modal briefly appeared on every startup
once the one-time genre backfill was already complete. Inspect first without
a blocking phase and only switch to 'running' when work is actually needed.
* fix(library): multi-genre local index with track_genre and backfill
Restore atomic genre browse, filters, and counts via track_genre:
OpenSubsonic genres[] first with Navidrome-default split fallback, sync
write path, read-path query switches, blocking startup backfill with
progress, and v12 repair migration for DBs that recorded legacy 002–011.
TS fallback adds genreTagsFor and migration gate i18n across locales.
* fix(library): address multi-genre review — robust TS genres and scope join
genreTagsFor routes raw genres through parseItemGenres (single-object
Subsonic quirk and bare strings). Library-scoped genre browse/counts join
track for raw_json library_id fallback. Statistics keeps empty-genre bucket.
* docs: CHANGELOG and credits for multi-genre local index (PR #1059)
* docs(changelog): credit HiveMind on Discord for multi-genre report (PR #1059)
* fix(whats-new): generate release-notes bundle before dev startup
Fresh clones failed Vite import analysis because releaseNotesBundle.ts
was gitignored and dev/tauri:dev did not run prebuild. Commit a sliced
stub and hook prebuild into dev scripts.
* docs: CHANGELOG PR #1060 for dev startup bundle fix
* fix(whats-new): keep release-notes bundle gitignored
Drop the committed generated slice — it would churn on every
CHANGELOG/WHATS_NEW edit. Prebuild before dev/tauri:dev is enough.
* 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).
* 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.
* fix(scrobble): report Now Playing on playback server with local bytes
Navidrome presence and play-count scrobbles no longer skip when audio plays
from hot cache, offline pins, or favorites-auto, and reachability follows the
queue/playback server instead of the browsed active server.
* docs: changelog and credits for PR #1055
* 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).
* 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
* chore(themes): drop the dead installs field from RegistryTheme
The store no longer reads install counts (the themes registry stopped
emitting them), so this optional field on RegistryTheme was unused.
* fix(themes): give dropdown border/shadow tokens a cascade default
--border-dropdown and --shadow-dropdown were set by only two themes and had
no base fallback, so dropdown/popover borders rendered without a themed value
in every other theme. Default them in the semantic cascade next to the other
menu tokens; themes that set them explicitly still override.
* fix(themes): unify input focus rings, drop the double border
Text inputs draw their own border + box-shadow focus ring, but the global
:focus-visible outline stacked a second ring outside it — a double border on
every field. Suppress the outline for text inputs centrally; the specificity
(0,1,1) beats the global ring but loses to the colour-blind-safe themes'
[data-theme] *:focus-visible (0,2,0), so those keep their stronger AAA ring on
every field by design — the header search now carries that ring on its cluster
there too. Align the few input classes that had a weaker or missing ring to the
shared border + 3px accent-dim standard.
* docs(changelog): add #1052 input/dropdown focus border fixes
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.
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.
* feat(now-playing): index-first metadata resolvers (#1046)
Add nowPlayingMetadataResolve.ts: four index-first resolvers (album,
discography, top songs, song meta) that read the local library index when
it has the row and fall back to Subsonic only on index miss / off / not
ready. Reuses loadAlbumFromLibraryIndex, loadArtistFromLibraryIndex,
libraryGetTrack, libraryAdvancedSearch, trackToSong, and libraryIsReady —
the same index-first family as queueTrackResolver, not a new network path.
getSongForServer keeps its byte-style trackId guard; the index arm runs
first so an index hit avoids the guarded call. artistInfo has no index
source and stays network-only (not handled here).
* feat(now-playing): wire index-first resolvers into fetchers + prewarm (#1046)
Replace the four direct Subsonic calls in useNowPlayingFetchers and
prewarmNowPlayingFetchers (album, discography, top songs, song meta) with
the resolveNp* resolvers — index-first, network fallback. artistInfo
stays on getArtistInfoForServer (no index source). Caches, id-gating
tuples, and the subsonicFetchAllowed guard are unchanged; the top-songs
effect gains artistId in its deps (index arm filters by it).
Net: on a populated, ready index those four cards render from SQLite with
no Subsonic call; index miss/off/not-ready falls back to the network path
exactly as before.
* docs: add CHANGELOG entry for PR #1049
* fix(now-playing): widen resolveNpTopSongs artistId to optional (tsc)
The top-songs fetch isn't guard-narrowed on artistId, so callers pass
string | undefined. The resolver already handles a missing artistId (falls
straight to the network arm); widening the param type fixes the CI tsc
failure that the wiring commit introduced.
* fix(now-playing): run index reads offline + deterministic top songs (#1049 review)
Address cucadmuh's two in-PR review points:
1. Index-first reads now run whenever there's a playback server id, including
when the server is unreachable — the offline win of index-first. Split the
hook/prewarm gate: resolvers run on server-id presence; the reachability
guard moved into each resolver's network fallback arm. artistInfo (no index)
stays network-only.
2. resolveNpTopSongs derives top songs from the artist's own discography albums
(sorted by play_count) instead of an FTS-on-name query, so name collisions
can't surface the wrong tracks. getTopSongsForServer remains the fallback.
Resolver tests extended with the unreachable (index-only) cases.
The Cover art cache settings section shipped with English (and Russian)
strings only. Add the 23 keys (scope/strategy labels, Lazy/Aggressive
descriptions, per-server clear dialog) for de, fr, es, nl, nb, ro, and zh.
Placeholders and the technical progress-value string are left verbatim.
* fix(now-playing): keep metadata cards when the track plays from local cache
The Now Playing metadata fetches (album, artist info, discography, top songs)
gated on shouldAttemptSubsonicForServer(serverId, trackId). That guard returns
false when the current track's audio resolves to a psysonic-local:// URL (hot
cache / offline bytes), which is correct for playback-byte calls but wrongly
suppressed the metadata calls. On track change the prefetched next track plays
from the hot cache, so every Subsonic-backed card blanked and only the Last.fm
bio fallback and Bandsintown tour (both non-Subsonic) remained.
Drop the trackId from the four Now Playing metadata gates so metadata is fetched
whenever the server is reachable, regardless of where the audio bytes come from.
True-offline is still handled by the online / reachability checks in the guard.
The guard and all other call sites are unchanged.
* docs: add CHANGELOG entry for PR #1042
* refactor(now-playing): single Subsonic network gate owned by the fetchers hook
The metadata reachability guard (shouldAttemptSubsonicForServer) ran 2-3
times per Now Playing render/effect cycle: callers folded it into
fetchEnabled in NowPlaying.tsx and useNowPlayingPrewarm on top of the
checks already inside useNowPlayingFetchers and prewarmNowPlayingFetchers.
Callers now pass fetchEnabled as intent only ("we have a playback server
id"); the single reachability decision lives in the hook / prewarm
function. Behaviour is unchanged — only the duplicated guard calls go.
* test(now-playing): cover local-playback metadata behaviour, not only call shape
Add a guard test proving the metadata gate (no trackId) bypasses the
psysonic-local:// skip while a byte-style call stays blocked, and a hook
test that fetches album/discography/top songs with a trackId-sensitive
guard — so a reintroduced trackId at the gate fails the suite instead of
silently blanking the cards.
* 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
* fix(library-db): name slow-write ops for macOS stall diagnosis (#1040)
Replace generic op=misc labels on production library-db write paths with
stable module.action names (sync_state.*, track.*, tombstone.*, cmd.*, …)
so SLOW write logs pinpoint the call site. Document the naming convention
on LibraryStore::with_conn.
Diagnostic step for #1040 — no behaviour change.
* docs: add CHANGELOG and credits for PR #1043
Library-db slow-write op naming diagnostic for issue #1040.
* 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.
* fix(audio): poll only the default device when none is pinned (#996)
The device watcher ran a full output_devices() + per-device description()
CoreAudio enumeration every 3s, even with no pinned device. On some macOS
setups this contends with the audio render thread and causes a brief dropout
once per poll — a stutter whose cadence tracks the poll interval exactly.
The full enumeration is only needed to detect a pinned device disappearing.
With no pin (system default, the common case) only the current default is
needed, so the enumeration is skipped entirely in that case; the cheap
single default_output_device() query still detects default-device changes.
Confirmed with a diagnostic build: throttling the enumeration to ~60s moved
the reporter's stutter cadence from ~3s to ~60s, isolating the enumeration
as the cause.
* docs: add CHANGELOG entry for PR #1039
* refactor(themes): drive the Now Playing page from semantic tokens
Replace the hardcoded white/black colours in the Now Playing dashboard,
the info-card glass panels, the queue sidebar, track lists, tags and the
Last.fm stats with the semantic token surface (text / surface / border /
glass). Light themes now render the page legibly instead of washed-out
white-on-light; dark themes keep their look because the tokens' dark values
match the previous hardcoded ones. Cover-art content (fullscreen player,
hero) is intentionally left fixed and out of scope.
* docs: CHANGELOG for themeable Now Playing page (#1038)
* 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.
* 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)
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.
* fix(artist): play top tracks when album list is empty on page
Top-track rows silently no-op'd when albums.length was 0 (common in
lossless artist view). Play the selected top songs immediately and only
append catalog tracks when albums are available.
* docs: note artist top tracks play fix in CHANGELOG for PR #1031
* feat(ui): add themed startup splash with deferred window show
Show a theme-aware loading splash before the Vite bundle mounts, hide the
native window until it paints, and use per-theme logo gradient colors.
* docs: note startup splash in CHANGELOG and credits for PR #1030
* docs: attribute PR #1030 startup splash to cucadmuh
* fix(library): All Albums compilation filter matches VA album artist
Local index SQL and track-grouped browse missed compilations tagged via
Various Artists on album_artist; genre-only browse also ignored combined
filters. Extend predicates on both sides and route genre+filter to advanced search.
* docs(changelog): All Albums compilation filter fix (PR #1026)
* 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)
* 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)
* 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)
* feat(themes): warn about animated themes on high-CPU setups
Show a warning icon + tooltip on animated themes (those defining
@keyframes) in the store and in Your Themes, on Linux setups where
animation is costly — the Nvidia WebKit quirk is active or compositing
is forced off. The Nvidia detection already runs once at startup; its
result is recorded (theme_animation.rs) and read via a new
theme_animation_risk command — no GPU re-probe. Display-only; never
shown off Linux. The animated flag comes from the registry (store) or
the theme CSS (Your Themes).
* docs(themes): note the animated-theme warning PR in the Theme Store entry
Debug builds only: `--theme-watch <theme.css>` polls a local theme file
and pushes it into the running app on save (a lightweight Rust watcher
thread emits a Tauri event). The frontend installs the CSS under its
[data-theme='<id>'] selector and applies it, so the existing
syncInjectedThemes effect re-injects live — no zip re-import, no reload.
Double-gated (debug_assertions + import.meta.env.DEV); never wired in
production.
* fix(themes): show store thumbnails in 16:9
Thumbnails are now 16:9 WebP. Render the store-row preview at 200x112
(16:9) so the screenshot isn't cropped in the grid; the lightbox already
shows it full size, now larger and crisper from the 1280x720 source.
* docs(themes): note the 16:9 thumbnail PR in the Theme Store entry
* feat(offline): local-bytes browse for artists and albums
Make Artists, All Albums, and artist/album detail pages work offline
from the library index limited to on-disk library and favorite-auto
tracks. Add a DEV header toggle to simulate offline browse for testing.
* feat(offline): reactive DEV offline toggle with full disconnect simulation
Subscribe nav and browse/detail hooks to useOfflineBrowseActive so UI
refreshes on toggle. DEV force-offline now blocks server probes, reports
disconnected status, and gates Subsonic like real offline for player parity.
* feat(offline): bytes-first favorites when offline browse is active
Load Favorites from local playback bytes, filter starred tracks client-side,
and restrict album-level star queries to local album ids. Drop interim perf
attempts (lean SQL, progressive load, connection singleton, prefetch UX).
* feat(offline): tracks, help, player stats; suspend library picker offline
- Offline browse for Tracks hub from local bytes; sidebar nav for tracks/help/statistics
- Statistics redirects to player-stats offline; server/Last.fm tabs skip network fetches
- Hide music-library picker offline; save filter and restore on reconnect (all libraries while disconnected)
- Unified isOfflineSidebarNavAllowed for library + system entries
* feat(offline): fork disconnect navigation by offline browse capability
When the server drops: stay on the page if nothing is browsable offline;
reload in place on offline-capable routes; otherwise redirect to All Albums
instead of the old /offline or /favorites bounce.
* feat(offline): browse cached playlists when the server is down
List and open manually pinned regular playlists from local library-tier
bytes offline, with sidebar/nav routing and read-only playlist UI.
* feat(offline): read-only artist detail and local play-all paths
Hide favorites and discography offline actions when browse is offline;
load Play All, Shuffle, and top-track continuation from local album bytes.
* feat(offline): read-only album detail and enqueue from local bytes
Hide favorites, download, and cache-offline actions on album pages when
offline browse is active. Favorites album cards enqueue via the same
resolveAlbumForServer path as play, including local playback bytes.
* chore: remove unused import in AlbumCard after enqueue refactor
* feat(offline): unify browse integration contract across the app
Add useOfflineBrowseContext, offlineMediaResolve, and offlineActionPolicy;
wire shell nav to a single capability source; migrate play/enqueue and
context-menu paths off raw getAlbum; replace readOnly with action policy
on detail surfaces. Tests updated for the media-resolve facade.
* feat(offline): close browse contract gaps and fix offline Home feed
Split offline browse modules, align favorites capability across servers,
wire action policy on context menus, migrate hooks to useOfflineBrowseContext,
and preserve stale Home feed cache when offline so the UI does not empty.
* fix(offline): block playbar stars, close audit gaps, trim dead exports
Hide star rating and favorite in PlayerBar when offline browse is active via
offlineActionPolicy playerBar surface. Wire stay-reload token into browse
hooks, migrate hooks to context.active, guard rating prefetch network calls,
and route playlist load through resolvePlaylist.
* docs: add CHANGELOG and credits for offline browse PR #1017
* fix(offline): stop DEV connection probe regression in tests
React to devForceOffline transitions only in useConnectionStatus so mount
does not double-fire check() or ignore disableBackgroundPolling. Add
pingWithCredentials to PlayerBar test mock and DEV-toggle unit tests.