Rewrite changelog entries in user voice

Trimmed the WHATS_NEW '2.4.0' block (27 entries) and the full
VERSION_MODAL_SECTIONS array (23 sections) from the diagnostic-paragraph
style I'd been defaulting to into something terse and casual:

- Descriptions are 1-2 short sentences instead of multi-clause writeups.
- Modal feature bullets capped at 3-7 short items each.
- Stripped parenthetical credits from titles (no more "(kettui Review)",
  "(Images, Counts, Title Hints)" — those belong in git history, not UI).
- Lowercase casual tone throughout description bodies.
- No reporter handles in entry text.

Net: 176 insertions / 194 deletions. helper.js parses, 553 tests pass.
pull/382/head
Broque Thomas 2 months ago
parent 7714b51a50
commit 04ff287c72

@ -3444,33 +3444,33 @@ const WHATS_NEW = {
'2.4.0': [
// --- April 26, 2026 — Search & Artists unification + reorganize queue ---
{ date: 'April 26, 2026 — 2.4.0 release' },
{ title: 'Reorganize Queue: Race-Condition Hardening (kettui Review)', desc: 'kettui\'s review of PR #377 caught two real concurrency bugs in the new reorganize queue and one input-deduplication gap. (1) Worker race: the worker thread looked up the next queued item, then released the lock, then re-acquired it to flip status to "running". A cancel() landing in that window would mark the item cancelled but the worker still ran it. Now picks and flips atomically under a single lock acquisition. (2) Wakeup race: the worker cleared its wakeup event after observing an empty queue, but enqueue could fire its wakeup.set() between the empty check and the clear, making a freshly-queued album sleep up to 60 seconds before the worker noticed. Replaced the lock + event pair with a single threading.Condition so check-and-wait happen under the same lock atomically. (3) Bulk-enqueue dedupe: enqueue_many called single-item enqueue in a loop, so two copies of the same album_id in one bulk request could both slip through if the worker finished the first copy before the loop reached the second. Now holds the queue lock for the entire batch and tracks a per-batch seen set, so intra-batch duplicates are deduped against each other, not just against pre-existing items. Also fixed two related issues from the same review: the reorganize-preview Apply button could get stuck disabled when an early return / network error skipped the re-enable line (moved into a finally), and the new DB helpers (get_album_display_meta, get_artist_albums_for_reorganize) used to swallow every exception and return None / [], which made a real DB outage look like "album not found" — they now let exceptions bubble so the route layer surfaces a proper 500', page: 'library' },
{ title: 'Reorganize Queue with Live Status Panel', desc: 'Reorganizing albums no longer locks up the page or runs as a JS-driven loop. Each click on the per-album reorganize button — or "Reorganize All" — now enqueues into a single FIFO queue that a backend worker drains one item at a time. Buttons stay clickable: spam-clicking the same album silently dedupes, and you can keep browsing while items run. A status panel mounted at the top of the artist actions bar shows what\'s active (with a progress bar, current track, and live moved/skipped/failed counts), how many items are queued behind it, and recently-finished items with success/warning indicators. The panel expands to show the full queue with per-item cancel buttons (running items can\'t be cancelled mid-flight; queued ones can) and a "Cancel All" button for the queued tail. Items belonging to a different artist than the page you\'re on are flagged with a "(other artist)" hint so you understand what you\'re seeing. Bonus: "Reorganize All" is now one backend call instead of N JS-driven calls — much faster, and the artist context is captured server-side per item so the queue can show cross-artist progress correctly. Also retired the old single-slot status endpoint and the polling loop that depended on it', page: 'library' },
{ title: 'Fix Album Completeness Job Reporting Zero Findings for Everyone', desc: 'sassmastawillis reported the Album Completeness maintenance job was finishing in 0.1s with 0 findings, even for users with obviously-incomplete albums. Root cause: the job used `albums.track_count` as the "expected total" to compare against the library\'s actual count. But `track_count` is populated by server syncs (Plex leafCount, SoulSync standalone len(tracks)) — it\'s always the OBSERVED count, never what the metadata provider says the album should contain. So expected == actual always, and every album looked complete. Fix: new `api_track_count` column on the albums table, written only by metadata-source code paths (Spotify, iTunes, Deezer, and Discogs enrichment workers now populate it whenever they fetch album data, so it piggybacks on existing API calls instead of making new ones). Server syncs never touch this column, so it stays authoritative. The repair job uses it as the expected total; if an album somehow hasn\'t been enriched yet, the job falls back to a live API lookup and caches the result. For users with an already-enriched library, the first completeness scan after the upgrade is fast because the workers will have populated the column during normal enrichment cycles', page: 'library' },
{ title: 'Library Reorganize: Reroute Through the Download Pipeline', desc: 'Reported by winecountrygames — using "Reorganize All" on a 3-disc Aerosmith deluxe collapsed it to a flat 1-disc layout, and on other albums it left half the tracks in their original location with no error or count of what was skipped. Root cause: the reorganize endpoint reinvented several wheels (its own template engine, its own disc-number resolution from file tags, its own sidecar sweep, its own collision detection) and each had drifted from the canonical post-processing path used by downloads. The reorganize-only logic read disc_number from file tags and silently defaulted to 1 on any failure, so a single tag-less file collapsed the whole album to single-disc. Tracks whose file paths didn\'t resolve on disk were silently skipped. Rewrote it to follow the import page\'s pattern: copy each file to a per-album staging folder under your download path, look up the canonical tracklist from your configured metadata source (Deezer / Spotify / iTunes / Discogs / Hydrabase) using the album\'s stored source IDs, then route each file through the same `_post_process_matched_download` function fresh downloads use — same template, same tagging, same multi-disc subfolder logic, same sidecar handling, same AcoustID verification. Albums with no stored source ID are reported back and skipped entirely (degrading silently to file tags is what caused the original bug). Tracks not in the source\'s catalog version (bonus tracks on a deluxe edition) are reported as skipped and left in place rather than force-fed wrong context. Files that don\'t resolve on disk are surfaced with the offending DB path so the UI can show them. The 230-line inline reorganize logic in web_server.py was extracted into core/library_reorganize.py — net -195 lines from the monolith, +13 unit tests for the new orchestrator. Frontend behavior change: the per-call template parameter in the reorganize modal is now ignored — reorganize uses your configured download template, matching the pipeline downloads use', page: 'library' },
{ title: 'Spotify: Longer Post-Ban Cooldown (30 min)', desc: 'A user reported their Spotify rate-limit ban expired after 4 hours, the system ran its 5-minute post-ban cooldown, and then 32 seconds after the cooldown ended a single get_artist_albums call from a background worker was hit with another 4-hour ban. Diagnosis: Spotify\'s server-side memory of the previous offense outlasted our 5-minute cooldown, so the very first call after cooldown got slapped immediately. The cooldown exists specifically to prevent the "ban expires → we probe → re-ban" cycle, but the value was too short. Bumped from 5 minutes to 30 minutes — same mechanism, just enough room for Spotify to actually forget. A more principled follow-up (adaptive cooldown that scales with the previous ban size, plus making the first post-cooldown call a single light probe rather than allowing background workers through) is documented as a future PR if reports persist after this bump', page: 'dashboard' },
{ title: 'Tidal: Reject Silent Quality Downgrades', desc: 'Netti93 reported that with Tidal set to "HiRes only" and quality fallback disabled, tracks were still downloading successfully — as m4a 320kbps files. Root cause: Tidal\'s API silently serves whatever tier your account + the track + your region permits. Ask for HI_RES_LOSSLESS on a track that\'s only in LOW_320K and Tidal returns the AAC stream without raising. The downloader wrote the m4a to disk, the filesize cleared the 100KB stub threshold, and the download reported success. The worker-level fallback chain (hires → lossless → high → low) also never got a chance to advance, because every tier "succeeded" at the first one that returned anything. Fix: after getting the stream, compare stream.audio_quality against what we requested using a rank-based tier comparison (LOW < HIGH < LOSSLESS < HI_RES < HI_RES_LOSSLESS). Same tier or better = accept (so occasional Tidal upgrades don\'t get thrown away). Lower tier = treat this tier as failed, which lets the fallback chain advance when fallback is enabled or fails the whole download honestly when the user has "HiRes only, no fallback" configured. Unrecognized audioQuality values (a new Tidal tier we haven\'t mapped yet) are rejected conservatively so the final diagnostic log can name the unknown value. Older tidalapi builds without the audio_quality attribute fall through to the pre-existing codec / file-size guards so nothing regresses', page: 'downloads' },
{ title: 'Search Source Picker Icon Row', desc: 'The Search page now has a row of source icons above the search bar — one per source (Spotify, Apple Music, Deezer, Discogs, Hydrabase, MusicBrainz, Music Videos, Soulseek). Typing searches only the currently-selected source instead of fanning out to every one by default. Click a different icon to switch; results come back on demand. The default icon on page load is your configured primary metadata source. Replaces the short-lived "Search from" dropdown that preceded this', page: 'search' },
{ title: 'Per-Query Source Cache (No More Re-Fetching)', desc: 'Once you\'ve searched a source for a given query, switching back to it is instant — results are cached for the current query. A small dot on each source icon shows which ones already have cached results this query. Type a new query and the whole cache resets. Same behavior in the sidebar global search popover. Net effect: roughly 6-7x fewer API calls per search compared to the old default fan-out', page: 'search' },
{ title: 'Global Search Widget Source Parity', desc: 'The sidebar Cmd+K / "/" search popover gained the same source icon row as the full Search page. Pick your source up front, see cache dots for already-fetched sources this query, and the rate-limit fallback banner appears if the backend substituted a different source than the one you clicked. Clicking the Soulseek icon hands off to the full Search page (raw file results need more room than the popover provides)', page: 'search' },
{ title: 'Rate-Limit Fallback Banner', desc: 'If you click Spotify but the backend auto-fell back to Deezer because Spotify was rate-limited, the search results now lead with a small amber banner ("Spotify unavailable — showing Deezer.") and the Spotify icon gets an amber border. Previously results just silently showed as the fallback source with no signal that anything unusual happened', page: 'search' },
{ title: 'Explicit Source Selection on /api/enhanced-search', desc: 'The enhanced-search endpoint now accepts an optional `source` body param (spotify, itunes, deezer, discogs, hydrabase, musicbrainz, auto). When a specific source is chosen, only that provider is queried and db_artists (local library matches) still come back. Cache keys isolate per-source so single-source and multi-source results don\'t collide. Omitted or `auto` preserves the old multi-source fan-out behavior unchanged — nothing breaks for existing callers', page: 'search' },
{ title: 'Shared Enhanced-Search Fetch Helper', desc: 'Internal refactor — the Search page dropdown and the global search widget now route through one shared enhancedSearchFetch helper in search.js instead of duplicating the POST boilerplate. Zero UX change, but it means any future source-picker tweak only needs wiring in one place', page: 'search' },
{ title: 'Search Page Renamed to /search', desc: 'The Search page\'s internal id is now "search" instead of the confusing "downloads" (which clashed with the actual Downloads page). Sidebar label unchanged. URL is now /search; /downloads still resolves so old bookmarks keep working. Profile ACL "Page Access" now saves as "search"; existing profiles with "downloads" in allowed_pages still resolve through a legacy-compat check', page: 'search' },
{ title: 'Embedded Download Manager Removed from Search Page', desc: 'The Search page used to carry a second copy of the Download Manager (active + finished queues, clear/cancel-all buttons) that was hidden by default and duplicated the dedicated Downloads page. That duplicate is gone — toggle button, side-panel HTML, and its 1-second polling loop all removed. About 330 lines of dead code cleaned up. The dedicated Downloads sidebar page is now the single downloads UI', page: 'search' },
{ title: 'Artists Sidebar Entry Retired — Use Search Instead', desc: 'Cin flagged that "Artists" in the sidebar read like a library section but was actually a dedicated artist-search page, duplicating what the unified Search already does. The sidebar entry is gone. New flow: Sidebar → Search → type artist name → click their result. "Browse Artists" on the empty Watchlist page and "View artist from Wishlist" now open Search pre-filled with the artist\'s name. Removed "Artists" from profile Home Page + Page Access options. Deep link to /artists still resolves so old bookmarks keep working — the page just isn\'t promoted anywhere', page: 'search' },
{ title: 'Artist Detail Back Button Fallback', desc: 'The back button on the Artists-page inline detail view used to dump users on an empty "Search for an artist..." screen when they arrived from outside the Artists page — a dead end now that Artists isn\'t in the sidebar. If you searched inside the Artists page, back still returns to your results list. Otherwise (arriving from Search, Discover, Watchlist, etc.), back uses the browser history to land you on whichever page you came from. Falls back to the Search page only when there\'s no browser history to go back to (the natural place to find another artist)', page: 'search' },
{ title: 'Interactive Help Updated for Unified Search', desc: 'The click-for-help annotations and the "Your First Download" guided tour were rewritten for the new Search page. Stale annotations pointing at removed elements (Basic/Enhanced toggle button, side-panel queues, download-manager controls) are deleted. The first-download tour now runs on /search and opens with the source picker. PAGE_TOUR_MAP accepts both "search" and the legacy "downloads" id so old bookmarks still match a tour. Retired the standalone "Browse Artists" tour', page: 'help' },
{ title: 'Unified Source-Picker Controller (Search Page + Global Widget)', desc: 'Internal refactor — the source picker state machine (query, active source, per-query cache, fallbacks, loading state, configured-source discovery) is now a single createSearchController factory in shared-helpers.js. Both the full Search page and the sidebar global search popover consume the same controller with per-surface wiring (DOM elements, Soulseek handoff, unconfigured-source click). About 380 lines of near-duplicated state + fetch + render code consolidated into one implementation, so a bug fix or behavior tweak to the picker lands everywhere at once. Zero UX change — every keystroke, icon click, cache hit, rate-limit fallback, and unconfigured-source redirect behaves identically to before', page: 'search' },
{ title: 'Fix Clean Search History Automation Failing with AttributeError', desc: 'The hourly Clean Search History maintenance automation was crashing with "DownloadOrchestrator object has no attribute base_url". Root cause: the check `soulseek_client.base_url` was written before the orchestrator refactor — `soulseek_client` is now a DownloadOrchestrator that wraps individual download clients, with the real Soulseek client at `.soulseek`. Two other call sites in web_server.py already used the correct `soulseek_client.soulseek.base_url` pattern; this one was missed. Now matches the same getattr-guarded pattern and the hourly cleanup runs again', page: 'stats' },
{ title: 'Search Results Always Visible — Show/Hide Button Removed', desc: 'The "Show Results / Hide Results" toggle next to the search bar is gone. There was nothing else on the page worth seeing instead of results, so toggling visibility never made sense. Cin flagged it during PR review. Dropdown visibility is now a pure function of query state — empty input hides it, results show it', page: 'search' },
{ title: 'Cached Search Results Restore on Navigate-Back', desc: 'Previously, navigating away from /search via a sidebar link dismissed the dropdown (the click registered as outside-click). When you came back, the input still held your query but the results were hidden until you typed again or clicked Show Results. Now the per-query cache renders automatically when you re-enter /search, so your results are right where you left them. Cin flagged the round-trip during PR review', page: 'search' },
{ title: 'Fix Soulseek Handoff from Global Search Going Through Metadata Flow', desc: 'When you clicked the Soulseek icon in the sidebar global search popover, it navigated to /search and wrote the query into the enhanced-search input — which then ran the metadata flow against whatever your default source was (Spotify, Deezer, etc.) instead of the raw Soulseek file search you actually wanted. Cin flagged it during PR review. Now the handoff pre-fills the basic-search input directly and clicks the Search page\'s Soulseek icon so the controller\'s onSoulseekSelected callback owns the section swap and runs performDownloadsSearch with the right query', page: 'search' },
{ title: 'Stale Search Requests No Longer Flash Empty Results on Fast Retype', desc: 'Cin flagged a race in createSearchController: when you typed a query then quickly re-typed before the first fetch returned, the first fetch\'s catch block (firing on AbortError after the second submitQuery aborted it) cleared loadingSources and notified the UI, causing a brief flash of empty/error state while the new query\'s fetch was still mid-flight. Added a monotonic _requestSeq token — each fetch captures the next value, and stale completions bail before mutating shared state. The controller still aborts in-flight fetches on supersession; this just keeps the abort-cleanup of the old request from clobbering the new one\'s spinner', page: 'search' },
{ title: 'Source Picker Dims Soulseek When slskd Isn\'t Configured', desc: 'Cin pointed out that the Soulseek icon was always rendered as configured, so users without slskd set up could click it and fire searches that would never succeed. Soulseek is now in the backend config-status registry as `required: [slskd_url]` and removed from the frontend\'s always-configured set. Without slskd, the icon dims and clicking it routes to Settings → Downloads tab (where the slskd URL field lives, gated behind the download-source dropdown) instead of Settings → Connections', page: 'search' },
{ title: 'Fix Discover Hero "View Discography" 404ing on Source Artists', desc: 'Clicking "View Discography" on the Discover page hero slideshow was calling navigateToArtistDetail without a source, so /api/artist-detail defaulted to a library lookup and returned 404 for artists that don\'t exist in your library (which is nearly every hero artist — they come from discover similar-artists, not the library). Regression from the unification PR that rewrote the click handler to route to /artist-detail but forgot to pass the source. Backend already sends artist.source on each hero entry; we now stash it as data-source on the discography button and thread it through to navigateToArtistDetail so the API call includes source=itunes/deezer/etc. and returns the synthesized discography', page: 'discover' },
{ title: 'MusicBrainz Search Actually Works Now', desc: 'kettui flagged during PR #371 review that the MusicBrainz source tab never returned artists and served garbage tracks/albums. Three things were wrong. First, the artist search was hardcoded to return an empty list — re-enabled with a proper fuzzy query (bare Lucene string against alias/artist/sortname indexes) and score-filtered at 80+ to drop tribute bands. Second, track and album searches used text-search on recording/release TITLES — so typing "metallica" matched random tracks literally named "Metallica" (all scoring 100 because they\'re exact title hits). Now a bare name query resolves to the top-scoring artist, then BROWSES that artist\'s release-groups and recordings directly — the same pattern Plex uses. Structured "Artist - Title" queries still take the text-search path since the user gave an explicit title to match. Third, the adapter was firing synchronous Cover Art Archive HEAD requests (up to 30s of blocking probes per search) — replaced with deterministic URL construction so the browser loads images lazily with <img onerror> fallback. Search completes in ~3 seconds instead of 30+ on cold cache. Also shipped: project URL in User-Agent per MB\'s rate-limit policy recommendations', page: 'search' },
{ title: 'MusicBrainz Search Follow-Ups (Images, Counts, Title Hints)', desc: 'Three fixes from kettui\'s follow-up pass on the MusicBrainz search PR. (1) Artist images were missing because MB doesn\'t store artist art — the lazy-load endpoint now accepts an optional `name` query param and resolves images by searching iTunes/Deezer for that artist name. (2) Track total_tracks was off by one because the counter initialized at 1 before summing release media track-counts — an 11-track album reported 12. Initialized to 0 now, with a special case for standalone recordings that have no release (report 1). (3) Queries like "The Beatles Abbey Road" used to browse The Beatles\' whole discography because the artist-first path resolved the artist and ignored the trailing title. Now extracts the title hint from queries shaped like "Artist Title", filters browse results to match, and falls back to text-search when no browse result matches (so "The Beatles Totally Fake Album" still finds something rather than nothing). 10 new tests covering title-hint extraction, browse-filter behavior, total_tracks edge cases', page: 'search' },
{ title: 'Reorganize Queue Polish', desc: 'cleaned up some race conditions in the reorganize queue. cancel + bulk dedupe behavior is solid now. preview button no longer gets stuck disabled on errors.', page: 'library' },
{ title: 'Reorganize Queue with Live Status Panel', desc: 'reorganize is now a queue with a live status panel. spam-click all you want — items run one at a time and you can keep browsing while they go. expand the panel to see queue + cancel buttons.', page: 'library' },
{ title: 'Album Completeness Job Actually Works', desc: 'completeness job was finding zero issues for everyone. now it works — uses real expected track counts from your metadata source instead of comparing your library to itself.', page: 'library' },
{ title: 'Reorganize Routes Through the Download Pipeline', desc: 'reorganize now uses the same pipeline downloads use. fixes 3-disc albums collapsing to single-disc and tracks silently disappearing on you. extracted to core/library_reorganize.py.', page: 'library' },
{ title: 'Spotify: Longer Post-Ban Cooldown', desc: 'bumped the post-ban cooldown from 5 to 30 minutes. first call after a ban was getting re-banned within seconds because spotify\'s memory outlasts the cooldown.', page: 'dashboard' },
{ title: 'Tidal: No More Silent Quality Downgrades', desc: 'tidal was silently serving 320kbps when you asked for hires. now it rejects the downgrade and the fallback chain advances properly — or fails honestly if you have "hires only, no fallback" set.', page: 'downloads' },
{ title: 'Search Source Picker Icon Row', desc: 'search page now has a row of source icons above the bar — one per source. typing only searches the active source instead of fanning out to all of them. click another icon to switch.', page: 'search' },
{ title: 'Per-Query Source Cache', desc: 'switching back to a source you already searched is instant — results are cached for the current query. cache resets when you type a new query. ~6-7x fewer api calls per search.', page: 'search' },
{ title: 'Global Search Widget Source Parity', desc: 'the sidebar global search popover got the same source icon row + cache dots + fallback banner as the full search page.', page: 'search' },
{ title: 'Rate-Limit Fallback Banner', desc: 'if the backend swaps your selected source for a working one (e.g. spotify rate-limited → deezer), you get a small amber banner explaining the swap. icon for the failed source gets an amber border.', page: 'search' },
{ title: 'Explicit Source Selection on /api/enhanced-search', desc: 'enhanced-search endpoint takes a source param now to skip the fan-out backend-side. cache keys isolate per-source so single and multi-source results don\'t collide.', page: 'search' },
{ title: 'Shared Enhanced-Search Fetch Helper', desc: 'internal — search dropdown and global widget share one fetch helper now instead of duplicating the post boilerplate.', page: 'search' },
{ title: 'Search Page Renamed to /search', desc: 'search page is now /search instead of the confusing /downloads (which clashed with the actual downloads page). old urls still work.', page: 'search' },
{ title: 'Embedded Download Manager Removed from Search Page', desc: 'killed the duplicate download manager on the search page (~330 lines of dead code). dedicated downloads page is the only one now.', page: 'search' },
{ title: 'Artists Sidebar Entry Retired', desc: 'removed the artists sidebar entry — unified search already does what it did. old /artists urls still resolve.', page: 'search' },
{ title: 'Artist Detail Back Button Fallback', desc: 'back button on inline artist detail uses browser history when you arrived from outside the artists page, instead of dumping you on an empty artists search.', page: 'search' },
{ title: 'Interactive Help Updated for Unified Search', desc: 'rewrote the click-for-help annotations and the first-download tour for the new search page. retired the standalone browse-artists tour.', page: 'help' },
{ title: 'Unified Source-Picker Controller', desc: 'internal — search page and global widget share one controller now (~380 lines of duplicate state/fetch/render code gone). bug fixes land everywhere at once.', page: 'search' },
{ title: 'Fix Clean Search History Automation Crashing', desc: 'hourly clean-search-history automation was crashing on a stale base_url path. fixed.', page: 'stats' },
{ title: 'Search Results Always Visible', desc: 'killed the show/hide results toggle. visibility is just based on whether you\'ve typed a query.', page: 'search' },
{ title: 'Cached Search Results Restore on Navigate-Back', desc: 'leaving and coming back to /search now re-renders your last query\'s results from cache instead of hiding them.', page: 'search' },
{ title: 'Fix Soulseek Handoff from Global Search', desc: 'clicking soulseek in the global search popover used to run metadata search against your default source instead of basic file search. fixed.', page: 'search' },
{ title: 'Stale Search Requests No Longer Flash Empty', desc: 'fast retypes used to flash an empty state for a moment while the new fetch was still mid-flight. added a request-sequence token so old responses don\'t clobber new ones.', page: 'search' },
{ title: 'Soulseek Icon Dims When slskd Isn\'t Configured', desc: 'soulseek icon dims if you don\'t have slskd set up. clicking it routes to settings → downloads instead of failing silently.', page: 'search' },
{ title: 'Fix Discover Hero View Discography 404', desc: 'view discography on the discover hero was 404ing for non-library artists. fixed by passing the source through to /api/artist-detail.', page: 'discover' },
{ title: 'MusicBrainz Search Actually Works', desc: 'musicbrainz search was returning empty/garbage results and taking 30+ seconds. rewrote it — artist, track, and album searches all work now and complete in ~3 seconds on cold cache.', page: 'search' },
{ title: 'MusicBrainz Search Follow-Ups', desc: 'three more musicbrainz fixes — artist images now resolve via itunes/deezer fallback, total_tracks off-by-one fixed, and "artist title" queries no longer browse the whole discography.', page: 'search' },
],
'2.39': [
// --- April 22, 2026 ---
@ -3696,284 +3696,266 @@ const WHATS_NEW = {
// usage_note?: 'optional hint shown at the bottom' }
const VERSION_MODAL_SECTIONS = [
{
title: "Reorganize Queue: Race-Condition Hardening (kettui Review)",
description: "Three concurrency / dedupe issues kettui caught in his review of PR #377, plus two related polish items from the same pass.",
title: "Reorganize Queue Polish",
description: "cleaned up some race conditions in the queue. behavior is solid now.",
features: [
"• Worker pick + status flip is now atomic — fixes a window where a cancel() landing between 'pick next queued' and 'flip to running' could mark an item cancelled but the worker still ran it",
"• Replaced the lock + wakeup-event pair with a single threading.Condition so newly-queued items can't sleep up to 60s waiting for the next wakeup tick (the old pair had an empty-check / clear-event race)",
"• enqueue_many now holds the queue lock for the whole batch and tracks a per-batch seen set, so duplicate album_ids inside one bulk call are deduped against each other (not just against pre-existing items)",
"• Reorganize-preview Apply button no longer gets stuck disabled when an early return / network error skipped the re-enable line — moved into a finally",
"• DB helpers get_album_display_meta and get_artist_albums_for_reorganize now let exceptions bubble instead of swallowing them as 'not found' / empty list — a real DB outage now surfaces as a 500 to the user instead of looking like a missing album",
"• worker pick + status flip is atomic now — cancel can\'t land between them and let a cancelled item still run",
"• swapped lock + wakeup-event for a single threading.Condition — newly-queued items don\'t sleep up to 60s anymore",
"• bulk enqueue dedupes within a single batch (was only deduping against pre-existing items)",
"• reorganize-preview Apply button no longer gets stuck disabled on errors",
"• db helpers let exceptions bubble instead of swallowing them as \"album not found\"",
],
},
{
title: "Reorganize Queue with Live Status Panel",
description: "Reorganizing albums is no longer a foreground operation that locks the page. Click → enqueue → keep working. A status panel surfaces live progress.",
description: "reorganize is now a queue with a live status panel. spam-click all you want — items run one at a time and you can keep browsing.",
features: [
"• Per-album Reorganize and Reorganize All both enqueue into a single FIFO queue with a backend worker that drains one item at a time",
"• Buttons stay clickable — spam-clicking the same album silently dedupes (returns 'already queued' instead of 409-ing)",
"• Status panel at the top of the artist actions bar shows: active item (progress bar, current track, moved/skipped/failed counts), queued count, and recently-finished items with success/warning indicators",
"• Click the panel to expand: full queue list with per-item cancel buttons; running item can't be cancelled mid-flight (Python threads aren't cleanly killable, post-process spawns subprocesses)",
"• 'Cancel All' button drops every queued item at once — the running one continues",
"• Items belonging to a different artist than the page you're on are flagged with the artist name so cross-artist progress is obvious",
"• Each queued item carries its own metadata source pick (Spotify / iTunes / Deezer / Discogs / Hydrabase) — switching modal selections per album works",
"• 'Reorganize All' is now one backend call instead of N JS-driven calls — the loop runs server-side and is much faster",
"• Continue-on-failure: a single failed album never stalls the queue; the worker logs and moves on",
"• Retired the old single-slot reorganize state endpoint plus the polling loops that depended on it",
"• per-album reorganize and reorganize all both enqueue into a single backend queue",
"• buttons stay clickable — clicking the same album twice silently dedupes",
"• status panel shows active progress, queued count, and recent finishes",
"• expand the panel for the full queue + per-item cancel buttons (running items can\'t be cancelled mid-flight)",
"• cross-artist items get tagged so you know what\'s queued from where",
"• continue-on-failure: one bad album never stalls the queue",
"• reorganize all is now one backend call instead of N js-driven calls — way faster",
],
},
{
title: "Fix Wrong-Artist Tracks Silently Downloading",
description: "A critical bug where searching for a track could silently download a completely different artist's song with the same name",
description: "searching for a track could silently download a completely different artist\'s song with the same name. fixed at two layers.",
features: [
"• Example: searching 'Maduk — Leave A Light On' on Tidal was downloading Tom Walker's unrelated song of the same name, then embedding Maduk's metadata into Tom Walker's audio",
"• Root cause 1: candidate artist gate used `< 0.4` similarity but Maduk/Tom Walker scored exactly 0.400, slipping past the fencepost — raised to `< 0.5`",
"• Root cause 2: AcoustID verification returned SKIP (accept) instead of FAIL (quarantine) when title matched but artist was clearly different — now FAILs when artist similarity is below 0.3",
"• Preserves SKIP for the ambiguous 0.30.6 range (covers, collabs, formatting differences) so legitimate tracks aren't falsely quarantined",
"• Both pre-download candidate validation AND post-download verification are now fixed — defense in depth",
"• example: \"maduk — leave a light on\" on tidal was downloading tom walker\'s song of the same name with maduk\'s metadata embedded",
"• tightened the candidate artist gate (was letting through 0.4 similarity, now blocks at 0.5)",
"• acoustid verification now FAILs (quarantines) clear artist mismatches instead of accepting them",
"• ambiguous matches (covers, collabs) still get the benefit of the doubt — only obvious mismatches get blocked",
],
},
{
title: "Tidal Search Falls Back on Long Queries",
description: "Tidal's search chokes on long remix-credit queries — now retries with progressively-shortened variants when the original returns 0 results",
description: "tidal\'s search chokes on long remix-credit queries. now retries with shorter variants when the original returns 0 results.",
features: [
"• Example: 'maduk transformations remixed fire away fred v remix' returned 0; now falls back to shorter queries until Tidal finds the track",
"• Up to 4 shortened variants tried, capped total 5 requests, 100ms between attempts",
"• Qualifier-safe: Live/Remix/Acoustic/Extended searches only accept fallback results that still contain the qualifier — studio version never replaces a '(Live)' request",
"• Returns empty if no variant preserves qualifiers — same outcome as before",
"• example: \"maduk transformations remixed fire away fred v remix\" returned 0 — falls back to shorter queries until tidal finds the track",
"• up to 4 shortened variants tried, capped at 5 total requests",
"• qualifier-safe: live/remix/acoustic searches only accept fallback results that keep the qualifier",
"• returns empty if no variant preserves the qualifiers — same as before",
],
},
{
title: "Manual Discovery Fixes Persist Across Restart",
description: "When you manually fix a discovery match, the fix is now saved under your active metadata source instead of always 'spotify' — so Deezer/iTunes/Discogs/Hydrabase users' fixes actually survive restart and re-scan",
description: "manual discovery fixes are now saved under your active metadata source instead of always \"spotify\" — so deezer / itunes / discogs / hydrabase users\' fixes survive restart.",
features: [
"• Affected Tidal, Deezer, Spotify Public, YouTube, and Discovery Pool manual fixes",
"• Symmetric with how the auto-discovery worker saves — no more mismatch",
"• Existing Spotify-primary users unaffected (the hardcoded value matched their source)",
"• affects tidal, deezer, spotify public, youtube, and discovery pool manual fixes",
"• matches how the auto-discovery worker already saved",
"• spotify-primary users unaffected (hardcoded value matched their source)",
],
},
{
title: "Watchlist Content Filters Fixed",
description: "Global Override settings and live-version detection now behave the way the UI implies",
description: "global override and live-version detection now behave the way the ui implies.",
features: [
"• Scheduled auto-watchlist now honors Watchlist → Global Override (was bypassing it and using per-artist defaults)",
"• 'Live' detection tightened — no more false positives on titles like 'What We Live For' or 'Live Forever'",
"• Same fix applies to the Library Maintenance Live/Commentary Cleaner",
"• Still catches (Live), - Live, Live at/from/in/on/version/session/recording, Unplugged, In Concert",
"• scheduled auto-watchlist honors watchlist → global override (was bypassing it)",
"• live detection tightened — no more false positives on titles like \"what we live for\"",
"• same fix applies to the library maintenance live/commentary cleaner",
"• still catches (live), - live, live at/from/in/on, unplugged, in concert",
],
},
{
title: "Discography Backfill",
description: "New maintenance job that fills gaps in your library — scans each artist's full discography and finds what you're missing",
description: "new maintenance job that scans each artist\'s full discography and finds what you\'re missing.",
features: [
"• Scans each artist in your library against metadata source discographies",
"• Creates findings for missing tracks — review and click 'Add to Wishlist' to queue downloads",
"• Respects all content filters (live, remix, acoustic, compilation, instrumental)",
"• Release type filters (album, EP, single) with configurable defaults",
"• Optional 'auto-add to wishlist' setting — create findings AND push to wishlist in one pass",
"• 3-option fix prompt (Add to Wishlist / Just Clear / Cancel) for manual review",
"• Batched in-memory library matching — same fast path the Library pages use",
"• Opt-in, disabled by default — runs weekly, processes up to 50 artists per run",
"• Rate-limited to avoid hammering metadata APIs",
"• scans each library artist against your metadata source",
"• creates findings for missing tracks — review and add to wishlist",
"• respects all content filters (live, remix, acoustic, etc.) and release type filters",
"• optional auto-add-to-wishlist setting for hands-off operation",
"• opt-in, runs weekly, processes up to 50 artists per run",
],
},
{
title: "Repair 'Run Now' Honored While Paused",
description: "Force-running a repair job no longer stalls forever when the master repair worker is paused",
description: "force-running a repair job no longer stalls forever when the master worker is paused.",
features: [
"• Jobs queued via 'Run Now' run to completion even if the master worker is paused",
"• Fixes silent stalls where Discography Backfill logged 'scanning 50 artists' then did nothing",
"• Master-pause still blocks scheduled runs — this only affects explicit user-triggered runs",
"• jobs queued via run now complete even if the master worker is paused",
"• fixes silent stalls where the job logged \"scanning 50 artists\" then did nothing",
"• master-pause still blocks scheduled runs — only affects user-triggered runs",
],
},
{
title: "Multi-Artist Tagging",
description: "Enhanced control over how multiple artists are written to audio file tags",
description: "more control over how multiple artists are written to audio file tags.",
features: [
"• Configurable artist separator: comma, semicolon, or slash",
"• Multi-value ARTISTS tag for Navidrome/Jellyfin multi-artist linking",
"• 'Move featured artists to title' mode — primary artist in ARTIST tag, others as (feat. ...) in title",
"• All opt-in with defaults matching current behavior",
"• configurable separator: comma, semicolon, or slash",
"• multi-value ARTISTS tag for navidrome / jellyfin multi-artist linking",
"• \"move featured artists to title\" mode — primary in ARTIST tag, others as (feat. ...) in title",
"• opt-in, defaults match current behavior",
],
},
{
title: "Enriched Downloads Page",
description: "Download cards now show rich metadata instead of just filenames",
description: "download cards now show rich metadata instead of just filenames.",
features: [
"• Album artwork thumbnail on each download card",
"• Artist name, album name, and source badge",
"• Quality badge appears after post-processing",
"• Falls back gracefully for transfers without metadata context",
"• album artwork thumbnail on each card",
"• artist name, album name, source badge",
"• quality badge appears after post-processing",
"• falls back gracefully for transfers without metadata context",
],
},
{
title: "Template Variable Delimiters",
description: "Use ${var} syntax to append literal text to template variables",
description: "use ${var} syntax to append literal text to template variables.",
features: [
"• ${albumtype}s produces 'Albums', 'Singles', 'EPs'",
"• Both $var and ${var} syntaxes work in all templates",
"• Validation updated to accept delimited variables",
"• ${albumtype}s produces \"Albums\", \"Singles\", \"EPs\"",
"• both $var and ${var} syntaxes work everywhere",
"• validation updated to accept delimited variables",
],
},
{
title: "Reorganize All Albums",
description: "Bulk reorganize all albums for an artist from the enhanced library view",
description: "bulk reorganize all albums for an artist from the enhanced library view.",
features: [
"• New 'Reorganize All' button in the artist header",
"• Processes albums sequentially with progress toasts",
"• Continues on error — one failed album doesn't block the rest",
"• Uses the same template and endpoint as per-album reorganize",
"• new reorganize all button in the artist header",
"• processes sequentially with progress toasts",
"• continues on error — one failed album doesn\'t block the rest",
"• uses the same template + endpoint as per-album reorganize",
],
},
{
title: "SoulSync Standalone Library",
description: "Use SoulSync without Plex, Jellyfin, or Navidrome — manage your library directly",
description: "use soulsync without plex, jellyfin, or navidrome — manage your library directly.",
features: [
"• New 'Standalone' server option in Settings → Connections",
"• Downloads and imports write artist/album/track to the library database immediately",
"• Pre-populated enrichment IDs (Spotify, Deezer, MusicBrainz) — workers skip re-discovery",
"• Deep scan finds untracked files in Transfer → moves to Staging for processing",
"• Deep scan removes stale DB records when files are deleted from disk",
"• Sync page and sync buttons hidden automatically in standalone mode",
"• Full library page, artist detail, discography, and enhanced view all work standalone",
"• new standalone server option in settings → connections",
"• downloads and imports write to the library db immediately",
"• pre-populated enrichment ids — workers skip re-discovery",
"• deep scan finds untracked files and removes stale db records",
"• sync page hidden automatically in standalone mode",
"• full library / artist detail / discography all work standalone",
],
usage_note: "Go to Settings → Connections and click the 'Standalone' button. No media server needed.",
usage_note: "settings → connections → standalone. no media server needed.",
},
{
title: "Auto-Import",
description: "Background import folder watcher that automatically identifies and imports music into your library",
description: "background folder watcher that automatically identifies and imports music into your library.",
features: [
"• Recursive scan — any folder depth (Artist/Album/tracks, Album/tracks, loose files)",
"• Single file support — loose audio files identified via tags, filename, or AcoustID",
"• Tag-based identification preferred over weak metadata matches (85% confidence for tagged files)",
"• AcoustID fingerprinting fallback for untagged or ambiguous files",
"• Stats bar, filter pills (All/Review/Imported/Failed), Scan Now, Approve All, Clear History",
"• Expandable track match details with per-track confidence scores",
"• Race condition fix prevents duplicate processing during multi-track albums",
"• recursive scan — any folder depth (artist/album/tracks, loose files, whatever)",
"• tag-based identification preferred, acoustid fingerprinting as fallback",
"• stats bar, filter pills, scan now, approve all, clear history",
"• expandable per-track match details with confidence scores",
"• race condition fix prevents duplicate processing on multi-track albums",
],
usage_note: "Enable on the Import page Auto tab. Set your import folder in Settings.",
usage_note: "import page → auto tab. set your import folder in settings.",
},
{
title: "Wishlist Nebula",
description: "Wishlist redesigned as an interactive artist orb visualization",
description: "wishlist redesigned as an interactive artist orb visualization.",
features: [
"• Each artist is a glowing orb with their photo — album fans and single moons orbit around them",
"• Click orbs to expand and see albums/singles, download directly from the nebula",
"• Processing state shows live progress with spinning ring animation",
"• Stats strip at top shows total artists, albums, singles, and tracks",
"• each artist is a glowing orb — albums and singles orbit around it",
"• click orbs to expand and download directly from the nebula",
"• live progress with spinning ring animation while processing",
"• stats strip up top: total artists, albums, singles, tracks",
],
usage_note: "Click Wishlist in the sidebar to see the Nebula view.",
usage_note: "click wishlist in the sidebar.",
},
{
title: "Automation Group Management",
description: "Organize and manage your automation groups with full control",
description: "organize and manage automation groups properly.",
features: [
"• Rename, delete, and bulk-toggle automation groups from group headers",
"• Drag-and-drop automations between groups to reorganize",
"• Delete confirmation dialog with group name and automation count",
"• rename, delete, and bulk-toggle groups from the group header",
"• drag-and-drop automations between groups",
"• delete confirmation shows group name and automation count",
],
usage_note: "Right-click or use the action buttons on group headers in the Automations page.",
usage_note: "use the action buttons on group headers in the automations page.",
},
{
title: "Bidirectional Artist Sync & Server Playlists",
description: "Artist sync now goes both ways, and server playlists show full coverage",
description: "artist sync now goes both ways, and server playlists show full coverage.",
features: [
"• Artist Sync pulls new content from your media server AND removes stale library entries",
"• Deep scan mode fetches full metadata for newly discovered tracks",
"• Server playlist view shows all playlists with clear synced vs unsynced visual separation",
"• artist sync pulls new content from your media server AND removes stale library entries",
"• deep scan mode fetches full metadata for newly-discovered tracks",
"• server playlist view shows all playlists with synced vs unsynced visual separation",
],
},
{
title: "Provider-Agnostic Discovery",
description: "Discovery features work with any configured metadata source instead of requiring Spotify",
description: "discovery features work with any configured metadata source instead of requiring spotify.",
features: [
"• Similar artist matching, discovery pool, and incremental updates use source priority",
"• Falls back through Spotify, iTunes, and Deezer in configured order",
"• MusicMap URL encoding fixed for artists with special characters",
"• Freshness check simplified to age-based — backfill handles missing IDs separately",
"• similar artist matching, discovery pool, and incremental updates use source priority",
"• falls back through spotify, itunes, deezer in configured order",
"• musicmap url encoding fixed for artists with special characters",
"• freshness check simplified to age-based",
],
},
{
title: "Dashboard & Navigation",
description: "Dashboard improvements and sidebar navigation enhancements",
description: "dashboard improvements and sidebar navigation enhancements.",
features: [
"• Library Status card on Dashboard — shows server state, track counts, scan buttons",
"• Tools page in sidebar — all maintenance tools moved from Dashboard modal",
"• Watchlist and Wishlist promoted to full sidebar pages with live count badges",
"• AcoustID scanner scans full library with actionable fix options (retag, redownload, delete)",
"• library status card on dashboard — server state, track counts, scan buttons",
"• tools page in sidebar — maintenance tools moved out of the dashboard modal",
"• watchlist and wishlist promoted to full sidebar pages with live count badges",
"• acoustid scanner scans full library with retag / redownload / delete fix options",
],
},
{
title: "MusicBrainz & Metadata Fixes",
description: "Critical tag embedding fix and Picard-style album consistency",
description: "critical tag embedding fix and picard-style album consistency.",
features: [
"• Fix: source ID tags (Spotify, MusicBrainz, Deezer, AudioDB) were silently skipped on every download — now embed correctly",
"• Picard-style release preference scoring prevents Navidrome album splits",
"• Source tags wiped when metadata enhancement is skipped or fails",
"• Spotify API no longer called when Deezer/iTunes is the configured primary source",
"• source id tags (spotify, musicbrainz, deezer, audiodb) were silently skipped on every download — now embed correctly",
"• picard-style release preference scoring prevents navidrome album splits",
"• source tags wiped when metadata enhancement is skipped or fails",
"• spotify api no longer called when deezer/itunes is your primary source",
],
},
{
title: "Downloads & Soulseek Improvements",
description: "Better download management, search accuracy, and queue control",
description: "better download management, search accuracy, and queue control.",
features: [
"• Downloads batch panel — color-coded batch cards with progress, cancel, expand, and 7-day history",
"• Soulseek search queries now include album name — reduces wrong-artist downloads",
"• Reject Soulseek results from Various Artists/VA/Unknown Artist folders",
"• Clearing wishlist now cancels the active wishlist download batch",
"• Album delete with 'Delete Files Too' option on enhanced library page",
"• Fix download modal freezing mid-download — M3U auto-save was exhausting server threads",
"• Fix Unknown Artist when adding playlist tracks to wishlist",
"• Fix slskd timeout spam when Soulseek is not the active download source",
"• downloads batch panel — color-coded cards with progress, cancel, expand, 7-day history",
"• soulseek queries include album name now — fewer wrong-artist downloads",
"• reject results from various artists / unknown artist folders",
"• clearing wishlist cancels the active wishlist download batch",
"• album delete with \"delete files too\" option on enhanced library",
"• fix download modal freezing mid-download (m3u auto-save was exhausting server threads)",
"• fix unknown artist when adding playlist tracks to wishlist",
],
},
{
title: "Recent Fixes",
description: "Bug fixes from recent releases and community reports",
description: "smaller bug fixes from recent releases and community reports.",
features: [
"• Fix watchlist scan false failures — empty discography no longer reported as error",
"• Fix deezer_artist_id column error on enhanced library sync",
"• Fix wishlist button intermittently not navigating to page",
"• Fix worker orb tooltips rendering behind dashboard content",
"• Fix OAuth callback port hardcoding — custom ports now respected",
"• Fix allow duplicates setting not saving",
"• Fix wishlist dropping cross-album tracks when duplicates enabled",
"• Fix replace lower quality setting not persisting",
"• Fix Spotify enrichment worker infinite loop on pre-matched artists",
"• Reject Qobuz 30-second sample/preview downloads",
"• Fix library page crash on All filter — non-string soul_id broke card rendering",
"• Auto Wing It fallback for failed discovery — unmatched tracks download via Soulseek with raw metadata",
"• Lidarr download source now production-ready — full orchestrator integration",
"• Fix album track lookup hardcoded to Spotify — now uses configured primary source",
"• Fix M3U showing all tracks as missing — regenerate with real paths after post-processing",
"• Fix AcoustID retag not writing corrected tags to audio file",
"• Fix wishlist albums cycle stuck at 1 concurrent worker instead of configured value",
"• Fix downloads badge dropping to 300 after opening Downloads page",
"• Fix server playlist Find & Add inserting at wrong position on Plex",
"• Smarter Fix modal results — standard album versions sorted above live/remix/cover/soundtrack variants",
"• Unmatch discovery tracks — red ✕ button to remove bad matches from playlist discovery",
"• Customizable music video naming — path template with $artist, $title, $year variables",
"• Fix soulseek log spam when not configured as download source",
"• fix watchlist scan false failures — empty discography no longer reported as error",
"• fix deezer_artist_id column error on enhanced library sync",
"• fix wishlist button intermittently not navigating",
"• fix worker orb tooltips rendering behind dashboard content",
"• fix oauth callback port hardcoding — custom ports respected now",
"• fix allow duplicates and replace-lower-quality settings not saving",
"• fix wishlist dropping cross-album tracks when duplicates enabled",
"• fix spotify enrichment worker infinite loop on pre-matched artists",
"• reject qobuz 30-second sample/preview downloads",
"• auto wing-it fallback for failed discovery",
"• fix album track lookup hardcoded to spotify — uses configured primary now",
"• fix m3u showing all tracks as missing after post-processing",
"• fix acoustid retag not writing corrected tags to file",
"• fix downloads badge dropping to 300 after opening downloads page",
"• unmatch discovery tracks (red ✕ button)",
"• customizable music video naming with $artist, $title, $year",
"• fix soulseek log spam when not configured as download source",
],
},
{
title: "Earlier in v2.3",
description: "Major features from earlier in this release cycle",
description: "major features from earlier in this release cycle.",
features: [
"• Centralized Downloads page with live-updating list and filter pills",
"• First-Run Setup Wizard — 7-step guided configuration",
"• Music Videos — search and download from YouTube",
"• Inbound Music Request API for external tools (Discord bots, Home Assistant)",
"• Lidarr download source (development) — 7th source for Usenet/torrent via Lidarr",
"• Graceful shutdown — all workers respond to shutdown signals immediately",
"• Unknown Artist prevention with 3-tier metadata fallback",
"• Deezer multi-artist tagging using contributors field",
"• Artist Map — Watchlist Constellation, Genre Map, and Artist Explorer canvas modes",
"• Discogs integration — enrichment worker, fallback source, enhanced search tab",
"• Wing It mode, Global Search Bar, Redesigned Notifications",
"• Server Playlist Manager, Sync History Dashboard, Playlist Explorer",
"• Enhanced Library Manager with inline tag editing and write-to-file",
"• Automation Signals, Multi-Source Search Tabs, Rich Artist Profiles",
"• centralized downloads page with live-updating list and filter pills",
"• first-run setup wizard — 7-step guided configuration",
"• music videos — search and download from youtube",
"• inbound music request api for external tools (discord bots, home assistant)",
"• lidarr download source (in development) for usenet / torrent",
"• graceful shutdown — all workers respond to shutdown signals immediately",
"• unknown artist prevention with 3-tier metadata fallback",
"• deezer multi-artist tagging via contributors field",
"• artist map — watchlist constellation, genre map, artist explorer",
"• discogs integration — enrichment worker, fallback source, search tab",
"• wing it mode, global search bar, redesigned notifications",
"• server playlist manager, sync history dashboard, playlist explorer",
"• enhanced library manager with inline tag editing and write-to-file",
"• automation signals, multi-source search tabs, rich artist profiles",
],
},
];

Loading…
Cancel
Save