fix(backend): Schraegstrich im Namen und zu lockere Titelsuche fuehrten auf die falsche Datei
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
eb72e5c27d
commit
24558497da
4 files changed
+193
-53
No files matched your search
+44
-53
@@ -18,7 +18,7 @@ const crypto = require('crypto');
|
||||
|
||||
const CONFIG = require('./lib/config');
|
||||
const { jobs, updateJob, log } = require('./lib/jobs');
|
||||
const { sanitize, findExistingTrack } = require('./lib/metadata');
|
||||
const { sanitize, findExistingTrack, findFileByStem, albumDirFromTags } = require('./lib/metadata');
|
||||
const dz = require('./lib/deezer');
|
||||
const tm = require('./lib/trackmeta'); // Deezer → MusicBrainz → clean fallback (single tracks)
|
||||
const { downloadTrack, downloadAlbumTracks } = require('./lib/downloader');
|
||||
@@ -94,57 +94,17 @@ function _findFileInDir(dir, basename) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Last-resort: deep recursive search by title stem (handles folder renames after metadata edit).
|
||||
// Convention: MusicRoot/AlbumArtist/Album/{01 - }Artist - Title.ext
|
||||
// The title stem is everything after the optional "NN - Artist - " prefix.
|
||||
// We match any audio file whose stem ends with the same title words.
|
||||
// Normalize a file stem for loose matching:
|
||||
// 1. Strip compound track prefixes like "01-15 – " or "01 - " (handles en-dash, em-dash, hyphen)
|
||||
// 2. Normalize all dash variants (en-dash, em-dash) to " - "
|
||||
function _normStem(s) {
|
||||
return s
|
||||
.replace(/^\d[\d\-–—]*\s*[-–—]\s*/, '') // strip leading "01-15 – " or "01 - "
|
||||
.replace(/\s*[–—]\s*/g, ' - '); // normalize remaining en/em-dash to " - "
|
||||
}
|
||||
|
||||
function _findFileByStem(dir, titleStem, depth = 0) {
|
||||
if (depth > 4) return null;
|
||||
const normTitle = _normStem(titleStem);
|
||||
try {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory()) {
|
||||
const found = _findFileByStem(path.join(dir, entry.name), titleStem, depth + 1);
|
||||
if (found) return found;
|
||||
} else if (AUDIO_EXT_RE.test(entry.name)) {
|
||||
// Strip extension + leading track prefix, then normalize dashes
|
||||
const bare = entry.name.replace(AUDIO_EXT_RE, '').replace(/^\d+\s*-\s*/, '');
|
||||
const bareStem = _normStem(bare.toLowerCase());
|
||||
// Match: exact, or "Artist - Title" ending with our title stem
|
||||
if (bareStem === normTitle ||
|
||||
bareStem.endsWith(' - ' + normTitle) ||
|
||||
bareStem.endsWith(normTitle) ||
|
||||
// Release-suffix desync: Navidrome's title-derived stem ("… - recognize")
|
||||
// may lack the "- Single"/"- EP" suffix the on-disk filename still carries
|
||||
// ("… - recognize - single"), or vice-versa. Match on the structured prefix.
|
||||
bareStem.startsWith(normTitle + ' - ') ||
|
||||
normTitle.startsWith(bareStem + ' - ')) {
|
||||
return path.join(dir, entry.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Like _findFileByStem but also tries a romaji-converted stem.
|
||||
// Die Tiefensuche nach Titelstamm liegt jetzt in lib/metadata.js (dort getestet) — sie ist der
|
||||
// letzte Ausweg, wenn Navidromes Pfad nicht existiert, und darf deshalb nicht raten.
|
||||
// Wie findFileByStem, versucht zusaetzlich einen Romaji-Stamm.
|
||||
// Handles the case where Navidrome has a Japanese path in its DB but the file
|
||||
// on disk was stored with a romaji filename by the download pipeline.
|
||||
function _findFileByStemOrRomaji(dir, stem) {
|
||||
const direct = _findFileByStem(dir, stem);
|
||||
const direct = findFileByStem(dir, stem);
|
||||
if (direct) return direct;
|
||||
try {
|
||||
const rt = (romajiForStorage(stem) || '').toLowerCase().trim();
|
||||
if (rt && rt !== stem) return _findFileByStem(dir, rt);
|
||||
if (rt && rt !== stem) return findFileByStem(dir, rt);
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
@@ -1273,13 +1233,14 @@ app.get('/api/musicbrainz/lookup', async (req, res) => {
|
||||
app.get('/api/song-sorttags', async (req, res) => {
|
||||
const { ndUrl, user, pass, songId } = req.query;
|
||||
if (!ndUrl || !user || !pass || !songId) return res.status(400).json({ error: 'ndUrl, user, pass, songId required' });
|
||||
let filePath;
|
||||
let filePath, ndSong;
|
||||
try {
|
||||
const salt = Math.random().toString(36).slice(2, 10);
|
||||
const token = crypto.createHash('md5').update(pass + salt).digest('hex');
|
||||
const ndRes = await fetch(`${ndUrl}/rest/getSong?id=${songId}&u=${encodeURIComponent(user)}&t=${token}&s=${salt}&v=1.16.1&c=NovaSonic-Backend&f=json`, { signal: AbortSignal.timeout(8000) });
|
||||
const nd = await ndRes.json();
|
||||
filePath = nd?.['subsonic-response']?.song?.path;
|
||||
ndSong = nd?.['subsonic-response']?.song;
|
||||
filePath = ndSong?.path;
|
||||
if (!filePath) return res.status(404).json({ error: 'Song not found' });
|
||||
} catch (e) { return res.status(502).json({ error: e.message }); }
|
||||
if (!path.isAbsolute(filePath)) filePath = path.join(CONFIG.musicPath, filePath);
|
||||
@@ -1288,9 +1249,11 @@ app.get('/api/song-sorttags', async (req, res) => {
|
||||
const rebased = _rebasePath(filePath, CONFIG.musicPath);
|
||||
const rebasedDir = rebased ? path.dirname(rebased) : null;
|
||||
const titleStem = fileName.replace(AUDIO_EXT_RE, '').replace(/^\d+\s*-\s*/, '').toLowerCase();
|
||||
const tagDir = albumDirFromTags(CONFIG.musicPath, ndSong?.albumArtist || ndSong?.artist, ndSong?.album);
|
||||
const found = rebased
|
||||
|| _findFileRecursive(CONFIG.musicPath, fileName)
|
||||
|| _findFileInDir(rebasedDir || path.dirname(filePath), fileName)
|
||||
|| (tagDir && (_findFileInDir(tagDir, fileName) || _findFileByStemOrRomaji(tagDir, titleStem)))
|
||||
|| _findFileByStemOrRomaji(CONFIG.musicPath, titleStem);
|
||||
if (found) filePath = found; else return res.status(404).json({ error: 'File not found' });
|
||||
}
|
||||
@@ -1436,9 +1399,14 @@ app.post('/api/edit-metadata', async (req, res) => {
|
||||
// but Navidrome's index is stale and still points to the old name.
|
||||
const originalDir = path.dirname(filePath);
|
||||
const titleStem = fileName.replace(AUDIO_EXT_RE, '').replace(/^\d+\s*-\s*/, '').toLowerCase();
|
||||
// Schraegstrich im Kuenstler-/Albumnamen ("K/DA", "POP/STARS"): Navidromes Tag-Pfad macht
|
||||
// daraus eine Verzeichnisebene, auf der Platte steht "_". Den Ordner deshalb aus den Tags
|
||||
// nachbauen und DORT suchen — das ist gezielt und geht jeder Aehnlichkeitssuche vor.
|
||||
const tagDir = albumDirFromTags(CONFIG.musicPath, ndSong?.albumArtist || ndSong?.artist, ndSong?.album);
|
||||
const found = rebased
|
||||
|| _findFileRecursive(CONFIG.musicPath, fileName)
|
||||
|| _findFileInDir(rebasedDir || originalDir, fileName)
|
||||
|| (tagDir && (_findFileInDir(tagDir, fileName) || _findFileByStemOrRomaji(tagDir, titleStem)))
|
||||
|| _findFileByStemOrRomaji(CONFIG.musicPath, titleStem)
|
||||
// Last resort: Subsonic getSong returns a *virtual* (tag-built) path, so the folder
|
||||
// can be "[Unknown Album]" while the file actually sits in "Singles" with an artist
|
||||
@@ -1649,7 +1617,7 @@ app.post('/api/delete-song', async (req, res) => {
|
||||
return res.status(400).json({ error: 'ndUrl, user, pass, songId required' });
|
||||
|
||||
// Resolve the file path from Navidrome
|
||||
let filePath;
|
||||
let filePath, ndSong;
|
||||
try {
|
||||
const salt = Math.random().toString(36).slice(2, 10);
|
||||
const token = crypto.createHash('md5').update(pass + salt).digest('hex');
|
||||
@@ -1658,7 +1626,8 @@ app.post('/api/delete-song', async (req, res) => {
|
||||
{ signal: AbortSignal.timeout(8000) }
|
||||
);
|
||||
const ndData = await ndRes.json();
|
||||
filePath = ndData?.['subsonic-response']?.song?.path;
|
||||
ndSong = ndData?.['subsonic-response']?.song;
|
||||
filePath = ndSong?.path;
|
||||
if (!filePath) return res.status(404).json({ error: 'Song not found in Navidrome' });
|
||||
} catch (e) {
|
||||
return res.status(502).json({ error: `Navidrome lookup failed: ${e.message}` });
|
||||
@@ -1669,14 +1638,36 @@ app.post('/api/delete-song', async (req, res) => {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
const fileName = path.basename(filePath);
|
||||
const titleStem = fileName.replace(AUDIO_EXT_RE, '').replace(/^\d+\s*-\s*/, '').toLowerCase();
|
||||
const tagDir = albumDirFromTags(CONFIG.musicPath, ndSong?.albumArtist || ndSong?.artist, ndSong?.album);
|
||||
const found = _rebasePath(filePath, CONFIG.musicPath)
|
||||
|| _findFileRecursive(CONFIG.musicPath, fileName)
|
||||
|| _findFileInDir(path.dirname(filePath), fileName)
|
||||
|| _findFileByStemOrRomaji(CONFIG.musicPath, titleStem);
|
||||
if (found) filePath = found;
|
||||
else return res.status(404).json({
|
||||
|| (tagDir && (_findFileInDir(tagDir, fileName) || _findFileByStemOrRomaji(tagDir, titleStem)))
|
||||
|| _findFileByStemOrRomaji(CONFIG.musicPath, titleStem)
|
||||
|| findExistingTrack(CONFIG.musicPath, ndSong?.albumArtist || ndSong?.artist || '', ndSong?.title || '');
|
||||
if (!found) return res.status(404).json({
|
||||
error: `File not found on disk. Navidrome path: "${ndPath}" — music root: "${CONFIG.musicPath}" — searched for: "${fileName}"`,
|
||||
});
|
||||
// Derselbe Abgleich wie im Metadaten-Editor, und hier WICHTIGER: der Treffer stammt aus einer
|
||||
// Aehnlichkeitssuche, und geloescht wird unwiderruflich. Passen die Tags der gefundenen Datei
|
||||
// nicht zu dem, was Navidrome fuer diesen Song meldet, wird abgebrochen statt geraten.
|
||||
const delTags = await _readTags(found);
|
||||
const istTitel = _tagNorm(delTags.title || delTags.TITLE);
|
||||
const istArtist = _tagNorm(delTags.artist || delTags.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 "` +
|
||||
`${delTags.artist || '?'} – ${delTags.title || '?'}" (${found}). ` +
|
||||
`Es wird nichts geloescht — bitte die Dubletten aufraeumen.`,
|
||||
});
|
||||
}
|
||||
filePath = found;
|
||||
}
|
||||
|
||||
// Safety: file must be inside the music root and must be an audio file
|
||||
|
||||
Reference in new issue
Block a user