feat: romaji artist names, Apple Music autofill, romaji index invalidation
- Artist grid and detail header now show romaji names when toggle is on. Builds a Map<artistId, artistRomaji> from the existing song romaji index so no extra API call is needed. - Apple Music autofill button added to edit modal next to MusicBrainz. Reuses the existing lookupItunesTrackMeta backend function via a new GET /api/itunes/lookup endpoint — fills album, year, genre, track#, and cover art. - Romaji index is invalidated after every successful download so newly added songs appear with correct sort tags without requiring a page reload. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
212491aa89
commit
b7afa19f70
6 files changed
+79
-9
No files matched your search
@@ -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 ──────────────────────────────────────────────────────
|
// ─── MUSICBRAINZ LOOKUP ──────────────────────────────────────────────────────
|
||||||
app.get('/api/musicbrainz/lookup', async (req, res) => {
|
app.get('/api/musicbrainz/lookup', async (req, res) => {
|
||||||
const { artist, title } = req.query;
|
const { artist, title } = req.query;
|
||||||
|
|||||||
@@ -927,6 +927,10 @@
|
|||||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||||
MusicBrainz
|
MusicBrainz
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn-outline blue" style="font-size:11px;padding:6px 10px;height:fit-content" onclick="autofillItunes()">
|
||||||
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||||
|
Apple Music
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-edit-fields">
|
<div class="info-edit-fields">
|
||||||
<label class="info-edit-label">Titel<input class="info-edit-input" id="ie-title" type="text" placeholder="Titel"></label>
|
<label class="info-edit-label">Titel<input class="info-edit-input" id="ie-title" type="text" placeholder="Titel"></label>
|
||||||
|
|||||||
+14
-3
@@ -53,11 +53,17 @@ export async function ndAuth() {
|
|||||||
// Fetched once per session from the backend, cached in memory.
|
// Fetched once per session from the backend, cached in memory.
|
||||||
let _romajiIndex = null;
|
let _romajiIndex = null;
|
||||||
let _romajiMap = null; // Map<songId, { titleRomaji, artistRomaji, albumRomaji }>
|
let _romajiMap = null; // Map<songId, { titleRomaji, artistRomaji, albumRomaji }>
|
||||||
|
let _artistRomajiMap = null; // Map<artistId, artistRomaji>
|
||||||
let _romajiIndexFetch = null; // in-flight promise dedup
|
let _romajiIndexFetch = null; // in-flight promise dedup
|
||||||
|
|
||||||
function _buildRomajiMap(idx) {
|
function _buildRomajiMap(idx) {
|
||||||
const m = new Map();
|
const m = new Map();
|
||||||
if (Array.isArray(idx)) idx.forEach(e => m.set(String(e.id), e));
|
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;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +79,12 @@ export async function fetchRomajiIndex() {
|
|||||||
return _romajiIndexFetch;
|
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.
|
// 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; }
|
export function romajiLookup(id) { return _romajiMap?.get(String(id)) ?? null; }
|
||||||
|
|||||||
+2
-2
@@ -45,7 +45,7 @@ import {
|
|||||||
toggleMobileSearch, closeMobileSearch, switchSetupTab,
|
toggleMobileSearch, closeMobileSearch, switchSetupTab,
|
||||||
toggleNotifPanel, deleteNotif, clearNotifs, initNotifBell, _notifNavigate,
|
toggleNotifPanel, deleteNotif, clearNotifs, initNotifBell, _notifNavigate,
|
||||||
_pickTheme,
|
_pickTheme,
|
||||||
toggleInfoEdit, onInfoArtFile, autofillMusicBrainz, saveMetadata,
|
toggleInfoEdit, onInfoArtFile, autofillMusicBrainz, autofillItunes, saveMetadata,
|
||||||
} from './ui.js';
|
} from './ui.js';
|
||||||
|
|
||||||
// ─── CONNECT ───
|
// ─── CONNECT ───
|
||||||
@@ -315,7 +315,7 @@ Object.assign(window, {
|
|||||||
loadSoundCloudImporter, resolveSoundCloudLink, clearSoundCloudPreview, scDlAll, scDlSingle, scDlTrack,
|
loadSoundCloudImporter, resolveSoundCloudLink, clearSoundCloudPreview, scDlAll, scDlSingle, scDlTrack,
|
||||||
executeSpotifyDownload, resolveYouTubeLink, executeYouTubeDownload,
|
executeSpotifyDownload, resolveYouTubeLink, executeYouTubeDownload,
|
||||||
openPlModal, closePlModal, addSongsToPlaylist, createAndAddPlaylist, createPlaylistPrompt,
|
openPlModal, closePlModal, addSongsToPlaylist, createAndAddPlaylist, createPlaylistPrompt,
|
||||||
showSongInfo, closeInfoModal, toggleInfoEdit, onInfoArtFile, autofillMusicBrainz, saveMetadata,
|
showSongInfo, closeInfoModal, toggleInfoEdit, onInfoArtFile, autofillMusicBrainz, autofillItunes, saveMetadata,
|
||||||
openDlModal, closeDlModal, selectDlType, executeDownload,
|
openDlModal, closeDlModal, selectDlType, executeDownload,
|
||||||
openEditPlModal, closeEditPlModal, saveEditPlaylist,
|
openEditPlModal, closeEditPlModal, saveEditPlaylist,
|
||||||
editPlUploadCover, editPlCoverSelected, editPlRemoveCover, deletePlaylist, _confirmOk, _confirmCancel,
|
editPlUploadCover, editPlCoverSelected, editPlRemoveCover, deletePlaylist, _confirmOk, _confirmCancel,
|
||||||
|
|||||||
@@ -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, fetchRomajiIndex, romajiConvert, romajiLookup } from './api.js';
|
import { api, coverUrl, ndAuth, fetchRomajiIndex, romajiConvert, romajiLookup, invalidateRomajiIndex } 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, refreshDisplayMode } from './player.js';
|
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 =
|
||||||
|
`<img src="${d.coverUrl}" style="width:100%;height:100%;object-fit:cover" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\' style=\\'width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:28px\\'>🎵</div>'">`;
|
||||||
|
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() {
|
export async function saveMetadata() {
|
||||||
if (!S.cfg.backendUrl) { toast('⚠ Kein Backend konfiguriert'); return; }
|
if (!S.cfg.backendUrl) { toast('⚠ Kein Backend konfiguriert'); return; }
|
||||||
const s = S.infoSong;
|
const s = S.infoSong;
|
||||||
@@ -890,6 +920,7 @@ async function pollJob(jobId) {
|
|||||||
});
|
});
|
||||||
if (d.status === 'done') {
|
if (d.status === 'done') {
|
||||||
toast(d.skipped ? `⏭ "${title}" bereits auf dem Server` : `✅ ${title} gespeichert!`);
|
toast(d.skipped ? `⏭ "${title}" bereits auf dem Server` : `✅ ${title} gespeichert!`);
|
||||||
|
if (!d.skipped) invalidateRomajiIndex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch { updateJob(jobId, 'error', 'Status-Abfrage fehlgeschlagen'); }
|
} catch { updateJob(jobId, 'error', 'Status-Abfrage fehlgeschlagen'); }
|
||||||
|
|||||||
+4
-3
@@ -6,7 +6,7 @@ import { playQueue, dlSongLocal, playPreview, toggleStar, parseArtists } from '.
|
|||||||
import { showCtxMenu, openEditPlModal, openDlModal, closeSidebar, switchNavMode } from './ui.js';
|
import { showCtxMenu, openEditPlModal, openDlModal, closeSidebar, switchNavMode } from './ui.js';
|
||||||
import { closeNowPlaying, closeNpMenu } from './nowplaying.js';
|
import { closeNowPlaying, closeNpMenu } from './nowplaying.js';
|
||||||
import { makeCard, makeAlbumCard, makeSongItem, makeGlobalTrackItem, mkSection } from './components.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 { loadSpotifyImporter } from './spotify.js';
|
||||||
import { loadSoundCloudImporter } from './soundcloud.js';
|
import { loadSoundCloudImporter } from './soundcloud.js';
|
||||||
|
|
||||||
@@ -283,9 +283,10 @@ export async function loadArtists() {
|
|||||||
try {
|
try {
|
||||||
const r = await api('getArtists');
|
const r = await api('getArtists');
|
||||||
const all = (r.artists?.index || []).flatMap(i => i.artist || []);
|
const all = (r.artists?.index || []).flatMap(i => i.artist || []);
|
||||||
|
if (S.cfg.showRomaji) await fetchRomajiIndex().catch(() => {});
|
||||||
g.innerHTML = '';
|
g.innerHTML = '';
|
||||||
all.forEach(a => {
|
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.classList.add('artist-card');
|
||||||
c.onclick = () => navigate('artist-detail', a);
|
c.onclick = () => navigate('artist-detail', a);
|
||||||
g.appendChild(c);
|
g.appendChild(c);
|
||||||
@@ -344,7 +345,7 @@ function _makeTopsongRow(s, num, subtitle, list, idx) {
|
|||||||
|
|
||||||
// ─── ARTIST DETAIL ───
|
// ─── ARTIST DETAIL ───
|
||||||
export async function loadArtistDetail(artist) {
|
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();
|
document.getElementById('artist-back-btn').onclick = () => _navGoBack();
|
||||||
|
|
||||||
const hero = document.getElementById('artist-detail-hero');
|
const hero = document.getElementById('artist-detail-hero');
|
||||||
|
|||||||
Reference in new issue
Block a user