change(backend): nur noch der erste Kuenstler im Dateinamen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-09-20 22:57:04 +02:00
1 parent 8d25bf0404
commit 073593d963
5 files changed
+84 -6

No files matched your search

+14
View File
@@ -208,6 +208,20 @@ nur zu zeigen, was drinsteht.
geprüft werden können: sieben neue Fälle in `backend/test/metadata.test.js` (49 Prüfungen grün). geprüft werden können: sieben neue Fälle in `backend/test/metadata.test.js` (49 Prüfungen grün).
### Changed ### Changed
- **Nur noch ein Künstler im Dateinamen (Backend + Editor)** — bisher stand dort die vollständige
Gästeliste: `01 - K_DA,Madison Beer,i-dle,Lexie Liu,Jaira Burns,Seraphine,League of Legends - MORE.mp3`.
Üblich ist das nirgends — Picard, beets und Lidarr schreiben höchstens **einen** Künstler in den
Namen —, und es hatte zwei handfeste Nachteile: `sanitize()` schneidet bei 100 Zeichen hart ab,
also mitten im letzten Namen, und mit dem Dateisystemlimit von 255 Byte wird es bei mehrbytigen
Namen eng, was Umbenennen und Verschieben scheitern lässt. Jetzt `01 - K_DA - MORE.mp3`.
- Genommen wird der **erste Künstler des Titels**, nicht der des Albums — so trägt ein
Soundtrack-Titel im Namen weiterhin seinen eigenen Interpreten, auch wenn der Ordner am
Album-Künstler hängt. Die vollständige Liste steht unverändert im `artist`-Tag, und nur den
liest Navidrome.
- Gilt für **neue** Downloads und für Dateien, die bearbeitet werden; Bestehendes bleibt liegen.
Beide Namensformen stehen also eine Weile nebeneinander. Die Dublettenprüfung erkennt beide —
eigener Testfall, damit die alten Dateien nicht ein zweites Mal geladen werden.
- **Künstlerbilder laden endlich zügig (Web)** — im Künstlerraster lief **pro Künstler ein eigener - **Künstlerbilder laden endlich zügig (Web)** — im Künstlerraster lief **pro Künstler ein eigener
`getArtistInfo2`-Aufruf**, nur um die Bildadresse zu erfahren. Bei 200 Künstlern sind das 200 `getArtistInfo2`-Aufruf**, nur um die Bildadresse zu erfahren. Bei 200 Künstlern sind das 200
API-Aufrufe auf einen Schlag, die der Browser zu sechst abarbeitet (mehr Verbindungen erlaubt er API-Aufrufe auf einen Schlag, die der Browser zu sechst abarbeitet (mehr Verbindungen erlaubt er
+14 -1
View File
@@ -6,6 +6,19 @@ function sanitize(s) {
return (s || 'Unknown').replace(/[\\/:*?"<>|]/g, '_').trim().slice(0, 100); return (s || 'Unknown').replace(/[\\/:*?"<>|]/g, '_').trim().slice(0, 100);
} }
// Welcher Kuenstler in den DATEINAMEN gehoert. Bisher stand dort die vollstaendige Gaesteliste
// ("K_DA,Madison Beer,i-dle,Lexie Liu,Jaira Burns,Seraphine,League of Legends"). Das ist nirgends
// ueblich — Picard, beets und Lidarr schreiben hoechstens einen Kuenstler in den Namen —, und es
// hatte zwei handfeste Nachteile: sanitize() schneidet bei 100 Zeichen hart ab, also mitten im
// letzten Namen, und mit dem Dateisystemlimit von 255 Byte wird es bei mehrbytigen Namen eng.
// Die vollstaendige Liste steht ohnehin im artist-Tag, und nur den liest Navidrome.
// Genommen wird der ERSTE Kuenstler — bei gemischten Alben der des Titels, nicht der des Albums,
// damit ein Soundtrack-Titel im Namen weiterhin seinen eigenen Interpreten traegt.
function dateinamensKuenstler(artist, fallback) {
const erster = (s) => String(s || '').split(/[;,]|\s+&\s+/)[0].trim();
return erster(artist) || erster(fallback) || '';
}
function navidromeArtist(name) { function navidromeArtist(name) {
if (!name) return name; if (!name) return name;
return name return name
@@ -202,7 +215,7 @@ async function fetchLyrics(artist, title, album) {
// fetchAlbumTracks, searchAlbumTracks and fetchArtistAlbums. // fetchAlbumTracks, searchAlbumTracks and fetchArtistAlbums.
module.exports = { module.exports = {
sanitize, navidromeArtist, sanitize, navidromeArtist, dateinamensKuenstler,
downloadArtwork, findExistingTrack, downloadArtwork, findExistingTrack,
normStem, findFileByStem, albumDirFromTags, normStem, findFileByStem, albumDirFromTags,
}; };
+4 -3
View File
@@ -4,7 +4,7 @@ const fs = require('fs');
const CONFIG = require('./config'); const CONFIG = require('./config');
const { log } = require('./jobs'); const { log } = require('./jobs');
const { const {
sanitize, navidromeArtist, sanitize, navidromeArtist, dateinamensKuenstler,
downloadArtwork, findExistingTrack, downloadArtwork, findExistingTrack,
} = require('./metadata'); } = require('./metadata');
const { romajiForStorage, isTokenizerReady } = require('./romaji'); const { romajiForStorage, isTokenizerReady } = require('./romaji');
@@ -140,8 +140,9 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb
const safeTitle = sanitize(cleanTitle); const safeTitle = sanitize(cleanTitle);
const trackPrefix = trackNum ? String(trackNum).padStart(2, '0') + ' - ' : ''; const trackPrefix = trackNum ? String(trackNum).padStart(2, '0') + ' - ' : '';
// Filename format: "{01 - }Artist1, Artist2 - Title.ext" // Filename format: "{01 - }Artist - Title.ext" — nur der erste Kuenstler, siehe
const safeArtistForFile = sanitize((artist || '').replace(/;/g, ',')); // dateinamensKuenstler() in lib/metadata.js. Die volle Liste steht im artist-Tag.
const safeArtistForFile = sanitize(dateinamensKuenstler(artist, mainArtist));
const fileBase = `${trackPrefix}${safeArtistForFile} - ${safeTitle}`; const fileBase = `${trackPrefix}${safeArtistForFile} - ${safeTitle}`;
// Dublettenpruefung: findExistingTrack durchsucht NUR den Ordner des uebergebenen Kuenstlers. // Dublettenpruefung: findExistingTrack durchsucht NUR den Ordner des uebergebenen Kuenstlers.
+2 -2
View File
@@ -18,7 +18,7 @@ const crypto = require('crypto');
const CONFIG = require('./lib/config'); const CONFIG = require('./lib/config');
const { jobs, updateJob, log } = require('./lib/jobs'); const { jobs, updateJob, log } = require('./lib/jobs');
const { sanitize, findExistingTrack, findFileByStem, albumDirFromTags } = require('./lib/metadata'); const { sanitize, findExistingTrack, findFileByStem, albumDirFromTags, dateinamensKuenstler } = require('./lib/metadata');
const dz = require('./lib/deezer'); const dz = require('./lib/deezer');
const tm = require('./lib/trackmeta'); // Deezer → MusicBrainz → clean fallback (single tracks) const tm = require('./lib/trackmeta'); // Deezer → MusicBrainz → clean fallback (single tracks)
const { downloadTrack, downloadAlbumTracks } = require('./lib/downloader'); const { downloadTrack, downloadAlbumTracks } = require('./lib/downloader');
@@ -1553,7 +1553,7 @@ app.post('/api/edit-metadata', async (req, res) => {
// Use original names (may be Japanese); romaji goes only into sort tags. // Use original names (may be Japanese); romaji goes only into sort tags.
if (title) { if (title) {
const safeTitle = sanitize(title); const safeTitle = sanitize(title);
const fileArtist = (normalArtist || ndSong?.artist || '').replace(/;/g, ','); const fileArtist = dateinamensKuenstler(normalArtist, ndSong?.artist);
const safeArtistPart = fileArtist ? sanitize(fileArtist) + ' - ' : ''; const safeArtistPart = fileArtist ? sanitize(fileArtist) + ' - ' : '';
const prefix = trackNum ? String(trackNum).padStart(2, '0') + ' - ' : ''; const prefix = trackNum ? String(trackNum).padStart(2, '0') + ' - ' : '';
const newName = `${prefix}${safeArtistPart}${safeTitle}${fileExt}`; const newName = `${prefix}${safeArtistPart}${safeTitle}${fileExt}`;
+50
View File
@@ -170,3 +170,53 @@ test('Mehrere Kuenstler: Ordner haengt am ersten', () => {
test('Kein Treffer bleibt null', () => { test('Kein Treffer bleibt null', () => {
assert.equal(albumDirFromTags(SLASH_ROOT, 'Niemand', 'Nichts'), null); assert.equal(albumDirFromTags(SLASH_ROOT, 'Niemand', 'Nichts'), null);
}); });
// ─── Kuenstler im Dateinamen ────────────────────────────────────────────────
// Frueher stand die vollstaendige Gaesteliste im Namen und wurde bei 100 Zeichen mitten im
// letzten Namen abgeschnitten. Jetzt nur noch der erste Kuenstler.
const { dateinamensKuenstler } = require('../lib/metadata');
test('Nur der erste Kuenstler landet im Dateinamen', () => {
assert.equal(
dateinamensKuenstler('K/DA;Madison Beer;i-dle;Lexie Liu;Jaira Burns;Seraphine;League of Legends'),
'K/DA',
);
assert.equal(dateinamensKuenstler('Dove Cameron, Sofia Carson'), 'Dove Cameron');
assert.equal(dateinamensKuenstler('Calvin Harris & Dua Lipa'), 'Calvin Harris');
});
test('Bandnamen mit & bleiben ganz, wenn sie keine Liste sind', () => {
// "&" trennt nur MIT umgebenden Leerzeichen — "Earth, Wind & Fire" waere ohnehin durch das
// Komma getrennt, aber ein reiner Bandname ohne Liste darf nicht zerlegt werden.
assert.equal(dateinamensKuenstler('AC/DC'), 'AC/DC');
assert.equal(dateinamensKuenstler('Simon&Garfunkel'), 'Simon&Garfunkel');
});
test('Leerer Kuenstler faellt auf den Album-Kuenstler zurueck', () => {
assert.equal(dateinamensKuenstler('', 'K/DA;Madison Beer'), 'K/DA');
assert.equal(dateinamensKuenstler(undefined, undefined), '');
});
test('Der Dateiname bleibt kurz genug fuers Dateisystem', () => {
const lang = 'K/DA;Madison Beer;(G)I-DLE;Lexie Liu;Jaira Burns;Seraphine;League of Legends;Bea Miller;Wolftyla;Duckwrth';
const teil = dateinamensKuenstler(lang);
assert.equal(teil, 'K/DA');
assert.ok(teil.length < 100, 'wird nie von sanitize() abgeschnitten');
});
test('Dublettenpruefung findet auch die alt benannten Dateien', () => {
// Nach der Umstellung liegen beide Namensformen nebeneinander. Beide muessen als "schon da"
// erkannt werden, sonst laedt der Downloader die alten Dateien ein zweites Mal.
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ns-namen-'));
const put = (album, file) => {
const dir = path.join(root, 'K_DA', album);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, file), '');
};
put('MORE', '01 - K_DA,Madison Beer,i-dle,Lexie Liu,Jaira Burns,Seraphine - MORE.mp3'); // alt
put('ALL OUT', '03 - K_DA - VILLAIN.mp3'); // neu
assert.ok(findExistingTrack(root, 'K/DA', 'MORE'), 'alte Namensform nicht erkannt');
assert.ok(findExistingTrack(root, 'K/DA', 'VILLAIN'), 'neue Namensform nicht erkannt');
assert.equal(findExistingTrack(root, 'K/DA', 'POP/STARS'), null);
fs.rmSync(root, { recursive: true, force: true });
});