diff --git a/CHANGELOG.md b/CHANGELOG.md index 3de9c9a..849b59f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). ### 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 `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 diff --git a/backend/lib/metadata.js b/backend/lib/metadata.js index 8ab7f1a..aa310c7 100644 --- a/backend/lib/metadata.js +++ b/backend/lib/metadata.js @@ -6,6 +6,19 @@ function sanitize(s) { 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) { if (!name) return name; return name @@ -202,7 +215,7 @@ async function fetchLyrics(artist, title, album) { // fetchAlbumTracks, searchAlbumTracks and fetchArtistAlbums. module.exports = { - sanitize, navidromeArtist, + sanitize, navidromeArtist, dateinamensKuenstler, downloadArtwork, findExistingTrack, normStem, findFileByStem, albumDirFromTags, }; diff --git a/backend/lib/ytdlp.js b/backend/lib/ytdlp.js index b5a1e96..e63fff0 100644 --- a/backend/lib/ytdlp.js +++ b/backend/lib/ytdlp.js @@ -4,7 +4,7 @@ const fs = require('fs'); const CONFIG = require('./config'); const { log } = require('./jobs'); const { - sanitize, navidromeArtist, + sanitize, navidromeArtist, dateinamensKuenstler, downloadArtwork, findExistingTrack, } = require('./metadata'); const { romajiForStorage, isTokenizerReady } = require('./romaji'); @@ -140,8 +140,9 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb const safeTitle = sanitize(cleanTitle); const trackPrefix = trackNum ? String(trackNum).padStart(2, '0') + ' - ' : ''; - // Filename format: "{01 - }Artist1, Artist2 - Title.ext" - const safeArtistForFile = sanitize((artist || '').replace(/;/g, ',')); + // Filename format: "{01 - }Artist - Title.ext" — nur der erste Kuenstler, siehe + // dateinamensKuenstler() in lib/metadata.js. Die volle Liste steht im artist-Tag. + const safeArtistForFile = sanitize(dateinamensKuenstler(artist, mainArtist)); const fileBase = `${trackPrefix}${safeArtistForFile} - ${safeTitle}`; // Dublettenpruefung: findExistingTrack durchsucht NUR den Ordner des uebergebenen Kuenstlers. diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 821fde3..1bc491c 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -18,7 +18,7 @@ const crypto = require('crypto'); const CONFIG = require('./lib/config'); 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 tm = require('./lib/trackmeta'); // Deezer → MusicBrainz → clean fallback (single tracks) 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. if (title) { const safeTitle = sanitize(title); - const fileArtist = (normalArtist || ndSong?.artist || '').replace(/;/g, ','); + const fileArtist = dateinamensKuenstler(normalArtist, ndSong?.artist); const safeArtistPart = fileArtist ? sanitize(fileArtist) + ' - ' : ''; const prefix = trackNum ? String(trackNum).padStart(2, '0') + ' - ' : ''; const newName = `${prefix}${safeArtistPart}${safeTitle}${fileExt}`; diff --git a/backend/test/metadata.test.js b/backend/test/metadata.test.js index 228e72f..48901c9 100644 --- a/backend/test/metadata.test.js +++ b/backend/test/metadata.test.js @@ -170,3 +170,53 @@ test('Mehrere Kuenstler: Ordner haengt am ersten', () => { test('Kein Treffer bleibt 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 }); +});