mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
9323f5fee678479bc669f9d144095c4f20c08c70
417 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9323f5fee6 | fix(albums): All Albums compilation and favorites filters (#1143) (#1151) | ||
|
|
93166c774a |
feat(i18n): add Hungarian (hu) translation (#1149)
* feat(i18n): add Hungarian (hu) translation files * feat(i18n): register Hungarian locale and language option * docs(changelog): Hungarian translation entry and credit (#1149) * fix(i18n): list Romanian in the Login language picker |
||
|
|
ec8cfbc1c8 |
fix(eq): show the active AutoEQ profile name in the preset picker (#1147)
* fix(eq): surface the active AutoEQ profile name in the preset picker * i18n(settings): add AutoEQ preset group label (10 locales) * docs(changelog): AutoEQ active-profile picker fix (#1147) |
||
|
|
887c940e1b |
feat(eq): remember EQ profile per audio output device (#1146)
* feat(eq): add per-device EQ snapshot state to eqStore * feat(eq): wire per-device EQ sync into audio listener setup * feat(settings): add 'remember EQ per device' toggle to audio output section * i18n(settings): add 'remember EQ per device' strings (10 locales) * test(eq): cover per-device EQ snapshot store + device sync orchestration * docs(changelog): per-device EQ entry, credit and what's new (#1146) * i18n(settings): note per-device EQ scope (system default + explicit switch) in toggle description * test(eq): cover system-default bucket and device-reset (null) path |
||
|
|
d9969ed76f | fix(library): Navidrome artist letter buckets + safer index open/swap (#1144) (#1145) | ||
|
|
986550c854 | fix(favorites): bulk add-to-playlist and play/enqueue selected (#1140) | ||
|
|
30ccaf51a8 |
fix(updater): rebuild the update modal on a reusable Modal (#1142)
* feat(ui): add reusable Modal component Portal + dimmed backdrop (no backdrop-filter — on WebKitGTK the blur bleeds onto modal content on some GPU stacks), Escape/backdrop/X to close, header (icon/title/subtitle) + scrollable body + footer slot. First consumer is the update modal; the other hand-rolled modals migrate onto it over time. * fix(updater): rebuild the update modal on the reusable Modal Fixes a user report (Manjaro / AMD / X11): blurry modal text (the eq-popup backdrop-filter bled onto the content), the version arrow sitting between the two header rows, and the unclear Skip / Remind buttons. Now uses the no-blur Modal shell; the header icon aligns with the title; Skip is a clear button and Remind me later is the accent action when there is no in-app install. Drops the now-dead update-modal shell CSS. * docs(changelog): update notification popup fix (PR #1142) |
||
|
|
b950d4704b |
feat(cover): artist artwork from fanart.tv (off by default) (#1137)
* feat(cover): add artist_artwork_lookup table + accessors
Image-scraper P0 (design-review §12): additive library-SQLite migration 013
plus get/upsert/clear-per-server accessors for the external artist-artwork
lookup (fanart.tv). Render never reads it; the on-demand cover ensure path
and the mbid_ambiguous 24h negative cache use it. server_id = serverIndexKey.
* feat(cover): add fanart.tv + getArtistInfo2 provider layer
Image-scraper P0 (§7/§19/§23): new cover_cache/external.rs — Rust-side
getArtistInfo2 tag-MBID resolution plus fanart.tv v3/music URL + first
artistbackground fetch (BYOK client_key sent in addition to the project
api_key per fanart.tv ToS, §22). Extract a shared build_subsonic_url helper
in fetch.rs (cover URL behaviour unchanged). Add a dedicated low-concurrency
fanart_http_sem so external HTTP never starves Navidrome (§26). URL builders
unit-tested; wired into ensure_inner next.
* feat(cover): wire fanart.tv external branch into ensure_inner
Image-scraper P0 (§16): on-demand artist `fanart` ensures try fanart.tv
before the Navidrome fallback. MBID resolved Rust-side via getArtistInfo2
(§23, tag MBID); on a miss it falls through WITHOUT a .fetch-failed marker so
Navidrome stays the display fallback (§28). External tiers are written as
{tier}-fanart.webp in the same entity dir (same cacheKind, §16) — 2000 + 512
(matryoshka §17); peek prefers them for the fanart surface. Dedicated
low-concurrency fanart lane (§26); .miss-fanart ~30min negative marker.
Additive IPC args (externalArtworkEnabled, surfaceKind), off by default and
gated by PSYSONIC_FANART_KEY — inert until a render surface opts in (P1).
Quality gate (§11), name->MusicBrainz (§19), and lookup-table writes are P1.
* feat(cover): fanart-first peek for the fanart surface
Image-scraper P0: for an artist `fanart` ensure, the early peek serves only
the external {tier}-fanart.webp tiers; if none exist yet it returns None so
ensure runs the external branch (fetch fanart) instead of short-circuiting on
a cached Navidrome tier. Realises "fanart prioritised" (§18) for the opt-in
surface; Navidrome stays the fallback inside the branch's miss path.
* chore(cover): dev-only artist-fanart spike helper
DEV-only window.psyFanartSpike(name) — resolves an artist by name and fires
the real cover_cache_ensure with externalArtworkEnabled+surfaceKind=fanart to
verify the P0 pipeline against a live server (with PSYSONIC_FANART_KEY set).
Not wired in production.
* feat(cover): §11 quality gate for the fanart surface
Before an external fanart fetch, check whether a Navidrome tier already on
disk is an HQ ~16:9 image (width >= 1280, aspect 1.6-2.0) and skip the fetch
if so — square artist portraits never satisfy it, so the common case still
fetches. Reads tier dimensions only (no full decode). Rust consts per the
design review. Unit-tested predicate.
* feat(cover): persist fanart resolution in artist_artwork_lookup (§12)
Wire the lookup table as both the MBID resolution cache and the negative
cache: a cached MBID skips the getArtistInfo2 round-trip; no_mbid/mbid_ambiguous
back off 24h and miss 30min from updated_at before re-querying. Writes
hit/miss/no_mbid with mbid/mbid_source/provider; transient network errors are
not cached. All store reads/writes run off the async executor via
spawn_blocking and no-op before login. Store reached via app.try_state::<LibraryRuntime>().
* feat(cover): compile-time fanart key fallback + album/name IPC args
A runtime PSYSONIC_FANART_KEY still wins (dev), else the key baked in at build
time via option_env! (release). Add additive artistName/albumTitle ensure args
as context for the §19 name->MusicBrainz fallback (inert until the render
passes them). Library backfill passes None.
* feat(settings): add External Artwork Scraper toggle under Integrations
Master toggle (themeStore, off by default per §20) in a new Integrations
subsection, alongside the other opt-in third-party categories. Contacts
fanart.tv only when enabled. i18n across all 9 locales.
* feat(cover): wire fanart background into the fullscreen player (§28)
New useArtistFanart hook resolves a fanart.tv 16:9 background via a dedicated
cover_cache_ensure (surfaceKind=fanart) — it bypasses the shared peek/disk-src
cache (the {tier}-fanart.webp surface is keyed differently) and reuses
coverDiskUrl for the asset URL. Fullscreen background priority is now
fanart -> Navidrome artist image (cover pipeline) -> album cover; the live
useFsArtistPortrait probe is deleted (§28). Additive ensure opts
(surfaceKind/artistName/albumTitle); externalArtworkEnabled is derived in
ensureArgsFromRef from the master toggle and restricted to the artist fanart
surface, so plain cover ensures are unaffected.
* feat(cover): generalize external surface to fanart + banner (§13)
surfaceKind='banner' fetches the fanart.tv musicbanner array -> {tier}-banner.webp
in the same entity dir; fanart stays the 16:9 artistbackground. The ensure
branch, peek, lookup rows (per-surface surface_kind), miss marker
(.miss-<surface>) and tier suffix are all surface-parameterised. The §11
quality gate stays fanart-only (the banner strip has its own aspect). Unit
test for the surface->fanart JSON key map.
* feat(artist): fanart banner on the artist-detail header (§13, Option B)
The artist-detail header gets an album-detail-style background layer: fanart.tv
banner (musicbanner) -> the 16:9 fanart background cropped to the strip ->
empty. Both via a shared useArtistExternalImage hook (useArtistBanner /
useArtistFanart); each fetches on demand and shares the Rust cache, so the
header and the fullscreen player warm each other's images. The header is its
own stacking context (isolation) so a z-index:-1 banner clips behind the avatar
+ meta with no content wrapper; the album-style framing (padding/radius/clip)
is applied only when a banner is shown, so the off-by-default case stays
pixel-identical.
* refactor(artist): reuse album-detail header structure for the fanart banner
The artist-detail header now uses the same album-detail-* container classes as
AlbumHeader (header/bg/overlay/content/hero) with the fanart banner as the
background; the Back button moves inside the header. A surgical
`artist-detail-bleed` cancels the artist page's .content-body padding so the
banner is full-bleed to the container edges, matching the album header exactly
instead of the earlier inset card. Reverts the experimental artist-specific bg
CSS.
* feat(cover): drop artist_artwork_lookup rows on clear-cover-cache (§12/B.4)
cover_cache_clear_server already removed the server's whole cover dir (so the
{tier}-fanart.webp / -banner.webp tiers + .miss-* markers go with it); also
clear the artist_artwork_lookup rows for that server (off-thread) so no stale
resolution state lingers. Automatic toggle-off purge deferred — turning the
toggle off already hides external artwork (render is gated), and explicit
cache-clear now cleans external state too.
* feat(cover): name->MusicBrainz album-confirmed MBID resolution (§19)
When getArtistInfo2 has no tag MBID and the ensure carries the artist name +
an album in context (fullscreen), one MusicBrainz release-search query resolves
the artist MBID: the primary artist across score>=90 releases wins, conflicting
ids -> mbid_ambiguous (24h backoff), none -> no_mbid. Sends the required
User-Agent; a single-permit musicbrainz_sem + >=1s spacing holds us under MB's
rate limit. mbid_source=musicbrainz persisted. Banner surface (no album
context) correctly skips this. Pure classify/escape helpers unit-tested.
* fix(cover): enable banner surface in ensureArgsFromRef
externalEnsureFields only set externalArtworkEnabled for surfaceKind 'fanart',
so the 'banner' surface never fired — the artist-detail header always fell back
to the fanart image instead of the fanart.tv musicbanner. Both external artist
surfaces (fanart/banner) now enable the external branch.
* feat(settings): optional BYOK personal fanart key field
Add an optional personal fanart.tv API key field to the External Artwork
Scraper block (shown when the toggle is on): a masked input, a saved/in-use
status line, and the simple note that it is sent in addition to the app key.
Persisted in themeStore and plumbed through cover_cache_ensure
(externalArtworkByok); Rust prefers the settings key, falling back to the
PSYSONIC_FANART_CLIENT_KEY dev env. i18n x9.
* fix(cover): resolve artist-page fanart image collision on navigation
The artist-detail header keyed its fanart/banner hooks on the route `id`,
which flips immediately on navigation while `artist`/`albums` refetch a beat
later. The mismatched ensure wrote the previous artist's image under the new
artist's id (e.g. Sepultura's image under Lordi's id).
- key on the loaded `artist.id`, not the route `id`, so id/name/album always
describe the same artist
- pick the §19 album context from an album that actually belongs to this
artist (`albums.find(a => a.artistId === artist.id)`), so a stale album can't
run a mismatched name→MusicBrainz query or cache a wrong `no_mbid`
- reset `src` on every input change in `useArtistExternalImage` so a previous
artist's image never lingers while the new one resolves
* fix(cover): strip trailing album qualifier before MusicBrainz lookup
Library titles like "Show No Mercy (2004 Remastered)" or "Album [Deluxe
Edition]" failed the §19 MusicBrainz release query, blocking name-confirmed
MBID resolution. `normalize_album_for_mb` strips a single trailing
parenthetical/bracketed qualifier; leading qualifiers (e.g. "(What's the
Story) Morning Glory?") are left intact. Unit-tested.
* fix(cover): don't cache no_mbid when album context is unavailable
The banner ensure could fire before the artist's albums loaded, with no album
in context. The old code cached `no_mbid` there and the 24h backoff then
blocked the later ensure that arrived *with* album context. Could-not-attempt
is not tried-and-failed: the no-album branch now returns without persisting.
* fix(cover): don't emit tier-ready for external fanart/banner surfaces
`try_external_fanart` emitted `cover:tier-ready` with the `{tier}-{surface}.webp`
path. That event is keyed by the canonical cover key (cacheKind/cacheEntityId/
tier, no surface), so the frontend `useCoverArtBridge` listener seeded the
Navidrome artist cover's disk-src cache with the external image — leaking
fanart/banner into the plain artist cover (avatar, fullscreen "navidrome-artist"
fallback) even with the scraper toggled off.
Remove the emit: the fanart/banner hooks read the path from the
`cover_cache_ensure` return value, so no event is needed. (No disk-level
overwrite — the suffixed files are never matched by `tier_exists`; this was
frontend disk-src-cache cross-contamination.)
* fix(cover): wait for the final external background before showing it, with fade-in
The fullscreen player and artist-detail header flashed several backgrounds in
sequence while the fanart resolved (upscaled album cover → Navidrome artist
image → fanart), and the artist header could show the fanart first and then
swap to the banner.
- the album cover is no longer a background source — it only feeds the
foreground thumbnail
- the external-artwork hooks return `{ src, pending }` so callers can tell
"still resolving" (hold back) from "resolved, no image" (fall back now)
- fullscreen background: scraper on → fanart, empty while it resolves, Navidrome
artist image only on a confirmed miss; scraper off → Navidrome artist image
- artist header: the banner is preferred — nothing shows while it resolves
(no fanart flash), fanart is the fallback only once the banner misses
- both backgrounds preload the chosen image and fade it in (`onLoad` plus a
`ref` `complete` check so an already-cached image, whose load event can fire
before React attaches the handler, still appears). The header fade is a
scoped inline opacity so the shared `album-detail-bg` class is untouched.
* ci(release): pass PSYSONIC_FANART_KEY into the macOS + Linux builds
* refactor(cover): extract external-artwork ensure into its own module
Pure code move: the on-demand fanart/banner fetch, the quality gate, the
surface-aware peek and the lookup-table cache move from cover_cache/mod.rs
into cover_cache/external_ensure.rs. Behaviour unchanged; mod.rs 1877 -> 1488.
* chore(cover): remove dev-only fanart spike helper
The real render wiring now exercises the external ensure branch, so the
dev-only window.psyFanartSpike helper is redundant.
* feat(cover): purge external artwork on opt-out (B3)
New cover_cache_purge_external command: when the External Artwork toggle is
turned off, drop every fetched {tier}-{provider}.webp, .miss-{provider}
marker and artist_artwork_lookup row across all configured servers, leaving
the canonical Navidrome covers intact. Opting out now removes the
third-party-sourced data instead of just hiding it (design-review §9/§12/B.4).
* docs: changelog, credits and what's new for artist fanart (PR #1137)
|
||
|
|
23f8008248 | fix(queue): suspend idle pull only on user queue edits (#1136) | ||
|
|
b9b4f76c11 | fix(connection): retry connect ping before unreachable (#1135) | ||
|
|
d8e5d4eed4 |
feat(i18n): add Japanese translation (#1134)
* feat(i18n): add Japanese translation Signed-off-by: Soli0222 <github@str08.net> * docs: changelog, what's new and credit for Japanese translation --------- Signed-off-by: Soli0222 <github@str08.net> Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> |
||
|
|
2d1a078186 | fix(queue): push edited queue when playback starts on paused client (#1133) | ||
|
|
c037ab459a | fix(queue): suspend idle pull after local queue edits (#1132) | ||
|
|
955a9fcbd6 | feat(queue): play queue sync — manual pull, idle auto-sync, multi-server push (#1131) | ||
|
|
c428d37e0e |
Settings — own Audio categories + Queue Settings consolidation (#1130)
* refactor(settings): promote Normalization and Track transitions to own Audio categories Pull Normalization and Track transitions out of the combined Playback section into their own top-level SettingsSubSection categories, placed directly under Audio Output Device. Both follow the established reusable pattern (SettingsSubSection header + title-less SettingsGroup inside a single-group settings-card, so the frame-collapse CSS applies). - New TrackTransitionsBlock extracted from PlaybackBehaviorBlock; the latter now holds only the Queue behaviour toggle (slated to move to Personalisation). - NormalizationBlock's SettingsGroup is now title-less; the section header and description name it. - Split the single audio search-index row into three (Normalization / Track transitions / Playback) so crossfade/replaygain/lufs keywords focus the right section. * refactor(settings): consolidate Queue Settings under Personalisation, drop Audio Playback section Combine Queue Display Mode and Queue Toolbar under one 'Queue Settings' category in the Personalisation tab, and move the Queue behaviour toggle (preservePlayNextOrder) there from Audio. The now-empty Audio Playback section is removed. - New 'Queue Settings' SettingsSubSection holds three titled groups: Queue Display Mode, Queue behaviour, and (advanced-only) Queue Toolbar. The toolbar group keeps its reset, now via a new optional 'action' slot on SettingsGroup (+ .settings-group-title-action CSS). - Delete PlaybackBehaviorBlock; its single toggle is inlined. - Search index: drop the audio 'playbackTitle' row; the personalisation queue row now points at 'queueSettingsTitle' with merged keywords. - i18n: add settings.queueSettingsTitle to all 9 locales. * fix(settings): align queue-toolbar separator label and restore Advanced badge - QueueToolbarCustomizer: the separator row rendered a 1px rule where other rows have a 16px icon, so its label sat shifted left. Reserve the full 16px icon column (1px rule centred) so the label lines up. (Preexisting.) - SettingsGroup gains an optional 'advanced' flag rendering the Advanced badge; badge + action now share a right-aligned title-end slot (.settings-group-title-end), so the badge sits just left of the reset button. Restores the Advanced indicator the Queue Toolbar lost when it moved from a SettingsSubSection into a group. * fix(audio): hide the output-device category on macOS instead of showing a notice Playback is pinned to the system default on macOS, so the picker showed a notice explaining it does nothing there. Gate the whole Audio Output Device category out on macOS (`!IS_MACOS` in AudioTab) and drop the now-dead notice branch + the `audioOutputDeviceMacNotice` string from all 9 locales. The device-probe hook already short-circuits on macOS, so no work is wasted. * refactor(settings): box the sidebar customizer groups consistently The sidebar display toggles sat as a bare div and looked unfinished. Box them in a SettingsGroup, with the nav-item drag list in a second group. Render the groups directly in the sub-section content (no settings-card wrapper) — matching the other Personalisation customizers, which use bare SettingsGroups. Also drop the settings-card around Queue Settings for the same reason, so every Personalisation section's boxes share one width and inset instead of the card-wrapped ones sitting narrower/indented. * docs(settings): changelog, what's new, and credits for the settings reorg (#1130) Fold the Audio/Personalisation reorganization into the existing 'Settings — consistent grouped layout' changelog entry (now #1126 + #1130) and the matching What's New highlight, and add a consolidated settings- refactor line to the contributor credits. |
||
|
|
4225146a16 | feat(autodj): smooth skip and interrupt blend transitions (#1128) | ||
|
|
d50c9c444d |
refactor(settings): reusable SettingsGroup/SettingsToggle + boxed sections across tabs (#1126)
* refactor(settings): extract reusable SettingsGroup component
Pull the boxed sub-section pattern (bordered panel + accent uppercase
header) introduced for the Audio tab into a reusable <SettingsGroup
title desc> component, and migrate NormalizationBlock and
PlaybackBehaviorBlock onto it. No visual change.
* feat(settings): box the System behavior section into groups
Split the System -> App Behavior card (which bundled tray toggles, Linux
rendering tweaks and the clock format) into titled SettingsGroup panels
(Tray / Linux rendering / Clock) for a clearer, consistent boxed look.
Adds the group titles across all 9 locales.
* feat(settings): box Appearance visual options; optional SettingsGroup title
Split the Appearance -> Visual options card into a Display group and a
Window group (Linux custom titlebar controls). SettingsGroup now allows an
optional/omitted title for plain boxed panels. Group titles added across
all 9 locales.
* feat(settings): box Discord cover source and templates
Group the Discord cover-source toggles and the activity templates into
separate SettingsGroup panels (templates reuse their existing title/desc),
dropping the manual indent and inline header. No new strings.
* feat(settings): box the Music Network section
Give the scrobble-master toggle, the enrichment-primary picker and the
provider list each a boxed SettingsGroup with an accent header, and switch
the add-a-service provider rows to the boxed panel style for consistent
contrast with the rest of settings.
* feat(settings): titled boxes for Integrations sections + cover source label
Wrap the Discord enable toggle, the Discord cover-source picker (now with a
"Cover art source" title and explainer), Bandsintown and Show-in-Now-Playing
in titled SettingsGroup panels so each single-item section gets the same
accent-headed boxed look. New cover-source strings across all 9 locales.
* feat(settings): box Lyrics tab sections
Wrap the lyrics-sources customizer and the sidebar-style picker in titled
SettingsGroup panels (reusing the existing section titles).
* refactor(settings): consistent content inset in SettingsGroup
Add a body wrapper with a small left inset so every boxed section indents its
controls uniformly (title stays flush at the box edge). Defined once in the
component instead of per section.
* feat(settings): box Storage tab sections incl. cover art cache
Wrap media directory, cover art cache strategy, next-track buffering and
downloads in titled SettingsGroup panels (reusing existing section titles);
align the cover-cache table's first column flush with the new content inset.
* feat(settings): box Library tab, relocate Lucky Mix, add SettingsToggle
- Add a reusable SettingsToggle component for the repeated label/desc/switch row.
- Box the Random Mix blacklist (grouping built-in keywords with the custom
filter), Ratings and the Analytics strategy section.
- Move the "Show Lucky Mix in menu" toggle out of the blacklist and up next to
split-mix navigation and now-playing-at-top in the sidebar customizer.
* feat(settings): box remaining Appearance sections
Wrap library grid, UI scale, font and seekbar style in titled SettingsGroup
panels (reusing the existing section titles).
* feat(settings): box System language, logging and backup sections
Wrap language and logging in titled SettingsGroup panels, and rework the
backup section to drop its redundant inline header (the subsection already
provides it) and box its content. About / Contributors / Licenses stay as
free display content.
* feat(settings): box remaining Audio sections
Wrap Hi-Res, equalizer, playback speed, audio output device and track
previews in titled SettingsGroup panels (reusing the existing section titles).
* feat(settings): box Input tab keybinding sections
Wrap the in-app and global shortcut lists in titled SettingsGroup panels.
* feat(settings): box the Personalisation tab
Convert every customizer (sidebar, home, artist, queue toolbar, playlist,
player bar) to plain inner containers and wrap each in a titled SettingsGroup,
so the boxed look is consistent without nested cards. Box the queue display
mode picker too.
* feat(settings): box the Themes sections above the store
Add a boxed option to the flat Themes sections and apply it to Your Themes,
Auto-Switch Theme and Import (the theme store stays unboxed). Drop the inner
settings-card from InstalledThemes, ThemeImportSection and the scheduler so
the SettingsGroup is the only frame.
* fix(settings): give the Themes boxes a card carrier and accent header
The flat Themes sections had no settings-card behind them, so the bg-app
SettingsGroup had no contrast against the page. Box each above-store section
as settings-card > titled SettingsGroup (accent header) like the rest.
* fix(settings): clean up the server library sync status line
Collapse the offline status to a single icon + phrase ("Server offline —
sync deferred") instead of a redundant "Deferred" badge alongside it, and
drop the now-unused string across all 9 locales.
* fix(queue): keep toolbar toggle buttons coloured active while hovered
The generic .queue-round-btn:hover rule outweighed .active in specificity, so
an active button kept the hover colour until the pointer left — the toggle
only looked on after moving the mouse away. Add an explicit active-hover rule
(mirroring the mini player) so active wins immediately on click.
* fix(settings): fix Lyrics triple title, drop dead Discord cover keys
- Remove LyricsSourcesCustomizer's own section header (the subsection already
titles it) and make its inner cards plain; the Lyrics boxes are now title-less.
- Drop the dead discordCoverSource/discordCoverSourceDesc i18n keys (superseded
by discordCoverTitle/Desc) and un-jam the libraryIndexServer status line in
all 9 locales.
* refactor(settings): drop redundant group titles and double frames
Single-group sections duplicated the subsection title in the group header — make
those groups title-less so the subsection header is the only title. A card whose
sole child is one group now collapses via CSS so single-group sections render a
single frame (matching the card-less Lyrics layout) instead of a card-in-group
double border. Multi-group cards keep their frame as the grouping container.
* fix(settings): keep the flat Themes cards framed when collapsing single-group cards
The single-group card collapse also flattened the Themes section cards, which —
not being inside an accordion — are themselves the contrast surface. Exclude
.themes-section descendants from the collapse.
* refactor(settings): use SettingsToggle for Appearance and System toggle rows
Replace the hand-rolled toggle-row markup in the Appearance visual options and
System tray/Linux/changelog rows with the shared SettingsToggle component, and
extend it with a searchText prop and ReactNode descriptions for the remaining
call sites.
* refactor(settings): roll SettingsToggle out to Integrations, Audio and Storage
Replace hand-rolled toggle rows in the Discord/Bandsintown/Now-Playing,
Hi-Res, hot cache and playback behaviour/rate sections with SettingsToggle.
Make its label optional (desc-only rows whose title is the group header) and
add searchText/id pass-throughs.
* refactor(settings): use SettingsToggle for the remaining standard toggle rows
Track previews master, sidebar lyrics style, YouLyPlus/static-only, queue
display mode and the sidebar split-nav/now-playing/lucky-mix rows now use the
shared component. Genuinely custom rows (drag lists, the skip-star threshold,
the AudioMuse row with its inline link, the fine-step advanced badge) stay
hand-rolled.
* fix(settings): drop duplicate titles on Hi-Res/Bandsintown/Now-Playing, un-jam locale line
These single-toggle sections kept a group title identical to their subsection
header; once the single-group card collapses the title rendered twice stacked.
Make them title-less (the subsection header is the label). Also split the
discordCoverTitle locale entry onto its own line in all 9 locales.
* feat(settings): restore section icons in boxed Themes headers
SettingsGroup gained an optional accent icon slot rendered before the
title; ThemesTab forwards the Palette/Clock/Upload icons the boxed
sections lost when they moved off the bespoke <h2> header.
* style(settings): re-indent SettingsGroup children to nesting level
Pure whitespace: children wrapped in SettingsGroup were left at their
pre-wrap indent. No content change (git diff -w is empty).
* fix(settings): clarify Native Hi-Res Playback description
The old copy led with the disabled-state behaviour ("forces 44.1 kHz"),
which read as if the toggle itself locked output to 44.1 kHz. Describe
what enabling actually does: play each track at its native sample rate
(reconfiguring the output device to match) instead of resampling to
44.1 kHz. Updated across all 9 locales.
* docs: changelog + what's-new for the settings layout refactor (#1126)
|
||
|
|
99c0b6cdac | fix(linux): detect Niri as a tiling window manager (#1127) | ||
|
|
ee044ece1a | fix(now-playing): poll Live listener count every 30s in the background (#1125) | ||
|
|
fde7ab432f |
feat: split AutoDJ into a standalone playback feature (#1124)
* feat(playback): add transition-mode helper Centralise the crossfade/AutoDJ/gapless mutual exclusivity in one place instead of the scattered setter combinations across the toolbar, mini player and settings. AutoDJ stays encoded as crossfade + trim-silence, so the persisted flags and the audio engine are unchanged. * feat(queue): split AutoDJ into its own toolbar button + playlist submenu - AutoDJ becomes a standalone toolbar button (Blend icon) next to crossfade, driven by the shared transition-mode helper. The crossfade right-click popover drops the mode switch and keeps only the seconds slider. - Save + load playlist collapse into one Playlist button opening a small submenu, freeing up toolbar space. - queueToolbarStore gains a position-preserving rehydrate migration (legacy save/load -> playlist, autodj inserted after crossfade) with unit tests. - Toolbar customizer and all 9 queue locales updated to match. * feat(mini-player): standalone AutoDJ button, shared transition helper Mirror the queue toolbar: AutoDJ gets its own Blend button, the crossfade popover keeps only the seconds slider. The mini player now drives all three transitions through a single additive `mini:set-transition-mode` event handled by the shared helper, replacing the per-flag mini events. Drop the now-dead crossfade-mode CSS. * feat(settings): segmented track-transition picker, regroup playback Replace the crossfade toggle + inner crossfade/AutoDJ switch with a single Off | Gapless | Crossfade | AutoDJ segmented control (mirroring the Normalization picker above), driven by the shared transition helper — the mutual exclusivity now reads at a glance. Crossfade keeps its seconds slider; AutoDJ shows its content-driven explainer. The block is regrouped under "Track transitions" and "Queue behaviour" headings. Drops the now unused crossfade/gapless description and not-available i18n keys across all 9 locales and adds the new transition strings. * fix(queue): open the playlist submenu inward The playlist submenu inherited the crossfade popover's right:0 anchor and, sitting on the left of the toolbar, opened out under the main container. Anchor it left:0 so it stays inside the queue panel. Update the toolbar test for the new playlist submenu (save/load moved off the toolbar). * feat(settings): box playback sub-sections into panels Wrap Normalization, Track transitions and Queue behaviour each in their own bordered panel with an accent uppercase header (new reusable .settings-group classes), so the sections read as distinct blocks instead of one wall of text. Drops the thin divider that separated them. * docs: changelog, credits and what's new for AutoDJ standalone Fold the standalone-AutoDJ changes into the existing AutoDJ entry in the changelog and the in-app What's New, and add the credit (#1124). |
||
|
|
a6ee0668c8 |
feat(crossfade): AutoDJ — content-aware silence-trimming crossfade (#1122)
* feat(crossfade): add "trim silence between tracks" toggle
New persisted setting `crossfadeTrimSilence` (default off; existing
installs rehydrate off via the persist default-merge). Surfaced in
Settings -> Audio and in the crossfade popovers of the queue toolbar
and the mini-player.
Crossfade buttons now separate the two actions: left-click toggles
crossfade on/off, right-click opens the settings popover (seconds +
trim). Shared the mini popover positioning into useMiniAnchoredPopover
(now backing both volume and crossfade). Mini bridge carries
crossfadeSecs/crossfadeTrimSilence and gains mini:set-crossfade-secs /
mini:set-crossfade-trim-silence.
The actual silence-trimming playback behaviour is wired in a follow-up;
this commit only persists the user intent. i18n added across 9 locales.
* feat(crossfade): trim silence between tracks (waveform-driven)
Wire the actual silence-aware crossfade behind the (default-off)
crossfadeTrimSilence toggle. Detection is derived on the fly from the
cached 500-bin waveform + track duration — no new analysis pass or
cache fields.
- waveformSilence.ts: computeWaveformSilence(bins, duration) → lead/trail
silence + content bounds, using the peak curve, a low absolute cut and
a per-side cap. Unit-tested.
- A-tail (JS): handleAudioProgress advances the crossfade early, at
contentEnd - crossfadeSecs, when the current track ends in real
trailing silence, so the fade overlaps music. Guarded once per play
generation.
- B-head: audio_play gains an additive optional start_secs; the freshly
built source is try_seek'd past the next track's leading silence before
append, then seek_offset/samples_played are re-anchored so position is
content-relative. Non-seekable / cold sources degrade to today.
- Pre-buffer: crossfade next-track download + B-head probe moved to
crossfadePreload.ts with a fixed ~30 s budget before the track needs to
play (widened by trailing silence so the early advance keeps the
budget). Also fired right after a seek into the window so jumping near
the end still buffers in time.
Checks: tsc, vitest (store suite + new units), cargo test/clippy for
psysonic-audio.
* feat(crossfade): recommend hot cache for trim; probe B-head regardless
Add a "for reliable results, enable the Hot playback cache" note to the
trim-silence toggle description across all 9 locales — hot cache keeps
the next track on disk so it starts instantly past its lead silence.
Fix: the leading-silence probe (B-head) now runs even when hot cache is
on; only the redundant byte pre-download is gated on !hotCache. Without
this, enabling hot cache (the recommended setting) would have skipped the
probe and disabled leading-silence trimming.
* feat(crossfade): content-driven smart crossfade overlap
Smart crossfade derives the per-transition overlap purely from the
waveform envelopes — max(A outro fade, B intro rise) clamped 0.5–12s —
instead of the fixed crossfadeSecs ("work by fact"). The JS early
advance arms the computed overlap and audio_play applies it through a
new crossfade_secs_override (capping only this swap's fade); plain
loud→loud endings fall back to the engine crossfade at crossfadeSecs.
* feat(crossfade): "Crossfade | Smart crossfade" mode switch in UI
Replace the standalone "trim silence" toggle with a Crossfade / Smart
crossfade segmented control in settings and both crossfade popovers
(queue toolbar + mini-player). Classic Crossfade shows the seconds
slider; Smart crossfade is content-driven with no duration to set.
Adds smartCrossfade / smartCrossfadeDesc strings to all nine locales
and the "smart crossfade" search keyword.
* feat(crossfade): don't double-fade a track that already fades out
Decouple the outgoing track's fade-out from the incoming fade-in. When
A carries its own recorded fade-out (outroFadeA ≥ 1s and ≥ B's intro
rise), planCrossfadeTransition now sets outgoingFadeSec = 0; the engine
then skips A's TriggeredFadeOut so A keeps full gain and its recording
carries it down while B rises underneath — no more double attenuation
that made A vanish early and B blare in. Hard-cut endings still get an
engine fade over the overlap.
audio_play gains outgoing_fade_secs_override (Some(0) = ride A's own
fade), threaded via SinkSwapInputs.outgoing_fade_secs; the JS advance
arms it alongside the overlap.
* feat(crossfade): rename the smart crossfade mode to "AutoDJ"
User-facing rename of the content-driven crossfade mode from "Smart
crossfade" to "AutoDJ" across the settings segmented switch, the queue
and mini-player popovers, all nine locales, and the settings search
keywords. The underlying store flag (crossfadeTrimSilence) is unchanged.
* feat(crossfade): standard ~2s blend for hard loud→loud meetings
When AutoDJ trims a track's protective trailing silence and the loud
ending butts straight into a loud intro, neither edge fades, so the old
0.5s anti-click floor sounded like an abrupt cut. Use a standard ~2s
equal-power crossfade for that case (both edges analysed, nothing
fades); real fade-outs/buildups keep their longer content-driven span,
and the bare floor only survives when an envelope is missing.
* fix(crossfade): keep B's fade-in across the B-head start-offset seek
EqualPowerFadeIn::try_seek jumped straight to unity gain for any seek
≥100ms, which also hit the initial start-offset seek that skips the
incoming track's leading silence — so a crossfaded track with trimmed
lead silence popped in at full gain instead of fading in. Only skip the
fade-in for mid-playback seeks (sample_count > 0); a seek before any
audio has played keeps the fade-in.
* feat(crossfade): gate AutoDJ early fade on next-track readiness
The early, content-driven advance now fires only when the next track's
audio is actually available — in the engine RAM preload slot
(enginePreloadedTrackId) or local on disk (offline library, favourite-auto
or hot-cache ephemeral) — via isCrossfadeNextReady(). Analysis alone is
not enough: a cold, still-buffering stream would fade in over silence.
When B isn't ready the gen guard stays unset so it re-checks on later
ticks; if B never readies, the plain engine crossfade handles the
transition (graceful degrade) instead of a broken fade. A RAM preload
copy suffices — the full track need not be cached to disk.
* feat(crossfade): suppress engine auto-crossfade for AutoDJ + eager preload
With the hot cache off the readiness gate alone wasn't enough: the engine's
progress task autonomously fires its crossfade audio:ended ~crossfadeSecs
before the end, independently of JS, and would start a still-buffering next
track and fade over it — an audible jump.
- Engine: add autodj_suppress_autocrossfade flag (audio_set_autodj_suppress);
the progress task treats it like crossfade-off, so the early timer never
fires and audio:ended only comes from real source exhaustion / watchdog.
- JS drives the transition: set the flag when a content fade is pending
(wantEarly) and clear it for plain loud→loud / non-AutoDJ, so the normal
engine crossfade is preserved there. When the next track never readies, A
plays out and we degrade to a clean sequential start instead of a jump.
- audio_preload gains an `eager` flag; the crossfade/AutoDJ pre-buffer passes
it to skip the 8s start throttle so the RAM slot fills before the fade.
* docs(changelog): AutoDJ content-aware crossfade (PR #1122)
Add the 1.49.0 Added entry and the cucadmuh credits line for AutoDJ.
|
||
|
|
ed52a9991f |
feat(albums): "Artist → Year" album sort option (#1120)
* feat(albums): "Artist → Year" sort option Adds a third album browse sort that groups albums by artist and orders each artist's albums chronologically (oldest first, title as a same-year tiebreak) — the double-sort requested in #1113. The local index sorts globally via [{artist},{year},{name}]; the server fallback fetches by artist and applies the year ordering per page (Subsonic has no compound sort). * i18n(albums): Artist → Year sort label (9 locales) * docs(changelog): add Artist → Year to the album sorting entry (#1120) |
||
|
|
ad74578ef6 |
feat(playlists): local playlist folders (sidebar + page, DnD, view toggle) (#1119)
* feat(playlists): playlist folder model — store + pure grouping core Local, per-server folder layer over the server's flat playlist list (the Subsonic API has no folder concept). Adds: - playlistFolders.ts: shared types + pure groupPlaylistsByFolder (used by every surface), with full unit coverage. - playlistFolderStore.ts: persisted Zustand store (create/rename/delete/ assign/collapse), per-server scoped; deleting a folder drops its assignments so playlists fall back to ungrouped. UI surfaces (sidebar + Playlists page) build on this in following commits. * i18n(playlists): folder strings across all 9 locales Adds the playlists.folders.* namespace (folder names, move/remove, expand/ collapse, group-by-folders toggle, count plurals, and the local-only notice explaining that Navidrome and the Subsonic API have no native folder support). * feat(playlists): folder views, drag-to-folder, move-to-folder menu, view toggle Surfaces the local folder layer on both the Playlists page and the sidebar: - Page: collapsible folder sections + ungrouped remainder, each reusing VirtualCardGrid; flat grid is kept verbatim when no folders exist or the group view is toggled off. - Drag-to-folder via the shared mouse-based psy-drop system (HTML5 DnD is unusable in WebKitGTK); the whole section is the drop zone, and the ungrouped zone appears during a drag so a playlist can always return to root. - "Move to folder" submenu in the playlist context menu (keyboard-accessible path; also creates folders on the fly) — stays available offline. - Header gets a "New folder" action and a "Group by folders" view toggle (both context-aware); a notice surfaces the local-only caveat. - Sidebar renders the same collapsible folder groups. - groupView preference added to the folder store. * docs(changelog): note playlist folders (#1119) |
||
|
|
ccb2d11fc4 |
fix(deps): bump transitive form-data to 4.0.6 (GHSA-hmw2-7cc7-3qxx) (#1118)
* fix(deps): bump transitive form-data to 4.0.6 (GHSA-hmw2-7cc7-3qxx) Close Dependabot alert #18: axios pulls form-data for multipart bodies; lockfile now pins the patched 4.0.6 release (CVE-2026-12143). * docs: changelog and credits for form-data security bump (PR #1118) * revert: drop settingsCredits entry for form-data bump * chore(nix): sync npmDepsHash with package-lock.json --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
44d373d7bb |
fix(player): player bar context menu acts on the current song, not its album (#1117)
* fix(player): player bar context menu acts on the current song, not its album Right-clicking the current track in the player bar built an album object from the playing track and opened the album context menu, so "Add to playlist" added the whole album instead of the song. It now opens a song-scoped menu for the current track. Left-click on the title still navigates to the album. * docs(changelog): note player bar add-to-playlist fix (#1117) |
||
|
|
116196f0d4 |
fix(albums): order each artist's albums by title when sorting by artist (#1115)
* fix(albums): order each artist's albums by title when sorting by artist Browsing albums by artist left the albums within each artist in an undefined order. The local-index sort only emitted the artist key; it now appends album title as a secondary key (and artist as the tiebreak for the by-name sort), matching the network path's per-page ordering. * docs(changelog): note album sort-within-artist fix (#1115) |
||
|
|
68b21643f8 |
fix(windows): restore taskbar thumbnail media buttons after deferred window show (#1112)
* fix(windows): restore taskbar thumbnail media buttons after deferred window show The Prev / Play-Pause / Next buttons in the Windows taskbar thumbnail preview stopped appearing. The taskbar code was unchanged; the regression came from the main window now starting hidden with a deferred show. ThumbBarAddButtons was still called at setup time, before the shell had created the window's taskbar button, so it returned S_OK but added nothing (no error logged). Register the shell's "TaskbarButtonCreated" message and add the buttons from the window subclass when it fires (first show, and again after an explorer restart), which is the documented requirement for ThumbBarAddButtons. * docs(changelog): note Windows taskbar media buttons fix (#1112) |
||
|
|
3ec65a6407 |
fix(audio): make seeking work on streamed Opus/Ogg via on-demand HTTP Range (#1110)
* fix(audio): make seeking work on streamed Opus/Ogg via on-demand HTTP Range Seeking inside an Opus/Ogg track streamed over ranged HTTP was a contained no-op (the seekbar snapped back); it only worked once the track had fully downloaded/cached. symphonia 0.6's Ogg demuxer seeks by bisecting the byte range (reading pages at midpoints across the whole file) and scans the last pages during the probe, but RangedHttpSource only filled the buffer linearly from offset 0, so any read ahead of the download front blocked until the linear download caught up. Keeping Ogg seekable through the probe without a real random-access source would have forced a full pre-download. Add an on-demand random-access fetcher to RangedHttpSource: when a read lands well ahead of the contiguous linear download (a seek, a bisection midpoint, or the end-of-stream probe), fetch the needed range over HTTP Range (1 MiB window) on the tokio runtime and let the read loop poll for it, instead of blocking on the linear filler. Ranged Ogg now stays seekable through the probe (records its byte range) so seeking works for real; the catch_unwind in try_seek stays as a safety net. - New OnDemand fetcher writes arbitrary ranges into the shared buffer (same bytes the linear download would write; idempotent under the buffer mutex, mirroring the existing MP4 moov tail-prefetch). It never touches downloaded_to/done, so full-download completion and the track-analysis seed are unaffected. - On-demand only fires on a forward gap > 512 KiB, so normal sequential read-ahead (and a slightly starved play cursor) still waits for the linear download without spurious range requests. - ranged-stream now passes random_access=true; preview keeps on_demand=None. Does not touch the Tauri boundary (no invoke/event changes). * docs(changelog): add 1.49.0 entry for streamed Opus/Ogg seeking (#1110) Also credit the streamed-seek work in settingsCredits. * fix(audio): require 206 for ranged Range fetches at a non-zero offset Address PR #1110 review note: ranged_write_http_range accepted a 200 the same as a 206. A server that ignored the Range header and replied 200 returns the whole body from byte 0; writing that at a non-zero offset would corrupt the buffer (affects both the on-demand seek fetcher and the MP4 moov-tail prefetch). Accept 200 only when the request started at offset 0; otherwise require 206. |
||
|
|
82967caa9c |
docs: add 1.48.1 release section to CHANGELOG and WHATS_NEW
Backport of the 1.48.1 hotfix notes onto main (which is on 1.49.0-dev). Insert the released [1.48.1] section between [1.49.0] and [1.48.0] in CHANGELOG.md (all eight Fixed entries with their attribution, identical to the fix/1.48.1 branch) and the matching [1.48.1] What's New section. Application version is intentionally left at 1.49.0-dev — only the notes are carried over. |
||
|
|
15fb0f6c56 |
Theme store: version display + animated/static filter (#1104)
* feat(themes): show theme version in the store and installed list * feat(themes): filter the theme store by animated / static * docs(changelog): add 1.49.0 entry for theme store version + animated filter |
||
|
|
6f555bdc96 |
docs: sync WHATS_NEW with 1.48 changelog, fix Fixed PR order (#1087)
Add highlights for Live status dots, playback Semitones, Linux title bar styles, Music Network self-hosted scrobble fix, and Navidrome radio admin gating. Remove meta What's New page section. Re-sort CHANGELOG Fixed entries (#1085/#1086 were at the top). |
||
|
|
c1403f8bd6 |
feat: now-playing liveness dot + admin-gated radio management (#1086)
* feat(now-playing): liveness indicator dot in the listening popover Replace the raw "Nm ago" line in the "Who is listening?" popover with a derived presence dot (green playing / amber paused / dim idle). The presence is computed in one tested helper that unifies the playbackReport transport state with the legacy getNowPlaying recency, instead of formatting a raw timestamp inline. The dot carries the localized status as an aria-label and tooltip so it is not conveyed by colour alone. * feat(radio): gate station create/edit/delete behind Navidrome admin role Navidrome >= 0.62 restricts internet-radio management to admins (GHSA-jw24-qqrj-633c); non-admin requests fail. Hide Add Station, Search Directory, the per-card edit chip and delete button for confirmed standard Navidrome users via a canManageNavidromeRadio() helper on the existing useNavidromeAdminRole framework. Admins, non-Navidrome servers and transient states stay unrestricted; playback and favourites remain available to all. * docs(changelog): now-playing status dot + admin-gated radio (#1086) |
||
|
|
0b7d9eae2d |
feat(playback): Semitones strategy, 2-decimal speed label, advanced fine steps (#1084)
* feat(playback): varispeed-by-semitones strategy and 2-decimal speed label Adds a fourth playback-rate strategy "Varispeed (semitones)" — a frontend lens over varispeed where the user dials the pitch change directly in semitones (±12 st, 0.1 step) and speed = 2^(st/12). Engine contract is unchanged: the store maps it to the existing "varispeed" Rust strategy via engineStrategy(), and switching between the two varispeed lenses at the same speed no longer restarts the track. Also widens the speed readout to two decimals (formatSpeedLabel → toFixed(2)) so every 0.05 slider step is visible (1.05×, 1.10×, 1.15×), addressing the feedback on issue #531 that the label looked stuck between steps. Includes new i18n keys and updated hint across all 9 locales, plus Vitest coverage for the new helper, label formatting, and the no-restart lens switch. * refactor(playback): shorten Semitones strategy label, add per-strategy tooltips Renames the fourth strategy to a concise "Semitones" (was "Varispeed (semitones)") across all 9 locales, and lets the four strategy buttons share the row width so they fit on one line in Settings while still wrapping in the narrow player popover. Each strategy button now has a short hover tooltip explaining how it is built (incl. the 2^(st/12) and 12 × log2(speed) maths). * feat(playback): advanced fine-step precision for speed/pitch sliders Adds an opt-in "Fine adjustment" toggle in Settings → Audio (visible only in Advanced mode) that shrinks the playback-rate slider steps to 0.01× for speed and 0.01 st for pitch/semitones, with the pitch readout widening to two decimals to match. Default behaviour (0.05× / 0.1 st) is unchanged. The preference is UI-only (persisted, not sent to the engine) and also applies to the player-bar popover and wheel. Addresses the finer-precision request on issue #531 without cluttering the default UX. * docs: changelog and credits for playback speed follow-up (PR #1084) |
||
|
|
41c8187186 |
fix(music-network): keep API suffix for self-hosted paste-token providers (#1085)
* fix(music-network): keep API suffix for self-hosted paste-token providers The api_key_only connect strategy persisted the raw origin from the baseUrl field instead of the resolved API base, dropping the preset's selfHostedApiSuffix (e.g. /apis/listenbrainz). Scrobbles and now-playing then hit <origin>/1/submit-listens (404/405, silently unlogged) instead of <origin>/apis/listenbrainz/1/submit-listens, so nothing was recorded. Return the runtime-resolved ctx.baseUrl (origin + suffix) and fall back to the field only when it is absent. Fixes Koito and both Maloja compat surfaces (ListenBrainz and Audioscrobbler). Existing accounts must reconnect to re-persist the corrected base. * docs(changelog): self-hosted scrobble URL fix (#1085) |
||
|
|
028eb65f7d |
feat(titlebar): selectable window button styles + minimize toggle (#1083)
* feat(titlebar): selectable window button styles + minimize toggle Custom title bar (Linux) gains a window-button style picker, mirroring the seekbar style picker pattern. Six form-named styles: dots, dotsGlyph, flat, pill, outline, glyph. All buttons now carry minimize/maximize/close glyphs for clear, colour-blind-friendly iconography; dots reveal glyphs on hover, dotsGlyph always shows them. - New authStore state windowButtonStyle (default dots) + showMinimizeButton, with rehydrate validation falling back to dots on unknown values. - WindowButtonPreview reuses the real .titlebar-btn classes for WYSIWYG tiles. - Picker + minimize toggle render under the Custom title bar setting, gated on the toggle being on. - Monochrome styles use --text-primary glyphs and stronger borders for contrast on dark themes. - Dev-build grey marker scoped to the real title bar so previews show true colours. - i18n keys in all 9 locales; setter and rehydrate tests. * docs(changelog): window button styles (#1083) |
||
|
|
be3f1dc299 |
docs(whats-new): refresh 1.48.0 release highlights (#1082)
* docs(whats-new): refresh 1.48.0 highlights for release screen Reorder and expand user-facing copy to match CHANGELOG Added order, add Music Network and Live, drop stale About/licenses line, replace the vague Fixed placeholder with grouped fixes, and add a short Under the hood section for significant non-UI work. * docs(whats-new): order 1.48 sections by user impact Sort Highlights, Fixed groups, and bullets within each block so the most noticeable changes for everyday use appear first. * docs: CHANGELOG 1.48 strict PR order; clarify WHATS_NEW sorting Reorder Added/Changed/Fixed entries in CHANGELOG [1.48.0] by ascending PR number. WHATS_NEW keeps impact-based order; add a maintainer note that the two files intentionally differ. |
||
|
|
947711a98d |
fix(installer): stderr logging, /dev/tty reinstall prompt, hardened download (#1079)
* fix(installer): write logs to stderr so the download URL isn't polluted * fix(installer): read the reinstall prompt from /dev/tty * fix(installer): add --fail --globoff to the package download * fix(installer): degrade gracefully when no controlling terminal is available |
||
|
|
891ab0dd5b |
feat(now-playing): OpenSubsonic playbackReport for live now-playing (#1080)
* feat(now-playing): adopt OpenSubsonic playbackReport for live now-playing Drive a small playback state machine (starting → playing ↔ paused → stopped) on the Subsonic-server channel when the server advertises the OpenSubsonic `playbackReport` extension (Navidrome ≥ 0.62), giving `getNowPlaying` a real transport state and an extrapolated position. Reports send `ignoreScrobble=true` so play counts stay on the existing `scrobble.view` 50% path (no double count), and the effective playback speed is included so the server extrapolates position correctly with the speed feature on. - New `playbackReportSession` FSM mirrors the existing `playListenSession` lifecycle hooks and is wired at the same player call sites (start / gapless switch / queue restore / resume / 15s heartbeat / pause / seek / stop / ended / error / app quit). Servers without the extension degrade to the unchanged legacy `scrobble.view?submission=false` presence call. - Gated through the existing serverCapabilities framework: a new `FEATURE_PLAYBACK_REPORT` (auto, extension-detected). The OpenSubsonic extensions probe now stores the full advertised list once and serves both AudioMuse `sonicSimilarity` and `playbackReport` from it, without disturbing the legacy Instant Mix opt-in on pre-0.62 servers. - Now Playing dropdown shows a live position bar and a paused indicator. - reportPlayback uses the real request params (mediaId / mediaType / positionMs). Tests: FSM transitions + gating + legacy fallback, capability resolution, extension-list probe storage/decoupling. Full suite green; no Tauri-boundary changes. * feat(now-playing): glide the Live position bar between polls Extrapolate the position of `playing` entries locally (elapsed × reported playbackRate from the last 10 s poll) and re-render once a second, so the Live progress bar advances smoothly instead of jumping on each refresh. Paused and position-less entries stay frozen. A linear width transition matched to the tick keeps the fill gliding. Only applies to clients that report a position via the playbackReport extension. * fix(now-playing): tighten Live timer layout and report resume immediately Keep the progress-bar width stable without a wide empty gap before the timer: reserve ~2ch inside the current-time span only (right-aligned), not on the whole clock block. Report `playing` to the server as soon as resume() runs, matching the immediate `paused` report on pause instead of waiting for the Rust `audio:playing` event. * docs: CHANGELOG and credits for playbackReport live now-playing (PR #1080) |
||
|
|
184e87a469 |
fix(audio): release idle output stream after 60s (#1071) (#1073)
* fix(audio): release idle output stream after 60s (#1071) Lazy-open CPAL on first playback and close the device handle after one minute without active audio so Windows can sleep; emit output-released for cold resume and skip post-wake reopen when idle. * docs: CHANGELOG and credits for idle audio stream fix (PR #1073) * fix(audio): satisfy clippy if-same-then-else in idle watcher * fix(audio): silence rodio DeviceSink drop unless logging is debug Gate log_on_drop(false) on runtime should_log_debug() so normal/off logging modes avoid stderr noise from intentional idle stream release. * feat(audio): cold-start paused restore and silent engine prepare After getPlayQueue on startup, apply saved seek position to the UI, prefetch the current track to hot cache, and load the engine paused via new audio_play startPaused so playback does not audibly start before pause. Shared engineLoadTrackAtPosition with queue-undo restore. * fix(audio): satisfy clippy too_many_arguments on stream arm helper Bundle spawn_legacy_stream_start_when_armed parameters into LegacyStreamStartWhenArmed so workspace clippy passes. * fix(audio): release output stream immediately on stop (#1071) Stop and natural queue end call audio_stop; close the CPAL device right away instead of waiting for the 60s idle timer. Pause keeps the grace period for warm resume. * fix(audio): keep waveform mounted after stop (#1071) Stop preserves currentTrack, so its cached analysis waveform stays valid. Stop no longer nulls waveformBins for the still-shown track and re-hydrates them from the analysis DB, instead of dropping to flat placeholder bars. * test(audio): cover output_stream_is_needed branches; harden audio_play arg (#1071) - Add unit tests for the idle-keepalive decision: empty/playing/paused main sink, preview and fading-out sinks, and radio playing/paused. Players are built device-less via rodio's Player::new + a Zero source, so empty()/state are exercised without an audio device. - Make audio_play's `start_paused` an Option<bool> defaulting to false, so the new field is strictly additive (omitting startPaused no longer fails serde). - Drop the unused `_engine` parameter from start_stream_idle_watcher; it resolves the engine from the AppHandle each poll. * refactor(audio): extract sink-swap lifecycle into sink_swap module (#1071) Move SinkSwapInputs/swap_in_new_sink and the legacy stream-arm helper (LegacyStreamStartWhenArmed/spawn_legacy_stream_start_when_armed) out of play_input.rs into a focused sink_swap.rs, so source selection and source building stay separate from sink lifecycle. play_input.rs drops from 953 to 799 lines. No behavior change. |
||
|
|
4902c0e25b |
Fix MPRIS player duplication during internet radio (#1048) (#1069)
* fix(mpris): disable WebKit media session so radio doesn't duplicate the player Internet radio plays through an HTML <audio> element, for which WebKitGTK auto-registers its own MPRIS player (org.webkit.*) alongside the app's souvlaki one. On Linux desktops that list every player, now-playing then showed twice during radio (issue #1048: one "psysonic", one "Psysonic"). Disable the WebKit media session at main-window setup (enable-media-session, set by GObject property name since the pinned binding has no typed setter, guarded by find_property) so souvlaki stays the single now-playing source; radio metadata still reaches it via mpris_set_metadata. The navigator.media Session push in useRadioMprisSync is kept as a fallback in case a WebKitGTK version still registers the player, so issue #816 cannot regress. * docs(changelog): MPRIS radio duplication fix (PR #1069) |
||
|
|
3de7b57cc5 |
Fullscreen player polish + Discord Rich Presence fixes (#1068)
* fix(fullscreen-player): drop track-number prefix from title, stop clipping descenders
The big title showed a zero-padded queue position ("11. ") before the
song name; remove it (the position still lives in the top bar). Also bump
line-height 1.05 -> 1.25 so overflow:hidden no longer crops descenders
(g, j, p, q, y) at 46px.
* feat(settings): clarify built-in Discord RP vs official Navidrome plugin
Add a bold notice atop the Discord Rich Presence block explaining it is
the built-in integration, and that users wanting the official Navidrome
Discord RP plugin should leave it off and enable "Show in Now Playing"
instead. Add a matching bold note to the Now Playing setting. New i18n
keys discordRichPresenceNotice / nowPlayingPluginNote across all 5 locales.
* fix(discord-rp): use HTTPS cover URL instead of local file:// path
Discord Rich Presence images are fetched by Discord's servers, so
large_image must be a key or an https:// URL they can reach. Since the
cover pipeline moved to an on-disk webp cache, coverArtUrlForDiscord
returned a file://.../800.webp path on cache hit, which Discord cannot
load and silently falls back to the app icon. Always hand it the
getCoverArt URL; MPRIS keeps the local file:// path unchanged.
* i18n(settings): fill discordRichPresenceNotice + nowPlayingPluginNote for es, nb, ro, ru
The two new Discord-RP notice keys had only landed in 5 of the 9 locales;
complete the set so no locale falls back to English.
* docs(changelog): fullscreen title + Discord RP fixes (PR #1068)
|
||
|
|
1a82376f8c |
feat(music-network): unified scrobble & enrichment framework (replaces hard-wired Last.fm) (#1066)
* feat(music-network): core domain types and wire contracts
Foundation for the Music Network framework: provider-agnostic domain
types, capability model, typed errors, and account shapes under
src/music-network/core, plus the ScrobbleWire / EnrichmentWire /
PresetManifest / AuthStrategy contracts. No runtime wiring yet.
* feat(music-network): generic audioscrobbler/listenbrainz/maloja transports
Generalize the Rust remote layer for the Music Network framework. Add
provider-agnostic transports parameterized by base_url:
- audioscrobbler_request: Audioscrobbler v2 with caller-supplied endpoint
(Last.fm, Libre.fm, Rocksky, custom GNU FM, Maloja compat share it)
- listenbrainz_request: Token-header JSON (direct + Maloja LB compat)
- maloja_request: native /apis/mlj_1 JSON
lastfm_request stays as a thin transition delegate against the fixed
host; it is removed once the framework owns all call sites. Wiremock
tests cover audioscrobbler_request with a custom base_url and API-error
mapping.
* feat(music-network): audioscrobbler wire with last.fm + libre.fm presets
Add the Audioscrobbler v2 wire, the behavioural successor to the legacy
src/api/lastfm.ts, implementing the full EnrichmentWire surface (scrobble,
now playing, love/unlove, loved sync, similar artists, track/artist stats,
top lists, recent tracks, user profile, urls).
- client.ts: transport wrapper over audioscrobbler_request, classifying
failures into MusicNetworkError without touching any store
- sign.ts: TS mirror of the api_sig base-string ordering rule (unit-tested)
- auth/tokenPoll.ts: browser token-poll connect flow as a reusable strategy
- presets/lastfm.ts, presets/librefm.ts: bundled, enrichment-capable,
token-poll presets (both endpoints verified live)
Extends WireContext with profileBase and ConnectContext with authBase so
URL builders and connect flows need no preset lookup.
* feat(music-network): listenbrainz + maloja-native wires, paste-auth presets
Add the scrobble-destination wires and presets:
- ListenBrainz wire (scrobble + now playing via playing_now), backing both
the direct api.listenbrainz.org preset and the Maloja /apis/listenbrainz
compat preset (one wire, two presets, differing only by base URL)
- Maloja native wire (/apis/mlj_1/newscrobble, scrobble only — Maloja has
no now-playing endpoint)
- Shared api_key_only paste-auth strategy (token/key/session-key paste);
the Audioscrobbler wire now dispatches token-poll vs paste by preset
- Presets: listenbrainz, maloja_listenbrainz, maloja_native, rocksky
(scrobble-only, session-key paste, bundled keys — verified live), and
custom_gnufm (token-poll, user-supplied url/key/secret)
- Contracts: ConnectContext.authStrategy, PresetManifest.selfHostedApiSuffix
maloja_compat (the {url}/apis/audioscrobbler mode) is intentionally omitted:
its protocol cannot be verified and is almost certainly the legacy handshake,
not the 2.0 web API; Maloja is covered by the native and ListenBrainz modes.
* feat(music-network): registry, orchestrator, enrichment router + runtime facade
Wire the framework together behind a single facade:
- registry: wireRegistry (WireId -> wire), presetRegistry (the 7 built-in
presets), registerBuiltinWires (one-time side-effect registration)
- CapabilityProbe: wire probe overlaid by manifest staticCapabilities as the
final authority (lets two presets on one wire diverge, e.g. Rocksky's
nowPlaying:false over the Audioscrobbler wire's optimistic yes)
- ScrobbleOrchestrator: best-effort fan-out; flips the per-account
session-error flag on AUTH_SESSION_INVALID and clears it on next success
- EnrichmentRouter: resolves the single primary to its EnrichmentWire; the
type guard rejects non-enrichment wires (Maloja/ListenBrainz)
- MusicNetworkRuntime: the only app entry point — accounts, roles, fan-out,
enrichment, urls, probe. Reads/writes state through the MusicNetworkStore
port (Phase 5 backs it with the auth store) and a RuntimeHost for side effects
- getMusicNetworkRuntime singleton + index.ts public barrel
Tests cover fan-out, master toggle, capability gating, session-error
flip/clear, primary eligibility, and enrichment routing.
* feat(music-network): auth-store state + lossless legacy migration + runtime bridge
Add the persisted Music Network state to the auth store and wire the runtime,
all additively — nothing existing breaks yet.
- authStoreTypes: musicNetworkAccounts / enrichmentPrimaryId /
scrobblingMasterEnabled + actions; legacy lastfm* fields kept until Phase 6
- authMusicNetworkActions + defaults/wiring (synchronous localStorage)
- accountPersistence: migrateLegacyLastfm (lossless — preserves session key,
username and scrobbling preference; fills bundled Last.fm key from the preset;
sets the migrated account as enrichment primary) + sanitizeAccounts
- authStoreRehydrate: one-shot migration guarded by a sentinel so a later
disconnect cannot resurrect the account from still-present legacy fields
- musicNetworkBridge: backs the MusicNetworkStore port with the auth store and
the RuntimeHost with the Tauri shell; initialized in pre-React bootstrap
nowPlayingEnabled stays a global toggle (not a lastfm* field); the Phase 6
playback call-site will gate dispatchNowPlaying on it, preserving behaviour.
* feat(music-network): route playback, enrichment and love through the runtime
Migrate every Last.fm call-site onto the Music Network runtime, preserving
behaviour:
- playback (audioEventHandlers, playTrackAction): scrobble@50% and now-playing
via dispatchScrobble/dispatchNowPlaying; loved-fetch via isTrackLoved. Now-
playing follows scrobbling (as Last.fm did), Navidrome now-playing keeps the
nowPlayingEnabled gate
- enrichment: useArtistSimilarArtists, useNowPlayingFetchers, Statistics, and
the ArtistDetail similar-artists gate now use the runtime, gated on an
enrichment primary
- love: PlayerBar, PlayerTrackInfo, all context menus, useNowPlayingStarLove and
the startup loved-sync route through setTrackLoved / toggleNetworkLove
- player store: lastfmLoved/lastfmLovedCache -> networkLoved/networkLovedCache;
lastfmActions -> networkLoveActions; loved cache storage renamed with a
lossless legacy-key fallback
- getMusicNetworkRuntimeOrNull() for best-effort callers so they no-op (not
throw) before the runtime is initialized
src/api/lastfm.ts and the Integrations UI still use the legacy path; they are
migrated and removed in the next phase.
* feat(music-network): manifest-driven Integrations UI + scrobble batch format
Replace the Last.fm Integrations card with a manifest-driven Music Network
section, and fix Audioscrobbler scrobbling to the batch/array shape.
- settings/musicNetwork/: MusicNetworkSection (master toggle, destination
cards, enrichment-primary picker, Maloja proxy warning, add-a-service list)
driven entirely off the preset registry; icon map from PresetManifest.icon
- IntegrationsTab delegates to MusicNetworkSection (Discord/Bandsintown/
Navidrome now-playing unchanged)
- i18n: musicNetwork.* across all 9 locales, incl. a per-field help hint for
Rocksky's CLI session-key flow (rocksky login)
- scrobble now uses the documented array form (artist[0]/track[0]/…); the bare
single form is only tolerated by Last.fm, Rocksky requires the indexed form
- auth-error detection keys off the response message (not the ambiguous numeric
code) so a Rocksky server-500 no longer flips the account to a reconnect state
- PresetManifest.PresetField gains an optional helpKey
Rocksky's server rejects some non-ASCII track metadata with a 500 — a Rocksky
backend bug; the client call is correct (verified).
* fix(music-network): clearer Integrations layout + scrobble batch fix
Address UI feedback on the Music Network section:
- per-account scrobble toggle moves inside its account block (was a loose
row between cards — unclear which account it belonged to)
- master toggle and the primary-service picker are now boxed blocks at the top,
not bare rows
- primary-service copy reworked: 'Primary service' + a line spelling out that
liked tracks/similar artists/stats come from it while scrobbling still goes
to all enabled services
- distinct zones separated by dividers (master/primary · connected · add)
Also folds in the verified scrobble fixes: documented array form
(artist[0]/track[0]/…) so Rocksky accepts scrobbles, and auth-error detection
by message rather than the ambiguous numeric code (a Rocksky server-500 no
longer flips the account to a reconnect state). Rocksky session-key field gains
a CLI help hint (rocksky login), all 9 locales.
* feat(music-network): indicator + remove legacy lastfm path
Phase 7b/7c — finish the cutover and delete the old Last.fm path.
- LastfmIndicator -> MusicNetworkIndicator (shows the enrichment primary's
status, click -> Integrations)
- delete src/api/lastfm.ts; remove Rust lastfm_request (remote.rs + lib.rs)
- remove legacy authStore lastfm* fields, actions and types; delete
authLastfmActions.ts; rehydrate migration reads the legacy blob via a cast
- migrate the remaining NowPlaying call-sites (NowPlaying.tsx + the now-playing
fetchers/prewarm/star-love hooks) off lastfmSessionKey/lastfmUsername onto the
enrichment primary (gate + cache key)
- type imports LastfmTrackInfo/LastfmArtistStats -> music-network TrackStats/
ArtistStats; drop 8 stale api/lastfm test mocks and the obsolete Last.fm auth
tests; update settingsTabs + src/CLAUDE.md
No lastfm imports remain outside src/music-network/; lastfm_request removed
(acceptance §12). tsc clean, 1947 frontend tests + remote rust tests green.
* test(music-network): cover scrobble shape + error classification, drop dead i18n keys
Remove the 14 unused legacy scrobble/connection i18n keys across all 9
locales (settings.lfm*/scrobble*, connection.lastfm*); the live love,
profile-link and now-playing keys stay.
Add regression tests for the parity-critical transport logic: the indexed
batch/array scrobble body, the auth-vs-network error classification
(numeric codes collide across providers), and the manifest-overrides-probe
capability merge.
* feat(music-network): provider-agnostic UI + Maloja Audioscrobbler & Koito presets
- de-hardcode the single provider name across every enrichment surface
(love labels, now-playing badge, stats title); derive it from the
enrichment primary and interpolate via {{provider}} i18n params
- surface a toast when a paste-auth connect probe fails — a static
'supported' capability flag no longer masks a runtime probe error
- add the Maloja Audioscrobbler (GNU FM) preset (the third Maloja wire
mode) and a Koito preset (ListenBrainz-compatible), both data-only
- generalise PRIVACY.md and the scrobbling help entry to the framework
- rename residual lfm* identifiers to network*; strip legacy flat
lastfm* fields from the persisted blob; neutral transport error prefix
- tests: error classification, scrobble body, capability probe, registry
* fix(music-network): validate paste-auth keys on connect + UI polish
- AudioscrobblerWire.probe now validates an api_key_only session with a
signed call and reports scrobble:'error' only on a genuine auth failure
(a scrobble-only service that rejects user.getInfo is not a bad key), so
an invalid Maloja Audioscrobbler / Rocksky key surfaces a connect toast
instead of failing silently; WireContext carries the preset authStrategy
- drop the unreachable Statistics empty-state branch and its dead
lfmNotConnected i18n key; use the useEnrichmentPrimaryLabel hook there
- drive the love-button glyph from the enrichment primary's manifest icon;
neutral Music Network section icon; remove a dead LastfmIcon import
- tests: paste-auth vs token-poll probe behaviour
* chore(music-network): rename showLastfmSimilar → showNetworkSimilar, refresh stale comments
Post-parity polish: the similar-artists toggle now sources from the generic
enrichment runtime, so rename the lingering lastfm-flavoured identifier; drop
stale 'Mirrors today's LastfmX' doc comments referencing the removed legacy
types, and generalise the scrobble-point comment.
* docs(changelog): Music Network entry (PR #1066)
Co-Authored-By: cucadmuh <49571317+cucadmuh@users.noreply.github.com>
* fix(music-network): bound request timeout on the provider transports
audioscrobbler_request / listenbrainz_request / maloja_request built a
reqwest client with no timeout, so a hung provider left scrobble / probe /
loved-sync promises unresolved. Add a shared provider_http_client() with a
15s timeout, matching the sibling fetch_* commands. Addresses review C3.
* refactor(music-network): dedupe wire transport + no-enrichment helpers
The three provider clients repeated the same invoke -> classify-error ->
MusicNetworkError boilerplate, and the three probe() bodies repeated the
"mark every enrichment capability no" loop. Extract
wires/shared/invokeTransport() (each wire keeps its own arg shape + auth
rule) and markNoEnrichment() in core/capabilities.ts. Addresses review C4.
* refactor(music-network): drop write-only malojaWireMode dead state
malojaWireMode was written on connect but never read — the wire is resolved
by wireId and the Maloja base URL by the preset's selfHostedApiSuffix.
Remove the field, the MalojaWireMode type, the malojaWireModeFor helper,
the AccountPatch entry, the PresetField union member, and the export.
Addresses review C1.
* refactor(music-network): one useEnrichmentPrimary hook, drop lastfm fallback
The enrichment-primary lookup (accounts.find by enrichmentPrimaryId) was
duplicated across two hooks and inlined in the indicator and both
context-menu builders, two of them with a hardcoded 'lastfm' icon fallback.
Add one music-network/ui/useEnrichmentPrimary() returning
{account,label,icon}|null; useEnrichmentPrimaryLabel/Icon delegate to it and
the indicator + context menus consume it directly. Icon fallback is the
neutral 'custom' glyph, never a provider (provider-agnostic, §7.3).
Addresses review C2.
---------
Co-authored-by: cucadmuh <49571317+cucadmuh@users.noreply.github.com>
|
||
|
|
ea304357ca |
fix(favorites): reflect player-bar song star in track lists (#1063)
* fix(favorites): reflect player-bar song star in track lists Liking a song from the player bar / fullscreen / shortcuts wrote only to the session `starredOverrides` map, which onStarSuccess deleted once the server sync resolved. List views (AlbumDetail, Favorites, RandomMix, playlists) seed their starred state from a one-shot fetch and reflect later changes only by merging that override, so the row reverted the instant the sync completed — the like never stuck in the list. Toggling from a list row also updated the row's own local state, which is why that direction already worked. Keep the star override as the durable session source of truth (stop deleting it on success); the in-memory Track / queue-cache patches stay. The override is unpersisted and superseded by the next toggle, so it never diverges from the server. Ratings keep their existing clear-on-success behavior. |
||
|
|
90452a8f8c |
fix(whats-new): allow plugin-fs write into AppData cache dir (#1062)
* fix(whats-new): allow plugin-fs write into AppData cache dir mkdir for release-notes/ succeeded but write_text_file to nested paths was denied without fs:allow-app-write-recursive — RC/stable re-fetched whats-new.md on every launch with an empty cache folder. * docs: CHANGELOG PR #1062 for release-notes cache write |
||
|
|
5cd01c90ac |
fix(library): multi-genre local index with track_genre and backfill (#1059)
* fix(library): multi-genre local index with track_genre and backfill Restore atomic genre browse, filters, and counts via track_genre: OpenSubsonic genres[] first with Navidrome-default split fallback, sync write path, read-path query switches, blocking startup backfill with progress, and v12 repair migration for DBs that recorded legacy 002–011. TS fallback adds genreTagsFor and migration gate i18n across locales. * fix(library): address multi-genre review — robust TS genres and scope join genreTagsFor routes raw genres through parseItemGenres (single-object Subsonic quirk and bare strings). Library-scoped genre browse/counts join track for raw_json library_id fallback. Statistics keeps empty-genre bucket. * docs: CHANGELOG and credits for multi-genre local index (PR #1059) * docs(changelog): credit HiveMind on Discord for multi-genre report (PR #1059) |
||
|
|
8593858f3a |
fix(whats-new): generate release-notes bundle before dev startup (#1060)
* fix(whats-new): generate release-notes bundle before dev startup Fresh clones failed Vite import analysis because releaseNotesBundle.ts was gitignored and dev/tauri:dev did not run prebuild. Commit a sliced stub and hook prebuild into dev scripts. * docs: CHANGELOG PR #1060 for dev startup bundle fix * fix(whats-new): keep release-notes bundle gitignored Drop the committed generated slice — it would churn on every CHANGELOG/WHATS_NEW edit. Prebuild before dev/tauri:dev is enough. |
||
|
|
c7d71ea57c |
feat(whats-new): remote release notes with dev workspace mode (#1058)
* feat(whats-new): remote release notes with dev workspace mode Add WHATS_NEW.md, CI whats-new.md asset upload, and client fetch/cache with embedded fallbacks. Dev and -dev builds read the full file from the repo for debugging; RC/stable download the release asset on first use. * fix(whats-new): render ## headings and add changelog tab Parse h2 sections in release-notes markdown; load changelog alongside highlights and let users switch views on the What's New page. * fix(whats-new): prefetch on startup and fix CI typecheck prebuild Prefetch whats-new asset when the shell loads on RC/stable builds. Run prebuild:release-notes before tsc and coverage jobs so the gitignored generated bundle exists in CI. * docs: CHANGELOG and credits for What's New remote notes (PR #1058) * fix(whats-new): always slice embedded release notes to current line Drop full CHANGELOG embed for -dev bundles; tauri:dev still reads live markdown from the repo. Ignore all of src/generated/ in git. * fix(whats-new): fetch release asset via Rust to bypass CORS Route whats-new.md download through fetch_url_bytes; rename the technical tab label; add fetch unit tests (PR #1058 review). |
||
|
|
fb5a257735 |
fix(library): show album artist correctly in album grids (#1056) (#1057)
* fix(library): show album artist in album grids (#1056) Prefer album-artist tags over track artist when building album rows from the local index, and align grid cards with OpenSubsonic displayArtist. * fix(library): align album artist in FTS, search, and offline paths (#1056) Apply album-artist preference in FTS album dedupe and live search, fix offline pin hydration order, and use albumArtistDisplayName in remaining cheap UI/export/download call sites. * docs(changelog): album artist grid fix for compilations (PR #1057) * fix(library): parity guard and live-search album artist helper (#1056) Align SQL ELSE branch with pick_album_group_artist trimming, add parity test, and use albumArtistDisplayName in LiveSearch and MobileSearchOverlay. |
||
|
|
707a41f615 |
fix(scrobble): Navidrome Now Playing with local playback and mixed-server queue (#1055)
* fix(scrobble): report Now Playing on playback server with local bytes Navidrome presence and play-count scrobbles no longer skip when audio plays from hot cache, offline pins, or favorites-auto, and reachability follows the queue/playback server instead of the browsed active server. * docs: changelog and credits for PR #1055 |
||
|
|
ae9be74719 |
feat(settings): compact server cards with capability badges (#1054)
* feat(settings): redesign server cards with identity line and capability badges Compact two-line server headers (entry name + user@host), HTTPS lock, and a clickable version info tooltip. Navidrome ≥0.62 shows a green AudioMuse inline badge; older Navidrome keeps the manual toggle row. Adds click-pinned tooltips via data-tooltip-click on TooltipPortal. * feat(settings): unify use/active slot and move delete into edit form Merge Active badge and Use button into one rightmost action; Active uses green styling. Reorder actions to edit, test, use/active. Remove the card delete icon — deletion lives in the server edit form footer. * docs: note compact server cards in CHANGELOG and credits (PR #1054) * fix(credits): move PR #1054 server cards line to cucadmuh block Was appended to Psychotoxical's contributions array by mistake; CHANGELOG already credited cucadmuh (gh pr view author). |