Merge album/FTS/track sources for title search (any-word LIKE + prefix FTS),
dedupe cluster text hits by album id instead of album_key, route scoped
cluster browse through per-server advanced search, and prefer the local
index over network race when the library index is enabled.
Roll up track candidates to one row per (server_id, album_id) before
partitioning by album_key so the same album on multiple servers (or mixed
keyed/unkeyed tracks) appears once with priority winner per spec §4.
When two or more sidebar libraries are selected in cluster mode, All Albums
text search uses the merged local index only so an empty search3 response from
one member cannot win the local-vs-network race.
Route plain album title search through the synced album table (LIKE)
instead of FTS over all tracks. Use album+EXISTS for single-library
scope; keep track GROUP BY only for multi-library union.
Add library_list_albums (album table when unscoped, track GROUP BY when
scoped), cluster single-member bypass, ready-member caching, and UI load
deferrals. Remove REST scope allowlists from local hot paths; fix multi-
library scope union and invalidate cluster scope cache on picker toggles.
Apply cached getAlbumList2 allowlists per server in SQL and post-filter
so narrowed sidebar scope does not leak albums from other libraries or
cluster members. Wire restrictAlbumScopes into cluster advanced search
and dedicated lossless browse; detect cluster scope in All Albums paging.
Introduce albumBrowseExecution with explicit filter layers (library scope,
AND attributes, OR genre union, finalize) so multi-genre and cluster paths
cannot leak albums from other libraries. Wire multi-select sidebar picker
scope through local index, cluster merge reads, and REST allowlist fallback.
Cluster mode lists music folders from every member (cached for instant
picker), applies per-server musicLibraryFilter scopes in merged browse,
and passes libraryScopes through Rust list/advanced-search SQL filters.
Dropdown width sizes to content; single-server picker unchanged.
Resolve preview container hints from HTTP headers, Subsonic suffix, and
magic-byte sniff after Symphonia 0.6. Route preview streams through
clusterBrowseServerId like main playback; guard CoverArtImage when the
preview cover ref is still loading.
Load artist album tracks via getAlbumForServer when clusterSeedServerId is set
so Play All, shuffle, and top-track clicks resolve on the correct member server.
Also improve connection indicator tooltips, Song Info server labels, artist browse
covers/detail fallbacks, and faster merged artist list SQL.
Add Rust library_cluster_album_detail and library_cluster_artist_detail
with cluster_key dedup, owner metadata fallback, and merged tracklists.
Wire cluster-mode hooks and navigation seedServerId so detail routes
never fall back to single-server getAlbum/getArtist.
Resolve tracks to cluster candidates at play time with cascade fallback;
fan-out scrobble/star/rating to all members; block Orbit host in cluster
mode; warn on mixed-server queue share; serve random picks from merged index.
Add merged favorites listing (starred on any member) and cluster-scoped
play_session stats with cluster_key dedup; wire Favorites page and player
statistics panel to cluster APIs when a cluster is active.
Add Rust list_merged_albums/artists commands and wire cluster-mode reads
into tracks, albums, artists catalog, and text search browse paths via
clusterBrowse helpers.
Add cluster management in Settings (create, reorder, rename, sync toggle),
connection indicator dropdown for servers vs clusters, delete guard when a
server is in a cluster, and i18n across all locales.
Add persisted clusters/activeClusterId, cluster CRUD actions, scope helpers,
representative recompute hook, library_cluster_* API wrappers, and vitest
coverage for cluster store rules and delete guard.
Add library_cluster_list_tracks, library_cluster_resolve_candidates, and
library_search_cluster with priority-ordered dedup by cluster_key and
duration guard. Register Tauri commands; no frontend changes in this step.
Add library-cluster.db (separate from library.sqlite) with track_cluster_key
normalization, batch rebuild, and ATTACH on both library store connections.
No Tauri commands or frontend changes in this PR.
* feat(audio): migrate Symphonia 0.5 -> 0.6
Port the audio + analysis pipeline to the Symphonia 0.6 API:
- bump symphonia to 0.6 and symphonia-adapter-libopus to 0.3; drop rodio's
symphonia-all feature to avoid a duplicate symphonia-core 0.5
- remove the local symphonia-format-isomp4 0.5 patch and rely on upstream 0.6
- switch codec registries to register_enabled_codecs / make_audio_decoder with
AudioCodecParameters + AudioDecoderOptions
- rework decode.rs SizedDecoder and analysis decode loops for the new
AudioDecoder trait, GenericAudioBufferRef, Time/Timestamp newtypes, and
next_packet() -> Result<Option<Packet>>
Streaming regression fixes:
- ProbeSeekGate hides seekability during probe for non-MP4 progressive streams
so Symphonia 0.6's trailing-metadata scan no longer forces a full download
before ranged FLAC/MP3/OGG playback can start
- guard the streaming probe() with a 20s timeout on a worker thread so a stalled
ranged source (e.g. right after a server switch) can no longer hang playback
start until a player restart; add probe start/done diagnostics
* docs(changelog): note Symphonia 0.6 migration and streaming fixes (#999)
Add CHANGELOG entries (Changed + Fixed) and a CONTRIBUTORS line for the
Symphonia 0.6 migration, ranged-stream start-latency fix, and probe timeout.
* test(audio): cover streaming probe path and ProbeSeekGate
Add unit tests for SizedDecoder::new_streaming (success + garbage) and
ProbeSeekGate (seekability toggle, byte_len, read/seek passthrough) to
restore decode.rs above the 70% hot-path coverage gate (67.3% -> 79.4%).
* feat(sidebar): add toggle to pin Now Playing to the top
The fixed Now Playing entry can now be pinned to the very top of the
sidebar (above the Library label) instead of sitting above the bottom
spacer. Adds a persisted `nowPlayingAtTop` setting (default off, so
existing layouts are unchanged) and a toggle in the sidebar customizer
next to the Mix-navigation split. The entry stays non-hideable and
keeps its playing indicator in both positions.
* i18n(settings): Now Playing-at-top toggle strings (9 locales)
* docs(changelog): note Now Playing top toggle (#1000)
* fix(settings): restore full border on active server card
The active server card mixed the `border` shorthand with borderTop/
borderBottom longhands in one inline style object. React clears the
unset longhand sides on mount, so the top and bottom borders fell back
to the subtle base border while only the left/right accent border
showed. Drive the drag drop-target indicator via an inset box-shadow
instead, leaving the border shorthand to apply on all four sides.
* docs(changelog): note active server card border fix (#998)
* chore(deps): bump npm patch/minor dependencies
Refresh frontend lockfile for React, Vite, Vitest, i18next, axios, Tauri
plugins, and related type packages within existing semver ranges.
* chore(deps): bump Rust patch dependencies
Update id3 to 1.17, reqwest to 0.13.4, and align root zbus with psysonic-audio 5.16.
* chore(deps): upgrade jsdom to v29
Major test-environment bump; all Vitest suites still pass and npm audit is clean.
* chore(deps): upgrade sysinfo 0.39 and zip 8
Align root sysinfo with psysonic-syncfs and migrate backup archives to zip 8.
mach2 0.6 deferred — cpal/rodio still require ^0.5.
* chore(nix): sync npmDepsHash with package-lock.json
* docs(changelog): note dependency refresh (PR #997)
* docs(changelog): move deps note to [1.48.0] section
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Add a thank-you note at the top of the 1.47.0 release notes for the
Discord community's support, quality checks, bug reports and general
collaboration, with the Discord invite link.
* fix(tray): stable tray icon id so KDE toggle stops piling up duplicates
TrayIconBuilder::new() assigns a fresh id on every rebuild. On KDE
(StatusNotifierItem) each new id registers a new item and the stale ones
linger in the hidden-icons list, so toggling the tray icon off/on stacked
up duplicate entries. Build with a constant id so every rebuild reuses the
same item.
* docs: changelog for tray icon duplicate fix (#991)
* fix(audio): cap pipewire-alsa client latency on Linux (#862)
On some Linux setups the pipewire-alsa bridge negotiates a multi-second
output buffer, so play/pause/seek/volume only take effect once it drains
(~10-20 s). cpal's buffer-size clamp is ignored by those bridges. Set
PIPEWIRE_LATENCY=256/48000 before the audio stream opens to cap the client
node latency — the reporter confirmed this makes the controls instant.
Linux-only, no-op without PipeWire, and a user-set value is left untouched.
* docs: changelog for pipewire latency fix (#990)
* chore: restore PR order in CHANGELOG [1.47.0] Fixed section
Re-sort ### blocks ascending by PR number per release-note placement rules after out-of-order Discord-fix batch inserts.
* fix(player): stable playbar clocks when showing remaining time
Pad seekbar time strings to a fixed width so ticking remaining time does not resize WaveformSeek via ResizeObserver; tighten clock-to-waveform spacing and keep the toggle icon inline.
* docs: CHANGELOG and credits for playbar remaining-time fix (PR #987)
* docs: CHANGELOG entry for playbar remaining-time waveform fix (#987)
* chore: drop settingsCredits entry for minor playbar fix (#987)
* docs: move #987 CHANGELOG entry to end of [1.47.0] Fixed section
* fix(ui): stop New badge flicker on mainstage album rail first hover
Dim cover via ::before under badges; keep play overlay transparent and
avoid visibility toggles on rail action buttons.
* docs: CHANGELOG for mainstage New badge hover fix (PR #986)
* fix(ui): keep New badge above rail cover zoom (match grid stacking)
Use contain:paint + translateZ layering like New Releases grid so scaled
cover art does not paint over the badge during hover animation.
* fix(ui): global album cover badge stacking for all rails and grids
Move img/badge/overlay z-index into album-card.css; stop album-grid from
resetting card transform or duplicating cover rules that hid New badges.
* docs: CHANGELOG — badge stacking applies to all album rails
* fix(ui): restore rail play buttons above scaled cover layer
Raise play overlay and badge z-index on horizontal rails so WebKit does not paint zoomed cover art over hover controls after the New badge stacking fix.
* fix(search): load album cover for song results
Live Search song rows carried the per-track `mf-…` coverArt id from
search3, which the cover pipeline does not resolve, so the thumbnails went
blank. Album rows worked because they use the album-scoped `al-…` id. A
song's search thumbnail is its album cover anyway — derive it from the
albumId so it loads, and show a music glyph when there is no album.
* docs: changelog for search song cover fix (#984)
* fix(search): reject FTS syntax chars in local index live search queries
FTS5 treats `=` and similar characters as query syntax, so tokens like
1=2 produced unrelated prefix hits. Skip FTS for unsafe tokens instead.
* docs: CHANGELOG for local index FTS syntax query fix (PR #983)
* fix(search): reject syntax junk in server search3 and live search race
Share FTS-safe token guard with search3; skip network invoke for ** and
similar queries; show empty dropdown without misleading source badge.
* fix(search): allow censorship stars in queries, reject wildcard-only tokens
Block ** and **** but keep ***Flawless-style title searches working
in local FTS and search3.
* fix(ui): square song-rail nav buttons to match other rails
The song rail's prev/next (and reroll) buttons were circular while every
other rail uses the square rounded-rect nav button. Align them.
* docs: changelog for square song-rail nav buttons (#982)
* 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(playlist): suggestion rows match the normal track row
The Suggested Songs table left the Favorite and Rating columns empty and
rendered only a single artist, so multi-artist tracks lost their split and
the blank columns read as a gap between Genre and Duration.
- Extract a shared PlaylistArtistCell that splits the OpenSubsonic artists
array into individually navigable links; use it for both the playlist
rows and the suggestions so a track reads the same before and after it is
added.
- Show the real favorite heart and star rating in suggestion rows (global
song operations), seeded from the song's own starred/userRating, removing
the empty-column gap.
* docs: changelog for suggestion row parity (#980)
* fix: show album artist on featured compilation cards
The 'Also featured on' cards are synthesised from search3 child songs,
which only read the flat `albumArtist` field. Compilation children leave
that empty — the album-artist credit lives in OpenSubsonic's structured
`albumArtists` / `displayAlbumArtist` — so the card fell back to the '—'
placeholder. Carry the structured credit (and the display string) onto
the synthesised album so it resolves a name (and stays navigable when the
server supplies an id).
* docs: changelog for featured-compilation artist credit (#979)
* fix: RC3 queue link underline, genre album artist split, Artists "Other" bucket
Three zunoz reports, one change:
- Queue now-playing card: the artist and album links now underline on hover
(not just recolour), matching clickable names everywhere else. The album
link sits on .queue-current-sub itself; artist links are nested .is-link
spans — both selectors covered.
- Genre album cards split multi-artist credits into individual links like the
rest of the app. Root cause was the local-index genre query hardcoding NULL
for the album raw_json column, so OpenSubsonic artists[] never reached the
card and it fell back to the flat "A • B" single link. Select a.raw_json
instead (parity with the All Albums / advanced-search album queries).
- Artists page alphabet index: # is now digits-only, and a new "Other" bucket
collects accented Latin (Æ/Ø/Å…) and non-Latin scripts (CJK, Cyrillic, …)
that previously fell into the # catch-all. Adds artistBucketKey/compareBuckets
helpers (unit-tested), an OTHER bucket sorted last, and the artists.other
label across 9 locales.
* docs(changelog): queue link hover, genre card artist split, Artists Other bucket (#977)
* fix(tracks): RC3 layout regressions + multi-artist links
- Restore vertical rhythm on the Tracks hub: the header/hero/rails/browse
sections sat two wrapper divs below .tracks-page so its flex gap never
reached them and they collapsed together. New .tracks-hub-stack re-applies
the gap, fixing the tagline-touching-hero and Random-Pick-overlapping-hero
spacing (the rail's nav buttons no longer ride into the box above).
- Stop the sticky browse-table header going transparent on hover: its base
background became opaque but the :hover rule still forced transparent, so
rows bled through. Header now keeps its card background on hover.
- Widen the Duration column (56px -> 72px; 56 -> 64 on mobile) so the
"DURATION" header no longer clips/overruns.
- Split multi-artist tracks into individually clickable artist links in both
the "Track of the moment" hero and the browse list rows (OpenSubsonic
artists[] with single-artist fallback), matching the album tracklist.
* docs(changelog): Tracks RC3 spacing, Duration, header hover, multi-artist (#976)
* fix(mainstage): rename Home Page setting, add start-page fallback + empty state
- Rename "Home Page" personalisation section to "Mainstage" across 9 locales
so the heading matches the sidebar entry; add "mainstage" search keyword.
- Index route "/" now redirects to the first visible library item when the
Mainstage sidebar entry is hidden, instead of stranding the app on a blank
page. New pure resolveStartRoute() mirrors sidebar order + nav-mode gating.
- Show a guided empty state on Mainstage when every section is toggled off,
with a CTA into Settings -> Personalisation.
- Unit tests for resolveStartRoute.
* docs(changelog): Mainstage rename, start-page fallback + empty state (#975)
* fix(ui): pointer cursor on the queue collapse handle
The round handle is a click-to-collapse button but showed the col-resize
cursor like the seam strip. Use a pointer on the handle; the seam keeps
col-resize and a real drag still switches the body cursor to col-resize.
* fix(favorites): make Plays, Last Played and BPM columns sortable
The header marked these columns sortable (pointer cursor) but handleSortClick
gated on a separate, narrower column set, so clicks did nothing. Both the
comparator and the header now share one SORTABLE_COLUMNS source, so the
affordance and the behaviour can't drift apart again.
* docs(changelog): queue handle cursor + favorites column sorting (#974)
* fix(random-mix): limit exclusion toggle click area to checkbox and title
The audiobook exclusion was a full-width label wrapping the checkbox,
title and description, so clicking empty space or the description text
toggled it. Make only the checkbox and its title clickable; the
description and surrounding space are no longer hit targets.
* fix(random-mix): stop excluding Thriller and Fantasy as audiobook genres
These keywords match regular music (e.g. Trance/Metal genre tags, a track
titled "Thriller") because the exclusion checks genre, title, album and
artist by substring, dropping a few legit songs per mix. Remove both from
the audiobook keyword list.
* docs(changelog): random mix audiobook exclusion fixes (#973)
* feat(tooltip): 2s open delay and shared tooltipAttrs helper
Add a 2s hover open delay in TooltipPortal (single behaviour source) so
tooltips no longer flash on quick pointer passes; hiding stays immediate.
Add tooltipAttrs() to pair data-tooltip with a matching aria-label for
buttons touched in the unification work. Covered by Vitest.
* feat(tooltip): lower open delay to 1s
2s felt too long in testing; 1s gives the same anti-flash behaviour
without making intentional hovers wait.
* feat(tooltip): action tooltips on the artist overview
Add tooltips describing the action to Last.fm, Wikipedia, Play All,
Shuffle and Radio. Shuffle/Radio now show a tooltip on desktop too,
not just mobile. Strings added to all 9 locales.
* feat(tooltip): action tooltips on the album overview
Add tooltips describing the action to the desktop Play, Artist Bio and
Download (ZIP) buttons, matching the mobile layout. Strings added to all
9 locales.
* feat(tooltip): action tooltips on the All Albums toolbar
Add tooltips describing the action to the sort, year and genre filter
buttons. SortDropdown gains an optional tooltip prop; the year and genre
filter components carry their own, so the tooltips also appear on the
other browse pages that reuse them. Strings added to all 9 locales.
* feat(tooltip): action tooltips on song-list rows
Add Play and Add-to-queue tooltips to the per-row icons in SongRow, used
by the Tracks browse list, Search and Advanced Search. Also localizes the
aria-labels, which were hardcoded English. New common.addToQueue in all 9
locales.
* fix(tooltip): uniform tooltip placement on the Artists toolbar
The favourite and multi-select buttons forced tooltips below while the
view-mode buttons auto-flipped above, so the row looked inconsistent.
Pin the view-mode buttons below too, matching the rest of the row and the
Albums toolbar.
* feat(tooltip): clarify and align the Advanced Search scope row
Add a leading "Search in:" label and per-chip tooltips so the
All/Artists/Albums/Songs row reads as a scope limiter. Drop the forced
below-placement on the small star filter (used only here) so the
favourites chip flips with the others instead of sitting alone below.
Strings added to all 9 locales.
* docs(changelog): tooltip unification (#972)
* fix(playlists): Smart Playlist editor theme, toggles, and exclude-all-genres
Replace native sort select with CustomSelect, fix mode-button layout shift,
color-code included vs excluded genres, collapse exclude-all to untagged rule,
and handle empty smart playlists without false "not found".
* docs: CHANGELOG and credits for Smart Playlist editor fix (PR #970)
* chore(credits): drop minor fix entries from PR #958 onward
* fix(ui): shrink-wrap floating player bar instead of full-width strip
The bar used fixed left and right insets, which stretched its background
across the whole main column. Center it with max-content width so only
the pill-shaped controls are painted, and soften the drop shadow.
* docs: note PR #969 in changelog and settings credits
* fix(settings): improve in-page search matching and coverage
Index AudioMuse and individual shortcut rows, tighten fuzzy matching so
junk queries return no hits, and scroll to the parent subsection when a
shortcut result is selected.
* docs: note PR #968 in changelog and settings credits
* fix(player): clamp custom delay input and align preview with armed timer
Reject absurd custom minute values that overflow setTimeout, share one
delay helper between the modal preview and schedule actions, and refresh
countdown ticks when a new deadline is armed.
* docs: note PR #967 in changelog and settings credits
* fix(artist-detail): keep ext-link border visible on hover
Hover used --border-subtle, which on Catppuccin matches --bg-card and
visually erased the rim while only the fill changed. Match btn-surface:
--ctp-surface1 border, --ctp-overlay0 on hover.
* docs(changelog): credit zunoz on Psysonic Discord for PR #966
* fix(playlists): tooltips on Play/Add Songs and song count pluralization
Add data-tooltip to Play and Add Songs in playlist hero; switch
playlists.songs to count-based _one/_other forms (was {{n}} without
i18next plural suffix, breaking spacing and singular).
* fix(playlists): render BPM and optional cols in Suggested Songs rows
PlaylistSuggestions shared column headers with the main tracklist but
its row switch omitted bpm, genre, playCount, and lastPlayed.
* fix(random-mix): honor keyword blocks and scope genre list to library
Keyword filter was gated behind the audiobook exclusion checkbox, so
blocked artists still appeared after Remix. Genre Mix now loads genres
via fetchGenreCatalog (scoped index / library filter) instead of raw
getGenres; show empty state when all tracks are filtered out.
* docs(changelog): credit zunoz on Psysonic Discord for PR #965
* fix(home): scope Because you listened rail to sidebar library
Similar-artist album picks used getArtist without library filtering;
session cache also ignored musicLibraryFilterVersion. Filter picks to
the scoped album set and key cache/reserve by filter version.
* docs(changelog): credit zunoz on Psysonic Discord for PR #964
* test: satisfy SubsonicAlbum required fields in new unit tests
Fix tsc --noEmit CI: songCount and duration are required on SubsonicAlbum.
* fix(composers): drop Navidrome role rows with zero composer albums
Navidrome can list performer-only artists under role=composer with
stats.composer.albumCount 0; filter them out of the Composers catalog
so search no longer surfaces ghost entries like Apollo 440.
* docs(changelog): credit zunoz on Psysonic Discord for PR #963