Commit Graph

733 Commits

Author SHA1 Message Date
kilyabin 97d99b8c5e feat(flatpak): add Flatpak packaging and CI release pipeline
- GNOME Platform 48 runtime (WebKitGTK included)
- GDK_BACKEND=wayland,x11 via finish-args (fixes GTK panic on pure-Wayland)
- libappindicator3 from flathub/shared-modules (not in GNOME 48 runtime)
- npm run tauri -- build --no-bundle (enables custom-protocol, fixes blank window)
- build-flatpak job in reusable-channel-publish.yml
- In-app updater disabled via VITE_PSYSONIC_FLATPAK=1
2026-04-29 01:57:51 +04:00
cucadmuh a9573625f4 ci(release): gate promote-main on explicit main checks (#349)
* ci(release): gate promote-main on explicit main checks

Add an always-on ci-main workflow that produces a stable check name, and validate that check via check runs (not branch protection APIs). Wire promote-main-to-next to require ci-main / ci-ok and document the required check in the release SOP.

* ci(release): accept ci-ok or UI-style name in main promote gate

GitHub check run names for normal workflows are often the job name only;
keep optional match for the UI-style workflow/job label. Drop unused
statuses:read permission.

Made-with: Cursor
app-v
2026-04-29 00:22:51 +03:00
cucadmuh 8fe75b3d74 ci(release): ignore current run when validating main checks (#348)
Exclude check runs and status contexts from the current workflow run in the validation fallback path so the promote gate does not fail on its own in-progress job when branch protection contexts are unavailable.
2026-04-28 22:48:11 +02:00
cucadmuh 408052adc8 ci(release): validate only required main checks before promote (#347)
Fix the main-branch promotion gate to evaluate required status contexts from branch protection instead of all check runs, preventing self-blocking while the validator job is still in progress.
2026-04-28 22:43:22 +02:00
cucadmuh 42c6b2274d chore(release): prepare 1.44.0 dev version and aur tag source (#346)
* chore(release): prepare 1.44.0 dev version and aur tag source

Set the app version to 1.44.0-dev in package manifests and update the AUR source tag template to app-v$pkgver while keeping pkgver on the current stable release.

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-28 23:34:03 +03:00
cucadmuh bf38288feb ci(release): split channel pipelines and automate version transitions (#345)
* ci(release): split channel pipelines and automate version transitions

Introduce dedicated next/release orchestration workflows backed by a reusable publish pipeline with RC/final package version updates and post-release main dev bump PRs. Add a strict release process SOP with RC freeze, hotfix override, and mandatory backport rules.

* ci(release): gate main-to-next promotion on green CI

Add a dedicated workflow that validates main branch checks/statuses and block the promote-main-to-next flow unless main is green. Update the release SOP to reflect the enforced pre-promotion validation.

* ci(release): fix channel promotion edge cases and policy gaps

Switch channel promotions to reset-based snapshots with force-with-lease pushes, stop auto-merging channel nix-refresh PRs, and guard main dev bump against version downgrades. Add RC changelog fallback, require AUR release updates in SOP, and extend npmDepsHash sync to main/next/release pushes.

* docs(release): clarify backport and nix refresh survivability rules

Require RC fix backports to reach main before the next main-to-next promotion, and document that channel-local nix refresh PRs are advisory unless equivalent changes are merged into main.
2026-04-28 23:20:01 +03:00
cucadmuh 8141c5213f fix(player): reserve preview row space in delay modal (#344)
Prevent the timer modal from shifting when the "Paused at/Starts at" preview appears on hover.
2026-04-28 09:23:19 +03:00
github-actions[bot] bce65be287 chore(nix): refresh lock + npmDepsHash for v1.44.0-rc1 (#341)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-27 18:20:47 +00:00
cucadmuh cfd7fb369a ci(nix): auto-sync npmDepsHash when package-lock changes (#340)
Add a GitHub Actions workflow that recomputes prefetch-npm-deps from
package-lock.json and updates nix/upstream-sources.json so Nix builds stay
aligned without manual hash edits.
2026-04-27 21:20:19 +03: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 8967ca825d chore(credits): catch up Settings contributors for #261, #324–#336 (#338)
- Move PR #261 (library deep links / psysonic2 scheme) from
  Psychotoxical to cucadmuh — original author
- Add cucadmuh: #324, #326, #331, #332, #333
- Add Psychotoxical: #328, #329, #330, #336

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:58:17 +02: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
cucadmuh cf09fd4bd3 fix(player): Lucky Mix + mix rating filter (Navidrome / OpenSubsonic) (#332)
* fix(player): Lucky Mix respects mix rating filter and harden rating reads

Wire Lucky Mix through the same Settings → Ratings filter as Random Mix
(enrich + passesMixMinRatings). Prefetch artist/album ratings via
getArtist/getAlbum using both userRating and Navidrome-style rating,
and bump the in-memory prefetch cache key so stale misses are not reused.
Resolve artist entity id from OpenSubsonic artists[] or contributors[]
when top-level artistId is missing; read rating on child song and artist
refs so thresholds apply consistently.

* chore: clarify JSDoc for mix rating enrich vs Lucky Mix filter
2026-04-27 01:30:41 +03:00
cucadmuh 8a97a35ce6 Merge pull request #331 from Psychotoxical/feat/player-queue-undo-redo
feat(player): queue undo/redo with hotkeys, playback-aware restore, and scroll restoration
2026-04-27 01:05:49 +03:00
Maxim Isaev 55a49a9fb0 feat(player): restore queue list scroll on queue undo/redo
Store the main queue panel viewport scrollTop in undo snapshots and
reapply it after undo/redo so Ctrl+Z / Ctrl+Shift+Z restores scroll
position alongside queue state.
2026-04-27 00:33:47 +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 39f4d03da5 feat(player): queue undo/redo with hotkeys and playback-aware restore
Snapshot queue, current track, time, and pause before queue mutations.
Ctrl+Z/Cmd+Z restores prior state; Ctrl+Shift+Z/Cmd+Shift+Z reapplies undone
edits when the redo stack is non-empty; new edits clear redo.

Resync the Rust audio engine only when the current song identity changes
from the pre-apply state so reorder/enqueue-style edits keep live playback.

Register the document capture listener from main.tsx after the window label
is set. Mini player forwards undo/redo via mini:undo-queue and mini:redo-queue.
2026-04-27 00:17:10 +03:00
Frank Stellmacher e0c53da94d feat(playlists): confirm before adding songs that are already in the playlist (#329)
Closes #327.

When every selected song is already in the target playlist, the
"Add to Playlist" flow used to silently show an "all skipped" toast.
Users could not tell whether they had hit the wrong target or whether
the action was deliberately ignored, and they had no way to add
duplicates intentionally (e.g. for a song they actually want twice).

New behavior: if every id is a duplicate, ask via the existing
GlobalConfirmModal — "Add anyway" appends them as duplicates,
"Cancel" keeps the previous silent-skip toast.

Mixed cases (some new, some duplicate) and the all-new path are
unchanged. Applied at all three add-to-playlist call sites in the
context menu (single/multi-track, album selection, artist selection).
Strings added to all 8 locales (en, de, es, fr, nl, nb, ru, zh).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:13:13 +02:00
Frank Stellmacher 9fe81ee6f6 feat(login): add language picker on the login page (#328)
The language selector previously lived only in Settings, which is
behind login. New users on a non-English system had no way to switch
to their language before connecting to a server.

Add a compact CustomSelect in the top-right of the login card. Reuses
the existing settings.languageXx labels and i18n.changeLanguage flow
(persists to localStorage as psysonic_language). English remains the
default for first launches — already the case in i18n.ts:12.

Styled to be visually quieter than the Settings variant (transparent
background, smaller font) so it doesn't pull focus from the logo and
form.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:58:06 +02:00
cucadmuh d67db230ad Merge pull request #326 from Psychotoxical/fix/loudness-lufs-and-analysis-cpu-seed-queue
fix(analysis,waveform): stabilize LUFS loop, serialize CPU seed work, and introduce mixed mean/peak waveform bins
2026-04-26 22:14:57 +03: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 faa931ff68 Merge pull request #324 from Psychotoxical/fix/queue-resizer-hit-test
fix(ui): queue panel resize handle no longer blocked by main scroll hit-test
2026-04-26 17:52:52 +03:00
Maxim Isaev 90a6f09b9f fix(ui): queue panel resize handle no longer blocked by main scroll hit-test
The queue resizer overlaps the main column by a few pixels; treating
clientX <= mainRight as "main" suppressed grabs on that strip. Cap overlay
thumb horizontal slop at mainRight so it does not steal hits on the
resizer. Sidebar / other overlay thumbs stay out of scope via selector.
2026-04-26 17:51:31 +03:00
cucadmuh 67a100ee3f Merge pull request #323 from Psychotoxical/feat/song-info-double-click-copy
feat(ui): copy song fields from song info via double-click
2026-04-26 17:37:25 +03:00
Maxim Isaev f92cfa183d feat(ui): copy song fields from song info via double-click
Double-click the Title, Artist, or Album value to copy plain text to the
clipboard with toast feedback. Apply user-select: none on those cells so
double-click does not trigger word selection.
2026-04-26 17:33:58 +03:00
cucadmuh 1dcc1e101c Merge pull request #322 from Psychotoxical/fix/context-menu-album-play-next-smart-playlists
fix(ui): queue full album after current; filter smart playlists in add targets
2026-04-26 17:24:15 +03:00
Maxim Isaev 577950dc22 fix(ui): queue full album after current; filter smart playlists in add targets
Album context menu loads tracks and inserts them immediately after the
current queue position, or starts album playback when nothing is playing.
Add-to-playlist and merge-target submenus omit psy-smart-* playlist names
so dynamic Navidrome lists are not offered as manual append destinations.
2026-04-26 17:22:17 +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
Frank Stellmacher c4a283b809 fix(imageCache): share refcounted blob URLs across consumers (Windows perf) (#321)
The previous design handed every <img> its own URL.createObjectURL for the
same cached Blob. WebKitGTK shrugged it off, but Chromium/WebView2 keys
its decoded-image cache by URL — so identical thumbnails were re-decoded
once per instance. On Windows this made cover/artist grids painfully slow
even when blobs were warm in memory.

Refactor the URL layer to be refcounted and shared:

- New acquireUrl(cacheKey) / releaseUrl(cacheKey) API. First acquire
  creates the URL; subsequent acquires return the same string and bump
  the refcount. Revoke is deferred 500 ms after the count hits zero so
  in-flight decodes finish cleanly.
- useCachedUrl uses a lazy useState initializer: when the blob is hot,
  the very first <img src> is already the blob URL. No fetchUrl→blobUrl
  swap, no decode thrash, no race against the LRU.
- CachedImage passes fallbackToFetch=false: previously the <img>
  briefly carried the raw server URL while the blob resolved, which
  triggered an HTTP fetch that the browser then aborted when src
  flipped to blob: — visible in DevTools as a flood of "Pending / 0 B"
  requests. Memory hits remain instant via the synchronous acquire
  path; cold paths now do a single fetch via getCachedBlob.
- invalidateCacheKey / clearImageCache now also purge URL entries.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:17:12 +02:00
Frank Stellmacher 6a39c5aaa1 Merge pull request #319 from Psychotoxical/chore/credits-v1.44
chore(credits): catch up Settings contributors list for v1.44
2026-04-26 11:55:50 +02:00
Psychotoxical 8b30d3bdfa chore(credits): catch up Settings contributors list for v1.44
Append-only update of CONTRIBUTORS in Settings.tsx for the v1.44 cycle.
cucadmuh gains the medulla-perch perf fix (PR #283), Navidrome smart
playlists (PR #289), and the LUFS loudness cache (PR #315).
Psychotoxical gains 20 entries since the sleep-timer ring (PR #272),
including Orbit (PR #304), the Tracks hub (PR #300), the genres tag
cloud (PR #311), the seekbar truewave/pseudowave split (PR #316),
and the cross-device resume fix (PR #318).

Skipped chore/CI/internal PRs: #285–#287 (credits self-update), #292–
#297 (Flatpak test tags + revert), #306–#310 (deps + devtools), #312
(debug log).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:55:30 +02:00
Frank Stellmacher 8a6ff04b05 Merge pull request #318 from Psychotoxical/fix/cross-device-resume
fix(player): cross-device resume — push queue position on pause + all exit paths
2026-04-26 11:15:41 +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
Frank Stellmacher c674651d35 Merge pull request #317 from Psychotoxical/ui/normalization-settings-refactor
ui(settings): restructure Normalization section
2026-04-26 03:11:43 +02:00
Psychotoxical 756b189bcc ui(settings): restructure Normalization section for clarity and breathing room
The mode picker (Off / ReplayGain / LUFS) used to live in the right-hand
action slot of a settings-toggle-row and the per-mode controls were
stacked into the same parent with footer-style help text. Hard to scan,
visually cramped, and odd compared to the rest of the Settings page.

Refactor:

- Mode picker becomes a full-width segmented row with even-flex buttons,
  using the new .settings-segmented utility.
- Each mode renders its own .settings-norm-block sub-section with a
  subtle accent tint and border so the active configuration reads as
  one coherent group.
- Inside the block, every setting is its own .settings-norm-field
  (control row + per-control help text immediately below). 1.1 rem gap
  between fields, 0.45 rem between row and help — clearly groups
  related text without crowding.
- Sliders no longer max-cap at 200 px and instead flex to fill the row.
- Inactive ghost buttons (Off, ReplayGain, RG mode, LUFS targets) get a
  visible border and a faint surface tint so they read as selectable
  slots in dark themes too.
- LUFS mode gets a dedicated note-box explaining that brief volume drift
  on the very first play of a new track is the analysis pass at work,
  not a bug — subsequent plays use the cached measurement, and queued
  tracks are usually pre-analysed during the previous song.
- "Trim before measurement (dB)" renamed to "Pre-analysis attenuation"
  (and equivalents in 8 locales).
- New i18n keys: normalizationDesc, normalizationOff/ReplayGain/Lufs,
  loudnessTargetLufsDesc, loudnessFirstPlayNote, replayGainPreGainDesc,
  replayGainFallbackDesc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 03:10:47 +02:00
Frank Stellmacher d467ea1779 Merge pull request #316 from Psychotoxical/fix/seekbar-truewave-pseudowave-split
fix(seekbar): split waveform style into truewave + pseudowave
2026-04-26 02:47:37 +02:00
Psychotoxical ed76090a54 fix(seekbar): split waveform style into truewave (analyzed) + pseudowave (deterministic)
The waveform-loudness-cache merge replaced the existing deterministic
per-track-ID waveform with a bins-based one driven by the analysis
cache. The bins-based variant is the better default but the old
deterministic look is still valuable when no analysis is available
(brand-new track, cache-miss, etc.) and several users prefer it.

Split into two explicit options in the seekbar style picker:

- 'truewave' (default, replaces old 'waveform') — bins from the analysis
  cache, with morph-on-arrival animation and flat-line fallback while
  empty.
- 'pseudowave' — pseudo-random heights derived deterministically from
  the track ID. No analysis dependency, no morph, instant render.

Existing persisted seekbarStyle: 'waveform' is migrated to 'truewave'
in onRehydrateStorage so users keep the visual they have today. The
useEffect that builds heightsRef now lists seekbarStyle in its deps so
switching between the two is live.

i18n labels added in all 8 locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 02:45:51 +02:00
cucadmuh 3c1611270a Merge pull request #315 from Psychotoxical/feat/waveform-loudness-cache
feat(playback): waveform loudness cache (EBU R128 + persistent analysis)
2026-04-26 03:29:54 +03:00
Psychotoxical 185cb8f7cd Merge branch 'main' into feat/waveform-loudness-cache 2026-04-26 02:05:10 +02:00
Frank Stellmacher f95391318f Merge pull request #314 from Psychotoxical/fix/queue-preserve-context-on-manual-click
fix(queue): preserve scroll context on manual queue click
2026-04-26 01:41:55 +02:00
Psychotoxical 77b2a5401a fix(queue): preserve scroll context when user clicks a track in the queue
Auto-scroll used to fire on every currentTrack change, including manual
clicks inside the queue list itself. The clicked track would slide off
screen as the list rebased onto the new "next track", which is
disorienting — the user just acted on something specific and expects to
keep seeing it.

Set a one-shot suppression flag from the queue-item onClick handler so
the immediately following auto-scroll effect skips its scrollIntoView
call. Natural advance (track end, prev/next button, anything that does
not originate inside the queue list) leaves the flag untouched and keeps
the original "show what's coming next" behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 01:41:25 +02:00
Maxim Isaev ea93f7fc4d fix(player): keep seekbar waveform on current track during gapless preload
Byte-preload for the next track called refreshWaveformForTrack(next), which
writes global waveformBins and replaced the playing track’s waveform when
seeking near the end. Drop that call and ignore stale RPC results if
currentTrack changed while the fetch was in flight.
2026-04-26 02:36:38 +03:00
Frank Stellmacher 0667d96085 Merge pull request #313 from Psychotoxical/fix/image-cache-per-component-urls-clean
fix(imageCache): per-component object URLs (no shared LRU)
2026-04-26 01:33:09 +02:00
Psychotoxical ab1b1dcffa fix(imageCache): give every cached <img> its own object URL
The previous design kept a single global Map<cacheKey, objectURL> with an
LRU cap of 150 and aggressively revoked the oldest URL on overflow. On
libraries with more than 150 cached covers (artist + album grids quickly
exceed that), an in-use URL would get revoked because a different
consumer pushed it out of the cache, producing the "Failed to load
resource: blob:..." flood that several users have reported.

Refactor the cache to be blob-centric:

- Public API is now getCachedBlob() returning the Blob itself.
- In-memory LRU now holds Blobs (cap 200), not URLs. Map-entry eviction
  drops the strong reference and lets the GC free the Blob once no
  consumer (object URL, <img>, <canvas>) still holds it. No revoke choreography needed.
- useCachedUrl creates its own URL.createObjectURL on blob arrival and
  revokes it on cleanup with a 500 ms grace delay so the DOM <img> has
  time to finish decoding the URL we just took away.
- All existing callers keep their string-returning API; no call-site
  changes outside the hook itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 01:32:17 +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