Release CSP blocked runtime <style> injection for community themes; add
explicit style-src/style-src-elem and font-src. Derive the effective theme
synchronously in useThemeScheduler so data-theme updates on the same commit.
* feat(ui): add themed startup splash with deferred window show
Show a theme-aware loading splash before the Vite bundle mounts, hide the
native window until it paints, and use per-theme logo gradient colors.
* docs: note startup splash in CHANGELOG and credits for PR #1030
* docs: attribute PR #1030 startup splash to cucadmuh
* fix(layout): collapse browse toolbar to icons on compact width
On a narrow window the labelled filter buttons wrapped into several rows
and, being a fixed-height sticky header, starved the album grid below them
down to a clipped strip. Wrap each toolbar label in a hideable span and drop
to icon-only in compact (mobile) mode; icons keep their tooltip and
aria-label so the action stays discoverable.
* fix(window): raise minimum window size to 520x640
The old 360x480 floor let the window shrink far past the point the layout
holds together. Floor it at a laptop-friendly size where the compact layout
(icon toolbar + scrollable lists) still works.
* fix(player): scale mobile cover to fit short windows
The cover width was viewport-derived with no height bound, so on a short
window the square grew past its slot and overlapped the title. Bind it to
the shrinkable wrap via max-height and keep it square with auto sizing.
* docs: changelog for small-window layout fixes (#981)
* fix(cover): tier fallback for sparse surfaces and Windows asset URLs
Sparse UI (player bar, queue) now reads disk covers via the same tier
ladder as dense grids, so a warm 800.webp satisfies a 128px request.
Reject non-asset convertFileSrc results on Windows, widen Tauri asset
scope, and seed ladder keys on cover:tier-ready. applyDiskPath uses
seedGridDiskSrcCache only to avoid notify/subscriber infinite loops.
* fix(artist): top-track thumb uses album coverArt already warm in grid
Song coverArt ids often differ from album cover ids (e.g. Octastorium in
the grid vs empty track thumb). Prefer the album row's coverArt on artist
pages and ensure high priority for 32px dense cells.
* fix(cover): albumId for playback/queue; no broken img until disk URL ready
Prefer albumId over track-id coverArt (Navidrome). Wire queue to CoverArtImage
with playback scope. CoverArtImage renders a placeholder div until asset src
exists to avoid the browser broken-image icon.
* fix(test): add song id to resolveArtistPageSongCoverArtId fixture
Pick<SubsonicSong, …> requires id; fixes tsc in CI/build.
* fix(cover): resolve albumId for Now Playing and artist top tracks
Prefer albumId when album.coverArt echoes track id; use sparse surface
on artist suggestion thumbs; apply resolveSubsonicSongCoverArtId across
playback surfaces (Now Playing, fullscreen, mobile, mini).
* fix(cover): decode PNG from Subsonic before WebP tier encode
Enable `png` in the image crate — some servers return PNG cover art;
failed decode left `.fetch-failed` and empty thumbs for those albums.
* refactor(cover): consolidate cover id resolution and align tests
Move resolveSubsonicSongCoverArtId helpers to src/cover/resolveCoverArtId.ts
with resolvePlaybackTrackCoverArtId for player surfaces; co-locate tests;
fix FullscreenPlayer expectations for albumId-first resolution.
* docs: CHANGELOG and credits for PR #878
* fix(cover): keep per-track coverArt when distinct from song id
Address PR #878 review (b): albumId only when coverArt is missing or
echoes track id; pin case with unit test; comment isRawFsPath symmetry.
* chore(cover): address PR #878 review nits (scope, tests, rename)
Narrow asset scope to cover-cache dirs only; add diskSrcCache Windows-path
tests; rename ArtistTopTrackCover; CHANGELOG symptom-first wording.
* fix(cover): restore asset scope to app data dirs (Windows regression)
$APPDATA/cover-cache/** did not match Tauri scope resolution — covers
were blocked after load. Use $APPDATA/** and $APPLOCALDATA/** (no $DATA).
* fix(cover): Windows asset URLs — restore DATA scope, path normalize
Regression after review nits: dropped $DATA/** and strict isAssetProtocolUrl
blocked valid http://asset.localhost URLs on Windows. Normalize C:/ paths
before convertFileSrc; CoverArtImage/Hero hide broken img on load error.
* fix(cover): disk peek fallbacks when cache folder id differs
Small surfaces resolve albumId while cover-cache often stores WebP under
track id or album.coverArt from the grid. Peek batch now tries legacy ids;
playback scope resolves server index key by URL key, not UUID-only lookup.
* fix(cover): Navidrome al-* vs mf-* disk id mismatch
UI used mf-* coverArtId while library backfill only cached al-* folders.
Prefer album id for display/peek when coverArt is mf-*; backfill now
queues both distinct album_id and cover_art_id values.
* fix(cover): mf→al disk peek when mf folder missing in cache
Navidrome Subsonic often returns mf-* coverArtId while backfill only
creates al-* folders. Peek mf first, then al-* from hints; load albumId
from library when Subsonic omits it; ensure fallback uses al-* id.
* feat(cover): CoverArtRef, segment disk layout, library-index backfill
Normalize cover caching around stable entity ids from the local library
and Navidrome fetch ids. Disk paths live in psysonic_core::cover_cache_layout
(album/<entityId>/); UI uses CoverArtRef with cacheEntityId + fetchCoverArtId.
- Remove SQLite/mf peek helpers (diskPeekIds, peekCoverOnDisk, mergeDiskIdHints)
- Backfill reads album/artist rows from library SQLite (bare Navidrome ids ok)
- Use stored cover_art_id for HTTP; per-disc dirs only when discs differ
- Migrate call sites to albumCoverRef / albumCoverRefForPlayback
* feat(cover): central CoverEntry resolver (artist, album, track)
Add resolveEntry.ts and Rust CoverEntry helpers as the single source of
truth for cache_entity_id vs fetch_cover_art_id. ref.ts delegates to them;
resolveCoverArtId becomes a thin compatibility shim.
* feat(cover): resolve cover entries from local library index
Add library_resolve_cover_entry IPC and cover_resolve.rs so album,
artist, and track covers use SQLite cover_art_id + disc detection.
TypeScript helpers in resolveEntryLibrary.ts prefer the index over
live API fields when rows exist.
* feat(cover): library-first hooks for grids and playback UI
Add useAlbumCoverRef, useArtistCoverRef, useTrackCoverRef, and
usePlaybackTrackCoverRef — sync fallback then SQLite index upgrade.
Wire album/artist cards, album header, song card, and all player
surfaces to resolve covers from the local library when indexed.
* feat(cover): complete library-first migration across all UI surfaces
Add Album/Artist/TrackCoverArtImage, useLibraryCoverPrefetch, and batch
resolve helpers. Migrate grids, search, home, playback sidecars, warm
peek, playlists, and share flows to hooks that upgrade from SQLite.
Backfill normalizes album rows through cover_resolve; document paths in
COVER_PATHS.md. Radio remains a deliberate non-library exception.
* fix(cover): stop render loop from unstable serverScope in library hooks
Default param `{ kind: 'active' }` created a new object every render, so
every grid cell re-ran library_resolve IPC and setState in a loop. Use
COVER_SCOPE_ACTIVE singleton, coverScopeKey deps, and guarded sync updates.
* chore(cover): remove COVER_PATHS.md from app tree (lives in workdocs)
Audit doc is team spec — see workdocs 2026-05-cover-art-pipeline/cover-paths-audit.md.
* fix(cover): unstick library backfill after route changes (PR #870 regression)
useCoverNavigationPriority cleanup called beginNavigation instead of end,
leaking navigationHoldDepth so ui_priority_hold never released and backfill
never downloaded. Also skip disk check after cover_resolve normalization.
* fix(cover): segment progress, cap backfill CPU, include artists in catalog
Progress and disk size now scan album/ and artist/ segments (canonical 800.webp).
Prune legacy flat server/al-* dirs on startup and backfill pass.
Backfill: max 2 concurrent ensures; JPEG decode and WebP encode run on the
blocking pool behind a shared 2-permit semaphore so Tokio workers stay cool.
Artists were missing because the catalog only read the empty artist table;
add distinct artist_id from track and album rows. Paginate with a composite
(kind, id) cursor so album and artist rows are not skipped.
* fix(cover): drop legacy prune; backfill per-disc and artist catalog
Remove prune_legacy_* and cover_cache_catalog_entry — layout is only
cover_dir (album|artist segments); stale flat dirs clear on LAYOUT_STAMP change.
Backfill: artists from track/album artist_id; expand albums to per-CD mf-* slots
when discs differ; fix resolve_album_cover_entry when album row is missing.
* fix(cover): reduce library IPC storms and fix multi-disc player art
Skip per-row library_resolve on live search and artist album grids; warm
grids from API coverArt after mount instead of blocking layout. Dedupe and
cap concurrent library_resolve calls. Restore per-disc cache keys in the
player and queue when track mf-* art differs from the album bucket.
* fix(cover): skip library resolve on advanced and full search rows
Use API coverArt for album/artist rails and lazy viewport artwork so
result pages do not fire hundreds of library_resolve IPC calls at once.
* fix(cover): default libraryResolve off for browse grids and rails
Skip per-card library_resolve on album/artist/song browse UI by default;
keep it on album/artist headers, playback queue rows, and orbit approval.
* fix(cover): split UI/backfill CPU pools and restore mainstage hero carousel
Library backfill no longer shares the 2-permit JPEG/WebP semaphore with
visible cover ensures. Hero initializes albums from props, re-binds scroll
visibility after mount, updates backdrop on slide change, and uses library
resolve for correct cover art on the banner.
* fix(analysis): resume full-library scan after candidates phase
Reset the SQL cursor when entering full-library mode so tracks with
partial analysis are not skipped. Tighten TS backfill completion and
CPU queue watermarking; align cover-cache key tests with album-scoped
storage keys.
* fix(library): remove useless map_err in cover_resolve (clippy)
CI treats clippy::useless-conversion as error on rusqlite optional() chains.
* fix(cover): satisfy clippy on cover_cache_ensure IPC args
Pass CoverCacheEnsureArgs as a single Tauri parameter instead of nine
positional fields; align frontend invoke payload with { args }.
* chore(cover): scaffold cover module and rust cover_cache stub
Wave 0: src/cover/ skeleton per contracts.md §12, stub IPC commands
in cover_cache/mod.rs (no-op returns until phase B).
* feat(cover): add unified cover module and tier resolver (phase A)
Wave 1A: tiers, storage keys, resolveJs with cold/sibling races,
useCoverArt, CoverArtImage, layoutSizes, playback scope helpers,
coverSiblings tier ladder, deprecated shims on subsonicStreamUrl.
* feat(cover): rust disk cache and tier-ready events (phase B)
Wave 1B: cover_cache module with WebP tier encode, HTTP canonical 800 fetch,
cover_cache_* commands, cover:tier-ready / cover:evicted events, disk layout tests.
* feat(cover): prefetch hook, tier-ready handoff, library backfill IPC (phase B/C)
Wave 2: useCoverArtPrefetch, cover:tier-ready/evicted bridge, one-time IDB
cover key clear, prefetch registry drain, MainApp wiring.
* feat(cover): migrate dense grids to CoverArtImage and prefetch (phase D)
Wave 3A: dense surfaces use layout-native displayCssPx, surface=dense,
coverPrefetchRegister on Home/Albums/search; AlbumCard cell width from grid.
* feat(cover): migrate sparse surfaces and integrations (phase E sparse)
Wave 3B: sparse CoverArtImage/useCoverArt, lightbox tier 2000, ArtistHeroCover,
MPRIS/Discord/export integrations, playback chrome and detail heroes.
* feat(cover): revalidation scheduler and disk pressure gate (phase E+)
Wave 4: coverCacheMaxMb settings (en/ru), StorageTab disk usage, cover_cache_configure,
useCoverRevalidateScheduler, playbackServer uses cover fetchUrl; pressure watermarks.
* docs: CHANGELOG and credits for cover art pipeline PR #869
* fix(cover): stop webview getCoverArt storm on dense grids (429)
Dense surfaces no longer put rotating getCoverArt URLs in img src; load
disk via Rust ensure + convertFileSrc. Tier-ready notifies listeners instead
of invalidating IDB. Throttle background prefetch and cap Home registry.
* fix(cover): omit empty img src until cover URL is ready
React 19 warns on src=""; CoverArtImage uses undefined until disk/IDB
resolves; queue current track shows placeholder when src is still empty.
* fix(cover): disk cache by host index key, parallel ensure, asset protocol
Bind cover storage to serverIndexKey (library host), rename cover IPC/events,
fix REST base URL and Tauri flat args, enable protocol-asset for disk paths,
add prioritized ensure queue, and wipe legacy profile-UUID cache once.
Limit Vite dep scan to index.html so research/target HTML is ignored.
* fix(cover): WebP tiers, disk peek, home cache, asset URLs for mainstage
Encode lossy WebP (~82), write only missing tiers, library cover backfill,
and cover_cache_peek_batch for fast paint from disk. diskSrcCache + CSP
asset protocol; no IDB fallback when server is up. Session Home feed cache
with warm peek on return; BecauseYouLike deduped cover hook and high prefetch.
* feat(cover): per-server cache strategy and native library backfill
Move cover disk cache settings to Offline & cache with Lazy/Aggressive
per server, per-server clear, and no size cap. Run full-catalog backfill
on the Rust runtime (sync-idle wake, bounded HTTP, bulk 800px writes
without flooding the webview). Drop global prefetch limits from auth store
and waveform clear from the offline storage block.
* fix(build): CSP connect-src for Subsonic API; quieter prod nix build
Prod webview blocked axios ping after cover CSP (missing connect-src).
Drop cargo tauri -v in flake build, raise Vite chunk limit, ignore tsbuildinfo.
* fix(cover): complete WebP ladder in library bulk backfill
Aggressive backfill now writes all derived tiers (128–800), skips IDs
only when the full ladder exists (not 800 alone), avoids fetch-failed
markers on bulk HTTP errors, and stops the pass when the active server
changes.
* fix(cover,home): navigation-priority backfill and Because You Like UX
Pause library cover backfill while navigating; split peek/ensure traffic
so grids and rails win over bulk work. Disk src lookup, grid warm hooks,
and non-blocking mainstage prime for faster visible covers.
Because You Like: session snapshot, staggered horizontal skeleton row,
text hidden until cover is ready, and layout aligned with loaded cards.
* feat(random-albums,library): local-first album fetch + cover art pipeline
Random Albums теперь запрашивает локальный SQLite-индекс (ORDER BY RANDOM()
LIMIT N) вместо сетевого запроса к серверу. При готовом индексе спиннер
исчезает практически мгновенно; сеть используется только как фолбэк.
- advanced_search.rs: добавляет `("random", _) => RANDOM()` в allowlist сортировок
- browseTextSearch.ts: runLocalRandomAlbums — SQLite-рандом для Albums
- RandomAlbums.tsx: doFetchRandomAlbums local-first для обоих путей (без жанра
и с жанром через runLocalAlbumsByGenres + JS-shuffle); speculative reserve
прогревает следующий батч в фоне после каждого Refresh
Также: обновление пайплайна обложек (coverTraffic, peekQueue, ensureQueue,
diskSrcLookup, warmDiskPeek, prefetchRegistry, useCoverArt, useWarmGridCovers,
useCoverNavigationPriority, resolveIntersectionScrollRoot и сопутствующие
компоненты/хуки).
* fix(random-albums): prevent double-load on Zustand rehydration
useEffect([selectedGenres, load]) fired twice on every visit: first with
default store values, then again ~50 ms later when Zustand rehydrated
mixMinRatingFilterEnabled/minAlbum/minArtist from localStorage.
Previously this was invisible because the first network fetch took ~1.5 s,
so loadingRef.current was still true on the second fire. With the new
local-first SQLite path the first load completes in ~50 ms, leaving the
guard cleared before rehydration triggers a second random batch.
Fix: ref-pattern — keep loadRef.current fresh on every render, effect
depends only on selectedGenres. Manual Refresh and genre-filter changes
still call the latest closure correctly.
* fix(random-albums): stop warmCoverDiskSrcBatch in fillReserve from causing visual flash
fillReserve вызывал warmCoverDiskSrcBatch для обложек резервного батча, что
вызывало bumpDiskSrcCache() для каждой новой обложки (~30+ вызовов). Это будило
всех подписчиков useCoverArt на текущей странице, провоцируя видимую перерисовку
примерно через ~1.5 с после загрузки (когда filterAlbumsByMixRatings делает
сетевые запросы к рейтингам артистов).
- fillReserve: убран warmCoverDiskSrcBatch — обложки прогреваются лениво при
consume резерва через primeAlbumCoversForDisplay
- reserve-путь в load(): добавлен primeAlbumCoversForDisplay перед setAlbums
(аналогично non-reserve пути; при уже прогретом кэше — мгновенно)
* feat(because-you-like): reserve-first pattern — instant display on return visits
Каждый визит на Mainstage после первого теперь отдаёт готовую заготовку
мгновенно, вместо spinner → сетевые запросы → контент.
Архитектура:
- resolvePicks / fetchBecauseYouLike вынесены на уровень модуля (выход из
замыкания useEffect); читают текущий localStorage, возвращают
{ anchor, recs, nextAnchorHistory, nextPicksHistory }
- fillBecauseReserve — fire-and-forget фоновая функция: запускается сразу
после отображения результата, кладёт следующий батч в _becauseReserve.
Covers намеренно не прогреваются (bumpDiskSrcCache на текущей странице не
нужен); они прогреваются через primeAlbumCoversForDisplay при consume.
- useLayoutEffect: если reserve готов — не сбрасывает стейт в skeleton
(контент появляется без мигания)
- useEffect: reserve-first path — consume → primeCovers → setState → fill;
full-fetch path сохранён как fallback при первом визите или промахе
Поведение:
- Визит 1: full fetch (как раньше) → показ → fillReserve R1
- Визит 2+: consume R1 → мгновенный показ → fillReserve R2
- При сетевом сбое: restore из session cache (как раньше)
* fix(because-you-like): initialise state from reserve — no skeleton flash on remount
При ремаунте компонент стартовал с refreshing=true/anchor=null/recs=[] и
показывал skeleton на один тик до того как useEffect отработает.
Теперь useState() использует lazy initializers, которые читают _becauseReserve
прямо в первом рендере: если reserve валиден — state сразу refreshing=false,
anchor=X, recs=[...] и skeleton не показывается вообще. Covers уже в diskSrcCache
(из предыдущего показа) и появляются без дополнительных запросов.
useLayoutEffect упрощён: вызывает hasValidReserve() и сбрасывает в skeleton
только если reserve отсутствует (для случая navigation без ремаунта).
* fix(because-you-like): apply reserve in useLayoutEffect to handle async pool arrival
Lazy initializers не могли применить reserve при первом рендере, потому что
mostPlayed/recentlyPlayed/starred приходят из Home.tsx асинхронно — pool=[]
на первом рендере, poolKey не совпадает с reserve.
useLayoutEffect теперь активно ставит стейт из reserve (а не просто не сбрасывает):
когда pool обновляется до реальных данных, useLayoutEffect срабатывает синхронно
до paint, проверяет reserve и сразу применяет anchor/recs/refreshing=false.
При отсутствии reserve — сбрасывает в skeleton как прежде.
* fix(because-you-like): reserve > cache > skeleton — eliminate skeleton flash on mount
Корневая причина: Home.tsx загружает mostPlayed асинхронно через useEffect,
поэтому на первом рендере pool=[], poolKey=''. Reserve хранится с реальным
poolKey → mismatch → lazy initializers запускали skeleton.
Теперь двухуровневый fallback без зависимости от poolKey:
1. reserve (serverId + poolKey совпадают) → мгновенный новый батч
2. becauseYouLikeCache (только serverId) → stale-while-revalidate, контент
доступен сразу с mount, обновляется тихо в фоне
3. skeleton → только при полном отсутствии данных (первый визит)
Применяется одинаково в lazy useState initializers, useLayoutEffect и
full-fetch path useEffect (не сбрасывать в skeleton пока есть cached контент).
* fix(because-you-like): key reserve by serverId only; guard useEffect on empty pool
Проблема: reserve хранился с poolKey, но на первом рендере pool=[] → poolKey=''
→ mismatch → показывался кэш (предыдущий набор) ~500ms пока Home.tsx не загружал
mostPlayed.
Исправления:
- BecauseReserve: убран poolKey — reserve валиден для любого pool-состояния
на том же сервере. Pool (топ-артисты) меняется медленно; один раз показать
reserve с чуть устаревшим anchor лучше чем показывать предыдущий набор 500ms
- hasValidReserve: проверяет только serverId
- fillBecauseReserve: убран poolKey из сигнатуры и хранилища
- useEffect: guard pool.length === 0 → возврат без fetch/consume;
effect перезапустится когда pool заполнится (реальные deps изменятся)
→ reserve применяется из useLayoutEffect ещё до pool, без стале-флэша
Итоговый порядок: reserve (instant, serverId) > cache (stale-while-revalidate)
> skeleton (только первый визит)
* fix(home): remove mix-rating deps from feed useEffect — prevent Zustand rehydration double-fetch
Корень: useAuthStore(mixMinRatingFilterEnabled/Album/Artist) были в deps
useEffect. Zustand persist реhydrates асинхронно — сначала activeServerId,
потом mix-rating значения. Это вызывало двойной запуск эффекта:
- Первый запуск: homeFeedCache hit → показывает набор предыдущего просмотра
- Второй запуск (после rehydration): cache miss или повторный fetch с
реальными mix-настройками → ~500ms → новый набор
Итог: Hero, AlbumRow, BecauseYouLikeRail показывали предыдущий набор
первые ~500ms при каждом возврате на Mainstage.
Fix: убраны mixMinRatingFilterEnabled/Album/Artist из deps. getMixMinRatingsConfigFromAuth()
читается внутри эффекта через getState() — всегда актуальные значения без
пересоздания замыкания. Mix-настройки по-прежнему применяются при fetch,
но не вызывают двойной запуск при rehydration.
* feat(home): local-first discover songs via SQLite ORDER BY RANDOM()
Добавлена runLocalRandomSongs (аналог runLocalRandomAlbums для треков)
в browseTextSearch.ts — использует libraryAdvancedSearch с sort random,
field уже поддерживается Rust-кодом через wildcarded ("random", _) ветку.
В Home.tsx: discoverSongs теперь сначала пробует локальный индекс,
и только при недоступности (индекс не готов, ошибка) падает обратно
на getRandomSongs.view. Ускоряет первую загрузку Mainstage — треки
берутся из SSD вместо сети.
* fix(home): pre-populate state from cache at mount — eliminate empty-state flash on return visits
Причина: Home.tsx размонтируется при навигации. При возврате первый рендер
всегда с пустыми массивами (heroAlbums=[], mostPlayed=[] и т.д.), потом
useEffect читает homeFeedCache и заполняет state. Даже один кадр с пустым
состоянием вызывает перерисовку Hero и BecauseYouLikeRail (pool=[]).
Решение: getInitialHomeFeed() читает homeFeedCache синхронно через
useAuthStore.getState() (не hook) в lazy useState initializers. К моменту
повторного визита store уже rehydrated — все state получают кэшированные
данные до первого рендера.
Дополнительно: wasPrePopulated предотвращает повторный applyFeedSnapshot
в useEffect когда state уже заполнен — иначе новые ссылки на массивы
вызывали бы ненужные ре-рендеры дочерних компонентов с теми же данными.
* fix(mainstage): keep refresh without return flicker
Keep Home and Because You Like visually stable during a single visit while still refreshing data for the next re-enter. Improve mainstage cover warmup by ensuring and pre-decoding above-the-fold artwork so hero and top rails appear instantly after navigation.
* fix(mainstage): stabilize because rail and hero background framing
Measure Because You Like layout before first paint to avoid width snap flicker, and render hero background as centered cover-fit images so the frame no longer jumps from top to middle on mount.
* fix(now-playing): prewarm track data and prevent stale carry-over
Warm Now Playing fetch caches and playback cover art on track change so entering the page no longer waits on first-load requests. Gate key-based sections (top songs, tour, Last.fm) by the active track/artist keys to avoid briefly rendering values from the previous track.
* fix(cover,test): refresh playback scope and default tauri cover mocks
Recompute playback cover scope when queue/server context changes so now-playing art resolves against the correct server after handoffs. Add default cover-cache invoke handlers to the shared Tauri test harness to prevent unhandled rejections in suites that mount cover-aware UI.
* fix(cover,now-playing,test): align prewarm scopes and tighten tauri mocks
Make cover-cache invoke defaults opt-in for tests, align radio prewarm scope with active rendering scope, and add targeted hook tests for prewarm + playback-scope reactivity. Also harden Rust cover URL building to avoid panic on malformed base URLs.
* test(cover): hoist mocked useCoverArt and clean EOF whitespace
Fix the new playback-scope hook test to use a hoisted vi.mock-safe stub and keep branch-wide diff checks clean by removing an accidental trailing blank line.
* fix(cover): align playback ensure auth and harden backfill retry flow
Use playback-server credentials for playback-scoped cover ensures, persist fetch-failed markers for bulk library backfill failures, and avoid advancing backfill cursor when UI-priority hold interrupts a batch.
* fix(ci): resolve clippy lint and update frontend node runtime
Move fetch helper before the test module to satisfy clippy's items-after-test-module rule, and modernize frontend CI to setup-node v6 with lts/* instead of pinned Node 20.
* chore(settings): simplify cover and analytics strategy copy
Move strategy summaries below tables, simplify Lazy/Aggressive wording, keep analytics warning always visible, and localize Russian texts to plain language without technical jargon.
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>
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>
Critical Windows bug-fix release.
Fixed
- Mini player no longer hangs the app on Windows (see 71fbc717): the
second WebView2 is now pre-built in .setup() so the first open is a
pure show/hide instead of a creation + minimize race. Windows is
back on native window decorations for the mini and skips the main-
window minimize/unminimize dance around show/hide.
- Mini player re-emits mini:ready on window focus so the snapshot from
main arrives reliably on first open even when pre-create finished
before main's bridge attached its listener.
Added
- "Preload mini player" toggle in Settings → General for Linux + macOS
so those platforms can opt into the instant-open behaviour that
Windows already has as a hang workaround.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consolidates everything since v1.40.0 (public) into one coherent
release. v1.41.0 remains as an internal Draft on GitHub that was used
to verify the Cachix substituter pipeline and never went public — this
release folds its intended contents in and adds the mini-player feature
work, the player-bar time toggle (kveld9), the ReplayGain expand badge
and related UX polish on top.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Major release consolidating several months of work into a single coherent
version. The 1.34.x patch series accumulated many small features; 1.40.0
packages them up and signals the infrastructure milestone (macOS signing
+ notarization + auto-updater) clearly.
Highlights (see CHANGELOG for the full rundown):
- macOS builds are signed with a Developer ID certificate and notarized
by Apple — no more Gatekeeper "unidentified developer" dialog
- In-app auto-update on macOS via the Tauri Updater plugin; polished
modal with trust badges, restart countdown, and state-dependent buttons
- Linux WebKitGTK wheel scroll toggle (PR #207 by cucadmuh)
- Device Sync: user-configurable filename template replaced with a
fixed cross-OS scheme; playlists now live in their own self-contained
folders with sibling-referencing .m3u8; one-shot migration tool for
existing sticks
- WCAG contrast audits for middle-earth and nucleo themes
- Contributors list in Settings → About updated (PRs #205, #206, #207)
Windows signing + Windows auto-updater are the remaining gap; 2.0.0 is
planned once both are active.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
No functional changes. Throwaway build to serve as the "latest" release
that v1.34.22 will find, download, verify, and install via the Tauri
Updater. Will be deleted once the updater round-trip is confirmed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A character was lost when the base64-encoded minisign public key was
transcribed into tauri.conf.json, producing a 41-byte decoded key
instead of the required 42 bytes. Every release built against that key
(v1.34.15 through v1.34.21) rejects any update manifest signature with
"Invalid encoding in minisign data". Replaced with the correct pubkey
read directly from ~/.tauri/psysonic-updater.key.pub.
v1.34.19 installed manually for testing cannot receive auto-updates
because the broken pubkey is baked into its binary; a fresh v1.34.22
install is required as the base for the updater test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AppUpdater.tsx: macOS now has a dedicated branch in the update modal —
no architecture-specific DMG asset shown (the Tauri Updater picks the
right platform from latest.json), clearer wording ("Downloads, verifies
and installs automatically"), and the primary button reads "Install
now" instead of "Download". Strings use defaultValue so locales without
the key fall back to English until translations catch up.
- Test release for the updater pipeline; CHANGELOG entry asks users to
ignore it. Will be deleted after the test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Throwaway release to validate the macOS Tauri Updater end-to-end:
v1.34.19 (installed manually) → check() → latest.json → download +
install → relaunch. Contains no actual changes. CHANGELOG entry asks
users to ignore it; will be deleted after the test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tauri-action always re-packs Psysonic.app into .app.tar.gz after tauri
CLI finishes (regardless of includeUpdaterJson), so the .sig that tauri
CLI produces never matches the final tarball — and the sig tauri-action
was supposed to produce silently goes missing ("Signature not found for
the updater JSON. Skipping upload...").
Rather than fight the repack, sign the repacked tarball ourselves in a
follow-up step using `tauri signer sign` against the same private key
(from TAURI_SIGNING_PRIVATE_KEY), then upload the fresh .sig. tauri-
action still handles the DMG + .app.tar.gz upload; we only add the .sig.
Also removes an unused `std::rc::Rc` import in the vendored cpal patch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: tauri-action runs a second "Packaging" pass after tauri CLI
finishes, which rewrites src-tauri/target/<target>/release/bundle/macos/
Psysonic.app.tar.gz with different bytes. The .sig produced by tauri CLI
no longer matches the new hash, so tauri-action silently deletes it —
leaving the directory with only .app and .app.tar.gz (no .sig for our
manifest generator to consume).
Fix: pass `includeUpdaterJson: false` to tauri-action so it skips the
repack + updater JSON upload entirely, then copy both the .app.tar.gz
and .app.tar.gz.sig produced by tauri CLI into the workspace root with
the expected asset names and `gh release upload` them.
Also disables build-linux and the Windows matrix entry during testing
to cut iteration time roughly in half.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace the glob find for the .sig file with an explicit target-based
path ($matrix → src-tauri/target/<target>-apple-darwin/release/bundle/
macos/Psysonic.app.tar.gz.sig) and dump directory listings so we can
see what tauri-action actually leaves behind if the path is wrong
- Skip verify-nix while we iterate fast on signing + updater (its auto-
commits of flake.lock / npmDepsHash cause rebase friction on every
release)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tauri-action on macOS cross-target builds produces the .app.tar.gz.sig
locally but silently skips its upload ("Signature not found for the
updater JSON. Skipping upload..."), which caused generate-manifest to
abort on v1.34.15 because the .sig asset was absent.
New step after tauri-action locates the .sig under src-tauri/target/*/
release/bundle/macos/ and uploads it as Psysonic_aarch64.app.tar.gz.sig
or Psysonic_x64.app.tar.gz.sig — the exact filenames generate-update-
manifest.js expects.
Also bumps version to 1.34.16.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- tauri-plugin-updater wired into lib.rs with pubkey + GitHub Releases
endpoint in tauri.conf.json; updater:default capability granted
- AppUpdater.tsx: on macOS, the download button now invokes the updater
plugin (check + downloadAndInstall) which downloads the signed
.app.tar.gz, verifies the minisign signature against the bundled
pubkey, replaces /Applications/Psysonic.app, and relaunches. Windows
and Linux keep the existing "download DMG/EXE/AppImage via reqwest
then point to the folder" flow
- CI: pass TAURI_SIGNING_PRIVATE_KEY + _PASSWORD to tauri-action so the
.sig files are produced alongside the update bundles
- New generate-manifest job (after build-macos-windows) runs
scripts/generate-update-manifest.js which downloads the .sig files
from the release, assembles latest.json for darwin-aarch64 and
darwin-x86_64, and uploads it back as a release asset
Windows will be added to latest.json once the Certum cert is active.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds YouLyPlus karaoke lyrics + static-only lyrics toggle, collapses
the advanced Discord options under one header, and ships the real
macOS microphone-prompt fix (cpal patch forcing DefaultOutput on all
output streams).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Changelog for v1.34.11 (Opus, Device Sync, community themes, SSL fix)
- Contributors updated in Settings About (PR #181, #182, #183)
- Version bumped to 1.34.11 in package.json, tauri.conf.json, Cargo.toml
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NowPlaying polling: only active while dropdown is open + respects
Page Visibility API — was firing every 10s unconditionally (~8.6k req/day)
- Connection check interval: 30s → 120s (4× reduction)
- Queue sync debounce: 1.5s → 5s (prevents burst on rapid skip)
- Rating prefetch: add 7-minute in-memory TTL cache for artist/album
ratings — repeated random album loads no longer re-fetch known ratings
Fixes server log flooding reported by users behind reverse proxies (Traefik).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Song ratings in context menu + player bar, entity ratings (PR #130),
5 new seekbar styles, custom Linux title bar, album multi-select,
mix rating filter, top-rated stats, compilation filter, scroll reset.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove tauri-plugin-updater, relaunch_after_update command, generate-update-
manifest.js, the generate-manifest CI job, and all signing/upload steps for
.sig files and latest.json. The updater plugin, its pubkey and endpoint config
in tauri.conf.json, and the updater:default capability are all gone.
Replace AppUpdater.tsx with a lightweight component that:
- Fetches https://api.github.com/repos/Psychotoxical/psysonic/releases/latest
after a 4 s delay (no install, no download, no progress bar)
- Shows a dismissible toast with two buttons:
GitHub → releases/latest
Website → https://psysonic.psychotoxic.eu/#downloads
i18n: remove updaterInstall/Downloading/Installing/Download/ExperimentalHint
keys from all 7 locales; add updaterWebsite; update updaterVersion wording.
CI: remove generate-manifest job and all .sig signing/upload steps from
build-macos-windows. Also remove TAURI_SIGNING_PRIVATE_KEY env vars (no
longer needed). Release now just builds and uploads the installable binaries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Internet Radio full release (HTML5 engine, card UI, RadioDirectoryModal,
cover upload), Backup/Restore, Albums year filter, Statistics Library
Insights (playtime/genres/formats), Playlist cover upload, resizable
tracklist columns for Playlists & Favorites, crossfade fine control,
Settings Storage tab redesign, various fixes and UI polish.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>