Lift version modal data into helper.js, delete /api/version-info

The version modal pulled its content from /api/version-info — a 295-line
hand-curated Python dict in web_server.py. The "What's New" panel pulled
its content from WHATS_NEW in helper.js. Same release notes, two files,
two languages, hand-edited at every release — drift was inevitable
(and happened: the kettui-fix entries I added recently differed in
detail between the two surfaces).

This commit makes helper.js the single editing surface:

- Adds VERSION_MODAL_SECTIONS const in helper.js right beside WHATS_NEW,
  with a comment block documenting the relationship: WHATS_NEW is the
  per-version detailed log used by the helper popover; VERSION_MODAL_SECTIONS
  is the curated highlight reel shown by the sidebar version button. Both
  edited at release time, both in the same file.
- Rewires showVersionInfo() in downloads.js to read from those consts
  directly. No backend round-trip; the changelog content ships in the
  same JS bundle the browser already loaded.
- Deletes the /api/version-info route and its 295-line version_data dict.
- Updates the line-39 comment to drop the now-stale "version-info endpoint"
  reference.

Note: this is collocation, not true unification. WHATS_NEW and
VERSION_MODAL_SECTIONS are still two distinct structures with overlapping
content, linked by a comment convention rather than a shared schema. A
deeper refactor (e.g. a `featured` flag on WHATS_NEW entries that the
modal aggregates) was rejected as out-of-scope — the curated section
titles ("Earlier in v2.3", "Recent Fixes") aren't 1:1 mappable to
WHATS_NEW entries. Saving for a follow-up if the drift problem persists.

Risk audit:
- Load order: helper.js loads at line 7967, downloads.js at line 7873.
  Both classic scripts execute synchronously before any clickable
  interaction, so showVersionInfo (only invoked on the version-button
  onclick) always sees both consts defined.
- populateVersionModal() unchanged — receives the same {title, subtitle,
  sections: [{title, description, features, usage_note?}]} shape.
- Stale-cache window during deploy: old downloads.js hitting a 404 on
  the deleted endpoint falls through to the existing catch + toast path
  ("Failed to load version information"). Cache-buster ?v=static_v
  resolves on next page load.

553 tests pass. helper.js + downloads.js parse cleanly. No residual
references to /api/version-info anywhere in the repo.
This commit is contained in:
Broque Thomas 2026-04-26 13:32:30 -07:00
parent edb2c2044f
commit 7714b51a50
3 changed files with 327 additions and 315 deletions

View file

@ -36,7 +36,7 @@ _log_path = config_manager.get('logging.path', 'logs/app.log')
_log_dir = Path(_log_path).parent
logger = setup_logging(_log_level, _log_path)
# App version — single source of truth for backup metadata, version-info endpoint, etc.
# App version — single source of truth for backup metadata, system-info, update check, etc.
# Semver: MAJOR.MINOR.PATCH. Bump at each dev→main release.
_SOULSYNC_BASE_VERSION = "2.4.0"
@ -22767,302 +22767,6 @@ def check_for_update():
'is_docker': os.path.exists('/.dockerenv'),
})
@app.route('/api/version-info', methods=['GET'])
def get_version_info():
"""
Returns version information and release notes, matching the GUI's VersionInfoModal content.
This provides the same data that the GUI version modal displays.
"""
version_data = {
"version": SOULSYNC_VERSION,
"title": "What's New in SoulSync",
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
"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.",
"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",
],
},
{
"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.",
"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",
],
},
{
"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",
"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",
],
},
{
"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",
"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",
],
},
{
"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",
"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)",
],
},
{
"title": "Watchlist Content Filters Fixed",
"description": "Global Override settings 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",
],
},
{
"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",
"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",
],
},
{
"title": "Repair 'Run Now' Honored While Paused",
"description": "Force-running a repair job no longer stalls forever when the master repair 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",
],
},
{
"title": "Multi-Artist Tagging",
"description": "Enhanced 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",
],
},
{
"title": "Enriched Downloads Page",
"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",
],
},
{
"title": "Template Variable Delimiters",
"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",
],
},
{
"title": "Reorganize All Albums",
"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",
],
},
{
"title": "SoulSync Standalone Library",
"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",
],
"usage_note": "Go to Settings → Connections and click the 'Standalone' button. No media server needed."
},
{
"title": "Auto-Import",
"description": "Background import 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",
],
"usage_note": "Enable on the Import page Auto tab. Set your import folder in Settings."
},
{
"title": "Wishlist Nebula",
"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",
],
"usage_note": "Click Wishlist in the sidebar to see the Nebula view."
},
{
"title": "Automation Group Management",
"description": "Organize and manage your automation groups with full control",
"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",
],
"usage_note": "Right-click or 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",
"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",
],
},
{
"title": "Provider-Agnostic Discovery",
"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",
],
},
{
"title": "Dashboard & Navigation",
"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)",
],
},
{
"title": "MusicBrainz & Metadata Fixes",
"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",
],
},
{
"title": "Downloads & Soulseek Improvements",
"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",
],
},
{
"title": "Recent Fixes",
"description": "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",
],
},
{
"title": "Earlier in v2.3",
"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",
],
},
]
}
return jsonify(version_data)
def _simple_monitor_task():
"""The actual monitoring task that runs in the background thread.

View file

@ -5752,27 +5752,26 @@ async function showVersionInfo() {
} catch (e) { /* ignore */ }
}
// Build version data straight from helper.js — single source of truth.
// No backend round-trip; the changelog content is shipped in the same
// bundle the browser already loaded.
const version = (typeof _getCurrentVersion === 'function')
? _getCurrentVersion()
: (btn ? btn.textContent.trim().replace('v', '') : '');
const sections = (typeof VERSION_MODAL_SECTIONS !== 'undefined')
? VERSION_MODAL_SECTIONS
: [];
const versionData = {
version,
title: "What's New in SoulSync",
subtitle: version ? `Version ${version} — Latest Changes` : 'Latest Changes',
sections,
};
try {
console.log('Fetching version info...');
// Fetch version data from API
const response = await fetch('/api/version-info');
if (!response.ok) {
throw new Error('Failed to fetch version info');
}
const versionData = await response.json();
console.log('Version data received:', versionData);
// Populate modal content
populateVersionModal(versionData, hadUpdate ? updateInfo : null);
// Show modal
const modalOverlay = document.getElementById('version-modal-overlay');
modalOverlay.classList.remove('hidden');
console.log('Version modal opened');
if (modalOverlay) modalOverlay.classList.remove('hidden');
} catch (error) {
console.error('Error showing version info:', error);
showToast('Failed to load version information', 'error');

View file

@ -3669,6 +3669,315 @@ const WHATS_NEW = {
],
};
// ═══════════════════════════════════════════════════════════════════════════
// VERSION MODAL — curated highlight reel
// ═══════════════════════════════════════════════════════════════════════════
//
// `WHATS_NEW` above is the per-version detailed log used by the "What's New"
// helper-popover panel — short one-liners, internal page links, every entry
// shown on every browse-back through versions.
//
// `VERSION_MODAL_SECTIONS` (this block) is the curated highlight reel shown
// when the user clicks the version button in the sidebar. It's NOT a
// mechanical view of WHATS_NEW — it's editorial curation: bigger-picture
// sections, bullet-list expansions, optional "usage" hints at the bottom.
// Some sections aggregate across multiple WHATS_NEW entries ("Recent Fixes",
// "Earlier in v2.3"); some don't have a 1:1 WHATS_NEW counterpart at all.
//
// Both consts live here so a release editor only opens one file. At release
// time:
// 1. Add the per-version block to `WHATS_NEW` (one entry per shipped item).
// 2. Promote any items worth a modal-section into `VERSION_MODAL_SECTIONS`
// at the top of the array (latest highlights lead).
// 3. Roll older sections down or merge them into a "Recent Fixes" /
// "Earlier in vX.Y" aggregator section as they age out of the spotlight.
//
// Section shape: { title, description, features: [bullet strings],
// 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.",
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",
],
},
{
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.",
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",
],
},
{
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",
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",
],
},
{
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",
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",
],
},
{
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",
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)",
],
},
{
title: "Watchlist Content Filters Fixed",
description: "Global Override settings 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",
],
},
{
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",
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",
],
},
{
title: "Repair 'Run Now' Honored While Paused",
description: "Force-running a repair job no longer stalls forever when the master repair 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",
],
},
{
title: "Multi-Artist Tagging",
description: "Enhanced 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",
],
},
{
title: "Enriched Downloads Page",
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",
],
},
{
title: "Template Variable Delimiters",
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",
],
},
{
title: "Reorganize All Albums",
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",
],
},
{
title: "SoulSync Standalone Library",
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",
],
usage_note: "Go to Settings → Connections and click the 'Standalone' button. No media server needed.",
},
{
title: "Auto-Import",
description: "Background import 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",
],
usage_note: "Enable on the Import page Auto tab. Set your import folder in Settings.",
},
{
title: "Wishlist Nebula",
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",
],
usage_note: "Click Wishlist in the sidebar to see the Nebula view.",
},
{
title: "Automation Group Management",
description: "Organize and manage your automation groups with full control",
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",
],
usage_note: "Right-click or 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",
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",
],
},
{
title: "Provider-Agnostic Discovery",
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",
],
},
{
title: "Dashboard & Navigation",
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)",
],
},
{
title: "MusicBrainz & Metadata Fixes",
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",
],
},
{
title: "Downloads & Soulseek Improvements",
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",
],
},
{
title: "Recent Fixes",
description: "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",
],
},
{
title: "Earlier in v2.3",
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",
],
},
];
function _getCurrentVersion() {
const btn = document.querySelector('.version-button');
return btn ? btn.textContent.trim().replace('v', '') : '2.4.0';