feat(web+backend): Dubletten im Editor aufloesen, Favoriten ueberleben das Umbenennen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-09-20 21:41:42 +02:00
1 parent 24558497da
commit 8d25bf0404
4 files changed
+239 -5

No files matched your search

+67
View File
@@ -291,6 +291,8 @@ export async function saveMetadata() {
if (d.warnings?.length) {
status.innerHTML = '⚠ Gespeichert, aber: ' + d.warnings.map(esc).join('<br>⚠ ') +
'<br><span style="color:var(--text3)">Datei: ' + esc(d.file || '?') + '</span>';
// Liegt derselbe Song doppelt, kann das Aufraeumen hier passieren statt auf der Konsole.
if (d.duplicate) _zeigeDublette(status, d.duplicate);
toast('⚠ Gespeichert — mit Hinweisen, siehe Fenster');
return;
}
@@ -302,6 +304,71 @@ export async function saveMetadata() {
}
}
// ─── DUBLETTE AUFLOESEN ───
// Das Verschieben wurde abgelehnt, weil am Ziel schon eine Datei liegt — derselbe Song liegt also
// zweimal auf der Platte. Statt den Nutzer zum Dateimanager zu schicken, werden beide Kopien
// gegenuebergestellt und die ueberzaehlige zum Loeschen angeboten.
const _mb = (n) => (n == null ? '?' : (n / 1048576).toFixed(1) + ' MB');
const _sek = (n) => (n == null ? '?' : formatDur(n));
function _zeigeDublette(status, dub) {
const box = document.createElement('div');
box.className = 'dublette-box';
const zeile = (titel, f) =>
`<div class="dublette-datei"><b>${esc(titel)}</b>` +
`<div class="dublette-pfad">${esc(f.pfad || '?')}</div>` +
`<div class="dublette-meta">${esc(f.artist || '?')} — ${esc(f.titel || '?')} · ` +
`${_sek(f.dauer)} · ${_mb(f.groesse)}</div></div>`;
box.innerHTML =
zeile('Bleibt liegen (richtiger Ordner)', dub.behalten) +
zeile('Überzählig (gerade bearbeitet)', dub.ueberzaehlig) +
(dub.gleich
? '<div class="dublette-urteil ok">Gleiche Länge und gleicher Titel — zwei Kopien desselben Songs.</div>'
: '<div class="dublette-urteil warn">Länge oder Titel weichen ab. Das Backend lehnt das Löschen ab, ' +
'solange das so ist — bitte erst selbst nachsehen.</div>');
const btn = document.createElement('button');
btn.className = 'btn-danger';
btn.textContent = '🗑 Überzählige Kopie löschen';
btn.onclick = () => _loescheDublette(btn, status, dub);
box.appendChild(btn);
status.appendChild(box);
}
async function _loescheDublette(btn, status, dub) {
const ok = await showConfirm(
'Dublette löschen',
`"${dub.ueberzaehlig.pfad}" wird unwiderruflich von der Festplatte gelöscht. ` +
`Die Kopie im richtigen Ordner bleibt bestehen.`,
'Löschen'
);
if (!ok) return;
btn.disabled = true;
btn.textContent = '🗑 Löschen…';
try {
const r = await fetch(`${S.cfg.backendUrl}/api/resolve-duplicate`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ndUrl: S.cfg.url, user: S.cfg.user, pass: S.cfg.pass,
behalten: dub.behalten.pfad, ueberzaehlig: dub.ueberzaehlig.pfad,
}),
});
const d = await r.json();
if (!r.ok) throw new Error(d.error || `HTTP ${r.status}`);
invalidateApiCache('getArtists');
invalidateRomajiIndex();
btn.remove();
status.insertAdjacentHTML('beforeend',
'<div class="dublette-urteil ok">✅ Gelöscht. Navidrome scannt — danach steht der Song nur noch einmal da.</div>');
toast('✅ Dublette gelöscht');
} catch (e) {
btn.disabled = false;
btn.textContent = '🗑 Überzählige Kopie löschen';
toast('❌ ' + e.message);
status.insertAdjacentHTML('beforeend',
'<div class="dublette-urteil warn">' + esc(e.message) + '</div>');
}
}
export async function deleteSong() {
if (!S.cfg.backendUrl) { toast('⚠ Kein Backend konfiguriert'); return; }
const s = S.infoSong;