Commit Graph

358 Commits

Author SHA1 Message Date
Frank Stellmacher dc2068303d chore(deps): bump Tauri 2.11.0 → 2.11.1 (GHSA-7gmj-67g7-phm9) (#509)
* chore(deps): bump Tauri 2.11.0 → 2.11.1 (GHSA-7gmj-67g7-phm9)

Patches the IPC origin-confusion advisory: Tauri 2.11.0 and below could
let a remote-origin page loaded inside the webview invoke local-only
IPC commands. Severity medium. Psysonic exposes a number of file-system
and credential-bearing IPC commands (download_zip, nd_get_song_path,
audio_*), so closing the gate is worth the lockfile-only bump.

Cargo.toml is unlocked at "2", so this is purely a Cargo.lock refresh
via `cargo update -p tauri --precise 2.11.1`. Full Tauri family bumped
together (tauri / tauri-build / tauri-codegen / tauri-macros /
tauri-runtime / tauri-runtime-wry / tauri-utils — all matching patch
releases). wry, tao and other transitive deps unchanged. phf 0.11.3
fell out of the dependency graph because the new tauri-utils no longer
pulls it.

Tested locally (Windows): tray hide/restore + single-instance second
launch + mini-player toggle + window-state persistence — all working.

* docs: changelog entry for PR #509

Logs the Tauri 2.11.1 GHSA security bump in v1.46.0 "## Fixed".
2026-05-07 22:43:15 +02:00
Frank Stellmacher a41e3a624a feat(song-info): show absolute file path on Navidrome via native API (#504)
* feat(song-info): show absolute file path on Navidrome via native API

Subsonic's `getSong.view` returns at most a relative path (or none on
Navidrome), so the Path row in the Song Info modal stayed empty for
most users. Feishin and the Navidrome web client surface the full
server-side path by hitting Navidrome's native `/api/song/{id}` instead.

Added an `nd_get_song_path` Tauri command that logs in to the native
API with the active server's credentials, fetches the song, and returns
the `path` string. Wired it into `SongInfoModal`: the Subsonic
`getSong` call still drives the rest of the dialog, and the native call
runs in parallel only when the active server's identity is
"navidrome". When it returns a path, that absolute path replaces the
relative Subsonic value; native-API failures are silent and the modal
falls back to whatever Subsonic provided.

No token cache yet — the modal is opened occasionally enough that one
fresh login per call is fine.

Closes discussion #479.

* docs: changelog entry for PR #504

Logs the Navidrome native-API absolute file path support for the
Song Info dialog in v1.46.0 "## Added".

* docs: settings contributor entry for PR #504, drop competitor mention

Adds the song-info absolute path bullet to Psychotoxical's contributors
list in Settings, and rewords the existing CHANGELOG entry so neither
text references competing clients.
2026-05-07 20:22:55 +02:00
Frank Stellmacher d7ff1d3113 fix(preview): keep preview sink volume in sync with player slider (#498) (#502)
* fix(preview): keep preview sink volume in sync with player slider (#498)

The Rust preview sink had its volume set once at audio_preview_play and
then never updated. audio_set_volume only ramps the main sink, so slider
movements during a preview had zero effect on the preview level. With
the default loudness normalization (-4.5 dB pre-analysis attenuation)
applied at start, even a 100% slider gives 1.0 × 0.596 × MASTER_HEADROOM
≈ 53% — matching the user-visible "fixed at around 50%" symptom.

- Add audio_preview_set_volume Rust command that updates the preview
  sink if one is active (clamp + master headroom mirror the path used
  in audio_preview_play).
- Extract the preview-volume calculation in previewStore into
  computePreviewVolume() so startPreview and the new sync path share
  one formula (slider value, plus the LUFS pre-analysis attenuation
  the engine already applies to the main sink).
- Subscribe to playerStore at module level: when volume changes and a
  preview is active, push the recomputed value to Rust. Auth /
  normalization tweaks during preview are intentionally not synced —
  preview is short and that case is rare.

Reported by netherguy4.

* docs: changelog entry for PR #502

Logs the preview-volume-slider sync fix in v1.46.0 "## Fixed".
2026-05-07 19:10:18 +02:00
Frank Stellmacher 3cabb64dbc fix(tray): resume rendering on second-launch restore (#497) (#501)
* fix(tray): resume rendering on second-launch restore (#497)

The single-instance plugin callback that handles a second launch (e.g.
via desktop / start-menu shortcut while the main window is hidden in
the tray) was missing the RESUME_RENDERING_JS injection that the tray-
icon restore path already does.

When the main window is closed to the tray, PAUSE_RENDERING_JS sets
window.__psyHidden = true, marks <html data-psy-native-hidden="true">,
and zeroes --psy-anim-speed. A global CSS rule then pauses every
animation under <html>. Page wrappers across the app use
.animate-fade-in (animation: fadeIn ... both) which starts at
opacity: 0 — so when a route mounts after navigation the new wrapper
stays frozen at opacity: 0 and the page looks blank.

Restoring via the tray icon worked because that path injects
RESUME_RENDERING_JS before show(); only the second-launch path was
missing it. Mirror the same eval here so both restore paths are
consistent.

Reported by netherguy4.

* docs: changelog entry for PR #501

Logs the tray second-launch restore-rendering fix in v1.46.0 "## Fixed".
2026-05-07 18:59:55 +02:00
Frank Stellmacher 59744601d4 feat(composer): Browse by Composer page (issue #465) (#487)
* feat(composer): Browse by Composer page (issue #465)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Two minor follow-ups from the latest review.

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

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

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

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

* docs: CHANGELOG + Contributors entry for composer browsing (PR #487)
2026-05-07 00:36:09 +02:00
cucadmuh b084e96c1f fix: prune stale analysis queues and cap loudness backfill window (#480)
* fix(analysis): prune stale backfill jobs and limit prefetch window

Drop pending backfill and cpu-seed jobs that are no longer in the active playback queue, and add debug counters for pruned work. Limit loudness backfill scheduling to the current track plus the next five tracks to prevent runaway queue growth in dev sessions.

* chore(analysis): remove unused loudness prefetch parameter

Drop the now-unused incoming-tracks parameter from the loudness prefetch helper and update internal call sites to match the current queue-window scheduling logic.

* docs(changelog): document analysis queue control fix (#480)

Add a short 1.46.0 Fixed entry describing stale backfill pruning, the current+5 loudness backfill window cap, and debug prune counters for diagnostics.

* docs(contributors): add cucadmuh entry for PR #480

Logs the analysis-queue prune + loudness backfill window cap in the
Settings → System → Contributors list.
2026-05-06 16:42:48 +03:00
Frank Stellmacher 8692e50603 feat(settings): Open Source Licenses section in System tab (#477)
* feat(licenses): tooling and initial data generation

Adds the maintainer-only generator that produces src/data/licenses.json:
- src-tauri/about.toml + about.hbs: cargo-about config + handlebars template
  for the Rust-side license enumeration
- scripts/generate-licenses.mjs: orchestrator that runs cargo-about and
  license-checker-rseidelsohn (via npx, no devDep), merges the outputs into
  a single per-crate JSON with full license texts, and writes the result to
  src/data/licenses.json

The script is invoked directly with `node scripts/generate-licenses.mjs` —
no npm script wrapper on purpose, since adding one to package.json would
trigger the nix-npm-deps-hash-sync workflow on every push.

Initial generation covers 575 cargo crates + 71 npm packages (646 entries
total, all with full license text bundled, ~1.4 MB JSON).

* feat(licenses): Settings panel UI

Adds a new Open Source Licenses section under Settings → System,
sitting below Contributors. Components:

- LicensesPanel.tsx: search input, curated highlight block of ~10 key
  dependencies (Tauri, React, rodio, symphonia, etc.), TanStack-Virtual
  list of all 600+ entries
- LicenseTextModal.tsx: full-screen-ish modal showing the bundled license
  text plus name/version/license-id badges + repository link
- licensesData.ts: lazy dynamic-import loader (Vite emits the JSON as a
  separate chunk, so the heavy ~1.4 MB payload is only loaded when the
  user actually opens the panel — no runtime fetch, the data is fixed
  into the build artifact)

The panel registers itself in the Settings in-page search index under
the System tab.

* feat(licenses): i18n in 8 locales

Adds the `licenses` namespace (title, intro, highlights, search
placeholder, no-results, loading / load error, no-license-text,
view-source, total line, generated-at) across en, de, fr, nl, zh, nb,
ru, es. License names themselves (MIT, Apache-2.0, GPL-3.0, …) stay
universal and are rendered as-is.

* docs(release): document licenses regeneration step

Adds a Step A.3 to the release SOP describing the maintainer-only
`node scripts/generate-licenses.mjs` workflow, the cargo-about
prerequisite, and explicitly notes why no npm script wrapper exists
(would trigger the nix-npm-deps-hash-sync workflow).
2026-05-06 14:17:13 +02:00
cucadmuh d48ea819c1 fix: stabilize preview seekbar, post-sleep audio recovery, and card hover behavior (#476)
* fix(player): freeze main seekbar during track preview

Preview pauses the main sink in Rust while isPlaying stays true in the
store, so WaveformSeek's interpolation rAF must not advance progress.

* fix(audio): recover output after sleep and stalled streams

Add platform-specific post-sleep recovery hooks for Windows and Linux, and add a watchdog that reopens the output stream when playback is active but sample progress stalls, so audio can recover without restarting the app.

* fix(ui): remove card hover lift and smooth artwork zoom

Remove vertical hover translation from album and artist cards, and move image fade transition out of inline styles so cover zoom uses CSS timing consistently.

* fix(player): prevent seekbar jump after preview ends

Reset interpolation anchor timing when preview freeze state changes so the main seekbar does not momentarily jump forward before resyncing.

* fix(audio): reduce false watchdog recoveries and add diagnostics

Arm stalled-output recovery only after long poll gaps that suggest sleep/resume, and add detailed watcher logs for arm/clear/trigger paths to diagnose unintended stream reopens.

* chore(ui): drop card GPU hints and clarify macOS sleep scope

Remove translateZ and will-change hints from album and artist cover images to avoid per-card compositing overhead on software-composited Linux paths, and document why post-sleep recovery hooks currently target only Windows and Linux.

* docs(audio): document intentional Win32 callback pointer lifetime

Add inline rationale for the two Box::into_raw pointers in Windows suspend/resume registration so future maintenance does not treat the process-lifetime pointers as accidental leaks.

* docs(changelog): summarize playback stability updates for PR #476

Add a high-level changelog entry for preview seekbar fixes, sleep/wake audio recovery hooks and watchdog diagnostics, and card-hover stability adjustments from PR #476.

* docs(contributors): add cucadmuh entry for PR #476

Logs the post-sleep audio recovery, preview-seekbar fixes and card
hover stability work in the Settings → System → Contributors list.
2026-05-06 13:28:38 +03:00
cucadmuh 8d8c1aa8a3 Environment upgrade & hot-cache playback (#463)
* chore: upgrade dependencies and migrate playback to rodio 0.22

Bump npm and Rust crates; adapt symphonia decoding, ringbuf 0.5, lofty tags,
and discord-rich-presence usage. Use native rodio Player/MixerDeviceSink and
cpal device descriptions; drop the unused cpal patch. Align Vite 8 build
targets and chunking; remove redundant dynamic imports and fix hot-cache debug
logging imports.

* perf(build): lazy-load routes and restore default chunk warnings

Lazy-load all routed pages with React.lazy to shrink the main bundle; wrap root
Routes in Suspense for lazy Login. Drop chunkSizeWarningLimit override so Vite
uses the default 500 kB threshold.

* fix(windows): tray double-click without spurious menu; clean unused import

Disable tray menu on left mouse-up on Windows so a double-click to hide the
main window does not immediately reopen the context menu (tray-icon default
menu_on_left_click). Gate std::fs in app_api/core behind cfg(linux) for
/proc-only code so Windows builds stay warning-free.

* fix(sidebar): preserve new-releases read state under storage cap

When merging seen album ids, keep the current newest sample first so the
500-id localStorage limit does not truncate freshly marked reads and bring
back the unread badge.

* fix(audio): hot-cache replay, analysis no-op skips, playback source UI

Retain stream_completed_cache across audio_stop so end-of-queue replay can
use RAM promote or disk hot file instead of re-ranging HTTP.

Add cpu_seed_redundant_for_track gate before file/bytes seeds and local-file
spawn; emit analysis:waveform-updated only on Upserted. Ranged/legacy promote
checks generation after await before filling the slot.

Frontend: promote on same-track and cold resume; set currentPlaybackSource on
resume, queue undo restore, and gapless track switch so cache/stream icons stay
accurate. Import tauri::Manager for try_state in audio_play.

* fix(ts): narrow activeServerId for hot-cache promote calls

promoteCompletedStreamToHotCache expects a string; bind non-null server ids
in repeat-one, playTrack prev/same-track, and cold resume paths so tauri
production build (tsc) succeeds.

* fix(player): handle same-track hot-cache promote promise chain

Add .catch for promoteCompletedStreamToHotCache → runPlayTrackBody so sync
throws and unexpected rejections do not surface as unhandled in DevTools;
reset defer-hot-cache prefetch and isPlaying on failure.

* chore(nix): sync npmDepsHash with package-lock.json

* chore(release): finalize 1.46.0 CHANGELOG with PR #463 links

Document the release with full GitHub PR #463 on every subsection so
entries stay attributable if sections are reordered. Fix ContextMenu
lines where dynamic imports were accidentally merged onto one line.

* docs(contributors): credit cucadmuh for #463
2026-05-05 22:00:29 +03:00
github-actions[bot] 6cc7f09e8b chore(release): bump main to 1.46.0-dev (#456)
* chore(release): bump main to 1.46.0-dev

* chore(nix): sync npmDepsHash with package-lock.json

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-04 22:22:48 +02:00
cucadmuh a6cc2e2ad4 perf(linux): WebKit probe, throttled progress IPC, snapshot playback UI (#452)
* feat(linux): optional native GDK for Nix gdk-session

Introduce PSYSONIC_ALLOW_NATIVE_GDK so main skips the default GDK_BACKEND=x11
pin when the Nix gdk-session wrapper sets the flag. Remove GDK_BACKEND from
the npm tauri:dev script so it does not override nix develop defaults.

* fix(ui): portal server switch menu above sidebar

Main column stacks below the sidebar (layout z-index), so an in-tree dropdown
could never win over the left nav. Render the menu via createPortal to
document.body with fixed coordinates, matching the library scope picker.

* feat(perf): add mainstage probe controls and cut WebKit repaint load

Add a dedicated performance probe surface for mainstage/home toggles and wire Linux CPU diagnostics to isolate expensive UI paths. Tune waveform drawing and Home artwork clipping/windowing so visible content loads immediately while reducing WebKit compositor pressure during playback.

* fix(perf): stop hero rotation when section is off-screen

Gate hero auto-rotation and backdrop crossfade by real viewport visibility using the actual scrolling ancestor. This prevents periodic 10-second CPU spikes from hidden hero updates while preserving normal behavior when the hero is visible.

* fix(perf): isolate player progress updates from mainstage diagnostics

Add probe toggles for PlayerBar waveform and live progress UI updates to confirm playback progress churn as the main CPU driver.
Restore Home artwork quality defaults and keep visual-degradation modes opt-in via debug flags only.

* fix(hero): resume background and autoplay after viewport return

Re-check hero visibility on focus/visibility changes and add a short recovery poll while off-screen so missed scroll/RAF events cannot leave hero animation paused.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(perf): decouple playback progress from mainstage compositing pressure

Throttle audio progress delivery and route live seekbar timing through a lightweight progress channel to cut focus-time WebKit CPU spikes.
Add focused diagnostics in Performance Probe and restore hero/waveform behavior so visuals remain stable while profiling.

* fix(debug): open performance probe with Ctrl+Shift+D

Replace logo-triggered opening with a keyboard shortcut and keep logo purely decorative to avoid accidental probe activation.

* docs(changelog): document experiment/performance probe and playback work

Add an [Unreleased] section for the performance probe, throttled audio
progress IPC, snapshot-based live UI updates, WaveformSeek scheduling
over the same canvas bar renderer, Hero/Home rail fixes, and Linux/Nix
GDK dev ergonomics.

* perf(linux): add WebKit probe, throttle progress IPC, snapshot playback UI

Ship Performance Probe (Ctrl+Shift+D), Rust-throttled audio:progress, a
playback progress snapshot channel with coarse Zustand timeline commits,
Linux /proc CPU readout for the probe, Hero and Home rail artwork fixes,
Tracks SongRail windowing parity, MPRIS cleanup, gated perf counters, and
WaveformSeek paused-seek correctness. Documented in CHANGELOG for PR #452.

* docs(changelog): fold perf work into 1.45.0 and refresh date

Drop the separate 1.45.1 heading; keep PR #452 notes under 1.45.0 Added and
set the section date to 2026-05-04. Restore the safety preface before the
versioned sections.

* docs(changelog): order 1.45.0 Added entries by PR number

Sort the 1.45.0 release notes so subsections follow ascending PR id (390
through 452), with PR #452 last.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 20:06:49 +03:00
Frank Stellmacher dcec30166a fix(audio): frame-align gapless-off track-separation silence (#439)
* fix(audio): frame-align gapless-off track-separation silence

The 500 ms silence prepended between tracks when gapless playback is
disabled and the previous track ended naturally was built with
`Zero<f32>::new(ch, sr).take_duration(500ms)`. Rodio's `TakeDuration`
computes its sample count via integer-nanosecond division
(`1_000_000_000 / (sr * ch)`), which truncates: at 44.1 kHz / 2 ch
this emits 44103 samples = 22051.5 frames, half a frame short.

That half-frame leak shifts the next source's L/R parity in the
device frame stream. Multiple users have reported the next track
playing only on the right channel — exactly when gapless is OFF and
the previous track ended naturally (manual skip and album-first-play
bypass the silence prepend, which matches the reproducer report).

Replace with `SamplesBuffer::new(ch, sr, vec![0; frames * ch])`
where `frames = sr / 2`. Frame-aligned by construction, same
audible effect.

* docs(changelog): add #439 mono-channel fix entry

* chore(credits): add #439 to Psychotoxical contributions

* docs(changelog): strip @ from non-contributor mention in #435 entry

Plain-text 'zunoz on Discord' instead of '@zunoz' so GitHub does not
attribute the requester as a contributor on subsequent merges.
2026-05-03 13:55:56 +02:00
cucadmuh 1e05180418 feat(shortcuts): action registry + dynamic CLI help + new input targets (#435)
* feat(shortcuts): unify action-driven shortcut and CLI routing

Centralize shortcut action metadata in one TypeScript registry and route keyboard, global shortcut, mini-window, and CLI inputs through shared runtime handlers.
Keep CLI as an abstract transport layer by emitting player-command payloads without depending on shortcut definitions.

* feat(shortcuts): generate CLI action help from shortcut registry

Move no-arg player commands and their descriptions into the central action registry so CLI parsing and --player help are derived dynamically from one source of truth. Also route runtime action execution through the registry and remove duplicated shortcut runtime handling.

* feat(shortcuts): add new input actions and hidden F1 help binding

Add the requested input actions (search, advanced search, sidebar, mute, equalizer, repeat, now playing, lyrics, favorite current track) to the central shortcut action registry and wire runtime handlers for sidebar/equalizer toggles. Keep Help bound to F1 by default while hiding it from Settings input lists, and backfill persisted keybindings with new defaults so F1 works for existing users.

Requested by @zunoz (Discord community).
2026-05-03 00:37:43 +03:00
Frank Stellmacher e44e6dcdf4 fix: restore audio refactor + features lost in #419 squash-merge (#429)
The squash-merge of PR #419 was performed against an outdated PR base
that predated several main-side refactors and features. The resulting
squash inadvertently re-introduced files that had already been removed
(`src-tauri/src/audio.rs` monolith, `app-icon.png`) and reverted main's
content for ~20 files (`src-tauri/src/lib.rs` decompose, `src/App.tsx`
animation-pause, `src/components/AlbumRow.tsx` headerExtra, etc).

This commit:

* Restores all collateral-damage files to their pre-#419 main state
  (5662dafe), including the audio module split, lib decompose,
  Cargo.toml `windows` dep, animation-pause-on-blur logic, and
  `reducedAnimations` toggle plumbing in WaveformSeek/Settings.
* Keeps the genuine #419 work intact: tri-state duration toggle,
  position counter, persistent Now Playing collapse, animated EQ
  indicator (in QueuePanel.tsx, 8 locales, authStore, components.css).
* Merges authStore.ts so both `reducedAnimations` (from #426) and
  `queueNowPlayingCollapsed` (from #419) coexist.
* Adds the `.eq-bars.paused` CSS rule manually since components.css
  needed a 5662dafe base + the single #419 addition.
* Fixes a latent type error in OverlayScrollArea.tsx (`useRef<T>(null)`
  is `RefObject<T>` / read-only under current `@types/react`; widened to
  `useRef<T | null>(null)` so the existing `wrapRef.current = el`
  assignment compiles).

Verified locally with `cargo check` and `npm run build` — both green.
2026-05-02 22:01:51 +02:00
Kveld. 18b4a982ef feat: queue-ux-improvements (#419)
* feat(queue): add ETA display, equalizer indicator and collapsible now playing

* deleted endsAt and showDuration strings, changed eta update to 30s

* feat(queue): ETA tooltip, persistent Now Playing collapse, EQ bar pause, remove redundant Play icon

* feat(queue): fold ETA into existing total/remaining toggle as third mode

The standalone ETA span next to the track counter is removed; instead the
clickable duration label in the queue header now rotates through three
modes per click: total → remaining → eta → total. Counter (N/M) stays
where it was.

ETA mode keeps the live-feel treatment from the original PR (accent
colour while playing, muted at 50% opacity when paused). The other two
modes use plain accent.

i18n: queue.etaTooltip removed (no longer a separate descriptive label),
queue.showEta added as the action tooltip ('Show estimated end time')
in all 8 locales — matches the showRemaining / showTotal pattern.

* docs(changelog): add #419 queue UX improvements entry

Adds the [1.45.0] / Added entry for this PR's queue panel refinements
(position counter, tri-state duration toggle including ETA, collapsible
Now Playing section, animated EQ indicator).

---------

Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
2026-05-02 21:45:55 +02:00
Frank Stellmacher 2e9618cf54 fix(audio): Windows playback stutter under GPU load (#334) (#426)
* fix(audio): promote WASAPI render thread to MMCSS Pro Audio on Windows

Wraps the outermost audio source in a `PriorityBoostSource` that calls
`AvSetMmThreadCharacteristicsW("Pro Audio")` on its first sample. The
cpal output-stream callback runs `Source::next` on the WASAPI render
thread, which is otherwise normal-priority and gets preempted under
WebView2 / DWM / GPU pressure — producing the audible click/stutter
reported in issue #334. No-op on Linux/macOS (PipeWire/rtkit and
CoreAudio promote their audio threads externally).


* fix(build): repair Windows compile after audio split + lib decompose

Two pre-existing build breakers on Windows that surfaced after the
`use super::*;` cleanup (9cc74a7) and the lib-decompose refactor
(cfeec22):

- `audio/device_watcher.rs` lost the `output_enumeration_includes_pinned`
  import. Add it cfg-gated to `not(target_os = "linux")` since it's only
  called inside the non-Linux pinned-device fallback path.
- `lib_commands/sync/tray.rs::is_tiling_wm()` is `#[cfg(target_os = "linux")]`,
  but its Tauri command wrapper `is_tiling_wm_cmd()` is unconditional.
  Add a `#[cfg(not(target_os = "linux"))]` stub returning `false`.

No behavior change on Linux. Restores `cargo build` on Windows + macOS.

* perf(ui): pause cosmetic animations when window loses OS focus

Companion to the WASAPI MMCSS fix in this branch — issue #334
reported audible audio stutter on low-end laptops, partly traced
to WebView2 still compositing CSS animations + waveform canvas at
full rate while the user has alt-tabbed into another app.

Existing `data-app-hidden` / `data-psy-native-hidden` flags pause
animations only when the window is fully hidden (minimized or
behind a tray). When the window is visible-but-unfocused — the
common "music in the background while I work" case — every infinite
keyframe + 60fps rAF loop keeps running.

Add a parallel `data-app-blurred` flag driven by `window.focus`/
`window.blur`, plus matching `__psyBlurred` on the global window
object for imperative checks. The CSS rule that pauses every
`animation-play-state` on hidden gets a third selector for blurred.
WaveformSeek's two 60fps rAF loops (animated styles + settings demo)
extend their existing `document.hidden || __psyHidden` short-circuit
to also include the new blurred flag, falling back to the same
400ms polling path that already exists for hidden.

Verified visually on the dashboard: alt-tab → `<html
data-app-blurred="true">`, mesh blobs in the fullscreen player +
marquee + waveform 60fps loop all pause. Click back into the
window → blob/marquee/waveform resume immediately.

Cross-platform: focus/blur events fire reliably on all targets, so
the optimization applies to Windows, Linux (incl. WebKitGTK with
software compositing where the savings are largest), and macOS.

* chore: remove stale app-icon.png from repo root

* perf(ui): add 'Reduce animations' toggle that caps animated seekbar styles to 30 fps

Off by default. When on, animated seekbar styles (pulsewave, particletrail,
liquidfill, retrotape) skip every other rAF and advance the animation timer by
a doubled delta, so wave speed is unchanged while draws are halved. Aimed at
low-end Windows GPUs where the 60 fps shadow-blur loop drives ~40 % WebView2
GPU usage even when focused.

Toggle lives directly under the seekbar style picker in Settings → Appearance.
i18n in all 8 supported languages.
2026-05-02 21:10:32 +02:00
Frank Stellmacher 297c9f1125 fix(preview): sync audio start, ring animation, and download timeout (#423)
* fix(preview): sync audio start, ring animation, and download timeout

Three coupled fixes for the track-preview engine:

1. Audio sync. `Sink::try_seek` was running on a worker thread after
   `sink.append(source)`, so the sink began playing position 0 while
   the seek was still iterating to the mid-track target. With the
   30 s `take_duration` cap counting wall-clock from append, audio
   could only become audible ~25% into the preview window. The seek
   now runs on the bare source before append, then `take_duration`
   wraps it — playback starts at the seek position with the cap
   measured from there.

2. Ring animation gating. The CSS progress-ring animation was
   bound to `is-previewing` (set on click), so the ring sprinted
   ahead of any download/decode/seek warmup and didn't reset
   cleanly when switching from one preview to another. Added an
   `audioStarted` flag in `previewStore` that flips on
   `audio:preview-start` from the engine; CSS animation is now
   gated on `audio-started` instead. `is-previewing` still drives
   tooltip/icon for instant click feedback. Same SVG is reused for
   a 25%-arc rotating loading spinner while waiting for audio,
   with a 150 ms delay so cached/short previews don't flash.

3. Download timeout. The shared `audio_http_client` caps at 30 s,
   which aborts mid-download on multi-hundred-MB uncompressed
   files (e.g. 18-min Hi-Res WAV ~600 MB). The preview engine now
   builds a dedicated client with a 5 min timeout for the bytes
   fetch. Watchdog still bounds the playback window at 30 s once
   the audio actually starts.

Touches `audio/preview.rs`, `previewStore.ts`, `components.css`
plus the eight tracklist/player-bar components that render the
preview button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(changelog): add preview audio sync fix for PR #423

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:44:49 +02:00
Psychotoxical 9cc74a7f88 style(tauri): drop redundant use super::*; imports
Five lib_commands submodules either had `use super::*;` duplicated
during the split (core.rs, offline.rs, mini.rs — leftover line 3) or
didn't reference the parent scope at all (navidrome.rs, bandsintown.rs).
Removing them silences all five `unused import` warnings; cargo check
is now warning-free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:44:25 +03:00
Maxim Isaev cfeec22c82 refactor(tauri): decompose lib command surface into domain modules
Split the large lib command module into nested app_api, cache, sync, and ui submodules while preserving command signatures and behavior. This keeps command responsibilities isolated and makes further extraction safer.
2026-05-02 13:44:25 +03:00
Maxim Isaev c917ee1071 refactor(audio): split monolithic audio module into submodules
Move audio engine functionality from a single large file into focused Rust modules while preserving behavior and command surface. This keeps boundaries clearer for future extraction and the upcoming lib.rs split.
2026-05-02 13:44:25 +03:00
cucadmuh 267f732397 fix(tray): gate playback_state cfg for Windows and Linux only (#416)
playback_state is only used for the Windows tray tooltip icon and the Linux
now-playing menu row; compiling it on macOS triggered an unused_variables
warning in CI.
2026-05-01 21:03:19 +00:00
cucadmuh 9ad0f8af6d feat(ui): UI refinements — sidebar indicators, adaptive header, and interaction polish (#397)
* feat(ui): unify queue toggle handle behavior

Show the queue toggle in the header when the queue is collapsed and use a seam-aligned drag handle when it is open. Hide the seam handle while the main content is actively scrolling to reduce accidental interactions.

* feat(ui): add adaptive header search collapse behavior

Collapse header search to a magnifier when top controls get crowded and expand it as an overlay only while active. Use measured header space with hysteresis to avoid flicker and keep neighboring controls stable.

* chore(ui): remove leftover search prototype artifacts

Drop an unused icon import from live search and remove an unused header container-type rule left from an earlier layout experiment.

* feat(ui): persist sidebar and queue visibility state

Save left sidebar collapse and right queue open/closed visibility in local storage helpers so both panel modes are restored after app restart.

* feat(ui): unify player overflow menu behavior

Use a single overflow menu for click and wheel interactions, with a volume-only mode that keeps the same layout and volume controls as the full menu.

* feat(ui): add wheel seek controls to waveform

Apply 10-second wheel seek steps with trailing 1-second debounce and keep the waveform preview stable so the playhead moves smoothly during rapid scroll input.

* fix(now-playing): stabilize narrow dashboard layout

Switch now-playing responsiveness to container-based breakpoints and prevent stacked widgets from overlapping when width is constrained.

* fix(search): reduce collapse jitter and avoid header overlap

Add a short collapse-state cooldown to prevent threshold flicker and hide conflicting header controls while collapsed search expands as an overlay.

* fix(i18n): localize player overflow controls across locales

Replace hardcoded player overflow labels with translation keys and add the missing keys for all shipped locale files.

* fix(search): keep advanced control clickable in collapsed mode

Prevent focus loss on the advanced search button in collapsed overlay mode so its click handler consistently runs.

* fix(i18n): restore queue translation in offline library

Use the existing queue.appendToQueue key for the offline enqueue button tooltip and label instead of a missing key and hardcoded English text.

* fix(ui): apply overlay scrollbar to right-panel text tabs

Switch now-playing content, lyrics, and info panes to OverlayScrollArea and harden tour-item layout so long concert metadata stays within panel bounds.

* fix(ui): add unread indicator for new releases and guard sidebar drag clicks

Track unread new-release IDs per server/library scope and clear the badge when opening the New Releases page. Also prevent click-through navigation after sidebar drag release and keep related i18n/responsive sidebar-adjacent refinements in this snapshot.

* fix(ui): stabilize live dropdown layering and unread reset flow

Render the topbar Live dropdown via a portal so it consistently overlays sidebar layers. Rework new-releases unread tracking to handle library scope baselines, ignore stale refresh races, and mark items as seen after a 5-second stay on the New Releases page.

* feat(ui): add localized New badges for recently added albums

Show a theme-consistent New badge on album cards and album detail for albums created within the last 48 hours. Localize the badge label across all supported locales and centralize recency logic in a shared utility to avoid duplication.

* fix(album): prevent tracklist jump when entering multiselect

Move bulk selection actions from the tracklist body into the album toolbar next to the track filter. Keep selection controls stable in the header area so enabling multiselect no longer shifts the tracklist content downward.

* fix(tray): add playback-state badge and finalize queue handle tooltip

Show play/pause/stop icons in the Linux tray now-playing entry and persist state safely in Tauri managed state.
Also switch the queue-resize handle tooltip to the dedicated localized key across all locales.

* fix(header): prioritize search collapse before Live/Orbit labels

Make topbar compression deterministic by collapsing search first and compacting Live/Orbit labels only in sustained low-space mode.
Add sticky hysteresis-based header compact state to prevent oscillation while resizing.

* fix(ui): stabilize header compaction and show tray state icons

Prevent topbar flicker in the narrow-width range by tightening compact-mode thresholds, gating on real overflow, and removing width transitions from live search.
Also include playback state icons in tray tooltip text across platforms while preserving tooltip length limits.

* fix(tray): keep tooltip iconization Windows-only

Revert Linux tray tooltip/title fallback attempts and keep state icons only in Windows tray tooltips, while Linux continues to show playback state in the now-playing menu entry.

* fix(ui): restore queue resize response after overlay scroll interactions

Hide the queue handle while scrolling on both the main route viewport and the now-playing viewport, and clear stale thumb-drag state before starting queue resize.
Also ignore inactive/faded overlay thumbs in resizer suppression so horizontal pointer transitions no longer leave the queue seam unresponsive.

* docs(changelog): summarize ui-refinements branch features

Document the branch-level feature additions in 1.45.0 as separate changelog sections and group remaining branch-local fixes under a single polish entry.

* docs(changelog): add PR #397 references for ui-refinements

Attach PR metadata to the new 1.45.0 ui-refinement sections and the polish entry so release notes map directly to the merged branch discussion.
2026-05-01 15:42:40 +03:00
Frank Stellmacher 2ea22635e5 feat(tray): show now-playing track in tray + i18n menu labels (#395)
* feat(tray): show now-playing track in tray icon tooltip

Mirrors the current track to the tray tooltip ("Artist – Title") on every
play/pause/track change, falling back to "Psysonic" when nothing is playing.
The cached value survives tray hide/show via a new TrayTooltip state, and
is truncated to 127 chars for Windows NOTIFYICONDATA.szTip.

Closes #383


* feat(tray): linux fallback — disabled menu item shows now-playing track

AppIndicator on Linux has no hover-tooltip API, so the tooltip command was
a silent no-op there. Adds a disabled menu entry at the top of the tray
right-click menu that mirrors the same text. Updated in lockstep with the
Win/macOS tooltip via set_tray_tooltip.

* i18n(tray): localize tray menu labels

Adds a `tray` namespace in all 8 locales (en/de/fr/nl/zh/nb/ru/es) for
Play/Pause, Next/Previous, Show/Hide, Exit, and the Linux-only
"Nothing playing" placeholder.

Rust side: TrayMenuLabels state holds the cached translations and
TrayMenuItems holds the live MenuItem handles. New set_tray_menu_labels
command pushes new strings + applies them via set_text without rebuilding
the tray icon.

Frontend pushes the labels on mount and on every i18n.on('languageChanged').
2026-05-01 12:58:16 +02:00
Frank Stellmacher 20a083a9a6 feat(player): preview indicator in player bar + smart stop semantics (#394)
* feat(player): preview-active state on play button (ring + stop icon)

Checkpoint: play button mirrors the inline preview button from tracklists
during preview playback — hollow circle, accent ring depleting over the
preview duration, Square (stop) icon. Click still resumes main playback,
which the Rust audio engine cancels the preview for.

i18n key player.previewActive in all 8 locales for tooltip + aria-label.


* feat(player): show preview track in player bar + smart stop semantics

The player-bar info cell (cover, title, artist) now mirrors the previewing
track during preview playback, with a small accent "Preview" pill above
the title and an accent top-border on the bar. Rating, fullscreen hint
and album/artist link clicks are suppressed while previewing — they
target the queued track, not the preview.

Stop semantics for the two transport buttons during preview:
- Big play button (Square+ring visual): stops preview, main auto-resumes
  if it was playing before. Matches the tracklist preview-button behaviour.
- Small Stop button: new audio_preview_stop_silent Rust command — stops
  preview AND leaves main paused, so "Stop = silence" actually goes silent.

previewStore now stores the full PreviewingTrack (id, title, artist,
coverArt) — the seven startPreview call sites pass it through.
i18n key player.previewLabel in all 8 locales.
2026-05-01 12:57:34 +02:00
Frank Stellmacher a14dba8167 feat(audio): rust track preview engine + inline play/preview buttons (#392)
* feat(audio): rust preview engine with secondary sink

Adds a parallel rodio Sink on the existing OutputStream for 30s
mid-track previews. Two new Tauri commands (audio_preview_play,
audio_preview_stop) plus three events (audio:preview-start /
-progress / -end). The main sink is paused with Sink::pause() and
auto-resumed on preview end iff it was playing beforehand.


* feat(playlists): migrate suggestion preview to rust audio engine

Replaces the HTML5 <audio> path with the new rust preview engine.
previewStore mirrors the engine's start/progress/end events so any
tracklist row can render preview UI from a single source of truth.

Spacebar redirects to stopPreview while a preview plays, hardware
mediakeys are silently dropped (Q5), and tray clicks cancel the
preview before forwarding the original action.


* feat(albums): inline play + preview buttons in tracklist rows

Track number stays static on hover instead of swapping to a play
icon — the dedicated Play and Preview buttons in the title cell
take over click-to-play and click-to-preview. Active+playing rows
keep the eq-bars (also on hover), active+paused rows fall back to
the static accent-coloured number.

Pilot for the wider rollout to other tracklists.

* feat(tracklists): roll out inline play + preview buttons

Mirrors the AlbumTrackList pilot across the remaining track-row
based lists: PlaylistDetail main tracks, Favorites, ArtistDetail
top tracks, RandomMix (both genre-mix and filtered-songs lists).

Track number stays static, the dedicated Play + Preview buttons
in the title cell take over click-to-play and click-to-preview.

* feat(settings): track preview toggle + configurable position and duration

Adds an opt-out switch and two sliders to Settings → Audio:
start position (0-90 % of track length, default 33 %) and preview
duration (5-60 s, default 30 s). The progress-ring animation
follows the duration via a CSS variable so the visual matches the
engine's auto-stop. Disabling the feature hides every inline
preview button via a single root-level data attribute, no per-row
conditional rendering required.

i18n keys added in all 8 locales.

* fix(audio): cancel preview when main playback (re)starts

audio_play, audio_play_radio, audio_resume and audio_stop did not
know about the parallel preview sink, so clicking Play on a track
that was currently being previewed left the preview running on top
of the freshly started main playback. New helper clears the resume
flag, bumps the preview generation, drops the sink and emits an
'interrupted' end event before any of those commands touches the
main sink.


* feat(settings): per-location track preview toggles

Splits the single trackPreviewsEnabled toggle into a master + 6 per-location
sub-toggles (suggestions, albums, playlists, favorites, artist, randomMix).
Master remains the kill switch; sub-toggles are only honoured when master
is on. Each tracklist container is marked with `data-preview-loc="<id>"` and
hidden via scoped CSS when the matching root attribute is "off".
startPreview now takes a location argument so the store can guard logic too.
i18n added in all 8 locales.


* fix(contextmenu): use ChevronsRight for Play Next to distinguish from preview
2026-05-01 09:11:28 +02:00
Frank Stellmacher 225f7c1406 feat(themes): add Kanagawa, Atom One, 1984 palettes; regroup OSS Classics by family (#390)
* feat(themes): add Kanagawa, Atom One, 1984 palettes; group OSS Classics by family

Adds three upstream-faithful theme families to Open Source Classics and
restructures the picker so the section is no longer alphabetically wild.

New themes (9 total):
- Kanagawa (rebelot/kanagawa.nvim): Wave, Dragon, Lotus
- Atom One (Th3Whit3Wolf/one-nvim): Dark, Light
- 1984 (juanmnl/vs-1984): Default, Cyberpunk, Light, Orwell
  (Fancy + Unbolded skipped — identical palette to Default, style-only)

Each theme defines the full token set (--bg-*, --accent, --text-*, all
--ctp-*, --waveform-*, --positive/warning/danger, --select-arrow), so
login screen, queue sidebar tabs, and all subpages inherit the palette
without component-level overrides.

Picker restructure:
- ThemeDef gains optional `family?: string`
- Open Source Classics regrouped: 1984, Atom One, Catppuccin, Dracula,
  Gruvbox, Kanagawa, Nightfox, Nord
- Family headings rendered inline (grid-column: 1 / -1) when family
  changes; new .theme-family-header style in components.css
- Theme scheduler dropdown labels prefixed with family for context

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(release): sync Cargo.lock to 1.45.0-dev
2026-05-01 08:51:34 +02:00
github-actions[bot] dd73e2b36b chore(release): bump main to 1.45.0-dev (#375)
* chore(release): bump main to 1.45.0-dev

* chore(nix): sync npmDepsHash with package-lock.json

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-30 02:41:53 +03:00
cucadmuh 106baec8fe chore: align Tauri package version with package.json on main (#370)
Cargo.toml and tauri.conf.json had stale RC-style semver; match root
package.json (1.44.0-dev) so local builds and artifact names stay consistent.
2026-04-29 21:46:59 +00:00
Frank Stellmacher 402120143e chore(release): bump to 1.44.0-rc1 (#339)
CHANGELOG entry covers everything merged on top of v1.43.0:
LUFS loudness normalization, Orbit multi-user listen-together,
Now-Playing dashboard, Tracks library hub, Lucky Mix, sleep-timer
ring UI, Genres tag-cloud, queue undo/redo, settings refactor +
perf suite, plus the usual fixes.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:02:51 +02:00
cucadmuh bd2242e4f3 feat(cli): add logs mode with tail/follow and completion support (#337)
Introduce a dedicated --logs mode for CLI log viewing with --tail <lines> and
-f/--follow streaming from the normal/debug log channel, keep user-facing CLI
output free of timestamped log formatting, and update bash/zsh completions for
the new logs flags.
2026-04-27 20:21:41 +03:00
Frank Stellmacher d2080588b9 fix(player): persist queue panel visibility + drop dead loudness binding (#336)
* feat(player): persist queue panel visibility across restarts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(audio): drop unused resolved_loudness_gain_db binding

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 17:37:22 +02:00
cucadmuh 87373edb17 fix(loudness): target sync, effective pre-analysis trim, and queue/settings copy (#333)
* fix(loudness): target sync, -14 pre-analysis ref, queue UI, and reseed

- Front: coalesce loudness refresh by target LUFS; replay-gain IPC dedupe keys
  include norm target and effective pre-attenuation so TGT changes apply.
- Rust: placeholder gain before integrated LUFS uses pivot at -14 LUFS; UI gain
  from effective trim; reseed loudness after delete when waveform cache would skip.
- Pre-analysis: store attenuation relative to -14 LUFS; engine and UI use an
  offset for other targets; migrate legacy absolute values on rehydrate.
- Queue/Settings: Loudness/TGT labels vs value buttons; styles; i18n for help.

* fix(i18n): simplify loudness pre-analysis helper copy

Remove reference-target wording from loudness pre-analysis helper text and keep
only the effective adjustment shown for the current LUFS target in all locales.
2026-04-27 02:54:58 +03:00
Frank Stellmacher c64a1e195e chore(discord): debug-build logging for Rich Presence IPC path (#330)
The Discord Rich Presence module deliberately swallows every IPC error so
the app stays clean when Discord is not running. The downside: when the
status genuinely stops working (Discord client glitches, app id rejected,
socket pipe stuck), nothing is logged anywhere and we have no signal.

Add debug-build logging at every step of the IPC handshake and every send:

- try_connect: separate failure logs for new() vs connect(), success log
  with the app id.
- discord_update_presence: log the rendered details/state on send, log
  set_activity failures with the underlying error.
- discord_clear_presence: log clear success and clear failures.
- The pause-path clear_activity inside discord_update_presence likewise
  logs failures.

All log lines are wrapped in #[cfg(debug_assertions)] so they compile out
of release builds — no runtime cost or log noise for end users.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:25:20 +02:00
Maxim Isaev 8f603e8de9 feat(waveform): store peak+mean bins and blend at 70/30
Persist waveform v4 as paired curves per bin (peak and mean absolute value),
invalidate cache rows with insufficient data shape, and render the seekbar as
0.7 * mean + 0.3 * peak for a denser, more stable contour.

Thanks to @peri4ko for proposing this waveform improvement.
2026-04-26 21:43:39 +03:00
cucadmuh 218aa00718 fix(analysis): CPU seed queue, single waveform emit, and log URL redaction
Serialize heavy PCM seeding through a dedicated queue with optional priority
for the current track. Emit waveform-updated once per completed seed, fix
Lucky Mix waveform refresh tokens, redact Subsonic URLs in logs, and align
hot-cache prefetch with the queued path.
2026-04-26 20:59:33 +03:00
Psychotoxical 5cb233e1c9 fix(analysis): dedupe concurrent seed_from_bytes for the same track_id
Six independent paths reach analysis_cache::seed_from_bytes for the same
track_id during a cache-miss play under LUFS:

- track_download_task legacy stream capture-complete
- ranged_download_task HTTP buffer full
- audio_preload bytes-ready
- psysonic-local file-read complete
- spawn_analysis_seed_from_in_memory_bytes (gapless reuse / preloaded /
  stream cache)
- analysis_enqueue_seed_from_url (frontend backfill triggered by
  refresh:miss while a playback path is mid-seed)

Whenever a track is streamed for the first time with loudness on, two of
these fire in parallel and Symphonia + EBU R128 decode the same buffer
twice — ~30 s of duplicate CPU per track, plus a redundant SQLite write
of an identical row. Reproduced on multiple cache-miss tracks (KjP… ranged
+ backfill, mPdz… preloaded bytes + backfill).

Coordinate at the funnel:

- New SeedFromBytesOutcome::SkippedConcurrent variant.
- SEEDS_IN_FLIGHT static (Mutex<HashSet<String>>) tracks track_ids whose
  analysis is currently running. First caller into seed_from_bytes
  wins the slot via HashSet::insert; later callers return SkippedConcurrent
  immediately and let the winner publish the analysis:waveform-updated
  event.
- SeedInFlightGuard (RAII) releases the slot on every exit path including
  panics, so a crashing analysis cannot leak the marker.
- enqueue_analysis_seed in lib.rs distinguishes the SkippedConcurrent log
  from the genuine "seed result" log so the frontend backfill case
  (when it arrives second) doesn't read like a backfill failure.

Existing call-site match arms in audio.rs already use Ok(_) => {} for
non-Upserted outcomes — no changes needed there. enqueue_analysis_seed in
lib.rs already gates the waveform-updated emit on outcome == Upserted, so
the new variant is silent there too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:59:32 +03:00
Psychotoxical 2d222e2691 fix(loudness): break feedback loop that froze Windows UI when LUFS active
When loudness normalization is on, every PARTIAL_LOUDNESS_EMIT_INTERVAL_MS
(900 ms per active stream task) the backend emits analysis:loudness-partial.
The frontend listener wrote cachedLoudnessGainByTrackId and unconditionally
called updateReplayGainForCurrentTrack, which invoked audio_update_replay_gain,
which in turn emitted audio:normalization-state back to the renderer — closing
a loop the UI had to drain on every tick. WebKitGTK absorbed it; WebView2 did
not, and after a few minutes the renderer thread stalled while Rust analysis
tasks kept progressing (visible in logs).

Five gates close the loop, smallest first:

Frontend (src/store/playerStore.ts):
- analysis:loudness-partial listener now short-circuits when the new gainDb is
  within 0.05 dB of the cached value.
- audio_update_replay_gain calls go through invokeAudioUpdateReplayGainDeduped
  (250 ms key-based dedupe), mirroring the audio_set_normalization helper from
  #320.
- refreshLoudnessForTrack now coalesces concurrent calls per (trackId, mode)
  via loudnessRefreshInflight, mirroring waveformRefreshInflight.

Backend (src-tauri/src/audio.rs):
- audio:normalization-state emits go through maybe_emit_normalization_state,
  which keeps the last-emitted payload behind a Mutex and skips when engine
  matches and current_gain_db (±0.05 dB) / target_lufs (±0.02) are unchanged.
  Applied at all three emit sites: audio_play, audio_update_replay_gain,
  audio_set_normalization.
- analysis:loudness-partial emits are gated by partial_loudness_should_emit:
  per-track-identity last-emitted-gain map; an emit is suppressed when the new
  gain is within PARTIAL_LOUDNESS_DELTA_THRESHOLD_DB (0.1 dB) of the previous
  one. Applied to both emit_partial_loudness_from_bytes (legacy path) and the
  inline ranged_download_task progress emit.

Drive-by: split estimated_output_latency_secs into two #[cfg]-gated
definitions so the sample_rate_hz parameter is no longer flagged as unused on
Windows / macOS builds. Function originally added in c6fc3ec.

Net effect: when loudness is steady (the common case), zero IPC traffic on
this path. UI stays responsive on Windows. Tested on Windows + Linux.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:59:32 +03:00
cucadmuh a910162cfe fix(analysis): gate partial LUFS, seed RAM paths, clarify logs, dedupe IPC (#320)
- Emit streaming partial loudness only when the loudness normalization engine is active
- Seed waveform/loudness from in-memory full buffers (preloaded, stream cache, gapless reuse) so offline hot cache does not rely on HTTP backfill
- Add explicit ranged/local/preload logs before full-file Symphonia + EBU analysis
- Coalesce concurrent waveform DB reads and skip duplicate audio_set_normalization within 450ms (e.g. StrictMode double init)
2026-04-26 14:25:26 +02:00
Psychotoxical 4217e8e6a0 fix(player): cross-device resume — push queue position on pause + all exit paths
Server queue position was effectively pinned at 0 because syncQueueToServer
only fired on track-change, seek, and queue edits (with a 5s debounce). A
user listening for minutes without seeking would close the app and the
server still held position=0, so a second device restarted the track.

Three pieces:
- 15s heartbeat from audio:progress flushes the live position while playing.
- pause() flushes immediately so a quick close after pause is captured.
- Tray "Exit" and macOS Cmd+Q used to call app.exit(0) directly, bypassing
  the JS close handler. Both now emit a new app:force-quit event that
  shares the same flush + Orbit-teardown path as window:close-requested,
  and exit_app stops the audio engine on its way out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:15:12 +02:00
Psychotoxical 185cb8f7cd Merge branch 'main' into feat/waveform-loudness-cache 2026-04-26 02:05:10 +02:00
Maxim Isaev 4b60495e38 feat(playback): loudness bind rules, analysis seeding, and normalization UI
Resolve integrated loudness from SQLite only at decode bind; keep pre-trim
until a row exists, then allow provisional gain from live updates. Pass
DB-stable hints from the web app into play and gapless preload.

Default pre-measurement attenuation -4.5 dB with an icon reset in Settings;
drop redundant normalization copy and shorten pre-trim descriptions.

analysis_cache seed_from_bytes returns an outcome and skips redundant
waveform work on cache hits; wire callers and related frontend/backend glue.
2026-04-26 02:29:54 +03:00
Maxim Isaev c6fc3ec844 fix(waveform): stabilize progressive rendering and reduce streaming contention
Improve seekbar fallback behavior by using consistent bar-based rendering and smoother transitions to analyzed waveform data. Reduce hot-path analysis overhead during ranged streaming and add controls to clear cached waveform entries.
2026-04-26 00:49:10 +03:00
Maxim Isaev 53cab7654c feat(player,queue): loudness strip controls and normalization readout fixes
- Add Tauri command to delete loudness_cache rows for a track and helpers in AnalysisCache.
- Queue tech strip: click dB to reseed loudness; LUFS target picker via body portal; metric styling aligned with strip (no link chrome).
- Player store: reseed clears local cache and replays analysis seed; show loudness dB from SQLite cache when live state is still null; allow first numeric normalization-state update through the short duplicate filter.
- audio_update_replay_gain: resolve loudness from the requested gain when playback URL is not pinned yet.
2026-04-25 23:38:27 +03:00
Psychotoxical 2fae1b4c0e chore(tauri): don't auto-open devtools on launch
Removes the #[cfg(debug_assertions)] open_devtools() block from setup().
DevTools remain available in dev builds via the standard WebKit shortcut
(Ctrl+Shift+I) and right-click → Inspect, but no longer pop up automatically
on every dev launch. Less visual clutter when iterating without needing the
inspector.

Production behaviour is unchanged — devtools stay hard-stripped from
release builds via Tauri's default `windows[].devtools=false` for release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:17:16 +02:00
Psychotoxical 453151f1fc Merge branch 'main' into feat/waveform-loudness-cache
# Conflicts:
#	src/store/playerStore.ts
2026-04-25 21:47:48 +02:00
Maxim Isaev 86704473ed fix(audio): stabilize loudness normalization and streaming startup behavior
Improve normalization consistency by separating cache refresh for non-playing tracks, hardening track-id cache lookups, and keeping UI gain state aligned with actual playback. Also reduce startup stalls by preferring non-seekable streaming when format hints are missing and by tuning ALSA/analysis paths to avoid underrun-prone churn.
2026-04-25 22:09:38 +03:00
Psychotoxical b4b786972e chore(deps): bump rustls-webpki to 0.103.13 and postcss to 8.5.10
Patches two Dependabot alerts:
- rustls-webpki 0.103.12 → 0.103.13 (high: DoS via panic on malformed CRL BIT STRING)
- postcss 8.5.8 → 8.5.10 (medium: XSS via unescaped </style> in CSS stringify output)

Both are semver-compatible patch bumps. cargo check + npm run build pass.

The remaining open Dependabot alert (glib 0.18 → 0.20) is blocked on
upstream tauri/wry/webkit2gtk pinning the gtk-rs 0.18 stack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:23:23 +02:00
Psychotoxical 0f75dada1e chore(tauri): enable devtools in dev builds only
Removes the explicit "devtools": false from tauri.conf.json so the Tauri
default applies (true in debug, false in release). Auto-opens the inspector
in the setup hook under #[cfg(debug_assertions)] so contributors get DevTools
on `npm run tauri:dev` without right-clicking.

`cargo check --release` confirms the open_devtools() call is hard-stripped
from production binaries — the symbol does not exist in the release build.

Helps debug WebView-side issues (CORS, TLS, axios responses) that the
Rust-only logging mode does not capture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:22:08 +02:00
Frank Stellmacher e3aabd98b7 feat(tracks): add Tracks library hub page (closes #299) (#300)
New /tracks route with three sections:

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

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

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

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

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:23:15 +02:00
Frank Stellmacher 67d51a0975 revert: roll back flatpak packaging experiment (#271 + follow-ups) (#297)
Reverts #271, #292, #293, #294, #295, #296.

The flatpak CI pipeline itself works end-to-end, but the installed bundle
surfaced three separate manifest issues during smoke-test on Wayland+NVIDIA:

1. GDK_BACKEND is not set, so without an X11 display in the sandbox GTK
   aborts with "Failed to initialize GTK".
2. libayatana-appindicator3 is not bundled in the GNOME 47 runtime, so
   libappindicator-sys panics the main thread.
3. The release binary is compiled via \`cargo build --release\` rather than
   \`cargo tauri build\`, so the \`custom-protocol\` feature is off and
   Tauri falls back to devUrl — the window opens but shows
   "Could not connect to localhost".

Rolling back so main stays on 1.43.0. A follow-up on the original PR
tracks the fixes needed before re-attempting.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:50:06 +02:00