fix: romaji for album names + Japanese playlist track matching
- ytdlp.js + novasonic-server.js: apply romajiForStorage to album tag and folder/rename paths (was only applied to artist/title before) - ui.js: _createNdPlaylist falls back to romaji index for Japanese titles since Navidrome search3 doesn't index Japanese text properly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
88533b8835
commit
392da7e021
3 files changed
+23
-9
No files matched your search
@@ -53,8 +53,9 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
|||||||
const mainArtistRomaji = romajiForStorage(mainArtist) || mainArtist;
|
const mainArtistRomaji = romajiForStorage(mainArtist) || mainArtist;
|
||||||
const artistRomaji = romajiForStorage(artist) || artist;
|
const artistRomaji = romajiForStorage(artist) || artist;
|
||||||
const titleRomaji = romajiForStorage(title) || title;
|
const titleRomaji = romajiForStorage(title) || title;
|
||||||
|
const albumRomaji = album ? (romajiForStorage(album) || album) : null;
|
||||||
const safeArtist = sanitize(mainArtistRomaji.split(/[,;&]/)[0].trim());
|
const safeArtist = sanitize(mainArtistRomaji.split(/[,;&]/)[0].trim());
|
||||||
const safeAlbum = sanitize(album || 'Singles');
|
const safeAlbum = sanitize(albumRomaji || 'Singles');
|
||||||
const dir = path.join(outputDir || CONFIG.musicPath, safeArtist, safeAlbum);
|
const dir = path.join(outputDir || CONFIG.musicPath, safeArtist, safeAlbum);
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
|
||||||
@@ -306,6 +307,7 @@ async function embedMetadata({ jobId, audioFile, dir, trackPrefix, safeTitle, ar
|
|||||||
const storedTitle = romajiForStorage(title) || title;
|
const storedTitle = romajiForStorage(title) || title;
|
||||||
const storedArtist = romajiForStorage(navidromeArtist(artist)) || navidromeArtist(artist);
|
const storedArtist = romajiForStorage(navidromeArtist(artist)) || navidromeArtist(artist);
|
||||||
const storedAlbumArtist = romajiForStorage(mainArtist.replace(/\s+&\s+/g, ', ')) || mainArtist.replace(/\s+&\s+/g, ', ');
|
const storedAlbumArtist = romajiForStorage(mainArtist.replace(/\s+&\s+/g, ', ')) || mainArtist.replace(/\s+&\s+/g, ', ');
|
||||||
|
const storedAlbum = album ? (romajiForStorage(album) || album) : null;
|
||||||
ffArgs.push(
|
ffArgs.push(
|
||||||
'-c', 'copy',
|
'-c', 'copy',
|
||||||
'-map_metadata', '-1',
|
'-map_metadata', '-1',
|
||||||
@@ -314,7 +316,7 @@ async function embedMetadata({ jobId, audioFile, dir, trackPrefix, safeTitle, ar
|
|||||||
'-metadata', `artist=${storedArtist}`,
|
'-metadata', `artist=${storedArtist}`,
|
||||||
'-metadata', `album_artist=${storedAlbumArtist}`,
|
'-metadata', `album_artist=${storedAlbumArtist}`,
|
||||||
);
|
);
|
||||||
if (album) ffArgs.push('-metadata', `album=${album}`);
|
if (storedAlbum) ffArgs.push('-metadata', `album=${storedAlbum}`);
|
||||||
if (year) ffArgs.push('-metadata', `date=${String(year)}`);
|
if (year) ffArgs.push('-metadata', `date=${String(year)}`);
|
||||||
if (trackNum) ffArgs.push('-metadata', `track=${totalTracks ? `${trackNum}/${totalTracks}` : String(trackNum)}`);
|
if (trackNum) ffArgs.push('-metadata', `track=${totalTracks ? `${trackNum}/${totalTracks}` : String(trackNum)}`);
|
||||||
if (totalTracks) ffArgs.push('-metadata', `totaltracks=${String(totalTracks)}`);
|
if (totalTracks) ffArgs.push('-metadata', `totaltracks=${String(totalTracks)}`);
|
||||||
|
|||||||
@@ -613,6 +613,7 @@ app.post('/api/edit-metadata', async (req, res) => {
|
|||||||
const romajiArtist = normalArtist ? romajiForStorage(normalArtist) || normalArtist : normalArtist;
|
const romajiArtist = normalArtist ? romajiForStorage(normalArtist) || normalArtist : normalArtist;
|
||||||
const romajiAlbumArtist = normalAlbumArtist ? romajiForStorage(normalAlbumArtist) || normalAlbumArtist : normalAlbumArtist;
|
const romajiAlbumArtist = normalAlbumArtist ? romajiForStorage(normalAlbumArtist) || normalAlbumArtist : normalAlbumArtist;
|
||||||
const romajiTitle = title ? romajiForStorage(title) || title : title;
|
const romajiTitle = title ? romajiForStorage(title) || title : title;
|
||||||
|
const romajiAlbum = album ? romajiForStorage(album) || album : album;
|
||||||
|
|
||||||
// Resolve file path from Navidrome
|
// Resolve file path from Navidrome
|
||||||
let filePath;
|
let filePath;
|
||||||
@@ -682,7 +683,7 @@ app.post('/api/edit-metadata', async (req, res) => {
|
|||||||
if (romajiArtist) ffArgs.push('-metadata', `artist=${romajiArtist}`);
|
if (romajiArtist) ffArgs.push('-metadata', `artist=${romajiArtist}`);
|
||||||
const resolvedAlbumArtist = romajiAlbumArtist || (romajiArtist ? romajiArtist.split(';')[0].trim() : null);
|
const resolvedAlbumArtist = romajiAlbumArtist || (romajiArtist ? romajiArtist.split(';')[0].trim() : null);
|
||||||
if (resolvedAlbumArtist) ffArgs.push('-metadata', `album_artist=${resolvedAlbumArtist}`);
|
if (resolvedAlbumArtist) ffArgs.push('-metadata', `album_artist=${resolvedAlbumArtist}`);
|
||||||
if (album) ffArgs.push('-metadata', `album=${album}`);
|
if (romajiAlbum) ffArgs.push('-metadata', `album=${romajiAlbum}`);
|
||||||
if (year) ffArgs.push('-metadata', `date=${year}`);
|
if (year) ffArgs.push('-metadata', `date=${year}`);
|
||||||
if (trackNum) ffArgs.push('-metadata', `track=${totalTracks ? `${trackNum}/${totalTracks}` : String(trackNum)}`);
|
if (trackNum) ffArgs.push('-metadata', `track=${totalTracks ? `${trackNum}/${totalTracks}` : String(trackNum)}`);
|
||||||
if (totalTracks) ffArgs.push('-metadata', `totaltracks=${String(totalTracks)}`);
|
if (totalTracks) ffArgs.push('-metadata', `totaltracks=${String(totalTracks)}`);
|
||||||
@@ -724,7 +725,7 @@ app.post('/api/edit-metadata', async (req, res) => {
|
|||||||
// 2. Move to new artist/album folder when albumArtist or album changed
|
// 2. Move to new artist/album folder when albumArtist or album changed
|
||||||
const oldAlbumDir = path.dirname(currentFile);
|
const oldAlbumDir = path.dirname(currentFile);
|
||||||
const oldArtistDir = path.dirname(oldAlbumDir);
|
const oldArtistDir = path.dirname(oldAlbumDir);
|
||||||
const newAlbumName = album ? sanitize(album) : path.basename(oldAlbumDir);
|
const newAlbumName = romajiAlbum ? sanitize(romajiAlbum) : path.basename(oldAlbumDir);
|
||||||
const newArtistName = romajiAlbumArtist ? sanitize(romajiAlbumArtist.split(/[,;&]/)[0].trim()) : path.basename(oldArtistDir);
|
const newArtistName = romajiAlbumArtist ? sanitize(romajiAlbumArtist.split(/[,;&]/)[0].trim()) : path.basename(oldArtistDir);
|
||||||
const newDir = path.join(CONFIG.musicPath, newArtistName, newAlbumName);
|
const newDir = path.join(CONFIG.musicPath, newArtistName, newAlbumName);
|
||||||
|
|
||||||
@@ -746,9 +747,9 @@ app.post('/api/edit-metadata', async (req, res) => {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
} catch (e) { console.error('[edit-metadata] move:', e.message); }
|
} catch (e) { console.error('[edit-metadata] move:', e.message); }
|
||||||
} else if (album && path.basename(oldAlbumDir) !== sanitize(album)) {
|
} else if (romajiAlbum && path.basename(oldAlbumDir) !== sanitize(romajiAlbum)) {
|
||||||
// Only album name changed (no artist move) — rename album folder in place
|
// Only album name changed (no artist move) — rename album folder in place
|
||||||
const renamedDir = path.join(oldArtistDir, sanitize(album));
|
const renamedDir = path.join(oldArtistDir, sanitize(romajiAlbum));
|
||||||
if (!fs.existsSync(renamedDir)) {
|
if (!fs.existsSync(renamedDir)) {
|
||||||
try { fs.renameSync(oldAlbumDir, renamedDir); }
|
try { fs.renameSync(oldAlbumDir, renamedDir); }
|
||||||
catch (e) { console.error('[edit-metadata] folder rename:', e.message); }
|
catch (e) { console.error('[edit-metadata] folder rename:', e.message); }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { S } from './state.js';
|
import { S } from './state.js';
|
||||||
import { esc, formatDur, toast } from './utils.js';
|
import { esc, formatDur, toast } from './utils.js';
|
||||||
import { api, coverUrl, ndAuth } from './api.js';
|
import { api, coverUrl, ndAuth, fetchRomajiIndex } from './api.js';
|
||||||
import { THEMES, applyTheme, getCurrentThemeId } from './themes.js';
|
import { THEMES, applyTheme, getCurrentThemeId } from './themes.js';
|
||||||
// Circular imports — safe (only used inside function bodies)
|
// Circular imports — safe (only used inside function bodies)
|
||||||
import { playQueue, dlSongLocal, toggleStar, togglePlay, renderQueue, syncNpStar, updateStarBtn, npToggleStar, playCurrent, _syncNpDirect, setPlayIcon, nativeInsertAt } from './player.js';
|
import { playQueue, dlSongLocal, toggleStar, togglePlay, renderQueue, syncNpStar, updateStarBtn, npToggleStar, playCurrent, _syncNpDirect, setPlayIcon, nativeInsertAt } from './player.js';
|
||||||
@@ -560,6 +560,11 @@ async function _createNdPlaylist(tracks, name) {
|
|||||||
const norm = s => s?.toLowerCase().trim() || '';
|
const norm = s => s?.toLowerCase().trim() || '';
|
||||||
// Strip non-word, non-space chars for loose artist first-word comparison
|
// Strip non-word, non-space chars for loose artist first-word comparison
|
||||||
const normA = s => norm(s).replace(/[^\w\s]/g, '');
|
const normA = s => norm(s).replace(/[^\w\s]/g, '');
|
||||||
|
const JP_RE = /[-ヿ一-鿿]/;
|
||||||
|
// Pre-fetch romaji index once if any track has Japanese chars — Navidrome search3
|
||||||
|
// doesn't handle Japanese text, so we need the index as a fallback.
|
||||||
|
const hasJp = tracks.some(t => JP_RE.test(t.trackName) || JP_RE.test(t.artistName));
|
||||||
|
const romajiIdx = hasJp ? await fetchRomajiIndex().catch(() => null) : null;
|
||||||
for (const t of tracks) {
|
for (const t of tracks) {
|
||||||
if (!t.trackName) continue;
|
if (!t.trackName) continue;
|
||||||
const missingLabel = t.artistName ? t.artistName + ' – ' + t.trackName : t.trackName;
|
const missingLabel = t.artistName ? t.artistName + ' – ' + t.trackName : t.trackName;
|
||||||
@@ -579,8 +584,14 @@ async function _createNdPlaylist(tracks, name) {
|
|||||||
const firstWord = s => normA(s).split(/\s+/)[0];
|
const firstWord = s => normA(s).split(/\s+/)[0];
|
||||||
const titleMatch = s => norm(s.title) === norm(t.trackName);
|
const titleMatch = s => norm(s.title) === norm(t.trackName);
|
||||||
const artistMatch = s => firstWord(s.artist) === firstWord(firstArtist || t.artistName);
|
const artistMatch = s => firstWord(s.artist) === firstWord(firstArtist || t.artistName);
|
||||||
const match = songs.find(s => titleMatch(s) && artistMatch(s))
|
let match = songs.find(s => titleMatch(s) && artistMatch(s))
|
||||||
|| songs.find(s => titleMatch(s));
|
|| songs.find(s => titleMatch(s));
|
||||||
|
// Fallback: search romaji index by original Japanese title
|
||||||
|
// (Navidrome search3 doesn't index Japanese text properly)
|
||||||
|
if (!match && romajiIdx && JP_RE.test(t.trackName)) {
|
||||||
|
const jpEntry = romajiIdx.find(e => norm(e.title) === norm(t.trackName));
|
||||||
|
if (jpEntry) match = { id: jpEntry.id };
|
||||||
|
}
|
||||||
if (match) songIds.push(match.id);
|
if (match) songIds.push(match.id);
|
||||||
else missing.push(missingLabel);
|
else missing.push(missingLabel);
|
||||||
} catch { missing.push(missingLabel); }
|
} catch { missing.push(missingLabel); }
|
||||||
|
|||||||
Reference in new issue
Block a user