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');