diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 6ab4181..11c28dc 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -570,6 +570,29 @@ app.post('/api/soundcloud/resolve', async (req, res) => { } }); +// ─── ITUNES LOOKUP ─────────────────────────────────────────────────────────── +app.get('/api/itunes/lookup', async (req, res) => { + const { artist, title } = req.query; + if (!artist || !title) return res.status(400).json({ error: 'artist and title required' }); + try { + const meta = await lookupItunesTrackMeta(artist, title); + if (!meta.album && !meta.genre && !meta.year) return res.json({ found: false }); + res.json({ + found: true, + album: meta.album || undefined, + year: meta.year || undefined, + trackNum: meta.trackNum || undefined, + totalTracks: meta.totalTracks || undefined, + genre: meta.genre || undefined, + coverUrl: meta.artworkUrl || undefined, + artist: meta.itunesArtistName || undefined, + }); + } catch (e) { + console.error('[itunes/lookup]', e.message); + res.status(500).json({ error: e.message }); + } +}); + // ─── MUSICBRAINZ LOOKUP ────────────────────────────────────────────────────── app.get('/api/musicbrainz/lookup', async (req, res) => { const { artist, title } = req.query; diff --git a/index.html b/index.html index e5997cb..27bd576 100644 --- a/index.html +++ b/index.html @@ -927,6 +927,10 @@ MusicBrainz +
diff --git a/src/api.js b/src/api.js index 457b071..f931dd1 100644 --- a/src/api.js +++ b/src/api.js @@ -53,11 +53,17 @@ export async function ndAuth() { // Fetched once per session from the backend, cached in memory. let _romajiIndex = null; let _romajiMap = null; // Map +let _artistRomajiMap = null; // Map let _romajiIndexFetch = null; // in-flight promise dedup function _buildRomajiMap(idx) { - const m = new Map(); - if (Array.isArray(idx)) idx.forEach(e => m.set(String(e.id), e)); + const m = new Map(); + const am = new Map(); + if (Array.isArray(idx)) idx.forEach(e => { + m.set(String(e.id), e); + if (e.artistId && e.artistRomaji) am.set(String(e.artistId), e.artistRomaji); + }); + _artistRomajiMap = am; return m; } @@ -73,7 +79,12 @@ export async function fetchRomajiIndex() { return _romajiIndexFetch; } -export function invalidateRomajiIndex() { _romajiIndex = null; _romajiMap = null; } +export function invalidateRomajiIndex() { _romajiIndex = null; _romajiMap = null; _artistRomajiMap = null; } + +export function dispArtistById(id, name) { + if (!S.cfg.showRomaji || !_artistRomajiMap) return name || ''; + return _artistRomajiMap.get(String(id)) || name || ''; +} // Synchronous lookup — returns index entry for a song ID, or null if not in index. export function romajiLookup(id) { return _romajiMap?.get(String(id)) ?? null; } diff --git a/src/main.js b/src/main.js index 951b83d..516a0a6 100644 --- a/src/main.js +++ b/src/main.js @@ -45,7 +45,7 @@ import { toggleMobileSearch, closeMobileSearch, switchSetupTab, toggleNotifPanel, deleteNotif, clearNotifs, initNotifBell, _notifNavigate, _pickTheme, - toggleInfoEdit, onInfoArtFile, autofillMusicBrainz, saveMetadata, + toggleInfoEdit, onInfoArtFile, autofillMusicBrainz, autofillItunes, saveMetadata, } from './ui.js'; // ─── CONNECT ─── @@ -315,7 +315,7 @@ Object.assign(window, { loadSoundCloudImporter, resolveSoundCloudLink, clearSoundCloudPreview, scDlAll, scDlSingle, scDlTrack, executeSpotifyDownload, resolveYouTubeLink, executeYouTubeDownload, openPlModal, closePlModal, addSongsToPlaylist, createAndAddPlaylist, createPlaylistPrompt, - showSongInfo, closeInfoModal, toggleInfoEdit, onInfoArtFile, autofillMusicBrainz, saveMetadata, + showSongInfo, closeInfoModal, toggleInfoEdit, onInfoArtFile, autofillMusicBrainz, autofillItunes, saveMetadata, openDlModal, closeDlModal, selectDlType, executeDownload, openEditPlModal, closeEditPlModal, saveEditPlaylist, editPlUploadCover, editPlCoverSelected, editPlRemoveCover, deletePlaylist, _confirmOk, _confirmCancel, diff --git a/src/ui.js b/src/ui.js index 7d1e258..18573f6 100644 --- a/src/ui.js +++ b/src/ui.js @@ -1,6 +1,6 @@ import { S } from './state.js'; import { esc, formatDur, toast } from './utils.js'; -import { api, coverUrl, ndAuth, fetchRomajiIndex, romajiConvert, romajiLookup } from './api.js'; +import { api, coverUrl, ndAuth, fetchRomajiIndex, romajiConvert, romajiLookup, invalidateRomajiIndex } from './api.js'; import { THEMES, applyTheme, getCurrentThemeId } from './themes.js'; // Circular imports — safe (only used inside function bodies) import { playQueue, dlSongLocal, toggleStar, togglePlay, renderQueue, syncNpStar, updateStarBtn, npToggleStar, playCurrent, _syncNpDirect, setPlayIcon, nativeInsertAt, refreshDisplayMode } from './player.js'; @@ -444,6 +444,36 @@ export async function autofillMusicBrainz() { } } +export async function autofillItunes() { + if (!S.cfg.backendUrl) { toast('⚠ Kein Backend konfiguriert'); return; } + const s = S.infoSong; + if (!s) return; + const artist = document.getElementById('ie-artist').value || s.artist || ''; + const title = document.getElementById('ie-title').value || s.title || ''; + const status = document.getElementById('info-edit-status'); + status.textContent = '🔍 Apple Music…'; + try { + const r = await fetch(`${S.cfg.backendUrl}/api/itunes/lookup?artist=${encodeURIComponent(artist)}&title=${encodeURIComponent(title)}`); + const d = await r.json(); + if (!d.found) { status.textContent = '❌ Kein Treffer auf iTunes'; return; } + if (d.album) document.getElementById('ie-album').value = d.album; + if (d.genre) document.getElementById('ie-genre').value = d.genre; + if (d.year) document.getElementById('ie-year').value = d.year; + if (d.trackNum) document.getElementById('ie-track').value = d.trackNum; + if (d.totalTracks) document.getElementById('ie-total').value = d.totalTracks; + if (d.coverUrl) { + S._infoArtData = { url: d.coverUrl }; + document.getElementById('info-edit-art-preview').innerHTML = + ``; + status.textContent = '✅ Metadaten + Cover von Apple Music'; + } else { + status.textContent = '✅ Metadaten von Apple Music (kein Cover)'; + } + } catch (e) { + status.textContent = '❌ Fehler: ' + e.message; + } +} + export async function saveMetadata() { if (!S.cfg.backendUrl) { toast('⚠ Kein Backend konfiguriert'); return; } const s = S.infoSong; @@ -890,6 +920,7 @@ async function pollJob(jobId) { }); if (d.status === 'done') { toast(d.skipped ? `⏭ "${title}" bereits auf dem Server` : `✅ ${title} gespeichert!`); + if (!d.skipped) invalidateRomajiIndex(); } } } catch { updateJob(jobId, 'error', 'Status-Abfrage fehlgeschlagen'); } diff --git a/src/views.js b/src/views.js index 27b302e..7bc07fc 100644 --- a/src/views.js +++ b/src/views.js @@ -6,7 +6,7 @@ import { playQueue, dlSongLocal, playPreview, toggleStar, parseArtists } from '. import { showCtxMenu, openEditPlModal, openDlModal, closeSidebar, switchNavMode } from './ui.js'; import { closeNowPlaying, closeNpMenu } from './nowplaying.js'; import { makeCard, makeAlbumCard, makeSongItem, makeGlobalTrackItem, mkSection } from './components.js'; -import { fetchRomajiIndex } from './api.js'; +import { fetchRomajiIndex, dispArtistById } from './api.js'; import { loadSpotifyImporter } from './spotify.js'; import { loadSoundCloudImporter } from './soundcloud.js'; @@ -283,9 +283,10 @@ export async function loadArtists() { try { const r = await api('getArtists'); const all = (r.artists?.index || []).flatMap(i => i.artist || []); + if (S.cfg.showRomaji) await fetchRomajiIndex().catch(() => {}); g.innerHTML = ''; all.forEach(a => { - const c = makeCard(a.name, a.albumCount+' Alben', null, '🎤'); + const c = makeCard(dispArtistById(a.id, a.name), a.albumCount+' Alben', null, '🎤'); c.classList.add('artist-card'); c.onclick = () => navigate('artist-detail', a); g.appendChild(c); @@ -344,7 +345,7 @@ function _makeTopsongRow(s, num, subtitle, list, idx) { // ─── ARTIST DETAIL ─── export async function loadArtistDetail(artist) { - document.getElementById('artist-detail-name').textContent = artist.name; + document.getElementById('artist-detail-name').textContent = dispArtistById(artist.id, artist.name); document.getElementById('artist-back-btn').onclick = () => _navGoBack(); const hero = document.getElementById('artist-detail-hero');