diff --git a/web_server.py b/web_server.py index ee850b76..410d4f39 100644 --- a/web_server.py +++ b/web_server.py @@ -36,7 +36,7 @@ _log_path = config_manager.get('logging.path', 'logs/app.log') logger = setup_logging(_log_level, _log_path) # App version — single source of truth for backup metadata, version-info endpoint, etc. -SOULSYNC_VERSION = "2.34" +SOULSYNC_VERSION = "2.35" # Dedicated source reuse logger — writes to logs/source_reuse.log import logging as _logging @@ -22475,16 +22475,54 @@ def get_version_info(): "subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes", "sections": [ { - "title": "MusicBrainz Search Tab", - "description": "Find tracks and albums on MusicBrainz's community database — covers obscure music that Spotify/Deezer/iTunes miss", + "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": [ - "• New tab in Enhanced Search and Global Search alongside existing sources", - "• Searches recordings, releases, and artists on MusicBrainz", - "• Cover art from Cover Art Archive (free, linked by release ID)", - "• Click results to open download modal with full tracklist — same flow as other sources", - "• Smart query parsing splits 'Artist Title' into structured artist + title search", - "• Deduplicates album results (keeps best version with date and art)", - "• Always available — public API, no authentication needed", + "• 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", + "• Opt-in, disabled by default — runs weekly, processes up to 50 artists per run", + "• Rate-limited to avoid hammering metadata APIs", + ], + }, + { + "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", ], }, { diff --git a/webui/static/helper.js b/webui/static/helper.js index 5c21d98a..4b8d5415 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3599,7 +3599,28 @@ function closeHelperSearch() { // ═══════════════════════════════════════════════════════════════════════════ const WHATS_NEW = { - '2.34': [ + '2.35': [ + // --- April 20, 2026 --- + { date: 'April 20, 2026' }, + { title: 'Discography Backfill Maintenance Job', desc: 'New library maintenance job that scans each artist in your library, fetches their full discography from metadata sources, and creates findings for any missing tracks. Review findings and click "Add to Wishlist" to queue them for download. Respects content filters (live/remix/acoustic/compilation) and release type filters. Opt-in, disabled by default', page: 'library' }, + { title: 'Multi-Artist Tagging Options', desc: 'Three new settings: configurable artist separator (comma/semicolon/slash), multi-value ARTISTS tag for Navidrome/Jellyfin multi-artist linking, and "Move featured artists to title" mode. All opt-in with defaults matching current behavior', page: 'settings' }, + { title: 'Reorganize All Albums for Artist', desc: 'New "Reorganize All" button in the enhanced library artist header. Processes all albums for an artist sequentially using the configured path template. Shows progress per album, continues on error', page: 'library' }, + { title: 'Enriched Downloads Page Cards', desc: 'Download cards now show album artwork thumbnail, artist name, album name, source badge, and quality badge — all pulled from existing metadata context. No extra API calls', page: 'downloads' }, + { title: 'Template Variable Delimiter Syntax', desc: 'Use ${var} syntax to append literal text to template variables: ${albumtype}s produces "Albums", "Singles", "EPs". Both $var and ${var} syntaxes work. Updated validation and hint text for all templates', page: 'settings' }, + { title: 'AcoustID Fix Action Prompt', desc: 'AcoustID mismatch findings now show a 3-option fix prompt (Retag/Re-download/Delete) instead of silently defaulting to retag. Works for both individual and bulk fix', page: 'library' }, + { title: 'Fix Sync Buttons on Undiscovered Playlists', desc: 'Sync buttons on ListenBrainz/Last.fm Radio playlists were visible before discovery due to the standalone mode handler resetting display:none on every WebSocket push. Now only restores buttons it specifically hid' }, + { title: 'Fix Wing It Tracks Added to Wishlist During Sync', desc: 'Wing It fallback tracks with no real metadata were being added to wishlist when they failed to match on the media server during playlist sync. Now skipped by checking the wing_it_ ID prefix' }, + { title: 'Fix iTunes Region-Restricted Albums', desc: 'iTunes API sometimes returns album metadata without song tracks for region-restricted releases. The empty result was cached permanently. Now tries fallback storefronts for actual songs, and skips caching empty results' }, + { title: 'Fix Disc Subfolder Missing on Single-Track Downloads', desc: 'Downloading a single track from search for a multi-disc album placed it without the Disc N/ subfolder. Now resolves total_discs from the album tracklist when not already known' }, + { title: 'Fix Allow Duplicate Tracks Setting Not Working', desc: 'The "Allow duplicate tracks across albums" setting was ignored during album download analysis. Tracks found in other albums were marked as owned and skipped. Now only checks ownership within the target album when duplicates are allowed' }, + { title: 'Stop slskd Log Spam When Not Active', desc: 'Download monitor and transfer cache were polling slskd every second during active downloads regardless of whether Soulseek was configured. Now skips slskd API calls entirely when Soulseek is not in the active download source' }, + { title: 'Fix AcoustID High-Confidence Skip', desc: 'AcoustID verification was letting wrong files through when the fingerprint score was high (0.95+) even with very low title/artist similarity. Now requires at least partial title or artist match before skipping verification' }, + { title: 'Fix Navidrome Multi-Library Import', desc: 'Full database refresh was importing albums from all Navidrome music folders even when only one was selected in settings. Now filters albums to the selected music folder using a cached album ID set' }, + { title: 'Fix Repair Worker Crash on Zero Interval', desc: 'Jobs with interval_hours set to 0 caused ZeroDivisionError in the repair worker staleness calculation. Now skips jobs with invalid intervals' }, + { title: 'Fix Playlist Mode Missing Metadata and Cover Art', desc: 'Playlist folder mode passed null album_info to metadata enhancement, causing the entire function to crash silently. All metadata was wiped from the file. Now normalizes null to empty dict and falls back to spotify_album context for cover art' }, + { title: 'Fix Unknown Artist Fixer Column Name', desc: 'The unknown_artist_fixer repair job crashed with "no such column: t.deezer_track_id". The tracks table uses deezer_id, not deezer_track_id' }, + { title: 'Fix Auto-Import Using Wrong Artist from Tags', desc: 'Auto-import trusted embedded file tags for artist names even when the parent folder clearly indicated the correct artist. Mixtapes tagged with DJ names (e.g. "Slim" instead of "2Pac") got organized under the wrong artist. Now uses parent folder structure as artist override when folder depth indicates an Artist/Album layout' }, + // --- April 19, 2026 --- { date: 'April 19, 2026' }, { title: 'Fix Wishlist Albums Cycle Stuck at 1 Concurrent', desc: 'Auto-wishlist processing during the "albums" cycle was limited to 1 concurrent download even with higher configured settings. The max_concurrent=1 restriction is only needed for Soulseek folder-based album grabs, not individual wishlist track downloads. Albums cycle now uses the configured concurrency like singles' }, @@ -3744,12 +3765,12 @@ const WHATS_NEW = { function _getCurrentVersion() { const btn = document.querySelector('.version-button'); - return btn ? btn.textContent.trim().replace('v', '') : '2.34'; + return btn ? btn.textContent.trim().replace('v', '') : '2.35'; } function _getLatestWhatsNewVersion() { const versions = Object.keys(WHATS_NEW).sort((a, b) => parseFloat(b) - parseFloat(a)); - return versions[0] || '2.34'; + return versions[0] || '2.35'; } function openWhatsNew() { diff --git a/webui/static/script.js b/webui/static/script.js index 639f67e3..b69ab87d 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -46452,6 +46452,13 @@ function renderArtistMetaPanel(artist) { }; headerRight.appendChild(syncBtn); + const reorgAllBtn = document.createElement('button'); + reorgAllBtn.className = 'enhanced-action-btn'; + reorgAllBtn.innerHTML = '📁 Reorganize All'; + reorgAllBtn.title = 'Reorganize all albums for this artist using path template'; + reorgAllBtn.onclick = () => _showReorganizeAllModal(); + headerRight.appendChild(reorgAllBtn); + header.appendChild(headerRight); panel.appendChild(header); @@ -49769,7 +49776,11 @@ async function showReorganizeModal(albumId) { } title.textContent = `Reorganize: ${albumData ? albumData.title : 'Album'}`; - if (applyBtn) applyBtn.disabled = true; + if (applyBtn) { + applyBtn.disabled = true; + applyBtn.textContent = 'Apply'; + applyBtn.onclick = () => executeReorganize(); + } // Build modal content const variables = [ @@ -50005,6 +50016,155 @@ function _pollReorganizeStatus() { _reorganizePollTimer = setTimeout(poll, 600); } +// ── Reorganize All Albums for Artist ── + +let _reorganizeAllRunning = false; + +async function _showReorganizeAllModal() { + if (!artistDetailPageState.enhancedData) { + showToast('No album data loaded', 'error'); + return; + } + const albums = artistDetailPageState.enhancedData.albums || []; + const artistName = artistDetailPageState.enhancedData.artist.name || 'Artist'; + + if (albums.length === 0) { + showToast('No albums to reorganize', 'error'); + return; + } + + const overlay = document.getElementById('reorganize-overlay'); + const body = document.getElementById('reorganize-modal-body'); + const title = document.getElementById('reorganize-modal-title'); + const applyBtn = document.getElementById('reorganize-apply-btn'); + if (!overlay || !body) return; + + title.textContent = `Reorganize All Albums — ${artistName}`; + + // Load saved template + let savedTemplate = '$albumartist/$albumartist - $album/$track - $title'; + try { + const settingsResp = await fetch('/api/settings'); + if (settingsResp.ok) { + const settings = await settingsResp.json(); + savedTemplate = settings.file_organization?.templates?.album_path || savedTemplate; + } + } catch (_) { } + + let html = '
/ to separate folders.