Add Reorganize All Albums button, version bump to 2.35, changelogs

New "Reorganize All" button in enhanced library artist header processes
all albums sequentially using the configured path template.

Version bumped to 2.35. Updated What's New modal with major features
(Discography Backfill, Multi-Artist Tagging, Enriched Downloads,
Template Delimiters, Reorganize All). Updated helper.js changelog
with all April 20 fixes and features.
pull/344/head
Broque Thomas 4 weeks ago
parent e39a3f2af7
commit 95cf1eeeea

@ -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",
],
},
{

@ -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() {

@ -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 = '<div class="reorganize-content">';
// Template input
html += '<div class="reorganize-template-section">';
html += '<label class="reorganize-label">Path Template</label>';
html += '<div class="reorganize-template-hint">This template will be applied to all albums below. Use <code>/</code> to separate folders.</div>';
html += `<input type="text" id="reorganize-template-input" class="reorganize-template-input" value="${savedTemplate.replace(/"/g, '&quot;')}" placeholder="$albumartist/$album/$track - $title" spellcheck="false">`;
html += '</div>';
// Album list
html += '<div style="margin-top:14px;">';
html += `<label class="reorganize-label">${albums.length} album${albums.length !== 1 ? 's' : ''} will be reorganized:</label>`;
html += '<div style="max-height:200px;overflow-y:auto;margin-top:6px;border:1px solid rgba(255,255,255,0.08);border-radius:8px;padding:6px 10px;">';
albums.forEach((a, i) => {
const trackCount = a.tracks ? a.tracks.length : '?';
html += `<div style="padding:4px 0;font-size:0.88em;color:rgba(255,255,255,0.7);border-bottom:${i < albums.length - 1 ? '1px solid rgba(255,255,255,0.04)' : 'none'};">`;
html += `${escapeHtml(a.title)} <span style="color:rgba(255,255,255,0.3);">(${trackCount} tracks)</span>`;
html += '</div>';
});
html += '</div></div>';
html += '</div>';
body.innerHTML = html;
// Wire apply button for bulk mode
if (applyBtn) {
applyBtn.disabled = false;
applyBtn.textContent = 'Reorganize All';
applyBtn.onclick = () => _executeReorganizeAll();
}
overlay.classList.remove('hidden');
}
async function _executeReorganizeAll() {
if (_reorganizeAllRunning) return;
_reorganizeAllRunning = true;
const templateInput = document.getElementById('reorganize-template-input');
const template = templateInput ? templateInput.value.trim() : '';
if (!template) {
showToast('Template cannot be empty', 'error');
_reorganizeAllRunning = false;
return;
}
const albums = artistDetailPageState.enhancedData.albums || [];
const total = albums.length;
const applyBtn = document.getElementById('reorganize-apply-btn');
if (applyBtn) { applyBtn.disabled = true; applyBtn.textContent = 'Working...'; }
// Close modal
const overlay = document.getElementById('reorganize-overlay');
if (overlay) overlay.classList.add('hidden');
let succeeded = 0, failed = 0;
for (let i = 0; i < total; i++) {
const album = albums[i];
showToast(`Reorganizing album ${i + 1}/${total}: ${album.title}`, 'info');
try {
const resp = await fetch(`/api/library/album/${album.id}/reorganize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ template }),
});
const result = await resp.json();
if (!result.success) {
showToast(`Failed: ${album.title}${result.error || 'unknown error'}`, 'error');
failed++;
continue;
}
// Wait for this album to finish
await _waitForReorganizeComplete();
succeeded++;
} catch (err) {
showToast(`Error: ${album.title}${err.message}`, 'error');
failed++;
}
}
let msg = `Reorganized ${succeeded} of ${total} album${total !== 1 ? 's' : ''}`;
if (failed > 0) msg += ` (${failed} failed)`;
showToast(msg, failed > 0 ? 'warning' : 'success');
_reorganizeAllRunning = false;
if (applyBtn) { applyBtn.disabled = false; applyBtn.textContent = 'Reorganize All'; }
// Refresh enhanced view
if (artistDetailPageState.currentArtistId && artistDetailPageState.enhancedView) {
loadEnhancedViewData(artistDetailPageState.currentArtistId);
}
}
function _waitForReorganizeComplete() {
return new Promise(resolve => {
const poll = setInterval(async () => {
try {
const resp = await fetch('/api/library/album/reorganize/status');
const state = await resp.json();
if (state.status === 'done' || state.status === 'idle') {
clearInterval(poll);
resolve();
}
} catch {
clearInterval(poll);
resolve();
}
}, 800);
});
}
async function playLibraryTrack(track, albumTitle, artistName) {
if (!track.file_path) {
showToast('No file available for this track', 'error');

Loading…
Cancel
Save