mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
41c8187186629dc6b08d48f5554e2f44ccb34ce6
383 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
abc2c0b579 |
refactor(audio): split source-build pipeline into source_build module (#1074)
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. |
||
|
|
184e87a469 |
fix(audio): release idle output stream after 60s (#1071) (#1073)
* 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. |
||
|
|
80822fd742 |
fix(themes): apply Theme Store themes in production release builds (#1070)
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. |
||
|
|
4902c0e25b |
Fix MPRIS player duplication during internet radio (#1048) (#1069)
* 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) |
||
|
|
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>
|
||
|
|
90452a8f8c |
fix(whats-new): allow plugin-fs write into AppData cache dir (#1062)
* 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 |
||
|
|
5cd01c90ac |
fix(library): multi-genre local index with track_genre and backfill (#1059)
* 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) |
||
|
|
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. |
||
|
|
b2a5baa48d |
fix(library-db): name slow-write ops for macOS stall diagnosis (#1043)
* 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. |
||
|
|
c6298d8c25 |
fix(audio): poll only the default device when none is pinned (#996) (#1039)
* 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 |
||
|
|
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) |
||
|
|
5167d8f49e |
feat(ui): themed startup splash with deferred window show (#1030)
* 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 |
||
|
|
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) |
||
|
|
32832246c0 |
fix(library): All Albums compilation filter matches VA album artist (#1026)
* 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) |
||
|
|
88f7a7bc90 |
feat(themes): warn about animated themes on high-CPU setups (#1020)
* 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 |
||
|
|
daa6fbbfd7 |
feat(dev): --theme-watch flag for live theme authoring (#1019)
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. |
||
|
|
b3573e7107 |
feat(themes): import a theme from a local .zip (#1012)
* feat(themes): validate and extract locally imported theme packages Add the backend + validation half of local theme import: users will be able to load a theme packaged as a .zip (manifest.json + theme.css). - `import_theme_zip` Tauri command unpacks only manifest.json + theme.css from the archive, outside the webview, with an archive-size cap, per-entry uncompressed caps, and path-traversal rejection. - `validateThemePackage` runs the full theme-store contract in-app: the manifest schema (field patterns copied from the repo schema), the CSS token whitelist with all core tokens required, color-scheme matching the declared mode, data-URI restricted to the arrow token, and a guard against ids that collide with built-in themes. The existing `validateThemeCss` containment guard is reused and runs again at injection time. The contract token list is a byte-identical copy of the themes repo's allowed-tokens.json. Covered by validateThemePackage tests for every rejection class. * feat(themes): add the Import a theme section to the Themes tab A dedicated section between the theme scheduler and the Theme Store lets users import a theme from a local .zip. After the package validates, a confirmation dialog names the theme and its author before it is installed. A rejected import shows a plain-language explanation aimed at end users; the raw contract diagnostics (token names, missing fields) are tucked into a collapsible "Technical details" block for theme authors. Fully localised across all nine languages. * docs(themes): changelog + credits for local theme import Fold the local .zip import (and the store pagination / refresh-scroll polish) into the still-unreleased Theme Store entry rather than a new section, and extend the credits line. PRs #1011 and #1012. |
||
|
|
2d3c723a6e |
feat(offline): unify local playback, offline library, and favorites sync (#1008)
* feat(local-playback): LP-1 media layout and download_track_local
Add library-index-backed path builder in psysonic-core and a unified
Tauri download command that writes under media/{cache|library}/ with
layout fingerprints; legacy hot/offline commands unchanged for now.
* feat(local-playback): LP-2 localPlaybackStore and media tier Rust helpers
Add unified Zustand index with legacy offline/hot-cache import, media_layout
TS mirror, and Rust commands for tier size/purge/delete/promote.
* feat(local-playback): LP-3 wire prefetch and playback to unified index
Route downloads through download_track_local, delegate hot/offline shims
to localPlaybackStore, and update resolve/promote/prefetch plus key rewrite.
* feat(local-playback): LP-4–LP-6 offline UI, invalidation, and mediaDir
Offline Library loads pinned groups via library index; sync-idle invalidates
stale paths; Settings uses a single mediaDir with cache/library tier sizes.
* feat(local-playback): migrate legacy offline files to media/library layout
Move flat psysonic-offline downloads into nested media/library paths using
library index metadata, with retry on sync-idle when tracks are not yet indexed.
* feat(local-playback): simplify offline disk migration and restore Offline Library UI
Scan psysonic-offline on disk and relocate by library track id; restore pinSource
and cover art for migrated pins; add find_live_by_id for segment/key resolution.
* feat(local-playback): disk-first offline reconcile and fast library tier discovery
Reconcile library-tier index against on-disk files using candidate track IDs
instead of scanning the full catalog. Refresh Offline Library from disk on
open and focus so deleted folders drop out of the UI. Add Rust discover/prune
helpers and wire album/server reconcile through the unified path.
* fix(offline): resolve local playback URLs across server index-key variants
Offline Library play failed when library-tier files were indexed under a
host key while playback looked up only the active profile UUID. Use
findLocalPlaybackEntry for URL resolution, pin queueServerId to the card
server, and build play queues from tracks that still have on-disk bytes.
* fix(offline): playlist cards, playback from Offline Library, and local URL routing
Show playlists with name and quad/custom cover instead of the first track's
album artist. Build play queues with library-batch fallback and offline-only
server switch. Prefer library-tier URLs in playTrack; add playback-unavailable
toast and missing trackToSong import.
* fix(cache): ephemeral disk reconcile, empty-dir prune, and Storage UI
Sweep media/cache after eviction (orphan files, stale index, empty folders).
Settings: split media folder from cover cache; in-browser image cache lives
under Cover art cache with aligned columns; clear only IndexedDB images.
* feat(offline): show library disk usage in Offline Library header
Query media/library tier size on reconcile and display it in a right-aligned
stat block beside the page title and album count.
* fix(cache): defer unindexed hot-cache eviction; drop legacy offline size cap
Reconcile ephemeral cache without deleting files from other app instances;
evict unindexed hot-cache files oldest-first only when over hotCacheMaxMb.
Remove the hidden maxCacheMb gate and offline-full banner on album pages.
* feat(offline): play-all cache card and stable Offline Library grid rows
Add a shuffle-and-play card for all on-disk library pins plus hot-cache
tracks when buffering is enabled. Fix virtual row height for offline cards
and reserve the year line so grid rows no longer overlap.
* feat(offline): queue-cache grid card limited to media/cache
Replace the full-width play-all banner with a playlist-style grid tile.
Shuffle/enqueue only ephemeral hot-cache tracks when buffering is enabled,
not offline library pins.
* chore(licenses): regenerate bundled OSS list for 1.48.0-dev
Set GPL-3.0-or-later on workspace crates and extend cargo-about accepted
licenses so generate-licenses.mjs runs; refresh src/data/licenses.json.
* feat(favorites): auto-sync starred tracks into separate media/favorites tier
Keep manual Offline Library in media/library/ and favorites offline in
favorite-auto/index + media/favorites/ so toggling sync cannot purge
user-pinned bytes; playback resolves library before favorites.
* feat(favorites): compact offline toggle with disk icon and sync semaphore
Move control to the page header (disk + switch, tooltips); show red/yellow/green
LED when enabled instead of the full-width save-offline card.
* fix(favorites): trigger offline sync on star/unstar from anywhere
Hook star/unstar API so favorites offline reconcile runs globally (songs,
albums, artists); optimistic unstar removes local bytes; drop Favorites-page-only sync.
* fix(cache): skip hot-cache prefetch when favorites or library bytes exist
Treat favorite-auto tier like offline library for prefetch, stream promote,
and same-track replay so synced favorites are not duplicated in media/cache.
* fix(favorites): reconcile offline files on merged track union only
Dedupe artist/album/song stars into one target set per track id; drop eager
unstar deletes so overlapping favorites do not remove bytes still needed.
* feat(offline): add Favorites card to Offline Library
Mirror queue-cache card for favorite-auto tier with play, enqueue, and
navigation to Favorites on card click.
* feat(offline): show library+favorites disk total with icon breakdown
Sum media/library and media/favorites in the On disk widget and open an
icon popover on hover with per-tier sizes for screen readers and sighted users.
* feat(favorites): enable offline Favorites tab when auto-save is on
Keep Favorites in the sidebar when disconnected, land on /favorites without
manual pins, and load starred rows from the local library index.
* feat(favorites): cross-server offline browse with per-server covers
When auto-save is on, Favorites merges starred items from every indexed
server and syncs each server independently. Detail links carry ?server=
for offline album/artist pages; cover art resolves disk cache by entity
serverId instead of the active server only.
* feat(playback): mixed-server queue scope and cross-server favorites sync
Per-ref server identity for playback (URL index key in queue refs, profile
UUID for API): trackServerScope, playbackServer helpers, gapless/scrobble/covers
by playing ref. Remove cross-server enqueue block; remap queueItems on URL
remigration.
Favorites: star/unstar and favorite-auto sync target the owning serverId
(not only active); queueSongStar passes server through pending sync.
* fix(offline): suppress Subsonic calls during favorites and local playback
Add reachability guards so offline favorites browse, album detail, queue
sync, scrobble, and Now Playing metadata skip network when the server is
down or the track plays from psysonic-local. Load starred albums/tracks
from the library index only (not the full artist table), refresh favorites
from index first, and pass server scope in favorites navigation.
* fix(queue): export share and playlist save for active server only
Mixed-server queues now filter queue refs by the browsed server profile
before copying a share link or saving/updating a playlist from the toolbar.
* fix(offline): complete album pins and resume interrupted downloads
Prefer full getAlbum track lists when online so partial library index
does not truncate offline pins; refresh songs before pin from album
detail. Resume incomplete persisted pins after reconcile and reconnect,
cancel in-flight work on delete, and chunk library batch fetches past
100 refs.
* fix(offline): pin queue, queued UI, and re-pin after remove
Serialize album and playlist offline pins so parallel enqueue no longer
drops in-flight work. Show an explicit queued state on album and playlist
actions with dequeue on repeat click, sidebar tooltips for long labels,
and clear stale cancel flags so Make available offline works after remove.
* fix(offline): remove Offline Library cards without full page reload
Optimistically drop the deleted card from local grid state, show the
loading spinner only on first visit, and ignore stale disk refreshes so
pin updates no longer flash the whole library view.
* fix(offline): artist discography pin state and queue handling
Detect cached/queued/downloading from persisted album pins instead of
ephemeral bulkProgress, skip already offline or in-flight albums when
enqueueing discography, and show the correct hero button after revisit.
* fix(local-playback): address LP-1 review handoff (B1, M1–M7)
Harden media path sanitization and tier containment, align Rust/TS layout
fingerprints, serialize per-track downloads, and fix favorites re-enable,
multi-server debounce, prev-track promote key, now-playing reachability,
and ephemeral prefetch soft-skip for unindexed tracks.
* fix(offline): cancel in-flight favorites downloads on unstar
Abort Rust streams with the real favorites downloadId when sync is
rescheduled or disabled, and drop completed bytes that no longer belong
in the starred set so unstar does not leave orphan files on disk.
* fix(build): resolve TypeScript errors blocking prod nix build
Align mediaLayout with LibraryTrackDto camelCase, extend analysis-sync
reasons, fix OfflineLibrary grid cover typing, and tighten vitest mocks
so `tsc && vite build` passes under the flake beforeBuildCommand.
* fix(rust): satisfy clippy too_many_arguments for CI
Bundle offline-library analysis and local path/migration helpers into
parameter structs so `cargo clippy -D warnings` passes on the branch.
* fix(settings): show correct hot-cache track count in Buffering section
Count ephemeral localPlayback rows instead of prefix-matching index keys,
which always missed host:port server segments and showed zero tracks.
* fix(media-layout): align truncation threshold on code points (M1)
Rust sanitize_and_truncate_segment now uses char count like TS so long
non-ASCII metadata does not diverge layout fingerprints; add Cyrillic
parity tests and clarify ephemeral cold-miss doc on download_track_local.
* fix(test): use numeric cachedAt in hotCacheStore count test
Align test fixture with LocalPlaybackEntry type so tsc passes in CI.
* docs: add CHANGELOG and credits for offline experience PR #1008
* feat(offline): auto-sync manually cached playlists when track list changes
Re-download new tracks and prune removed ones for playlist pins only, triggered
from updatePlaylist, playlist detail load, smart-playlist polling, and reconnect.
* docs: note cached-playlist sync in CHANGELOG and credits for PR #1008
* fix(offline): exclude smart playlists from manual offline cache and sync
Hide cache-offline for psy-smart-* playlists, block download/sync paths, and
document the distinction in CHANGELOG.
* feat(offline): auto-sync cached albums and artist discographies
Generalize pinned playlist reconcile into pinnedOfflineSync so manually
pinned albums and artist discographies re-download added tracks and prune
removed ones on reopen, reconnect, and catalog changes.
* feat(offline): split pinned sync triggers by pin kind
Album and artist pins reconcile after library index sync and reconnect;
regular playlists reconcile hourly and on in-app playlist edits only.
Remove reconcile-on-open for album, artist, and playlist detail views.
* fix(offline): address PR #1008 review (N1, tests, pin queue)
Scope playlist reconcile to the owning server via getPlaylistForServer.
Add artist discography and mixed-server playlist tests; dedupe pending
sync jobs; skip pinTasks overwrite during active downloads.
|
||
|
|
40db6b08d2 |
chore(playback): remove Preload Next Track setting (#1007)
* chore(playback): remove Preload Next Track setting The configurable next-track RAM preload duplicated hot cache and is obsolete now that ranged streaming starts playback sooner. Gapless and crossfade still use the internal audio_preload backup when hot cache is off. * docs: add CHANGELOG entry for Preload Next Track removal (PR #1007) |
||
|
|
a66d932afe |
fix(preview): Symphonia format sniff and ranged stream startup (#1006)
* fix(preview): Symphonia format sniff and cluster member stream URLs Resolve preview container hints from HTTP headers, Subsonic suffix, and magic-byte sniff after Symphonia 0.6. Route preview streams through clusterBrowseServerId like main playback; guard CoverArtImage when the preview cover ref is still loading. * fix(preview): adapt Symphonia sniff branch for main without cluster routing Keep formatSuffix and cover-ref guards from the cluster work; use buildStreamUrl on the active server instead of clusterBrowseServerId. * fix(preview): use ranged HTTP so preview starts without full-file download Open preview via RangedHttpSource when the server supports byte ranges; fall back to buffered download otherwise. Gate in-memory probe with ProbeSeekGate so Symphonia 0.6 does not scan the entire file before audio. * docs: CHANGELOG and credits for preview fix PR #1006 * chore: drop PR #1006 from settings credits (minor fix) * fix(preview): allow clippy too_many_arguments on audio_preview_play formatSuffix pushed the Tauri command to 8 args; matches other IPC commands. |
||
|
|
c674e4515b |
feat(audio): migrate Symphonia 0.5 -> 0.6 (#999)
* feat(audio): migrate Symphonia 0.5 -> 0.6 Port the audio + analysis pipeline to the Symphonia 0.6 API: - bump symphonia to 0.6 and symphonia-adapter-libopus to 0.3; drop rodio's symphonia-all feature to avoid a duplicate symphonia-core 0.5 - remove the local symphonia-format-isomp4 0.5 patch and rely on upstream 0.6 - switch codec registries to register_enabled_codecs / make_audio_decoder with AudioCodecParameters + AudioDecoderOptions - rework decode.rs SizedDecoder and analysis decode loops for the new AudioDecoder trait, GenericAudioBufferRef, Time/Timestamp newtypes, and next_packet() -> Result<Option<Packet>> Streaming regression fixes: - ProbeSeekGate hides seekability during probe for non-MP4 progressive streams so Symphonia 0.6's trailing-metadata scan no longer forces a full download before ranged FLAC/MP3/OGG playback can start - guard the streaming probe() with a 20s timeout on a worker thread so a stalled ranged source (e.g. right after a server switch) can no longer hang playback start until a player restart; add probe start/done diagnostics * docs(changelog): note Symphonia 0.6 migration and streaming fixes (#999) Add CHANGELOG entries (Changed + Fixed) and a CONTRIBUTORS line for the Symphonia 0.6 migration, ranged-stream start-latency fix, and probe timeout. * test(audio): cover streaming probe path and ProbeSeekGate Add unit tests for SizedDecoder::new_streaming (success + garbage) and ProbeSeekGate (seekability toggle, byte_len, read/seek passthrough) to restore decode.rs above the 70% hot-path coverage gate (67.3% -> 79.4%). |
||
|
|
d1320ea2c8 |
chore(deps): refresh npm and Rust dependencies (#997)
* chore(deps): bump npm patch/minor dependencies Refresh frontend lockfile for React, Vite, Vitest, i18next, axios, Tauri plugins, and related type packages within existing semver ranges. * chore(deps): bump Rust patch dependencies Update id3 to 1.17, reqwest to 0.13.4, and align root zbus with psysonic-audio 5.16. * chore(deps): upgrade jsdom to v29 Major test-environment bump; all Vitest suites still pass and npm audit is clean. * chore(deps): upgrade sysinfo 0.39 and zip 8 Align root sysinfo with psysonic-syncfs and migrate backup archives to zip 8. mach2 0.6 deferred — cpal/rodio still require ^0.5. * chore(nix): sync npmDepsHash with package-lock.json * docs(changelog): note dependency refresh (PR #997) * docs(changelog): move deps note to [1.48.0] section --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
76dd5c2087 |
chore(release): bump main to 1.48.0-dev (#992)
* chore(release): bump main to 1.48.0-dev * chore(nix): sync npmDepsHash with package-lock.json --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
2f50a1bade |
fix(tray): stable tray icon id (no KDE duplicate pile-up on toggle) (#991)
* fix(tray): stable tray icon id so KDE toggle stops piling up duplicates TrayIconBuilder::new() assigns a fresh id on every rebuild. On KDE (StatusNotifierItem) each new id registers a new item and the stale ones linger in the hidden-icons list, so toggling the tray icon off/on stacked up duplicate entries. Build with a constant id so every rebuild reuses the same item. * docs: changelog for tray icon duplicate fix (#991) |
||
|
|
ec2bee1400 |
fix(audio): cap pipewire-alsa client latency on Linux (#862) (#990)
* fix(audio): cap pipewire-alsa client latency on Linux (#862) On some Linux setups the pipewire-alsa bridge negotiates a multi-second output buffer, so play/pause/seek/volume only take effect once it drains (~10-20 s). cpal's buffer-size clamp is ignored by those bridges. Set PIPEWIRE_LATENCY=256/48000 before the audio stream opens to cap the client node latency — the reporter confirmed this makes the controls instant. Linux-only, no-op without PipeWire, and a user-set value is left untouched. * docs: changelog for pipewire latency fix (#990) |
||
|
|
19fdba006a |
fix(search): local index rejects FTS syntax in live search queries (#983)
* fix(search): reject FTS syntax chars in local index live search queries FTS5 treats `=` and similar characters as query syntax, so tokens like 1=2 produced unrelated prefix hits. Skip FTS for unsafe tokens instead. * docs: CHANGELOG for local index FTS syntax query fix (PR #983) * fix(search): reject syntax junk in server search3 and live search race Share FTS-safe token guard with search3; skip network invoke for ** and similar queries; show empty dropdown without misleading source badge. * fix(search): allow censorship stars in queries, reject wildcard-only tokens Block ** and **** but keep ***Flawless-style title searches working in local FTS and search3. |
||
|
|
b0f35eabc9 |
fix: usable layout when the window is small (#981)
* fix(layout): collapse browse toolbar to icons on compact width On a narrow window the labelled filter buttons wrapped into several rows and, being a fixed-height sticky header, starved the album grid below them down to a clipped strip. Wrap each toolbar label in a hideable span and drop to icon-only in compact (mobile) mode; icons keep their tooltip and aria-label so the action stays discoverable. * fix(window): raise minimum window size to 520x640 The old 360x480 floor let the window shrink far past the point the layout holds together. Floor it at a laptop-friendly size where the compact layout (icon toolbar + scrollable lists) still works. * fix(player): scale mobile cover to fit short windows The cover width was viewport-derived with no height bound, so on a short window the square grew past its slot and overlapped the title. Bind it to the shrinkable wrap via max-height and keep it square with auto sizing. * docs: changelog for small-window layout fixes (#981) |
||
|
|
47e16ebfef |
fix: RC3 queue link underline, genre album artist split, Artists "Other" bucket (#977)
* fix: RC3 queue link underline, genre album artist split, Artists "Other" bucket Three zunoz reports, one change: - Queue now-playing card: the artist and album links now underline on hover (not just recolour), matching clickable names everywhere else. The album link sits on .queue-current-sub itself; artist links are nested .is-link spans — both selectors covered. - Genre album cards split multi-artist credits into individual links like the rest of the app. Root cause was the local-index genre query hardcoding NULL for the album raw_json column, so OpenSubsonic artists[] never reached the card and it fell back to the flat "A • B" single link. Select a.raw_json instead (parity with the All Albums / advanced-search album queries). - Artists page alphabet index: # is now digits-only, and a new "Other" bucket collects accented Latin (Æ/Ø/Å…) and non-Latin scripts (CJK, Cyrillic, …) that previously fell into the # catch-all. Adds artistBucketKey/compareBuckets helpers (unit-tested), an OTHER bucket sorted last, and the artists.other label across 9 locales. * docs(changelog): queue link hover, genre card artist split, Artists Other bucket (#977) |
||
|
|
47832632fd |
fix(cover): follow connect-URL flips in library cover backfill (#952)
* fix(cover): follow connect-URL flips in library cover backfill The native cover backfill was configured once with a snapshot of the runtime connect URL. When a laptop moved off the LAN, the smart endpoint switch flipped the sticky connect URL to the public address (playback/UI covers rebuild it per request and followed it), but backfill kept fetching from the now-unreachable local address — flooding the log with "error sending request" failures. Make the connect cache observable (notify on effective flips) and have the backfill hook reconfigure when the resolved URL changes, forcing a pass so the .fetch-failed backoff from the stale address is cleared and those covers retry on the reachable endpoint. * docs(changelog): note cover backfill endpoint-switch fix (PR #952) * fix(cover): abort stale backfill pass + rerun on connect-URL flip Frontend reconfigure alone didn't fully cover the boot case: at startup the first backfill pass starts on the primary (LAN) URL before the reachability probe resolves, so when the probe flips to public the forced rerun was dropped by the pass_running guard and the slow LAN pass (every cover timing out) ran to completion with nothing re-running on the reachable address. set_session now bumps a session generation; the running pass checks it on every focus gate and abandons promptly when the URL flips (same server_index_key, new rest_base_url). try_schedule_full_pass records a rerun when a pass is in flight and drains it once the abandoned pass returns, so a fresh forced pass runs on the new address. * fix(cover-backfill): resolve connect URL per fetch instead of baking it into the queue The backfill worklist no longer carries a URL. Each cover fetch reads the current reachable address live from a single worker cell, so a LAN↔public flip is honoured even by the pass already in flight — its remaining covers download against the new endpoint without aborting/rebuilding the worklist. - Drop rest_base_url from CoverBackfillSession; add live base_url cell read in ensure_one. Remove the session-generation abort machinery (no longer needed). - New lightweight library_cover_backfill_set_base_url command pushes the URL on every connect-cache flip; a real change clears the stale .fetch-failed backoff and runs a forced pass so covers that timed out on the old address retry. - Split useLibraryCoverBackfill into a configure effect (server/creds/strategy) and a flip effect that only pushes the URL. - Keep a single rerun_pending flag for the boot case (flip mid-pass), since the finished pass re-arms the idle gate. |
||
|
|
2224ddbe78 |
feat(perf): add on-demand (ui) throughput to cover pipeline cpm (#947)
* feat(perf): add on-demand (ui) throughput to cover pipeline cpm Cover cpm previously measured only the native backfill (lib) via the cover:library-progress done delta. Add a parallel UI series: every completed on-demand Rust cover ensure (grid / now-playing) records a timestamp, surfaced as a covers-per-minute rate. Both are exposed in the live cover diag, shown as separate Backfill (lib) / On-demand (ui) cards in the Monitor tab (each pinnable to the overlay) and as lib/ui rows in the Cover pipeline overlay block. * docs(changelog): note cover on-demand (ui) throughput (PR #947) Add CHANGELOG entry and credits line for the UI cover cpm metric. * fix(perf): source on-demand (ui) cpm from backend produced-cover count The JS ensure-queue counter never tracked: produced covers return hit:true (only misses/errors are hit:false), and ensure-queue dedup/HMR made client counting unreliable. Count on-demand covers natively in ensure_inner on the produce path (non-bulk, past the cache-hit gate), expose a cumulative uiEnsuredTotal in the pipeline stats, and derive the per-minute rate on the frontend from polled deltas — mirroring the lib backfill series. * perf(cover): measure cpm over trailing 5s instead of full minute A 60s rolling average added too much inertia, flattening real bursts and stalls in both the lib backfill and on-demand (ui) cover throughput. Compute the rate from the trailing 5s of samples (still extrapolated to per-minute), so the figure reacts promptly and decays to 0 within the window when idle. |
||
|
|
975bb6d9af |
feat(perf): live runtime logs tab in Performance Probe (#946)
* feat(perf): live runtime logs tab in Performance Probe Add a Logs tab that streams the backend runtime log ring buffer in-app, so the stdout/stderr console (unreachable on Windows without exporting) can be read live. The buffer now tracks a monotonic seq; a new tail_runtime_logs command returns lines incrementally and get_logging_mode reports the current depth. The tab has a depth switch (off/normal/debug) mirroring app settings, a line cap (500-5000), pause/clear, auto-follow, and an ordered comma-separated word filter where a plain word includes and a -word excludes, applied left to right as layers (sequence matters). * docs(changelog): note Performance Probe logs tab (PR #946) Add CHANGELOG entry and credits line for the live runtime logs tab. * fix(perf): pin log view position when scrolled up Auto-scroll keeps the logs tab at the tail, but once the user scrolls up the view now stays put — the previously-topmost line is re-pinned each tick while the log keeps appending below for further scrolling. History under the viewport is no longer trimmed while scrolled up (kept up to the ring-buffer ceiling); the cap is re-applied when following resumes. Buffer overflow is shown in the status line instead of an injected marker row. * fix(perf): scope logs tab to its own internal scroll The whole probe body scrolled (controls + filter + log) because the log container sized via height:100%, which WebKitGTK does not resolve against the flex body. Make the body a flex column with hidden overflow on the Logs tab and let the log view flex-fill, so depth/keep/pause/clear and the filter stay fixed while only the log lines scroll. |
||
|
|
42aec6720c |
fix(cover): stop per-song over-fetch + log failed cover downloads (#944)
* fix(cover): stop per-song cover over-fetch (album/mf-* explosion) album_has_distinct_disc_covers returned true as soon as two tracks on the same disc had different cover ids. On Navidrome every song has its own mf-<id> coverArt, so almost every album was flagged "distinct disc covers" and backfill warmed one cover per track (~520k for ~170k tracks), filling album/ with mf-* dirs instead of ~one cover per album. Treat a release as having distinct disc covers only when each disc has a single consistent cover that differs across discs (genuine box set); per-song ids now collapse to one cover per album. Mirror the same fix in the TS albumHasDistinctDiscCovers used by on-demand warming. Adds regression tests on both sides. * docs(changelog): record per-song cover over-fetch fix (PR #944) Give the album/mf-* over-fetch fix its own [1.47.0] Fixed entry and a settingsCredits line under PR #944. * feat(cover): log failed cover downloads with album/artist name A non-200 (or network-failed) getCoverArt download was swallowed silently. Now the failure is logged with the resolved album/artist name and the server error, so a server refusing covers under backfill load (5xx/429/timeouts) is visible. - cover_resolve: describe_cover_entity() resolves a human label from the local index (album "Name" — Artist / artist "Name"), best-effort with id fallback. - cover_cache: log_cover_fetch_failure() in ensure_inner logs on the download error path; threads optional library_server_id through CoverCacheEnsureArgs so the name lookup happens only on failure. Backfill logs at normal level, on-demand misses at debug level. |
||
|
|
a63ba3c9cb |
fix(cover-backfill): kill idle CPU spin and offline-cache menu re-walks (#943)
* fix(cover-backfill): snapshot-diff worklist and live-tunable parallelism Aggressive cover backfill pegged one tokio worker at ~100% on large, fully-synced libraries while the download queues stayed empty. - Take two snapshots once per pass — the DB catalog (single GROUP BY) and the on-disk cover bucket (one directory walk) — and download the set-difference. No per-row `stat` syscalls and no re-scan loop; the empty cache case (heavy backfill) costs zero per-item disk hits. - Replace the front-loaded enumeration with a producer/consumer pipeline: the producer streams the catalog in chunks and feeds misses into a bounded channel; a fixed consumer pool keeps the download/encode pools saturated. - Make cover backfill parallelism runtime-tunable from the Performance Probe (threads slider + "Run full pass now"); HTTP download and CPU encode semaphores resize live. Not surfaced in app settings. - Add a "nothing changed" idle gate (catalog signature) so a settled pass is not re-run on every library:sync-idle, mirroring the analysis worker. - Cancel promptly on switch to lazy: consumers bail on enabled/focus change and the producer feeds via try_send so a full channel cannot deadlock. - Drop the per-item recursive disk walk from the ensure hot path. * fix(cover-backfill): cheap idle gate, settle on 404s, transient retries Follow-up to the snapshot-diff backfill: stop the periodic CPU spikes and the 89%-plateau wake storm on libraries whose covers can never reach 100%. - Idle gate is now disk-free: compare only the catalog COUNT(DISTINCT) instead of walking ~all cover dirs on every sync-idle. "Did the server change?" never touches the filesystem. Clear-cache commands re-arm the gate (rearm_idle_gate) since a clear leaves the catalog total unchanged, and the settings UI wakes the active server after a clear. - Settle the gate on any completed pass regardless of pending: remaining items are unfetchable-for-now (404), so the wake/sync-idle storm stops once the fetchable set is exhausted. - Stop auto-clearing .fetch-failed markers every pass (it defeated the 30-min backoff and re-attempted 404s forever). The manual "Run full pass now" sends force=true to clear them and retry; wake/sync-idle/configure stay opportunistic. - Rate-limit sync-idle passes (60s cooldown) as defence against chatty syncs. - Retry cover downloads up to 3x with backoff on transient failures (5xx / 429 / network), but never on a real 4xx so missing covers don't hammer the server. * fix(cover-cache): stop re-walking cover dirs from offline & cache menu The settings cover-cache section polled disk usage + progress every 15s for every server, each call doing a full recursive walk of the per-server cover directory. On a fully populated cache this caused periodic CPU spikes whenever that menu was open. - mod.rs: add a 10s TTL memo around the per-server cover dir walk (cached_dir_usage_for_server), shared by cover_cache_stats_server and library_cover_progress; invalidate on clear (per-server and clear-all). - CoverCacheStrategySection: recompute on entry only; rely on the cover:library-progress and cover:cache-cleared events for live updates; drop the per-cover cover:tier-ready refresh storm; turn the 15s loop into a 5-minute safety net. * fix(cover-backfill): keep emitting progress during the whole pass The producer finishes enumerating the worklist long before the consumer pool finishes downloading it, so progress was only emitted while feeding the channel — the "offline & cache" menu and overlay then froze through the entire drain phase. Replace the per-chunk emit with a 3s progress ticker that runs for the lifetime of the pass and is aborted once the consumers drain (final accurate emit still happens at settle). * docs(changelog): record cover-backfill idle-CPU fix (PR #943) Add [1.47.0] Fixed + Changed entries and a settingsCredits line for the cover-backfill idle CPU / offline & cache menu work. |
||
|
|
08b6aeeb17 |
fix(perf): reduce idle Rust CPU and stabilize Performance Probe overlay (#939)
* fix(perf): skip Performance Probe CPU snapshot poll on Windows Windows has no Rust CPU/RSS sampler, but the probe still invoked performance_cpu_snapshot every 2s when the modal or overlay pins were active. Skip the IPC on unsupported platforms and only poll JS-side metrics; do not start overlay polling for CPU/memory pins alone. * fix(analysis): park backfill coordinator until Advanced is configured #881 started run_coordinator_forever at app init with a 2s sleep even when disabled, waking tokio on every platform for no work. Park on Notify instead; wake on configure (enable/disable) and library sync-idle. Long sleeps use select with wake so sync-idle can interrupt COMPLETED_RECHECK waits. Investigation branch — not for merge until periodic CPU root cause is confirmed. * fix(perf): stop probe overlay flicker on live poll updates Publish CPU samples only after a valid jiffies baseline, skip no-op snapshot emits, and sync sparkline history atomically in the store. Overlay uses wall-clock sparkline time and auto-scales low CPU values. * fix(perf): cut idle Rust CPU from probe scan, cover prefetch, and storage poll Move performance_cpu_snapshot /proc work to spawn_blocking so tokio workers are not charged with probe sampling. Stop lazy cover strategy from running route prefetch disk stats every 1.5s, and slow hot-cache size refresh on Settings → Storage to 15s. * fix(perf): stabilize probe sparkline clock between live poll ticks Track sampleAt separately from updatedAt so CPU rate history and overlay sparklines only advance on real % changes, not FPS re-renders or RSS-only poll ticks. Hold CPU sparkline Y scale with a peak ref to avoid scale jumps. * fix(cover): restore lazy route prefetch without idle disk stats poll Re-enable lazy cover registry warm-up so cached WebP paths reach diskSrcCache before cells mount. Skip cover_cache_stats on every 1.5s tick — drain batches via ensure only, poll full disk usage every 30s when the registry is idle. * docs: CHANGELOG and credits for PR #939 idle CPU perf fix * fix(cover): peek before route prefetch ensure to match main responsiveness Route prefetch moved batch drain ahead of cover_cache_stats for idle CPU, which removed the accidental throttle and flooded ensure invoke slots. Use warmCoverDiskSrcBatch first (cached hits skip ensure), ensure misses only, and yield while high-priority viewport work is queued. |
||
|
|
4ac373a65b |
feat(search): scoped live search on browse pages (#938)
* feat(artists): scoped live search badge replaces page filter Move Artists browse text search into the header Live Search with a page scope badge (Users icon), field-local undo, and double-click/backspace to clear scope. Block the live-search dropdown while scoped so results only filter the Artists grid; mobile overlay follows the same rules. * fix(artists): plain grid for scoped search fixes broken card layout Route the browse grid through VirtualCardGrid, switch to non-virtual CSS grid when live search filters the catalog, reset scroll on filter changes, and skip content-visibility on plain tiles to avoid blank/black cards. * fix(search): scope badge double-Backspace and single clear control Require two Backspaces on an empty scoped field after prior text input; one Backspace still clears the badge when the field was never filled. Move live-search clear/advanced controls inside the field pill, drop the extra outer clear button, and use type=text to avoid native search clears. * fix(search): drop duplicate outer live-search clear button Keep the native in-field clear on type=search and the original pill layout; remove only the extra × control outside the search border. Reset dropdown state when the query is cleared via the native control. * refactor(search): generic scoped browse query helper, drop dead code Rename artistsBrowseSearchQuery to scopedBrowseSearchQuery with an expectedScope argument; wire Artists via useScopedBrowseSearchQuery. Remove unused liveSearchScoped dropdown helper (scoped mode blocks it). * feat(search): ghost scope badge and single-click badge remove After clearing the artists scope on /artists, show a faded ghost chip to restore page-only search while keeping the global search placeholder. Active badge removes on one click; tooltips and styles updated. * feat(search): scoped live search for All Albums and New Releases Wire albums and newReleases scope badges with debounced album title search (local index title-only FTS + filtered search3). Plain grid, scroll reset, and session query restore on album grid browse pages. * fix(browse): preserve scroll restore after album/artist detail back Only reset in-page scroll when filter resetKey changes, not when isScrollRestorePending clears after session restore. * feat(search): scoped live search for Tracks browse Wire /tracks to header live search with wide title/artist/album FTS, hide hero and discovery rails while search is active, and remove the inline search field from the browse list. * fix(search): clear header query when leaving scoped browse pages Prevent global live search from firing on album/detail routes after a scoped browse query; browse session stashes still restore on back. * fix(tracks): restore scroll after back from detail during scoped search Hold stashed song results across fetchSongPage churn, defer leave-stash teardown past AppShell scroll reset, restore tracks scroll after the list is ready, and save scroll snapshot when opening artist from song context menu. * fix(tracks): hide discovery headings during scoped search Hide the page subtitle and "Browse all tracks" section title when tracks search is active, matching hero/rails chrome behavior. * feat(search): scoped live search for Composers browse Wire /composers to header live search with composers scope badge, session stash, scroll restore, and plain grid/list during text filter. Remove the in-page filter input; add i18n and navigation helpers. * docs: CHANGELOG and credits for scoped browse live search (PR #938) |
||
|
|
ddf10ee01d |
feat(genres): local index genre browse with Subsonic fallback (#937)
* feat(genres): genre detail browse via local index with aligned counts Move genre detail albums/play/shuffle onto the local library index with Albums-style in-page scroll, session restore, and genre-scoped stash. Unify genre album totals between the cloud and detail pages via library_get_genre_album_counts, and fix grouped browse totals to count distinct albums rather than matching tracks. * perf(genres): local genre browse with scoped counts cache Add dedicated Rust genre album pagination and indexes, slice-mode grid loading, library-filter-aware counts, and a long-lived in-memory catalog cache invalidated on sync so genre pages avoid repeated full-library SQL. * fix(genres): restore scroll after album back; play hold-to-shuffle Pin restore display count in refs so clearing the return stash no longer reloads the genre grid mid-restore. Load the first SQL page only (60 rows), use long-press on Play for shuffle, and add genre play tooltips. * fix(genres): fall back to Subsonic byGenre when local index unavailable Genre detail album grid now matches All Albums: try library_list_albums_by_genre first, then getAlbumsByGenre when the index is off, not ready, or errors. * docs: add CHANGELOG and credits for PR #937 |
||
|
|
d3e5a6b704 |
feat: library browse navigation — restore filters, scroll, and search on back (#936)
* feat(albums): restore scroll position when returning from album detail Save in-page scroll and grid depth when opening an album from All Albums, then on browser back restore filters, preload enough rows, and apply scroll before revealing the grid to avoid a visible jump from the top. * feat(albums): smart back navigation and restore browse session on return Remember the originating route when opening album detail, restore All Albums filters/scroll on back (including explicit returnTo navigation), hide the grid until scroll is applied, and fix filters being cleared after albumBrowseRestore state is stripped from the location. * feat(search): restore Advanced Search session when returning from album Stash filters and results when leaving /search/advanced for album detail, then restore them on back navigation (POP or returnTo with advancedSearchRestore). * feat(search): restore Advanced Search album row scroll on return from album Save horizontal scrollLeft when opening an album from Advanced Search and reapply it via AlbumRow on return; keep main viewport at top. Add snapshot helpers and session stash fields; extend AlbumRow with restoreScrollLeft. * feat(search): restore Advanced Search session scroll and artist return path Save filters, main scroll, and album-row scroll when leaving to album or artist; restore without flash via hidden-until-ready. Add useNavigateToArtist, restoreMainViewportScroll helper, and AppShell scroll reset only on pathname change. * feat(search): speed up Advanced Search back restore and year-only queries Reveal the page right after sync scroll instead of blocking on full viewport and album-row restore. Retry local index without the ready gate during sync; use open-ended byYear params on network fallback, matching All Albums browse. * feat(search): restore Advanced Search artist row scroll on back Save leave snapshot when opening artist from ArtistCardLocal, persist artistRowScrollLeft in session stash, and keep row restore targets in refs so horizontal scroll survives finishLeaveRestoreUi like vertical scrollTop. * feat(nav): route mouse back on album/artist detail like UI back Trap history popstate when returnTo is set and call navigateAlbumDetailBack so browser/mouse back restores browse/search session the same way as the header button. * feat(artists): restore browse filters and scroll on back from artist detail Persist Artists page filters, view settings, and vertical scroll when opening an artist and returning via UI or mouse back, matching All Albums behavior. * feat(search): unify quick and advanced search; fix LiveSearch dismiss on Enter Serve /search and /search/advanced from one page with shared session restore and scroll snapshot. Reset live search overlay state when navigating to full search so the dropdown does not linger or reopen. * feat(tracks): unify with search session and restore scroll on back Route /tracks through AdvancedSearch with shared leave snapshot, song browse stash, and main-viewport scroll restore when returning from album or artist detail. Wait for hero/rails layout before applying scroll. * refactor(search): rename AdvancedSearch page to SearchBrowsePage The shared route shell serves /search, /search/advanced, and /tracks; rename the page component and refresh stale file references in comments. * feat(albums): restore New Releases and Random Albums on back from detail Unify album grid leave-restore with surface-scoped session stash, live scroll snapshot sync, and in-page scroll for Random Albums. Keep the same random batch when returning from album detail; Refresh fetches anew and scrolls up. * docs: add CHANGELOG and credits for PR #936 |
||
|
|
fc7964fb07 |
fix(perf): use mach2 for macOS host CPU tick Mach ports (#932)
Replace deprecated libc mach_host_self/mach_task_self with mach2 APIs while keeping host_processor_info on libc (no mach2 binding). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.com> |
||
|
|
ea63b35396 |
fix(perf): use Mach API for macOS host CPU ticks (#931)
* fix(perf): use Mach host_processor_info for macOS CPU ticks
KERN_CP_TIME and CPUSTATES are not exposed by libc on Darwin; switch
read_host_total_cpu_ticks to host_processor_info so aarch64-apple-darwin CI builds succeed.
* docs: CHANGELOG and credits for macOS perf CI fix (PR #931)
* Revert "docs: CHANGELOG and credits for macOS perf CI fix (PR #931)"
This reverts commit
|
||
|
|
a0980379fa |
fix(deps): bump tar to 0.4.46 (GHSA-3pv8-6f4r-ffg2) (#923)
* fix(deps): bump tar to 0.4.46 (GHSA-3pv8-6f4r-ffg2) Transitive dependency via tauri-plugin-updater; closes Dependabot alert #16. * docs(release): CHANGELOG and credits for tar security bump (PR #923) * revert: drop CHANGELOG and credits for tar security bump |
||
|
|
5377f3b737 |
fix(deps): bump zip to 4.6.1 with backup API fix (#920)
Complete the Dependabot #910 bump: update Cargo.toml and migrate backup archive writes to SimpleFileOptions (zip 4.x API). Fixes lockfile drift where cargo downgraded zip back to 0.6.6 on every dev build. |
||
|
|
a7d533d580 |
chore(library): squash pre-RC migrations into single 001_initial baseline (#919)
* chore(library): squash pre-RC migrations into single 001_initial baseline Library SQLite never shipped in a release, so fold migrations 002–008 into 001_initial.sql and drop the dev-only mood-facts purge (009). Sets LIBRARY_DB_SCHEMA_VERSION to 1 for the RC baseline; analysis migrations unchanged. * docs: note library migration squash in CHANGELOG and credits (PR #919) * fix(library): keep migration SQL files on disk after RC baseline squash Restore 002–009 as historical dev migration scripts. Runner still ships only 001 for fresh installs; existing DBs with applied versions are unchanged. Drop credits/CHANGELOG note for this small internal change. |
||
|
|
f32fe514f1 |
chore(deps): bump zip from 0.6.6 to 4.6.1 in /src-tauri (#910)
Bumps [zip](https://github.com/zip-rs/zip2) from 0.6.6 to 4.6.1. - [Release notes](https://github.com/zip-rs/zip2/releases) - [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md) - [Commits](https://github.com/zip-rs/zip2/commits/v4.6.1) --- updated-dependencies: - dependency-name: zip dependency-version: 4.6.1 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
061b97cb68 |
chore(deps): bump serde_json from 1.0.149 to 1.0.150 in /src-tauri (#914)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.149 to 1.0.150. - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150) --- updated-dependencies: - dependency-name: serde_json dependency-version: 1.0.150 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
f7c32e6954 |
chore(deps): bump sysinfo from 0.38.4 to 0.39.3 in /src-tauri (#912)
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.38.4 to 0.39.3. - [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/main/CHANGELOG.md) - [Commits](https://github.com/GuillaumeGomez/sysinfo/compare/v0.38.4...v0.39.3) --- updated-dependencies: - dependency-name: sysinfo dependency-version: 0.39.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
794ddf966e |
chore(deps): bump zbus from 5.15.0 to 5.16.0 in /src-tauri (#908)
Bumps [zbus](https://github.com/z-galaxy/zbus) from 5.15.0 to 5.16.0. - [Release notes](https://github.com/z-galaxy/zbus/releases) - [Changelog](https://github.com/z-galaxy/zbus/blob/main/release-plz.toml) - [Commits](https://github.com/z-galaxy/zbus/compare/zbus-5.15.0...zbus-5.16.0) --- updated-dependencies: - dependency-name: zbus dependency-version: 5.16.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
90f86d3f87 |
chore(deps): bump rusqlite to 0.40 workspace-wide (#916)
* chore(deps): bump rusqlite to 0.40 workspace-wide Align root src-tauri and workspace crates on rusqlite 0.40 so libsqlite3-sys resolves to a single version (fixes Dependabot #913 links conflict). * chore(nix): refresh flake.lock for rustc 1.95 dev shell libsqlite3-sys 0.38 (rusqlite 0.40) needs cfg_select; nixpkgs pin was on rustc 1.94. Bump workspace MSRV to 1.95 to match. |
||
|
|
949c4d8921 |
chore(deps): bump tauri from 2.11.1 to 2.11.2 in /src-tauri (#899)
Bumps [tauri](https://github.com/tauri-apps/tauri) from 2.11.1 to 2.11.2. - [Release notes](https://github.com/tauri-apps/tauri/releases) - [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.11.1...tauri-v2.11.2) --- updated-dependencies: - dependency-name: tauri dependency-version: 2.11.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
26e9e4e6d8 |
chore(deps): bump tauri-plugin-global-shortcut in /src-tauri (#901)
Bumps [tauri-plugin-global-shortcut](https://github.com/tauri-apps/plugins-workspace) from 2.3.1 to 2.3.2. - [Release notes](https://github.com/tauri-apps/plugins-workspace/releases) - [Commits](https://github.com/tauri-apps/plugins-workspace/compare/os-v2.3.1...os-v2.3.2) --- updated-dependencies: - dependency-name: tauri-plugin-global-shortcut dependency-version: 2.3.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
cb1c255645 |
chore(deps): bump tokio from 1.52.2 to 1.52.3 in /src-tauri (#902)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.52.2 to 1.52.3. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.52.2...tokio-1.52.3) --- updated-dependencies: - dependency-name: tokio dependency-version: 1.52.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |