diff --git a/backend/lib/metadata.js b/backend/lib/metadata.js index 038ce76..317f89d 100644 --- a/backend/lib/metadata.js +++ b/backend/lib/metadata.js @@ -88,6 +88,19 @@ function normalizeTitle(s) { .trim(); } +// iTunes JP store returns localized genre names — map them to English equivalents +const JP_GENRE_MAP = { + 'アニメ': 'Anime', 'イージーリスニング': 'Easy Listening', 'ポップ': 'Pop', + 'J-Pop': 'J-Pop', 'ロック': 'Rock', 'クラシック': 'Classical', 'ジャズ': 'Jazz', + 'エレクトロニック': 'Electronic', 'R&B/ソウル': 'R&B/Soul', 'ヒップホップ/ラップ': 'Hip-Hop/Rap', + 'オルタナティブ': 'Alternative', 'サウンドトラック': 'Soundtrack', 'ボーカル': 'Vocal', + 'インストゥルメンタル': 'Instrumental', 'ワールド': 'World', 'フォーク': 'Folk', + 'カントリー': 'Country', 'レゲエ': 'Reggae', 'ソウル': 'Soul', 'ブルース': 'Blues', + 'メタル': 'Metal', 'パンク': 'Punk', 'ダンス': 'Dance', 'コメディ': 'Comedy', + 'スピーチ': 'Speech', 'キッズ/ファミリー': 'Kids/Family', +}; +function toEnglishGenre(g) { return JP_GENRE_MAP[g] || g; } + // iTunes track lookup — fills in album, year, trackNum, totalTracks, genre when missing async function lookupItunesTrackMeta(artist, title) { try { @@ -118,8 +131,9 @@ async function lookupItunesTrackMeta(artist, title) { let usMatch = strictMatch(usResults) || titleOnlyMatch(usResults); if (!match) match = usMatch; if (!match) return {}; - // JP genre names are localized (e.g. イージーリスニング); always prefer US genre - const genre = (usMatch || match).primaryGenreName; + // JP genre names are localized (e.g. イージーリスニング); prefer US genre, map JP→EN as fallback + const rawGenre = (usMatch || match).primaryGenreName; + const genre = rawGenre ? toEnglishGenre(rawGenre) : undefined; return { album: match.collectionName || '', year: match.releaseDate ? new Date(match.releaseDate).getFullYear() : undefined, @@ -127,7 +141,7 @@ async function lookupItunesTrackMeta(artist, title) { totalTracks: match.trackCount || undefined, discNum: match.discNumber || undefined, discTotal: match.discCount || undefined, - genre: genre || undefined, + genre: genre, artworkUrl: (match.artworkUrl100 || '').replace('100x100bb', '1000x1000bb'), durationMs: match.trackTimeMillis || undefined, itunesArtistName: match.artistName || undefined, diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 67b3a62..7eaece3 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -416,14 +416,19 @@ app.post('/api/download', async (req, res) => { artworkUrl: spArt, explicit: sp.trackExplicitness === 'explicit' || !!explicit, durationMs: sp.durationMs || undefined, }; + const searchArtist = artist.split(',')[0].trim(); if (!meta.album) { - const searchArtist = artist.split(',')[0].trim(); log(jobId, '📋 Suche Album-Metadaten von iTunes…'); const extra = await lookupItunesTrackMeta(searchArtist, title); if (extra.album) log(jobId, `📋 Album: "${extra.album}" (${extra.year || '?'})`); meta = { ...meta, ...extra, artworkUrl: spArt || extra.artworkUrl || '', genre: meta.genre || extra.genre || undefined }; } else { log(jobId, `📋 Album aus Spotify: "${meta.album}"`); + // Spotify has no per-track genre — still look up genre from iTunes + if (!meta.genre) { + const genreMeta = await lookupItunesTrackMeta(searchArtist, title).catch(() => ({})); + if (genreMeta.genre) { meta.genre = genreMeta.genre; log(jobId, `📋 Genre: "${genreMeta.genre}"`); } + } } const finalArtist = meta.itunesArtistName || artist; const result = await downloadTrack({ jobId, artist: finalArtist, rawArtist: artist, title, ...meta, sourceUrl: sourceUrl || sp.sourceUrl || undefined }); @@ -492,11 +497,14 @@ app.post('/api/download', async (req, res) => { // trackNumber + durationMs all present), skip the re-lookup to avoid a different // iTunes result overriding the already-correct enriched metadata. const alreadyEnriched = !!(track.collectionName && track.trackNumber && track.durationMs); + const searchArtist = trackArtist.split(',')[0].trim(); let itunes = {}; if (alreadyEnriched) { log(jobId, `📋 Frontend-Metadaten: "${trackArtist} – ${trackTitle}" (${track.collectionName})`); + // Still look up genre — frontend enrichment doesn't include it and Spotify has no genre per track + const genreMeta = await lookupItunesTrackMeta(searchArtist, trackTitle).catch(() => ({})); + itunes = { genre: genreMeta.genre }; } else { - const searchArtist = trackArtist.split(',')[0].trim(); log(jobId, `📋 iTunes: "${trackArtist} – ${trackTitle}"`); itunes = await lookupItunesTrackMeta(searchArtist, trackTitle); }