feat: normalize Japanese artist/title names to romaji at storage time

- Add romajiForStorage() to romaji.js — smart word spacing + Title Case
- ytdlp.js: apply to folder names, filenames, and embedded tags
- novasonic-server.js: apply in edit-metadata endpoint and Spotify resolve
- spotify.js: update track artist/name from iTunes canonical English names

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-24 20:22:22 +02:00
1 parent c03463b66b
commit 446c583f7f
4 files changed
+57 -15

No files matched your search

+13 -6
View File
@@ -7,6 +7,7 @@ const {
sanitize, navidromeArtist,
downloadArtwork, findExistingTrack,
} = require('./metadata');
const { romajiForStorage } = require('./romaji');
function buildTitleKeywords(title) {
const VARIANT_WORDS = new Set(['sped','speedup','slowed','reverb','nightcore','karaoke','parody','remix','cover','acoustic','instrumental']);
@@ -49,16 +50,19 @@ function buildVariantFilter(title) {
async function downloadTrack({ jobId, artist, albumArtist, title, album, year, trackNum, totalTracks, discNum, discTotal, compilation, genre, artworkUrl, explicit, outputDir, durationMs, sourceUrl }) {
const mainArtist = albumArtist || artist;
const safeArtist = sanitize(mainArtist.split(/[,;&]/)[0].trim());
const mainArtistRomaji = romajiForStorage(mainArtist) || mainArtist;
const artistRomaji = romajiForStorage(artist) || artist;
const titleRomaji = romajiForStorage(title) || title;
const safeArtist = sanitize(mainArtistRomaji.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(title);
const safeTitle = sanitize(titleRomaji);
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(artist.replace(/;/g, ','));
const safeArtistForFile = sanitize(artistRomaji.replace(/;/g, ','));
const fileBase = `${trackPrefix}${safeArtistForFile} - ${safeTitle}`;
const existing = findExistingTrack(dir, trackPrefix, safeTitle, safeArtistForFile);
@@ -299,13 +303,16 @@ async function embedMetadata({ jobId, audioFile, dir, trackPrefix, safeTitle, ar
if (hasCover) ffArgs.push('-i', coverPath);
ffArgs.push('-map', '0:a');
if (hasCover) ffArgs.push('-map', '1:v');
const storedTitle = romajiForStorage(title) || title;
const storedArtist = romajiForStorage(navidromeArtist(artist)) || navidromeArtist(artist);
const storedAlbumArtist = romajiForStorage(mainArtist.replace(/\s+&\s+/g, ', ')) || mainArtist.replace(/\s+&\s+/g, ', ');
ffArgs.push(
'-c', 'copy',
'-map_metadata', '-1',
'-id3v2_version', '3',
'-metadata', `title=${title}`,
'-metadata', `artist=${navidromeArtist(artist)}`,
'-metadata', `album_artist=${mainArtist.replace(/\s+&\s+/g, ', ')}`,
'-metadata', `title=${storedTitle}`,
'-metadata', `artist=${storedArtist}`,
'-metadata', `album_artist=${storedAlbumArtist}`,
);
if (album) ffArgs.push('-metadata', `album=${album}`);
if (year) ffArgs.push('-metadata', `date=${String(year)}`);