feat: keep Japanese in main tags, romaji in sort tags + frontend toggle
- Downloads now store original Japanese in TITLE/ARTIST/ALBUM tags so Apple Music, iTunes, YTM etc. all see the authentic original text - Sort tags (TITLESORT/ARTISTSORT/ALBUMSORT) written with romaji for apps that use them for sorting/display - iTunes-resolved English artist name used for ARTISTSORT and folder names when available (more accurate than kuromoji for known artists) - rawArtist parameter added to downloadTrack so YTM search query uses the original Japanese channel name (fixes wrong song downloads) - spotify.js: artistName preserved as original; itunesArtistName stored separately so the Spotify importer shows Japanese titles - romaji index now includes titleRomaji/artistRomaji/albumRomaji fields - Frontend: showRomaji setting (Settings > Bibliothek) — when on, player bar, now-playing, queue, and song lists all show romaji via index lookup - refreshDisplayMode() updates all live UI when the toggle is saved Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
f1eabf7099
commit
1aff19e319
10 files changed
+140
-55
No files matched your search
+12
-9
@@ -130,15 +130,18 @@ async function buildRomajiIndex(ndUrl, user, pass) {
|
||||
const index = songs
|
||||
.filter(s => JP_RE.test(s.title) || JP_RE.test(s.artist) || JP_RE.test(s.album))
|
||||
.map(s => ({
|
||||
id: s.id,
|
||||
r: [toRomaji(s.title), toRomaji(s.artist), toRomaji(s.album)].join(' '),
|
||||
title: s.title,
|
||||
artist: s.artist || '',
|
||||
album: s.album || '',
|
||||
coverArt: s.coverArt || '',
|
||||
duration: s.duration || 0,
|
||||
albumId: s.albumId || '',
|
||||
artistId: s.artistId || '',
|
||||
id: s.id,
|
||||
r: [toRomaji(s.title), toRomaji(s.artist), toRomaji(s.album)].join(' '),
|
||||
title: s.title,
|
||||
artist: s.artist || '',
|
||||
album: s.album || '',
|
||||
titleRomaji: romajiForStorage(s.title) || s.title,
|
||||
artistRomaji: romajiForStorage(s.artist) || s.artist || '',
|
||||
albumRomaji: romajiForStorage(s.album) || s.album || '',
|
||||
coverArt: s.coverArt || '',
|
||||
duration: s.duration || 0,
|
||||
albumId: s.albumId || '',
|
||||
artistId: s.artistId || '',
|
||||
}));
|
||||
romajiCache.set(key, { ts: Date.now(), index });
|
||||
return { fromCache: false, index };
|
||||
|
||||
+37
-20
@@ -48,8 +48,11 @@ function buildVariantFilter(title) {
|
||||
return parts.length ? parts.join(' & ') : '';
|
||||
}
|
||||
|
||||
async function downloadTrack({ jobId, artist, albumArtist, title, album, year, trackNum, totalTracks, discNum, discTotal, compilation, genre, artworkUrl, explicit, outputDir, durationMs, sourceUrl }) {
|
||||
async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, album, year, trackNum, totalTracks, discNum, discTotal, compilation, genre, artworkUrl, explicit, outputDir, durationMs, sourceUrl }) {
|
||||
const mainArtist = albumArtist || artist;
|
||||
|
||||
// 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(title) || title;
|
||||
@@ -73,11 +76,12 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
||||
}
|
||||
|
||||
const outputTemplate = path.join(dir, `${fileBase}.%(ext)s`);
|
||||
// Strip only Unicode symbol chars (☆ ★ ♥ etc.) for ytsearch — YouTube search chokes on them
|
||||
// and returns 0 results. Keep ASCII punctuation (!&-) so e.g. "N!GHT" stays "N!GHT" rather
|
||||
// than collapsing to "NGHT" which YouTube misreads as the word "night".
|
||||
const searchArtist = mainArtist.replace(/\p{S}/gu, '').trim();
|
||||
const query = `${searchArtist} - ${title}`;
|
||||
// For YTM search: use the original (raw) artist when available — the YTM channel is registered
|
||||
// under the Japanese/original name, not the iTunes-resolved English name.
|
||||
// rawArtist is undefined for non-Japanese tracks, so fall back to mainArtist.
|
||||
const ytmArtist = (rawArtist || mainArtist).replace(/\p{S}/gu, '').trim();
|
||||
const searchArtist = ytmArtist;
|
||||
const query = `${ytmArtist} - ${title}`;
|
||||
|
||||
const baseArgs = [
|
||||
'--extract-audio',
|
||||
@@ -112,8 +116,8 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
||||
});
|
||||
|
||||
const YTM_SONGS_PARAM = 'EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D';
|
||||
// Use original artist name (with special chars) for YTM URL — better search ranking than stripped version
|
||||
const ytmQuery = `${mainArtist} - ${title}`;
|
||||
// Use original (raw) artist for YTM URL — better ranking since channel is in original language
|
||||
const ytmQuery = `${rawArtist || mainArtist} - ${title}`;
|
||||
const ytmUrl = `https://music.youtube.com/search?q=${encodeURIComponent(ytmQuery)}¶ms=${YTM_SONGS_PARAM}`;
|
||||
const expectedAudio = path.join(dir, `${fileBase}.${CONFIG.audioFmt}`);
|
||||
const hasAudio = () => fs.existsSync(expectedAudio);
|
||||
@@ -124,7 +128,7 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
||||
await spawnYtdlp([sourceUrl, '--no-playlist', ...baseArgs]);
|
||||
const audioFile = hasAudio() ? expectedAudio : null;
|
||||
if (!audioFile) { log(jobId, '⚠️ Download fehlgeschlagen'); return { success: true, dir }; }
|
||||
return embedMetadata({ jobId, audioFile, dir, trackPrefix, safeTitle, artist, mainArtist, album, year, trackNum, totalTracks, discNum, discTotal, compilation, genre, artworkUrl, explicit, title });
|
||||
return embedMetadata({ jobId, audioFile, dir, trackPrefix, safeTitle, rawArtist, artist, mainArtist, album, year, trackNum, totalTracks, discNum, discTotal, compilation, genre, artworkUrl, explicit, title });
|
||||
}
|
||||
|
||||
// Fetch YTM search result URLs as a flat list (no channel expansion) and return only
|
||||
@@ -172,7 +176,7 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
||||
// downloading a same-title song by a different artist (e.g. wrong "Moonlight" for N!GHT).
|
||||
// Dropped in later fallback steps so genuine mismatches (label channels etc.) still get found.
|
||||
const escRe = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const artistFirstWord = mainArtist.split(/[\s,/]+/)[0];
|
||||
const artistFirstWord = (rawArtist || mainArtist).split(/[\s,/]+/)[0];
|
||||
const artistChannelPart = artistFirstWord.length >= 3
|
||||
? ` & channel ~= (?i)${escRe(artistFirstWord)}` : '';
|
||||
|
||||
@@ -287,11 +291,11 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
||||
return { success: true, dir };
|
||||
}
|
||||
|
||||
return embedMetadata({ jobId, audioFile, dir, trackPrefix, safeTitle, artist, mainArtist, album, year, trackNum, totalTracks, discNum, discTotal, compilation, genre, artworkUrl, explicit, title });
|
||||
return embedMetadata({ jobId, audioFile, dir, trackPrefix, safeTitle, rawArtist, artist, mainArtist, album, year, trackNum, totalTracks, discNum, discTotal, compilation, genre, artworkUrl, explicit, title });
|
||||
}
|
||||
|
||||
// Shared ffmpeg metadata embedding — used by both yt-dlp and squid paths
|
||||
async function embedMetadata({ jobId, audioFile, dir, trackPrefix, safeTitle, artist, mainArtist, album, year, trackNum, totalTracks, discNum, discTotal, compilation, genre, artworkUrl, explicit, title }) {
|
||||
async function embedMetadata({ jobId, audioFile, dir, trackPrefix, safeTitle, rawArtist, artist, mainArtist, album, year, trackNum, totalTracks, discNum, discTotal, compilation, genre, artworkUrl, explicit, title }) {
|
||||
const coverPath = await downloadArtwork(artworkUrl, dir);
|
||||
const hasCover = !!(coverPath && fs.existsSync(coverPath));
|
||||
|
||||
@@ -304,19 +308,32 @@ 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, ', ');
|
||||
const storedAlbum = album ? (romajiForStorage(album) || album) : null;
|
||||
// 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.
|
||||
const tagArtist = navidromeArtist(rawArtist || artist);
|
||||
const tagAlbumArtist = (rawArtist || 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.
|
||||
const sortTitle = romajiForStorage(title) || title;
|
||||
const sortArtist = romajiForStorage(navidromeArtist(artist)) || navidromeArtist(artist);
|
||||
const sortAlbumArtist = romajiForStorage(mainArtist.replace(/\s+&\s+/g, ', ')) || mainArtist.replace(/\s+&\s+/g, ', ');
|
||||
const sortAlbum = album ? (romajiForStorage(album) || album) : null;
|
||||
ffArgs.push(
|
||||
'-c', 'copy',
|
||||
'-map_metadata', '-1',
|
||||
'-id3v2_version', '3',
|
||||
'-metadata', `title=${storedTitle}`,
|
||||
'-metadata', `artist=${storedArtist}`,
|
||||
'-metadata', `album_artist=${storedAlbumArtist}`,
|
||||
'-metadata', `title=${title}`,
|
||||
'-metadata', `artist=${tagArtist}`,
|
||||
'-metadata', `album_artist=${tagAlbumArtist}`,
|
||||
);
|
||||
if (storedAlbum) ffArgs.push('-metadata', `album=${storedAlbum}`);
|
||||
if (album) ffArgs.push('-metadata', `album=${album}`);
|
||||
// Sort tags — only write when they differ from main (no-op for non-Japanese tracks)
|
||||
if (sortTitle !== title) ffArgs.push('-metadata', `TITLESORT=${sortTitle}`);
|
||||
if (sortArtist !== tagArtist) ffArgs.push('-metadata', `ARTISTSORT=${sortArtist}`);
|
||||
if (sortAlbumArtist !== tagAlbumArtist) ffArgs.push('-metadata', `ALBUMARTISTSORT=${sortAlbumArtist}`);
|
||||
if (sortAlbum && sortAlbum !== album) ffArgs.push('-metadata', `ALBUMSORT=${sortAlbum}`);
|
||||
if (year) ffArgs.push('-metadata', `date=${String(year)}`);
|
||||
if (trackNum) ffArgs.push('-metadata', `track=${totalTracks ? `${trackNum}/${totalTracks}` : String(trackNum)}`);
|
||||
if (totalTracks) ffArgs.push('-metadata', `totaltracks=${String(totalTracks)}`);
|
||||
|
||||
Reference in new issue
Block a user