fix(backend): Metadaten-Edit trifft nicht mehr die falsche Datei
Findet das Backend die von Navidrome gemeldete Datei nicht am angegebenen Pfad (Subsonic liefert oft einen aus Tags gebauten virtuellen Pfad), sucht es ueber Dateiname bzw. Kuenstler+Titel weiter. Liegt derselbe Song mehrfach auf der Platte, konnte das die falsche Kopie treffen: die wurde getaggt, der bearbeitete Song blieb unveraendert — die Aenderung "sprang zurueck". Das erklaert, warum es bei Songs ohne Dublette immer ging und bei anderen nie. Der ueber Aehnlichkeit gefundene Kandidat wird jetzt per ffprobe gegen die Tags geprueft, die Navidrome fuer diesen Song meldet, und bei Widerspruch mit 409 und erklaerender Meldung abgebrochen statt geraten. Zusaetzlich melden uebersprungene Umbenennungen und Verschiebungen sich als Warnung, statt stumm zu bleiben; die Antwort nennt den tatsaechlich bearbeiteten Pfad, der Editor zeigt beides an. Ausserdem: -dontwarn org.slf4j.** — der Release-APK-Build brach seit laengerem an R8 ab (Ktors Logging referenziert den slf4j-1.x-Binder, den es auf Android nicht gibt). Vorbestehend, mit HEAD~1 verifiziert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aae358c4c7
commit
f62b248bb6
4 files changed
+86
-8
No files matched your search
@@ -108,6 +108,18 @@ Versioning: two-part `MAJOR.MINOR` — each release bumps the minor (features an
|
||||
`scrollHeight` kleiner als das Ziel, und der Browser kappte den Wert stillschweigend: man landete
|
||||
wieder oben. Der Wert wird jetzt über bis zu ~0,5 s erneut gesetzt, bis er sitzt.
|
||||
|
||||
- **Metadaten-Änderungen „sprangen zurück" — bei manchen Songs immer, bei anderen nie.** Findet das
|
||||
Backend die von Navidrome gemeldete Datei nicht am angegebenen Pfad (Navidrome liefert oft einen
|
||||
aus Tags gebauten *virtuellen* Pfad), sucht es über Dateiname bzw. Künstler+Titel weiter. Liegt
|
||||
derselbe Song mehrfach auf der Platte — etwa ein Fehlgriff des Downloaders neben der richtigen
|
||||
Datei —, konnte das die **falsche Kopie** treffen: die wurde getaggt, der bearbeitete Song blieb
|
||||
unverändert. Genau deshalb ging es bei Songs ohne Dublette immer und bei anderen nie. Der über
|
||||
Ähnlichkeit gefundene Kandidat wird jetzt gegen die Tags geprüft, die Navidrome für den Song
|
||||
meldet, und bei Widerspruch mit einer erklärenden Meldung abgebrochen, statt zu raten.
|
||||
- **Stumm übersprungene Umbenennungen** — lag am Zielnamen schon eine Datei, blieb der Song unter
|
||||
altem Namen bzw. im alten Ordner liegen, ohne Hinweis. Solche Fälle kommen jetzt als Warnung
|
||||
zurück; der Editor zeigt sie samt bearbeitetem Dateipfad an und bleibt offen.
|
||||
|
||||
### Added
|
||||
- **Android: Songs von der Festplatte löschen** — im Song-Kontextmenü, unten abgesetzt und rot, mit
|
||||
derselben Sicherheitsabfrage wie im Web. Nutzt denselben Backend-Endpunkt (`/api/delete-song`) und
|
||||
|
||||
Vendored
+6
@@ -6,3 +6,9 @@
|
||||
# Keep Media3 session / exoplayer classes used by reflection / service binding.
|
||||
-keep class androidx.media3.** { *; }
|
||||
-keep class com.google.android.exoplayer2.** { *; }
|
||||
|
||||
# Ktors Logging referenziert ueber slf4j den Binder aus slf4j 1.x. Auf Android existiert der nicht
|
||||
# (slf4j 2.x sucht Implementierungen per ServiceLoader, und wir binden ohnehin keine), deshalb bricht
|
||||
# R8 den Release-Build mit "Missing class org.slf4j.impl.StaticLoggerBinder" ab. Die Referenz ist zur
|
||||
# Laufzeit unerreichbar — slf4j faengt das Fehlen selbst ab und schaltet auf einen No-op-Logger.
|
||||
-dontwarn org.slf4j.**
|
||||
@@ -1342,6 +1342,19 @@ async function _migratePlaylists(ndUrl, user, pass, plIds, oldId, newId) {
|
||||
}
|
||||
|
||||
// ─── METADATA EDIT ───────────────────────────────────────────────────────────
|
||||
// Liest die vorhandenen Tags einer Datei. Wird gebraucht, um einen ueber Namensaehnlichkeit
|
||||
// gefundenen Kandidaten zu PRUEFEN, bevor er ueberschrieben wird.
|
||||
async function _readTags(file) {
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn('ffprobe', ['-v', 'quiet', '-print_format', 'json', '-show_entries', 'format_tags', file]);
|
||||
let buf = '';
|
||||
proc.stdout.on('data', d => buf += d);
|
||||
proc.on('error', () => resolve({}));
|
||||
proc.on('close', () => { try { resolve(JSON.parse(buf)?.format?.tags || {}); } catch { resolve({}); } });
|
||||
});
|
||||
}
|
||||
const _tagNorm = (v) => (v || '').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, '');
|
||||
|
||||
app.post('/api/edit-metadata', async (req, res) => {
|
||||
const { ndUrl, user, pass, songId, title, artist, albumArtist, album, year, trackNum, totalTracks,
|
||||
discNum, discTotal, genre, artworkBase64, artworkMime, artworkUrl: newArtworkUrl,
|
||||
@@ -1401,8 +1414,32 @@ app.post('/api/edit-metadata', async (req, res) => {
|
||||
// can be "[Unknown Album]" while the file actually sits in "Singles" with an artist
|
||||
// prefix. findExistingTrack matches by normalized artist+title across all album folders.
|
||||
|| findExistingTrack(CONFIG.musicPath, ndSong.albumArtist || ndSong.artist || '', ndSong.title || '');
|
||||
if (found) { filePath = found; }
|
||||
else return res.status(404).json({ error: `File not found: ${filePath} (music root: ${CONFIG.musicPath})` });
|
||||
if (!found) {
|
||||
return res.status(404).json({ error: `File not found: ${filePath} (music root: ${CONFIG.musicPath})` });
|
||||
}
|
||||
// Der Treffer stammt aus einer Aehnlichkeitssuche ueber Dateiname bzw. Kuenstler+Titel. Liegt
|
||||
// derselbe Song mehrfach auf der Platte — etwa ein Fehlgriff des Downloaders neben der richtigen
|
||||
// Datei —, kann das die FALSCHE Kopie sein. Die wurde dann getaggt, waehrend der bearbeitete Song
|
||||
// unveraendert blieb: die Aenderung "sprang zurueck". Deshalb wird der Kandidat gegen die Tags
|
||||
// geprueft, die Navidrome fuer diesen Song meldet, und im Zweifel abgebrochen statt geraten.
|
||||
const kandidatTags = await _readTags(found);
|
||||
const istTitel = _tagNorm(kandidatTags.title || kandidatTags.TITLE);
|
||||
const istArtist = _tagNorm(kandidatTags.artist || kandidatTags.ARTIST);
|
||||
const sollTitel = _tagNorm(ndSong?.title);
|
||||
const sollArtist = _tagNorm(ndSong?.artist);
|
||||
const titelPasst = !sollTitel || !istTitel || istTitel === sollTitel;
|
||||
const artistPasst = !sollArtist || !istArtist ||
|
||||
istArtist.includes(sollArtist) || sollArtist.includes(istArtist);
|
||||
if (!titelPasst || !artistPasst) {
|
||||
return res.status(409).json({
|
||||
error: `Datei nicht eindeutig zuzuordnen. Navidrome meldet "${ndSong?.artist || '?'} – ` +
|
||||
`${ndSong?.title || '?'}", die gefundene Datei enthaelt aber "` +
|
||||
`${kandidatTags.artist || '?'} – ${kandidatTags.title || '?'}" (${found}). ` +
|
||||
`Vermutlich liegt derselbe Song mehrfach in der Bibliothek — bitte die Dubletten ` +
|
||||
`aufraeumen, sonst wuerde die falsche Datei bearbeitet.`,
|
||||
});
|
||||
}
|
||||
filePath = found;
|
||||
}
|
||||
|
||||
// Resolve artwork: base64 > URL
|
||||
@@ -1481,6 +1518,10 @@ app.post('/api/edit-metadata', async (req, res) => {
|
||||
|
||||
// ── Post-save: rename file and/or move to correct artist/album folder ──
|
||||
let currentFile = filePath;
|
||||
// Umbenennen und Verschieben werden uebersprungen, wenn am Ziel schon eine Datei liegt. Das war
|
||||
// bisher stumm — der Song blieb dann unter dem alten Namen bzw. im alten Ordner liegen, ohne dass
|
||||
// es jemand erfuhr. Solche Faelle kommen jetzt als Warnung mit in die Antwort.
|
||||
const warnungen = [];
|
||||
|
||||
// 1. Rename file if title changed — convention: {01 - }?{Artist} - {Title}.ext
|
||||
// Use original names (may be Japanese); romaji goes only into sort tags.
|
||||
@@ -1491,9 +1532,15 @@ app.post('/api/edit-metadata', async (req, res) => {
|
||||
const prefix = trackNum ? String(trackNum).padStart(2, '0') + ' - ' : '';
|
||||
const newName = `${prefix}${safeArtistPart}${safeTitle}${fileExt}`;
|
||||
const newPath = path.join(path.dirname(currentFile), newName);
|
||||
if (newPath !== currentFile && !fs.existsSync(newPath)) {
|
||||
if (newPath !== currentFile && fs.existsSync(newPath)) {
|
||||
warnungen.push(`Datei nicht umbenannt: "${path.basename(newPath)}" existiert bereits im ` +
|
||||
`selben Ordner. Die Tags wurden geschrieben, der Dateiname bleibt alt.`);
|
||||
} else if (newPath !== currentFile) {
|
||||
try { fs.renameSync(currentFile, newPath); currentFile = newPath; }
|
||||
catch (e) { console.error('[edit-metadata] file rename:', e.message); }
|
||||
catch (e) {
|
||||
console.error('[edit-metadata] file rename:', e.message);
|
||||
warnungen.push(`Umbenennen fehlgeschlagen: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1508,7 +1555,10 @@ app.post('/api/edit-metadata', async (req, res) => {
|
||||
try {
|
||||
fs.mkdirSync(newDir, { recursive: true });
|
||||
const destFile = path.join(newDir, path.basename(currentFile));
|
||||
if (!fs.existsSync(destFile)) {
|
||||
if (fs.existsSync(destFile)) {
|
||||
warnungen.push(`Nicht verschoben: "${destFile}" existiert bereits. Die Datei bleibt in ` +
|
||||
`"${oldAlbumDir}" — vermutlich eine Dublette.`);
|
||||
} else {
|
||||
fs.renameSync(currentFile, destFile);
|
||||
currentFile = destFile;
|
||||
}
|
||||
@@ -1548,7 +1598,9 @@ app.post('/api/edit-metadata', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ ok: true });
|
||||
// Den tatsaechlich bearbeiteten Pfad mitgeben: wer eine Aenderung "zurueckspringen" sieht, kann
|
||||
// damit sofort erkennen, ob die richtige Datei erwischt wurde.
|
||||
res.json({ ok: true, file: currentFile, warnings: warnungen });
|
||||
} catch (e) {
|
||||
try { if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile); } catch {}
|
||||
if (artFile) try { fs.unlinkSync(artFile); } catch {}
|
||||
|
||||
+10
-2
@@ -281,10 +281,18 @@ export async function saveMetadata() {
|
||||
});
|
||||
const d = await r.json();
|
||||
if (!r.ok) throw new Error(d.error || `HTTP ${r.status}`);
|
||||
status.textContent = '✅ Gespeichert! Navidrome scannt…';
|
||||
toast('✅ Metadaten gespeichert');
|
||||
invalidateRomajiIndex();
|
||||
invalidateApiCache('getArtists');
|
||||
// Warnungen des Backends (Dublette am Zielnamen, nicht verschoben, …) sichtbar machen und das
|
||||
// Fenster offen lassen — sonst sieht es nach "gespeichert" aus, obwohl die Datei liegen blieb.
|
||||
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>';
|
||||
toast('⚠ Gespeichert — mit Hinweisen, siehe Fenster');
|
||||
return;
|
||||
}
|
||||
status.textContent = '✅ Gespeichert! Navidrome scannt…';
|
||||
toast('✅ Metadaten gespeichert');
|
||||
setTimeout(() => { _leaveEditMode(); closeInfoModal(); }, 1500);
|
||||
} catch (e) {
|
||||
status.textContent = '❌ Fehler: ' + e.message;
|
||||
|
||||
Reference in new issue
Block a user