From 50b6876d6b0ddc11e23483852b2a8a555505a6ec Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Thu, 4 Jun 2026 09:01:29 -0700 Subject: [PATCH] Library re-tag: render the per-track old->new diff in the finding card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire library_retag into the repair findings UI: a 'Re-tag' type badge, an 'Apply Tags' fix button, and an expandable detail that shows, per track, every tag that would change as old -> new (plus source/mode/cover-action summary and any unmatched tracks). So the dry-run finding is actually reviewable before you apply it — the rich details_json the job stores now surfaces in the card. --- webui/static/enrichment.js | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/webui/static/enrichment.js b/webui/static/enrichment.js index efdfbb29..36208c30 100644 --- a/webui/static/enrichment.js +++ b/webui/static/enrichment.js @@ -2725,7 +2725,7 @@ async function loadRepairFindings() { duplicate_tracks: 'Duplicate', incomplete_album: 'Incomplete', path_mismatch: 'Path Mismatch', metadata_gap: 'Missing Metadata', missing_cover_art: 'Missing Art', track_number_mismatch: 'Track Number', - missing_lossy_copy: 'No Lossy Copy' + missing_lossy_copy: 'No Lossy Copy', library_retag: 'Re-tag' }; // Finding types that have an automated fix action @@ -2740,6 +2740,7 @@ async function loadRepairFindings() { missing_lossy_copy: 'Convert', acoustid_mismatch: 'Fix', missing_discography_track: 'Add to Wishlist', + library_retag: 'Apply Tags', }; container.innerHTML = items.map(f => { @@ -2905,6 +2906,40 @@ function _renderFindingDetail(f) { if (f.file_path) rows.push(['Full Path', f.file_path, 'path']); return _gridRows(rows) + _renderPlayButton(f); + case 'library_retag': { + const tracks = Array.isArray(d.tracks) ? d.tracks : []; + const changed = tracks.filter(t => t.changes && Object.keys(t.changes).length); + const meta = []; + if (d.source) meta.push(`Source: ${d.source}`); + if (d.mode) meta.push(`Mode: ${d.mode}`); + if (d.cover_action) meta.push(`Cover: ${d.cover_action}`); + let html = ''; + if (meta.length) { + html += `
${_escFinding(meta.join(' · '))}
`; + } + if (!changed.length && d.cover_action) { + html += `
Tags already correct — this would refresh cover art only.
`; + } + // Per-track old → new diff (cap the rendered list so huge albums stay sane). + changed.slice(0, 40).forEach(t => { + const label = t.title || (t.file_path || '').split(/[\\/]/).pop(); + const rows = Object.entries(t.changes).map(([field, c]) => [ + field.replace(/_/g, ' '), + `${(c.old === '' || c.old == null) ? '∅' : c.old} → ${c.new}`, + 'highlight', + ]); + html += `
${_escFinding(label)}
`; + html += _gridRows(rows); + }); + if (changed.length > 40) { + html += `
…and ${changed.length - 40} more track(s)
`; + } + if (Array.isArray(d.unmatched) && d.unmatched.length) { + html += `
Unmatched (left untouched): ${_escFinding(d.unmatched.join(', '))}
`; + } + return html || '
No changes.
'; + } + case 'acoustid_mismatch': { let html = media + '
'; html += _renderScoreBar(d.fingerprint_score, 'Fingerprint');