From fa4c954ab91b34db39d93487c746d5ca53813d0f Mon Sep 17 00:00:00 2001 From: Honey Date: Sun, 21 Jun 2026 13:26:11 +0200 Subject: [PATCH] feat(0.9): romaji sort-tags only, robust dupe detection, Mittelweg no-duration, autofill all fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Japanese tracks stored with original names in folder/filename/tags; romaji only in sort tags - findExistingTrack: case-insensitive, searches all album folders under artist, strips release suffix - --max-filesize 30M + 2x duration cap on all yt-dlp calls - iTunes collectionArtistName captured as album_artist; fixes compilation album splitting - album_artist tag uses mainArtist (not rawArtist) so all tracks in one album are consistent - No-duration cascade: steps 5-8 (title-only/no-filter) removed — fail cleanly like with-duration - Autofill iTunes: fills title, artist, albumArtist, discNum (was album/genre/year/track/total only) - Autofill MusicBrainz: fills albumArtist, discNum (was missing both) Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 60 +++++++++++++++++------------------ CLAUDE.md | 19 ++++++++--- backend/lib/metadata.js | 55 +++++++++++++++++++++++++++----- backend/lib/ytdlp.js | 63 ++++++++++++++++--------------------- backend/novasonic-server.js | 35 +++++++++++++-------- src/ui/metadata.js | 14 ++++++--- 6 files changed, 148 insertions(+), 98 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33aeadc..2cb2f0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,31 @@ Versioning: two-part `MAJOR.MINOR` — each release bumps the minor (features an --- +## [0.9] — 2026-06-21 + +### Changed +- **Backend** — Japanese tracks now stored with original Japanese names in folders, filenames, and all main tags (title/artist/album/album\_artist); Romaji goes only into sort tags (TITLESORT / ARTISTSORT / ALBUMARTISTSORT / ALBUMSORT) for sorting and search in Navidrome. Previously, romaji was applied to filenames and tags too, causing name drift, duplicate misses, and wrong iTunes resolutions (e.g. "Hisaishi Yuzuru" instead of "Joe Hisaishi") +- **Backend** — Metadata editor: files and album/artist folders are renamed using the original name typed in the form (may be Japanese), not romaji; sort tags still receive the romaji conversion +- **Backend** — `album_artist` tag in downloaded tracks now consistently uses `mainArtist` (the iTunes-resolved album artist) rather than the raw per-track artist. This prevents Navidrome from splitting one album into multiple when track artists vary (e.g. compilations) +- **Backend (download)** — No-duration cascade now ends after step 4 (same Mittelweg philosophy as the with-duration branch): no title-only or filter-free fallback grabs that pull wrong covers (observed: Yorushika 車窓 → Piano Echoes Shasou) +- **Backend (download)** — `--max-filesize 30M` added to all yt-dlp calls; Mittelweg/no-duration steps also cap at 2× expected duration — a 28-min video can no longer be saved as a ~52 MB mp3 + +### Fixed +- **Backend (download)** — Duplicate detection now matches case-insensitively (DJ OKAWARI vs DJ Okawari) and across all album folders under the artist folder, comparing normalized title without track prefix or release/variant suffix — prevents re-downloading songs whose filename drifted between runs +- **Backend (download)** — iTunes `collectionArtistName` (album artist) is now captured and passed through the download pipeline as the `album_artist` tag; fixes compilation albums showing "Various Artists" for some tracks and the individual track artist for others +- **Metadata editor (autofill)** — iTunes button now fills all available fields: title (`trackName`), artist (`artistName`), album-artist (`collectionArtistName`), album, genre, year, track #, disc #, total tracks, cover +- **Metadata editor (autofill)** — MusicBrainz button now fills album-artist (from release artist-credit) and disc # in addition to the existing fields + +### Investigated (Point 4 — album splitting) +Primary causes identified: +1. Missing `album_artist` tag / inconsistent value across tracks — fixed above (mainArtist + collectionArtistName) +2. `tagAlbumArtist` was using `rawArtist` (per-track Spotify name) instead of `mainArtist` — fixed +3. Album name comes from iTunes `collectionName` — consistent within a collection, no fix needed +4. Year from iTunes `releaseDate` — album-level date, consistent within a collection +5. MBIDs not set — still missing; not the primary splitting cause + +--- + ## [0.8] — 2026-06-19 ### Removed @@ -224,35 +249,6 @@ Versioning: two-part `MAJOR.MINOR` — each release bumps the minor (features an - **Web + Android** — yt-dlp backend update / maintenance ### Known Bugs -- **Backend (download)** — Duplicate detection is filename-only and exact: a track re-downloads - when the cleaned filename changes (e.g. `… - recognize - Single.mp3` vs `… - recognize.mp3`), - leaving two copies. Should match on normalized artist+title (ignore release suffix / variant / - track prefix), not the raw filename. -- **Backend (download)** — No size/length safety cap: a wrong long video can be saved as a huge - file (observed a 52 MB mp3 ≈ 28 min vs the correct 4 MB ~3 min). Needs a `--max-filesize` / - duration cap as defense-in-depth. -- **Backend (metadata/sorting)** — iTunes `collectionArtistName` (album artist) is not captured, - so `album_artist` can be wrong on compilations / various-artists releases, splitting albums and - mis-sorting them in Navidrome. -- **Metadata editor (autofill)** — Both autofill buttons fill only a partial subset of the form: - iTunes sets album/genre/year/track/total/cover (no title, artist, album-artist, disc); - MusicBrainz sets title/artist/album/genre/year/track/total (no album-artist, disc). They should - fill **every** field they can resolve (title, artist, album-artist, album, genre, year, track #, - disc #, total tracks, cover). This needs both the backend endpoints to return those fields - (e.g. iTunes `collectionArtistName`, `discNumber`, `trackName`) and the frontend to populate the - inputs. Artists are stored `;`-separated (feat./ft./&/comma normalized to `;` at download, so the - field never contains feat./ft.) and both lookups already search with the first artist — but - multi-artist tracks still mostly fail because the match is strict and gives up when iTunes/MB - credit a different/combined primary artist; needs a fallback (title-only, or try the other listed - artists). -- **Backend (download)** — Re-downloading a playlist re-fetches some already-present tracks: the - duplicate check matches the exact computed path only (same album folder, exact track-number - prefix, exact resolved artist/title). Any drift between runs — different iTunes/romaji name, a - different track number, a different album folder, or a `- Piano Version`/`(var)` variant suffix - — makes the file look new. The check is also case-sensitive (filename-existence on a - case-sensitive Linux FS), so `DJ OKAWARI` vs `DJ Okawari` misses too. Should match by - normalized, case-insensitive first-artist + title across the artist's folders, not the exact path. -- **Backend (download)** — The no-duration branch (tracks where iTunes returns no duration) has - **no Mittelweg protection**: it still falls through to title-only / no-filter grabs and pulls - wrong covers by other uploaders (observed: `Yorushika – 車窓` saved as `Piano Echoes – Shasou`). - The channel-required relaxation should apply to the no-duration branch too. +- **Backend (download)** — The duplicate check requires at least a `firstArtist` folder match. If the same song was previously stored under a very different artist name (e.g. before an iTunes-name fix), it will not be detected as a duplicate and will be re-downloaded. +- **Backend (metadata/sorting)** — MusicBrainz `release['artist-credit']` is often absent in the standard recording search response (requires `inc=artist-credits` which can slow the endpoint). The albumArtist field from MusicBrainz autofill may fall back to the recording artist rather than the release artist. +- **Backend (metadata/sorting)** — MBIDs are not embedded in downloaded tracks; Navidrome cannot use them for authoritative album grouping (low priority — album splitting is mostly solved via consistent `album_artist`). diff --git a/CLAUDE.md b/CLAUDE.md index 2fcf05a..978d911 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,16 +43,22 @@ Backend runs in Docker (see `docker-compose.yml`); on the deploy host it lives a - Downloads are **yt-dlp only** (the old squid.wtf/Qobuz backend was removed). - **File naming:** `{NN - }Artist - Title.ext` with the release suffix stripped from the - title (no `- Single`/`- EP`). The **album folder keeps the full album name**. The cleaned - title (`cleanTitle`/`searchTitle`) drives both the filename and the embedded title tag. + title (no `- Single`/`- EP`). The **album folder keeps the full album name**. Original names + are used for folder, filename, and all main tags (artist/title/album/album\_artist) — even + for Japanese tracks. Romaji goes **only** into sort tags (TITLESORT / ARTISTSORT / etc.). - **Search cascade** (`ytdlp.js`): YTM search first (`web_music` player client, Songs filter param), then regular YouTube; each candidate filtered by duration (±10%, min 10s), title keyword, artist channel, and variant exclusions (remix/cover/sped/…). -- **"Mittelweg" rule:** when the duration is known but nothing matches the duration window, +- **"Mittelweg" rule** (with AND without duration): when nothing matches in the filtered steps, relax only the duration constraint while **still requiring the artist channel**. If nothing matches, download **nothing** and fail cleanly with "Nicht eindeutig gefunden — bitte - Direktlink im Importer nutzen". Never fall back to a blind first-result grab (it pulled - same-titled videos by other uploaders). + Direktlink im Importer nutzen". Never fall back to a blind title-only/no-filter grab — it + pulled same-titled covers by other uploaders, and wrong Piano Echoes for Japanese tracks. +- **Size cap:** `--max-filesize 30M` on every yt-dlp call; Mittelweg steps also cap at 2× + expected duration. Prevents saving a 28-min video (52 MB) when the wrong result slips through. +- **Duplicate detection** (`metadata.js findExistingTrack`): searches case-insensitively across + all album folders under the matching artist folder; compares normalized artist+title ignoring + track prefix and release/variant suffix. Handles DJ OKAWARI vs DJ Okawari and "- Single" drift. - **Known limitation:** yt-dlp's YTM/YouTube *search* can't surface very obscure Art Tracks by ambiguously-named artists (e.g. "MINA") — the browser finds them, yt-dlp's API extraction doesn't (confirmed across params/clients/deno). For those, the **direct link in the importer** @@ -60,6 +66,9 @@ Backend runs in Docker (see `docker-compose.yml`); on the deploy host it lives a - **File resolution for edit/delete:** Navidrome's stored path may use a different mount root or a stale stem. `metadata.js` rebases the path and falls back to recursive/stem matching (tolerant of release-suffix mismatch). A ghost Navidrome entry needs a full library rescan. +- **Album splitting:** prevented by consistent `album_artist` tag. All tracks in a download + share the same `mainArtist` value (= iTunes `collectionArtistName` when available, else + `albumArtist` param, else track artist). Never use `rawArtist` for `album_artist`. ## UI / references diff --git a/backend/lib/metadata.js b/backend/lib/metadata.js index 8d0a906..4965b28 100644 --- a/backend/lib/metadata.js +++ b/backend/lib/metadata.js @@ -48,13 +48,50 @@ async function downloadArtwork(url, dir) { const AUDIO_EXTS = ['mp3', 'flac', 'opus', 'm4a', 'ogg', 'wav']; -function findExistingTrack(dir, trackPrefix, safeTitle, safeArtist) { - // Check new format "{prefix}{artist} - {title}.ext" first, then old format "{prefix}{title}.ext" - const names = safeArtist - ? [`${trackPrefix}${safeArtist} - ${safeTitle}`, `${trackPrefix}${safeTitle}`] - : [`${trackPrefix}${safeTitle}`]; - return AUDIO_EXTS.flatMap(ext => names.map(n => path.join(dir, `${n}.${ext}`))) - .find(f => fs.existsSync(f)) || null; +// Strips release-type suffixes like "- Single", "- EP", "2024" from end of title/stem. +const _RELEASE_SUFFIX_RE = /\s*[-–]\s*(?:(?:official\s+)?(?:music\s+)?(?:single|ep|lp|album|ost|soundtrack|remaster(?:ed)?|deluxe(?:\s+edition)?|bonus\s+tracks?|b-?sides?|version|edit)\b.*|(?:20|19)\d{2})\s*$/i; + +// Robust duplicate detection: search case-insensitively across all album folders +// under the artist folder matching firstArtist, compare normalized title ignoring +// release suffixes and track prefixes. Handles DJ OKAWARI vs DJ Okawari, and +// "recognize - Single.mp3" vs "recognize.mp3" (drift between runs). +function findExistingTrack(musicRoot, firstArtist, cleanTitle) { + const norm = s => (s || '').toLowerCase() + .replace(/[\[((][^\]))]*[\]))]/g, '') + .replace(/[,.'!?]/g, '') + .replace(/\s+/g, ' ') + .trim(); + const normArtist = norm(firstArtist); + const normTitle = norm(cleanTitle); + // Accept both the raw cleanTitle and version without release suffix (e.g. "recognize" matches "recognize - Single") + const titleVariants = new Set([normTitle, norm(cleanTitle.replace(_RELEASE_SUFFIX_RE, ''))]); + + try { + for (const ad of fs.readdirSync(musicRoot, { withFileTypes: true })) { + if (!ad.isDirectory() || norm(ad.name) !== normArtist) continue; + const artistPath = path.join(musicRoot, ad.name); + try { + for (const abd of fs.readdirSync(artistPath, { withFileTypes: true })) { + if (!abd.isDirectory()) continue; + const albumPath = path.join(artistPath, abd.name); + try { + for (const f of fs.readdirSync(albumPath)) { + const ext = path.extname(f).slice(1).toLowerCase(); + if (!AUDIO_EXTS.includes(ext)) continue; + // Strip extension + leading track prefix ("01 - ", "01-15 – " etc.) + const stem = f.slice(0, -(ext.length + 1)).replace(/^\d[\d-]*\s*[-–—]\s*/, ''); + // Stem may be "Artist - Title" or just "Title" + const di = stem.indexOf(' - '); + const rawTitle = di >= 0 ? stem.slice(di + 3) : stem; + const normStemTitle = norm(rawTitle.replace(_RELEASE_SUFFIX_RE, '')); + if (titleVariants.has(normStemTitle)) return path.join(albumPath, f); + } + } catch {} + } + } catch {} + } + } catch {} + return null; } // Fetch plain + synced lyrics from LRCLIB @@ -140,7 +177,9 @@ async function lookupItunesTrackMeta(artist, title) { const rawGenre = (usMatch || match).primaryGenreName; const genre = rawGenre ? toEnglishGenre(rawGenre) : undefined; return { - album: match.collectionName || '', + album: match.collectionName || '', + albumArtist: match.collectionArtistName || undefined, + trackName: match.trackName || undefined, year: match.releaseDate ? new Date(match.releaseDate).getFullYear() : undefined, trackNum: match.trackNumber || undefined, totalTracks: match.trackCount || undefined, diff --git a/backend/lib/ytdlp.js b/backend/lib/ytdlp.js index 78fa23b..47a748f 100644 --- a/backend/lib/ytdlp.js +++ b/backend/lib/ytdlp.js @@ -84,25 +84,20 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb const RELEASE_SUFFIX_RE = /\s*[-–]\s*(?:(?:official\s+)?(?:music\s+)?(?:single|ep|lp|album|ost|soundtrack|remaster(?:ed)?|deluxe(?:\s+edition)?|bonus\s+tracks?|b-?sides?|version|edit)\b.*|(?:20|19)\d{2})\s*$/i; const cleanTitle = title.replace(RELEASE_SUFFIX_RE, '').trim() || title; - // Folder / filename: romaji of the resolved artist (iTunes name if available — already in English, - // otherwise kuromoji). rawArtist (original Japanese) is only used for tags + YTM search. - const mainArtistRomaji = romajiForStorage(mainArtist) || mainArtist; - const artistRomaji = romajiForStorage(artist) || artist; - const titleRomaji = romajiForStorage(cleanTitle) || cleanTitle; - const albumRomaji = album ? (romajiForStorage(album) || album) : null; - const safeArtist = sanitize(mainArtistRomaji.split(/[,;&]/)[0].trim()); - const safeAlbum = sanitize(albumRomaji || 'Singles'); - const dir = path.join(outputDir || CONFIG.musicPath, safeArtist, safeAlbum); + // Folder / filename: use original names (Japanese or whatever was passed in). + // Romaji goes ONLY into sort tags (TITLESORT, ARTISTSORT, etc.) via embedMetadata. + const safeArtist = sanitize((mainArtist || '').split(/[,;&]/)[0].trim()); + const safeAlbum = sanitize(album || 'Singles'); + const dir = path.join(outputDir || CONFIG.musicPath, safeArtist, safeAlbum); fs.mkdirSync(dir, { recursive: true }); - const safeTitle = sanitize(titleRomaji); - const trackPrefix = trackNum ? String(trackNum).padStart(2, '0') + ' - ' : ''; + const safeTitle = sanitize(cleanTitle); + const trackPrefix = trackNum ? String(trackNum).padStart(2, '0') + ' - ' : ''; // Filename format: "{01 - }Artist1, Artist2 - Title.ext" - // Use track artist (not albumArtist) so all credited artists appear in the filename - const safeArtistForFile = sanitize(artistRomaji.replace(/;/g, ',')); - const fileBase = `${trackPrefix}${safeArtistForFile} - ${safeTitle}`; + const safeArtistForFile = sanitize((artist || '').replace(/;/g, ',')); + const fileBase = `${trackPrefix}${safeArtistForFile} - ${safeTitle}`; - const existing = findExistingTrack(dir, trackPrefix, safeTitle, safeArtistForFile); + const existing = findExistingTrack(outputDir || CONFIG.musicPath, safeArtist, cleanTitle); if (existing) { log(jobId, `⏭ Bereits vorhanden: ${path.basename(existing)} — übersprungen`); return { success: true, dir, skipped: true }; @@ -161,6 +156,8 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb '--no-playlist', '--no-warnings', '--newline', + // Hard upper size limit — prevents saving a wrong 28-min video as a ~52 MB mp3 + '--max-filesize', '30M', // Use the web_music player client (falling back to default) so YouTube Music // "Art Track" / topic uploads are reachable. The android_vr client returns // "This video is not available" for many YTM tracks that play fine in the browser. @@ -254,7 +251,15 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb }); const mf = (dur, variant, strict = false) => { - const durPart = dur ? `duration > ${dur.min} & duration < ${dur.max} & ` : ''; + let durPart; + if (dur) { + durPart = `duration > ${dur.min} & duration < ${dur.max} & `; + } else if (durationMs) { + // Even when relaxing the window (Mittelweg), keep a 2× cap so a 28-min video can't slip through + durPart = `duration < ${Math.round(durationMs / 1000 * 2)} & `; + } else { + durPart = ''; + } return ['--match-filter', `${durPart}!is_live${titleKwPart}${strict ? artistChannelPart : ''}${variant ? variantPart : ''}`]; }; @@ -325,24 +330,9 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb // 4. YT top-5 — no channel check await spawnYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true), ...baseArgs]); } - if (!hasAudio()) { - // 5. YTM all — title only - log(jobId, '⚠️ Letzter gefilterter Versuch…'); - await tryYtm(0, mf(null, false)); - } - if (!hasAudio()) { - // 6. YT top-5 — title only - await spawnYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, false), ...baseArgs]); - } - if (!hasAudio()) { - // 7. YTM — first video result, no filter - log(jobId, '⚠️ Letzter Versuch: keine Filter…'); - await tryYtm(3, []); - } - if (!hasAudio()) { - // 8. YT — no filter - await spawnYtdlp([`ytsearch1:${query}`, ...PI_UNFILTERED, ...baseArgs]); - } + // Mittelweg (no-duration): after step 4, do NOT fall through to title-only or + // no-filter grabs — those pull wrong covers (observed: Yorushika → Piano Echoes). + // Fail cleanly, same as the with-duration branch. Use the Direktlink instead. } const audioFile = hasAudio() ? expectedAudio : null; @@ -370,9 +360,10 @@ async function embedMetadata({ jobId, audioFile, dir, trackPrefix, safeTitle, ra if (hasCover) ffArgs.push('-map', '1:v'); // Main tags: store original text (Japanese or whatever was passed in). // rawArtist = original Spotify/source artist (may be Japanese); artist = iTunes-resolved (may be English). - // Use rawArtist for the stored tag so Apple Music / other apps see the authentic original. + // Use rawArtist for the per-track artist tag; mainArtist for album_artist so all tracks + // in an album get the same value (prevents Navidrome album-splitting). const tagArtist = navidromeArtist(rawArtist || artist); - const tagAlbumArtist = (rawArtist || mainArtist).replace(/\s+&\s+/g, ', '); + const tagAlbumArtist = mainArtist.replace(/\s+&\s+/g, ', '); // Sort tags: romaji for display / sorting in apps that support them (TITLESORT, ARTISTSORT, etc.) // artist is already the iTunes-resolved English name when available — use directly (more accurate // than kuromoji). For non-iTunes tracks romajiForStorage acts as kuromoji fallback. diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 62b74fe..881ed78 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -292,13 +292,16 @@ app.get('/api/itunes/lookup', async (req, res, next) => { if (!meta.album && !meta.genre && !meta.year) return res.json({ found: false }); res.json({ found: true, - album: meta.album || undefined, - year: meta.year || undefined, - trackNum: meta.trackNum || undefined, - totalTracks: meta.totalTracks || undefined, - genre: meta.genre || undefined, - coverUrl: meta.artworkUrl || undefined, + title: meta.trackName || undefined, artist: meta.itunesArtistName || undefined, + albumArtist: meta.albumArtist || undefined, + album: meta.album || undefined, + year: meta.year || undefined, + trackNum: meta.trackNum || undefined, + totalTracks: meta.totalTracks || undefined, + discNum: meta.discNum || undefined, + genre: meta.genre || undefined, + coverUrl: meta.artworkUrl || undefined, }); } catch (e) { console.error('[itunes/lookup]', e.message); @@ -994,15 +997,20 @@ app.get('/api/musicbrainz/lookup', async (req, res) => { const trackNum = release?.media?.[0]?.['track-offset'] != null ? (release.media[0]['track-offset'] + 1) : undefined; const totalTracks = release?.media?.[0]?.['track-count'] || undefined; + const discNum = release?.media?.[0]?.position || undefined; const coverUrl = release?.id ? `https://coverartarchive.org/release/${release.id}/front` : null; + // album-artist from release artist-credit if present, else fall back to recording artist + const mbAlbumArtist = (release?.['artist-credit'] || []).map(c => c.name || c.artist?.name).filter(Boolean).join('; ') || mbArtist; res.json({ found: true, title: rec.title || title, - artist: mbArtist || artist, + artist: mbArtist || artist, + albumArtist: mbAlbumArtist || undefined, album: release?.title || '', year: year ? parseInt(year, 10) : undefined, trackNum: trackNum, totalTracks: totalTracks, + discNum: discNum, genre: genre, coverUrl: coverUrl, mbid: rec.id, @@ -1253,9 +1261,10 @@ app.post('/api/edit-metadata', async (req, res) => { let currentFile = filePath; // 1. Rename file if title changed — convention: {01 - }?{Artist} - {Title}.ext + // Use original names (may be Japanese); romaji goes only into sort tags. if (title) { - const safeTitle = sanitize(romajiTitle || title); - const fileArtist = (romajiArtist || ndSong?.artist || '').replace(/;/g, ','); + const safeTitle = sanitize(title); + const fileArtist = (normalArtist || ndSong?.artist || '').replace(/;/g, ','); const safeArtistPart = fileArtist ? sanitize(fileArtist) + ' - ' : ''; const prefix = trackNum ? String(trackNum).padStart(2, '0') + ' - ' : ''; const newName = `${prefix}${safeArtistPart}${safeTitle}${fileExt}`; @@ -1269,8 +1278,8 @@ app.post('/api/edit-metadata', async (req, res) => { // 2. Move to new artist/album folder when albumArtist or album changed const oldAlbumDir = path.dirname(currentFile); const oldArtistDir = path.dirname(oldAlbumDir); - const newAlbumName = romajiAlbum ? sanitize(romajiAlbum) : path.basename(oldAlbumDir); - const newArtistName = (albumArtistSort || romajiAlbumArtist) ? sanitize((albumArtistSort || romajiAlbumArtist).split(/[,;&]/)[0].trim()) : path.basename(oldArtistDir); + const newAlbumName = album ? sanitize(album) : path.basename(oldAlbumDir); + const newArtistName = normalAlbumArtist ? sanitize(normalAlbumArtist.split(/[,;&]/)[0].trim()) : path.basename(oldArtistDir); const newDir = path.join(CONFIG.musicPath, newArtistName, newAlbumName); if (newDir !== oldAlbumDir) { @@ -1291,9 +1300,9 @@ app.post('/api/edit-metadata', async (req, res) => { } catch {} } } catch (e) { console.error('[edit-metadata] move:', e.message); } - } else if (romajiAlbum && path.basename(oldAlbumDir) !== sanitize(romajiAlbum)) { + } else if (album && path.basename(oldAlbumDir) !== sanitize(album)) { // Only album name changed (no artist move) — rename album folder in place - const renamedDir = path.join(oldArtistDir, sanitize(romajiAlbum)); + const renamedDir = path.join(oldArtistDir, sanitize(album)); if (!fs.existsSync(renamedDir)) { try { fs.renameSync(oldAlbumDir, renamedDir); } catch (e) { console.error('[edit-metadata] folder rename:', e.message); } diff --git a/src/ui/metadata.js b/src/ui/metadata.js index 8372a52..a4f3d31 100644 --- a/src/ui/metadata.js +++ b/src/ui/metadata.js @@ -179,11 +179,13 @@ export async function autofillMusicBrainz() { if (!d.found) { status.textContent = '❌ Kein Treffer auf MusicBrainz'; return; } if (d.title) document.getElementById('ie-title').value = d.title; if (d.artist) document.getElementById('ie-artist').value = d.artist; + if (d.albumArtist) document.getElementById('ie-albumartist').value = d.albumArtist; if (d.album) document.getElementById('ie-album').value = d.album; if (d.genre) document.getElementById('ie-genre').value = d.genre; if (d.year) document.getElementById('ie-year').value = d.year; if (d.trackNum) document.getElementById('ie-track').value = d.trackNum; if (d.totalTracks) document.getElementById('ie-totaltracks').value = d.totalTracks; + if (d.discNum) document.getElementById('ie-disc').value = d.discNum; if (d.coverUrl) { status.textContent = '✅ Metadaten gefunden — Cover wird geladen…'; S._infoArtData = { url: d.coverUrl }; @@ -210,11 +212,15 @@ export async function autofillItunes() { const r = await fetch(`${S.cfg.backendUrl}/api/itunes/lookup?artist=${encodeURIComponent(artist)}&title=${encodeURIComponent(title)}`); const d = await r.json(); if (!d.found) { status.textContent = '❌ Kein Treffer auf iTunes'; return; } - if (d.album) document.getElementById('ie-album').value = d.album; - if (d.genre) document.getElementById('ie-genre').value = d.genre; - if (d.year) document.getElementById('ie-year').value = d.year; - if (d.trackNum) document.getElementById('ie-track').value = d.trackNum; + if (d.title) document.getElementById('ie-title').value = d.title; + if (d.artist) document.getElementById('ie-artist').value = d.artist; + if (d.albumArtist) document.getElementById('ie-albumartist').value = d.albumArtist; + if (d.album) document.getElementById('ie-album').value = d.album; + if (d.genre) document.getElementById('ie-genre').value = d.genre; + if (d.year) document.getElementById('ie-year').value = d.year; + if (d.trackNum) document.getElementById('ie-track').value = d.trackNum; if (d.totalTracks) document.getElementById('ie-totaltracks').value = d.totalTracks; + if (d.discNum) document.getElementById('ie-disc').value = d.discNum; if (d.coverUrl) { S._infoArtData = { url: d.coverUrl }; document.getElementById('info-edit-art-preview').innerHTML =