diff --git a/static/v3/match-review.js b/static/v3/match-review.js index f04e612e..edbc8e05 100644 --- a/static/v3/match-review.js +++ b/static/v3/match-review.js @@ -514,12 +514,13 @@ '
' + '
' + '' + - '

Your edits show in the library right away and are never written to the song files. Use Match to pull info from MusicBrainz, or lock a field to keep it.

' + + '

Save keeps edits as a reversible library overlay — the song files aren\'t touched. Write to file bakes them into the pack itself. Lock a field to keep an auto-match from changing it.

' + '
' + DETAIL_FIELDS.map(row).join('') + - '

' + + '

' + '
' + - '
' + + '
' + + '' + '' + '
'; body.querySelectorAll('[data-df-input]').forEach((inp) => { @@ -532,6 +533,7 @@ b.addEventListener('click', () => { const f = b.getAttribute('data-df-revert'); st[f].value = st[f].pack || ''; st[f].locked = false; paintDetails(body, song); }); }); body.querySelector('[data-df-save]')?.addEventListener('click', () => saveDetails(body, song)); + body.querySelector('[data-df-write]')?.addEventListener('click', () => writeToFile(body, song)); } async function saveDetails(body, song) { @@ -572,6 +574,70 @@ if (status) { status.className = 'text-xs h-4 text-fb-good'; status.textContent = 'Saved.'; } } + // "Write to file" — bake the shown title/artist/album/year INTO the pack + // itself (the one action here that touches the file), via the existing + // POST /api/song/{fn}/meta (writes the manifest, re-stats, coalesces a + // rescan). On a real file write the display overrides for those fields are + // now redundant, so clear their VALUES (keeping any locks) and re-render — + // the field then reads from the file as "Pack". Loose-folder / unwritable + // packs fall back to a DB-only update: we say so and keep the overlay. + async function writeToFile(body, song) { + const st = song._detailsState; + const fields = {}; + for (const [f] of DETAIL_FIELDS) fields[f] = String(st[f].value || '').trim(); + const status = body.querySelector('[data-df-status]'); + const writeBtn = body.querySelector('[data-df-write]'); + const saveBtn = body.querySelector('[data-df-save]'); + if (writeBtn) writeBtn.disabled = true; + if (saveBtn) saveBtn.disabled = true; + if (status) { status.className = 'text-xs leading-relaxed text-fb-textDim'; status.textContent = 'Writing to the song file…'; } + let ok = false, persisted = false; + try { + const r = await fetch('/api/song/' + enc(song.filename) + '/meta', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(fields), + }); + ok = r.ok; + const j = await r.json().catch(() => ({})); + persisted = !!(j && j.persisted); + } catch (_) { ok = false; } + if (writeBtn) writeBtn.disabled = false; + if (saveBtn) saveBtn.disabled = false; + if (!ok) { + if (status) { status.className = 'text-xs leading-relaxed text-fb-accent'; status.textContent = 'Could not write to the file — try again.'; } + return; + } + // Keep the in-memory song + grid in step with what was *persisted*, not + // the raw input: the server coerces a non-numeric/empty year to "" (see + // update_song_meta), so mirror that here or the grid card flashes the + // typed text (e.g. "abcd") until the next natural refresh corrects it. + const applied = { ...fields }; + if ('year' in applied) { + const yr = /^[+-]?\d+$/.test(applied.year) ? parseInt(applied.year, 10) : 0; + applied.year = yr ? String(yr) : ''; + } + for (const [f] of DETAIL_FIELDS) song[f] = applied[f]; + try { window.feedBack?.emit('library:changed', { reason: 'write' }); } catch (_) { } + if (persisted) { + const clear = {}; + for (const [f] of DETAIL_FIELDS) clear[f] = { value: null, locked: !!st[f].locked }; + try { + await fetch('/api/song/' + enc(song.filename) + '/overrides', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ overrides: clear }), + }); + } catch (_) { /* the file write still succeeded; the overlay just lingers */ } + await renderDetailsTab(body, song); // re-fetch: pack now = written values, overrides cleared + const s2 = body.querySelector('[data-df-status]'); + if (s2) { s2.className = 'text-xs leading-relaxed text-fb-good'; s2.textContent = 'Written to the song file.'; } + } else if (status) { + status.className = 'text-xs leading-relaxed text-fb-textDim'; + status.textContent = 'Saved to the library — this pack’s file couldn’t be written, so it may revert on a full rescan.'; + } + } + // Cover-art tab: the current art + a button that hands off to the shared // cover picker (image-picker.js, its own z-[200] modal). A pick there // refreshes every for this song's art — including this thumbnail — so