fix(web): 'Unknown Artist' download from artist page + rename itunes*→deezer* identifiers

Fix (the reported bug):
- Deezer's /artist/{id}/albums omits the artist object, so discography album cards had an
  empty artist name → the download modal fell back to the literal "Unknown Artist", which
  poisoned the yt-dlp search ("Unknown Artist <album>") and its artist/channel filter, failing
  with "Nicht eindeutig gefunden".
- deezerLookupArtistAlbums now backfills the known artist name (we're on that artist's page).
- openDlModal now defaults album cards to an ALBUM download (was always Track), so a one-click
  download no longer grabs an album as a single track named after the album.

Rename (requested cleanup, dependency-checked so nothing breaks):
- All frontend itunes* identifiers → deezer* (functions, the ItunesItem-shape field keys
  itunesData/itunesArtistId/itunesFanCount/itunesId, the itunes-album-detail route + DOM ids,
  the badge-itunes CSS class). The backend reads none of these fields, so the boundary is safe.
- Verified: build passes; route/DOM-id/CSS producer↔consumer all consistent; only the factual
  "Apple's iTunes /search 403" comments keep the historical name. SW ns-v62.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-08-01 00:31:09 +02:00
1 parent bc87a04da5
commit 1e206e8df3
13 files changed
+118 -108

No files matched your search

+40 -40
View File
@@ -1,6 +1,6 @@
import { S } from '../state.js';
import { esc, toast, fmtCount } from '../utils.js';
import { api, coverUrl, itunesSearch, itunesLookupAlbum, itunesLookupArtistSongs, itunesLookupArtistAlbums, deezerArtistImage, fetchRomajiIndex, dispArtistById, dispAlbumName, cacheArtistSortNames, ensureArtistSortMap } from '../api.js';
import { api, coverUrl, deezerSearch, deezerLookupAlbum, deezerLookupArtistSongs, deezerLookupArtistAlbums, deezerArtistImage, fetchRomajiIndex, dispArtistById, dispAlbumName, cacheArtistSortNames, ensureArtistSortMap } from '../api.js';
import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem } from '../ui/components.js';
import { playQueue, dlSongLocal, toggleStar, parseArtists } from '../player/player.js';
import { showCtxMenu, openDlModal } from '../ui/ui.js';
@@ -87,8 +87,8 @@ export async function loadArtistDetail(artist) {
if(albEl) albEl.innerHTML = '<div class="spinner"></div>';
const hasND = !!artist.id;
const knownItunes = artist.itunesArtistId || null;
const knownFans = artist.itunesFanCount ?? null;
const knownDeezer = artist.deezerArtistId || null;
const knownFans = artist.deezerFanCount ?? null;
if (knownFans) _showFans(knownFans); // from the global-search card — show immediately
// Fire all requests immediately so they run in parallel.
@@ -96,7 +96,7 @@ export async function loadArtistDetail(artist) {
const albumP = hasND ? api('getArtist', `&id=${artist.id}`) : Promise.resolve(null);
const infoP = hasND ? api('getArtistInfo2',`&id=${artist.id}&count=8`) : Promise.resolve(null);
const topP = hasND ? api('getTopSongs', `&artist=${encodeURIComponent(artist.name)}&count=10`) : Promise.resolve(null);
const iArtistP = knownItunes ? Promise.resolve([{ artistId: knownItunes }]) : itunesSearch(artist.name, 'musicArtist', 5);
const iArtistP = knownDeezer ? Promise.resolve([{ artistId: knownDeezer }]) : deezerSearch(artist.name, 'musicArtist', 5);
const eventsP = fetchEvents(artist.name);
// Phase 1 — hero: only wait for albumP (fast local Navidrome call, no external API).
@@ -138,7 +138,7 @@ export async function loadArtistDetail(artist) {
document.getElementById('artist-hero-pfp-initial')?.style.setProperty('display', 'none');
}).catch(() => {});
// Phase 2 — content (may be slow: Last.fm bio, iTunes, events)
// Phase 2 — content (may be slow: Last.fm bio, Deezer, events)
const [infoRes, topRes, iArtistRes, eventsRes] = await Promise.allSettled([infoP, topP, iArtistP, eventsP]);
const _normWords = s => new Set((s || '').toLowerCase().replace(/[^a-z0-9]/g, ' ').split(/\s+/).filter(Boolean));
@@ -163,7 +163,7 @@ export async function loadArtistDetail(artist) {
// artist we own songs for, prefer the Deezer candidate whose top tracks actually overlap our
// library — each artist has different songs, so this nails the right person. Only runs on a real
// collision (≥2 name matches) and costs a few calls just for those.
if (!knownItunes && hasND && nameMatches.length > 1) {
if (!knownDeezer && hasND && nameMatches.length > 1) {
const _nt = s => (s || '').toLowerCase().replace(/\([^)]*\)/g, '').replace(/[^a-z0-9]/g, '');
const refTitles = new Set(
((topRes.status === 'fulfilled' ? topRes.value?.topSongs?.song : []) || [])
@@ -172,7 +172,7 @@ export async function loadArtistDetail(artist) {
if (refTitles.size) {
const cands = nameMatches.slice(0, 3);
const scored = await Promise.all(cands.map(async c => {
const tt = await itunesLookupArtistSongs(c.artistId, 15).catch(() => []);
const tt = await deezerLookupArtistSongs(c.artistId, 15).catch(() => []);
const overlap = tt.reduce((n, t) => n + (refTitles.has(_nt(t.trackName)) ? 1 : 0), 0);
return { c, overlap };
}));
@@ -181,12 +181,12 @@ export async function loadArtistDetail(artist) {
}
}
const itunesArtistId = knownItunes ?? iArtistMatch?.artistId ?? null;
const deezerArtistId = knownDeezer ?? iArtistMatch?.artistId ?? null;
if (!knownFans) _showFans(iArtistMatch?.fanCount); // local artist: show the matched Deezer artist's fans
// Non-local (Deezer) artist has no Navidrome photo — fetch the Deezer artist picture for the hero.
if (!hasND && itunesArtistId && pfpWrap) {
const dzImg = iArtistMatch?.artworkUrl100 || await deezerArtistImage(itunesArtistId);
if (!hasND && deezerArtistId && pfpWrap) {
const dzImg = iArtistMatch?.artworkUrl100 || await deezerArtistImage(deezerArtistId);
if (dzImg) {
const pfpImg = document.createElement('img');
pfpImg.className = 'artist-hero-pfp';
@@ -197,10 +197,10 @@ export async function loadArtistDetail(artist) {
}
}
const [iAlbumsRes, iTracksRes] = itunesArtistId
const [iAlbumsRes, iTracksRes] = deezerArtistId
? await Promise.allSettled([
itunesLookupArtistAlbums(itunesArtistId),
itunesLookupArtistSongs(itunesArtistId, 15),
deezerLookupArtistAlbums(deezerArtistId, 100, artist.name),
deezerLookupArtistSongs(deezerArtistId, 15),
])
: [{ status: 'rejected' }, { status: 'rejected' }];
@@ -332,7 +332,7 @@ async function _renderArtistAlbums(el, ndAlbums, iAlbums, artist) {
await Promise.all(entries
.filter(e => e.match && !(e.ia.trackCount > 0) && e.ia.collectionId)
.map(async e => {
const tracks = await itunesLookupAlbum(e.ia.collectionId).catch(() => []);
const tracks = await deezerLookupAlbum(e.ia.collectionId).catch(() => []);
if (tracks.length) e.ia = { ...e.ia, trackCount: tracks.length };
}));
@@ -342,11 +342,11 @@ async function _renderArtistAlbums(el, ndAlbums, iAlbums, artist) {
if(!match) { notDl.push(ia); continue; }
const have = match.songCount || 0;
const total = ia.trackCount || 0;
if(total > have) { partial.push({ nd: match, itunes: ia, have, total }); usedNdIds.add(match.id); }
else { downloaded.push({ nd: match, itunes: ia }); usedNdIds.add(match.id); }
if(total > have) { partial.push({ nd: match, deezer: ia, have, total }); usedNdIds.add(match.id); }
else { downloaded.push({ nd: match, deezer: ia }); usedNdIds.add(match.id); }
}
for(const na of ndAlbums) {
if(!usedNdIds.has(na.id)) downloaded.push({ nd: na, itunes: null });
if(!usedNdIds.has(na.id)) downloaded.push({ nd: na, deezer: null });
}
el.innerHTML = '';
@@ -370,20 +370,20 @@ async function _renderArtistAlbums(el, ndAlbums, iAlbums, artist) {
}
if(partial.length) {
const g = addGrid('Unvollständig', partial.length);
partial.forEach(({ nd, itunes, have, total }) => {
const art = itunes?.artworkUrl100?.replace('100x100','512x512') || coverUrl(nd.coverArt, 512);
partial.forEach(({ nd, deezer, have, total }) => {
const art = deezer?.artworkUrl100?.replace('100x100','512x512') || coverUrl(nd.coverArt, 512);
const c = makeCard(nd.name, `${have} / ${total} Tracks`, art, '💿');
c.querySelector('.card-art').onclick = (e) => { e.stopPropagation(); quickPlayAlbum(nd); };
c.querySelector('.card-info').onclick = () => itunes ? navigate('partial-album-detail', { nd, itunes }) : navigate('album-detail', { ...nd, _fromArtist: artist });
c.querySelector('.card-info').onclick = () => deezer ? navigate('partial-album-detail', { nd, deezer }) : navigate('album-detail', { ...nd, _fromArtist: artist });
const artEl = c.querySelector('.card-art');
const badge = document.createElement('div');
badge.className = 'album-partial-badge'; badge.textContent = `${have}/${total}`;
artEl.appendChild(badge);
if(itunes) {
if(deezer) {
const btn = document.createElement('button');
btn.className = 'card-dl-btn'; btn.title = 'Fehlende Tracks laden';
btn.innerHTML = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
btn.onclick = (e) => { e.stopPropagation(); openDlModal({ artistName: artist.name, trackName: nd.name, collectionName: nd.name, artworkUrl100: itunes.artworkUrl100||'', isAlbum: true, itunesData: itunes, collectionId: itunes.collectionId }); };
btn.onclick = (e) => { e.stopPropagation(); openDlModal({ artistName: artist.name, trackName: nd.name, collectionName: nd.name, artworkUrl100: deezer.artworkUrl100||'', isAlbum: true, deezerData: deezer, collectionId: deezer.collectionId }); };
artEl.appendChild(btn);
}
g.appendChild(c);
@@ -394,12 +394,12 @@ async function _renderArtistAlbums(el, ndAlbums, iAlbums, artist) {
notDl.forEach(ia => {
const art = ia.artworkUrl100?.replace('100x100','300x300');
const c = makeCard(ia.collectionName, ia.releaseDate?.slice(0,4)||'', art, '💿');
c.onclick = () => navigate('itunes-album-detail', ia);
c.onclick = () => navigate('deezer-album-detail', ia);
const artEl = c.querySelector('.card-art');
const btn = document.createElement('button');
btn.className = 'card-dl-btn'; btn.title = 'Album laden';
btn.innerHTML = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
btn.onclick = (e) => { e.stopPropagation(); openDlModal({ artistName: ia.artistName, trackName: ia.collectionName, collectionName: ia.collectionName, artworkUrl100: ia.artworkUrl100||'', isAlbum: true, itunesData: ia, collectionId: ia.collectionId }); };
btn.onclick = (e) => { e.stopPropagation(); openDlModal({ artistName: ia.artistName, trackName: ia.collectionName, collectionName: ia.collectionName, artworkUrl100: ia.artworkUrl100||'', isAlbum: true, deezerData: ia, collectionId: ia.collectionId }); };
artEl.appendChild(btn);
g.appendChild(c);
});
@@ -438,32 +438,32 @@ export async function loadArtistAllSongs({ albums, artist }) {
});
}
export async function loadItunesAlbumDetail(album) {
document.getElementById('itunes-album-back-btn').onclick = () => _navGoBack();
document.getElementById('itunes-album-detail-name').textContent = album.collectionName || '—';
document.getElementById('itunes-album-detail-artist').textContent = album.artistName || '';
const cw = document.getElementById('itunes-album-cover-wrap');
export async function loadDeezerAlbumDetail(album) {
document.getElementById('deezer-album-back-btn').onclick = () => _navGoBack();
document.getElementById('deezer-album-detail-name').textContent = album.collectionName || '—';
document.getElementById('deezer-album-detail-artist').textContent = album.artistName || '';
const cw = document.getElementById('deezer-album-cover-wrap');
const art = (album.artworkUrl100 || '').replace('100x100bb','600x600bb').replace('100x100','600x600');
cw.innerHTML = art ? `<img src="${art}" onerror="_imgRetry(this,'💿')">` : '<div class="no-art">💿</div>';
const dlBtn = document.getElementById('itunes-album-dl-btn');
dlBtn.onclick = () => openDlModal({ artistName: album.artistName, trackName: album.collectionName, collectionName: album.collectionName, artworkUrl100: album.artworkUrl100||'', isAlbum: true, itunesData: album, collectionId: album.collectionId });
const sl = document.getElementById('itunes-album-songs');
const dlBtn = document.getElementById('deezer-album-dl-btn');
dlBtn.onclick = () => openDlModal({ artistName: album.artistName, trackName: album.collectionName, collectionName: album.collectionName, artworkUrl100: album.artworkUrl100||'', isAlbum: true, deezerData: album, collectionId: album.collectionId });
const sl = document.getElementById('deezer-album-songs');
sl.innerHTML = '<div class="spinner"></div>';
try {
const tracks = await itunesLookupAlbum(album.collectionId);
const tracks = await deezerLookupAlbum(album.collectionId);
const year = album.releaseDate ? new Date(album.releaseDate).getFullYear() : '';
document.getElementById('itunes-album-detail-stats').textContent = `${tracks.length} Titel${year ? ' · ' + year : ''}`;
document.getElementById('deezer-album-detail-stats').textContent = `${tracks.length} Titel${year ? ' · ' + year : ''}`;
sl.innerHTML = '';
if(!tracks.length) { sl.innerHTML = '<div style="padding:16px;color:var(--text3)">Keine Tracks gefunden</div>'; return; }
tracks.forEach((t, i) => sl.appendChild(makeGlobalTrackItem(t, i, tracks)));
} catch(e) { sl.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
}
export async function loadPartialAlbumDetail({ nd, itunes }) {
export async function loadPartialAlbumDetail({ nd, deezer }) {
document.getElementById('partial-album-back-btn').onclick = () => _navGoBack();
document.getElementById('partial-album-detail-name').textContent = nd.name || '—';
document.getElementById('partial-album-detail-artist').textContent = nd.artist || itunes?.artistName || '';
const art = (itunes?.artworkUrl100 || '').replace('100x100bb','600x600bb').replace('100x100','600x600')
document.getElementById('partial-album-detail-artist').textContent = nd.artist || deezer?.artistName || '';
const art = (deezer?.artworkUrl100 || '').replace('100x100bb','600x600bb').replace('100x100','600x600')
|| (nd.coverArt ? coverUrl(nd.coverArt, 512) : '');
const cw = document.getElementById('partial-album-cover-wrap');
cw.innerHTML = art ? `<img src="${art}" onerror="_imgRetry(this,'💿')">` : '<div class="no-art">💿</div>';
@@ -476,7 +476,7 @@ export async function loadPartialAlbumDetail({ nd, itunes }) {
try {
const [ndRes, iTracks] = await Promise.all([
api('getAlbum', `&id=${nd.id}`),
itunes?.collectionId ? itunesLookupAlbum(itunes.collectionId) : Promise.resolve([]),
deezer?.collectionId ? deezerLookupAlbum(deezer.collectionId) : Promise.resolve([]),
]);
const ndSongs = (ndRes.album?.song || []).sort((a,b) => (a.track||999)-(b.track||999));
@@ -492,9 +492,9 @@ export async function loadPartialAlbumDetail({ nd, itunes }) {
if(ndSongs.length) playQueue(ndSongs, 0);
};
// Download button offers missing via iTunes
// Download button offers missing via Deezer
document.getElementById('partial-album-dl-btn').onclick = () => {
if(itunes) openDlModal({ artistName: itunes.artistName, trackName: itunes.collectionName, collectionName: itunes.collectionName, artworkUrl100: itunes.artworkUrl100||'', isAlbum: true, itunesData: itunes, collectionId: itunes.collectionId });
if(deezer) openDlModal({ artistName: deezer.artistName, trackName: deezer.collectionName, collectionName: deezer.collectionName, artworkUrl100: deezer.artworkUrl100||'', isAlbum: true, deezerData: deezer, collectionId: deezer.collectionId });
};
// Section: downloaded
+9 -9
View File
@@ -1,11 +1,11 @@
import { S } from '../state.js';
import { esc } from '../utils.js';
import { itunesSearch, itunesTopCharts, itunesTopSongs } from '../api.js';
import { deezerSearch, deezerTopCharts, deezerTopSongs } from '../api.js';
import { makeCard, makeGlobalTrackItem } from '../ui/components.js';
import { playPreview } from '../player/player.js';
import { openDlModal } from '../ui/ui.js';
// Circular — safe (called inside event handlers)
import { loadItunesAlbum, searchAndPreview } from './search.js';
import { loadDeezerAlbum, searchAndPreview } from './search.js';
// ─── NEW RELEASES ───
export async function loadNewReleases() {
@@ -13,12 +13,12 @@ export async function loadNewReleases() {
const songEl = document.getElementById('nr-songs');
albumEl.innerHTML = '<div class="spinner"></div>'; songEl.innerHTML = '';
try {
const [albums, songs] = await Promise.all([itunesTopCharts(), itunesTopSongs()]);
const [albums, songs] = await Promise.all([deezerTopCharts(), deezerTopSongs()]);
albumEl.innerHTML = '';
albums.slice(0,30).forEach(a => {
const art = a.artworkUrl100?.replace('100x100','300x300') || a.artworkUrl;
const c = makeCard(a.name, a.artistName, art, '💿');
c.onclick = () => itunesSearch(a.name+' '+a.artistName,'album',5).then(res => { if(res[0]) loadItunesAlbum(res[0]); });
c.onclick = () => deezerSearch(a.name+' '+a.artistName,'album',5).then(res => { if(res[0]) loadDeezerAlbum(res[0]); });
albumEl.appendChild(c);
});
songs.slice(0,40).forEach((s, i) => {
@@ -36,7 +36,7 @@ export async function loadNewReleases() {
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
</button>
</div>`;
el.onclick = () => { itunesSearch(s.name+' '+s.artistName,'musicTrack',1).then(res => { if(res[0]?.previewUrl) playPreview(res[0]); }); };
el.onclick = () => { deezerSearch(s.name+' '+s.artistName,'musicTrack',1).then(res => { if(res[0]?.previewUrl) playPreview(res[0]); }); };
songEl.appendChild(el);
});
} catch(e) { albumEl.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
@@ -62,7 +62,7 @@ export async function loadRecommendations() {
if(S.cfg.lastfmKey) {
await _loadLastfmRecommendations(el, artist, title, html);
} else {
await _loadItunesFallbackRecommendations(el, artist, title, html);
await _loadDeezerFallbackRecommendations(el, artist, title, html);
const note = document.createElement('div');
note.style.cssText = 'margin-top:16px;padding:12px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;font-size:12px;color:var(--text3)';
note.innerHTML = '💡 <strong>Tipp:</strong> Füge einen Last.fm API Key in den Einstellungen hinzu für bessere Empfehlungen & Similar Artists.';
@@ -123,11 +123,11 @@ async function _loadLastfmRecommendations(el, artist, title, html) {
});
sec.appendChild(list); el.appendChild(sec);
}
} catch { await _loadItunesFallbackRecommendations(el, artist, title, html); }
} catch { await _loadDeezerFallbackRecommendations(el, artist, title, html); }
}
async function _loadItunesFallbackRecommendations(el, artist, title, html) {
const results = await itunesSearch(artist, 'musicTrack', 20);
async function _loadDeezerFallbackRecommendations(el, artist, title, html) {
const results = await deezerSearch(artist, 'musicTrack', 20);
el.innerHTML = html;
const sec = document.createElement('div');
sec.innerHTML = `<div class="section-title" style="margin-bottom:10px">Mehr von ${esc(artist)}</div>`;
+2 -2
View File
@@ -5,7 +5,7 @@ import { loadSpotifyImporter } from './spotify.js';
import { loadSoundCloudImporter } from './soundcloud.js';
// Circular — safe (all calls happen inside function bodies)
import { loadHomeData, loadAlbumRow, loadArtists, loadAlbums, loadGenreDetail } from './home.js';
import { loadArtistDetail, loadArtistAllSongs, loadAlbumDetail, loadItunesAlbumDetail, loadPartialAlbumDetail } from './detail.js';
import { loadArtistDetail, loadArtistAllSongs, loadAlbumDetail, loadDeezerAlbumDetail, loadPartialAlbumDetail } from './detail.js';
import { loadPlaylists, loadPlaylistDetail, loadStarred, loadRandom } from './playlists.js';
import { loadNewReleases, loadRecommendations } from './discover.js';
import { invalidateApiCache } from '../api.js';
@@ -79,7 +79,7 @@ export function navigate(page, data=null) {
if(page==='artist-all-songs' && data) loadArtistAllSongs(data);
if(page==='album-detail' && data) loadAlbumDetail(data);
if(page==='genre-detail' && data) loadGenreDetail(data);
if(page==='itunes-album-detail' && data) loadItunesAlbumDetail(data);
if(page==='deezer-album-detail' && data) loadDeezerAlbumDetail(data);
if(page==='partial-album-detail' && data) loadPartialAlbumDetail(data);
if(page==='new-releases') loadNewReleases();
if(page==='playlists') loadPlaylists();
+17 -17
View File
@@ -1,6 +1,6 @@
import { S } from '../state.js';
import { esc, toast, fmtCount } from '../utils.js';
import { api, itunesSearch, itunesLastError, itunesLookupAlbum, fetchRomajiIndex, dispArtistById } from '../api.js';
import { api, deezerSearch, deezerLastError, deezerLookupAlbum, fetchRomajiIndex, dispArtistById } from '../api.js';
import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem, mkSection } from '../ui/components.js';
import { playPreview } from '../player/player.js';
import { openDlModal } from '../ui/ui.js';
@@ -106,7 +106,7 @@ export function onHeaderSearch(val) {
export function onMobileSearch(val) { onHeaderSearch(val); }
// ─── GLOBAL (ITUNES) SEARCH ───
// ─── GLOBAL (DEEZER) SEARCH ───
export function onGlobalSearch(val) {
clearTimeout(S.globalSearchTimer);
const el = document.getElementById('global-search-results');
@@ -119,9 +119,9 @@ export async function doGlobalSearch(q) {
const el = document.getElementById('global-search-results');
try {
const [tracks, albums, artists] = await Promise.all([
itunesSearch(q,'musicTrack',25),
itunesSearch(q,'album',10),
itunesSearch(q,'musicArtist',6),
deezerSearch(q,'musicTrack',25),
deezerSearch(q,'album',10),
deezerSearch(q,'musicArtist',6),
]);
el.innerHTML = '';
const trackResults = tracks.filter(t => t.wrapperType === 'track');
@@ -132,10 +132,10 @@ export async function doGlobalSearch(q) {
artists.forEach(a => {
const sub = a.fanCount ? `${fmtCount(a.fanCount)} Fans` : 'Deezer Künstler';
const c = makeCard(a.artistName, sub, a.artworkUrl100 || null, '🎤');
c.onclick = () => loadItunesArtistResults(a.artistId, a.artistName, a.fanCount);
c.onclick = () => loadDeezerArtistResults(a.artistId, a.artistName, a.fanCount);
row.appendChild(c);
if (a.artworkUrl100) return; // Deezer artists already carry a photo — no extra lookup
itunesSearch(a.artistName,'album',1).then(res => {
deezerSearch(a.artistName,'album',1).then(res => {
if(!res[0]) return;
const imgUrl = res[0].artworkUrl100?.replace('100x100','300x300');
if(!imgUrl) return;
@@ -157,15 +157,15 @@ export async function doGlobalSearch(q) {
albums.forEach(a => {
const art = a.artworkUrl100?.replace('100x100','300x300');
const c = makeCard(a.collectionName, a.artistName, art, '💿');
c.querySelector('.play-btn').onclick = (e) => { e.stopPropagation(); loadItunesAlbum(a); };
c.querySelector('.play-btn').onclick = (e) => { e.stopPropagation(); loadDeezerAlbum(a); };
c.querySelector('.dl-btn-overlay')?.remove();
const dlOverlay = document.createElement('button');
dlOverlay.className = 'dl-btn-overlay';
dlOverlay.title = 'Auf Server laden';
dlOverlay.innerHTML = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
dlOverlay.onclick = (e) => { e.stopPropagation(); openDlModal({artistName:a.artistName,trackName:a.collectionName,artworkUrl100:a.artworkUrl100,collectionName:a.collectionName,isAlbum:true,itunesData:a}); };
dlOverlay.onclick = (e) => { e.stopPropagation(); openDlModal({artistName:a.artistName,trackName:a.collectionName,artworkUrl100:a.artworkUrl100,collectionName:a.collectionName,isAlbum:true,deezerData:a}); };
c.querySelector('.play-overlay').appendChild(dlOverlay);
c.onclick = () => loadItunesAlbum(a);
c.onclick = () => loadDeezerAlbum(a);
row.appendChild(c);
});
s.appendChild(row); el.appendChild(s);
@@ -177,7 +177,7 @@ export async function doGlobalSearch(q) {
s.appendChild(list); el.appendChild(s);
}
if(!artists.length && !albums.length && !trackResults.length) {
const err = itunesLastError();
const err = deezerLastError();
if (err) {
// Distinguish "genuinely no results" from "search backend is down" so the user knows where
// the problem is.
@@ -192,9 +192,9 @@ export async function doGlobalSearch(q) {
} catch(e) { el.innerHTML = `<p style="color:var(--danger)">Suche fehlgeschlagen: ${e.message}</p>`; }
}
export async function loadItunesAlbum(album) {
export async function loadDeezerAlbum(album) {
try {
const tracks = await itunesLookupAlbum(album.collectionId);
const tracks = await deezerLookupAlbum(album.collectionId);
if(!tracks.length) { toast('Keine Tracks gefunden'); return; }
const el = document.getElementById('global-search-results');
const s = document.createElement('div'); s.className = 'search-section';
@@ -204,7 +204,7 @@ export async function loadItunesAlbum(album) {
<div style="font-size:15px;font-weight:700">${esc(album.collectionName)}</div>
<div style="font-size:12px;color:var(--text2)">${esc(album.artistName)}</div>
</div>
<button class="btn-outline blue" style="font-size:12px;padding:7px 12px" onclick="openDlModal({artistName:'${esc(album.artistName)}',trackName:'${esc(album.collectionName)}',artworkUrl100:'${album.artworkUrl100||''}',collectionName:'${esc(album.collectionName)}',isAlbum:true,itunesData:${JSON.stringify(album).replace(/"/g,'&quot;')}})">⬇ Album laden</button>
<button class="btn-outline blue" style="font-size:12px;padding:7px 12px" onclick="openDlModal({artistName:'${esc(album.artistName)}',trackName:'${esc(album.collectionName)}',artworkUrl100:'${album.artworkUrl100||''}',collectionName:'${esc(album.collectionName)}',isAlbum:true,deezerData:${JSON.stringify(album).replace(/"/g,'&quot;')}})">⬇ Album laden</button>
</div>
<div class="search-section-title">Tracks</div>`;
const list = document.createElement('div'); list.className = 'song-list';
@@ -214,8 +214,8 @@ export async function loadItunesAlbum(album) {
} catch(e) { toast('Fehler: ' + e.message); }
}
export function loadItunesArtistResults(artistId, artistName, fanCount) {
navigate('artist-detail', { id: null, name: artistName, itunesArtistId: artistId, itunesFanCount: fanCount });
export function loadDeezerArtistResults(artistId, artistName, fanCount) {
navigate('artist-detail', { id: null, name: artistName, deezerArtistId: artistId, deezerFanCount: fanCount });
}
// ─── SEARCH HELPERS ───
@@ -227,7 +227,7 @@ export function searchSimilarArtist(artistName) {
export async function searchAndPreview(trackName, artistName) {
try {
const res = await itunesSearch(trackName+' '+artistName, 'musicTrack', 1);
const res = await deezerSearch(trackName+' '+artistName, 'musicTrack', 1);
if(res[0]?.previewUrl) playPreview(res[0]); else toast('Kein Preview verfügbar');
} catch { toast('Suche fehlgeschlagen'); }
}
+4 -4
View File
@@ -1,6 +1,6 @@
import { S } from '../state.js';
import { esc, toast } from '../utils.js';
import { itunesSearch } from '../api.js';
import { deezerSearch } from '../api.js';
// Circular imports — safe (only used inside function bodies)
import { makeGlobalTrackItem } from '../ui/components.js';
import { openDlModal } from '../ui/ui.js';
@@ -236,7 +236,7 @@ function _renderSpotifyPreview(data) {
(data.tracks || []).forEach((t, i) => list.appendChild(makeGlobalTrackItem(t, i, data.tracks)));
section.style.display = '';
// Lazy-load correct per-track artwork and canonical names via iTunes in the background
// Lazy-load correct per-track artwork and canonical names via Deezer in the background
if (count > 0 && S.cfg.backendUrl) {
_loadSpotifyTrackArtwork(data.tracks, list);
}
@@ -284,7 +284,7 @@ async function _loadSpotifyTrackArtwork(tracks, listEl) {
const idx = i + bi;
try {
const firstArtist = (t.artistName || '').split(/\s*[,/•]\s*/)[0].trim();
const results = await itunesSearch(`${t.trackName} ${firstArtist}`, 'musicTrack', 1);
const results = await deezerSearch(`${t.trackName} ${firstArtist}`, 'musicTrack', 1);
const match = results[0];
if (!match) return;
// Always safe to enrich: artwork, preview, explicit, canonical artist name
@@ -305,7 +305,7 @@ async function _loadSpotifyTrackArtwork(tracks, listEl) {
if (match.collectionName) t.collectionName = match.collectionName;
if (match.releaseDate) t.releaseDate = match.releaseDate;
if (match.trackNumber) t.trackNumber = match.trackNumber;
// Only use iTunes duration if it looks like a full song (> 30s) — jingles/previews
// Only use Deezer duration if it looks like a full song (> 30s) — jingles/previews
// returning a small trackTimeMillis would corrupt the yt-dlp duration filter.
if (match.trackTimeMillis > 30000) t.durationMs = match.trackTimeMillis;
}