feat(0.9): romaji sort-tags only, robust dupe detection, Mittelweg no-duration, autofill all fields

- 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 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-21 13:26:11 +02:00
1 parent 8be3a515e6
commit fa4c954ab9
6 files changed
+148 -98

No files matched your search

+47 -8
View File
@@ -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,