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

+20 -14
View File
@@ -271,7 +271,7 @@ export async function romajiConvert(texts) {
// Apple's iTunes /search returns 403 from server IPs, so Deezer is the metadata backend for
// everything: search, album/artist lookups AND the "Neu & Beliebt" charts. Free, no key, returns
// explicit versions + artist images + track durations. Proxied through the backend to avoid any
// CORS surprises. All results are mapped to the legacy "ItunesItem" shape so the UI + download
// CORS surprises. All results are mapped to the legacy "DeezerItem" shape so the UI + download
// flow stay unchanged.
function _deezerBase() {
return S.cfg.backendUrl ? `${S.cfg.backendUrl}/api/deezer` : 'https://api.deezer.com';
@@ -306,23 +306,23 @@ function _dzArtist(a) {
artistId: a.id, artistName: a.name,
artworkUrl100: a.picture_big || a.picture_medium || '',
// Deezer's artist search does NOT rank by popularity — several artists can share a name
// (5× "Joji"). fanCount lets us pick the one the user means (see itunesSearch + detail.js).
// (5× "Joji"). fanCount lets us pick the one the user means (see deezerSearch + detail.js).
fanCount: a.nb_fan ?? undefined,
};
}
// Last search outcome: null = ok, else HTTP status or 'network'. Read by the search view to tell
// "no results" apart from "search backend down".
let _itunesLastError = null;
export function itunesLastError() { return _itunesLastError; }
let _deezerLastError = null;
export function deezerLastError() { return _deezerLastError; }
// Keeps the old name so all callers stay unchanged. entity: musicTrack | album | musicArtist.
export async function itunesSearch(query, entity='musicTrack', limit=25) {
export async function deezerSearch(query, entity='musicTrack', limit=25) {
try {
const path = entity === 'album' ? 'search/album' : entity === 'musicArtist' ? 'search/artist' : 'search';
const r = await fetch(`${_deezerBase()}/${path}?q=${encodeURIComponent(query)}&limit=${limit}`);
if (!r.ok) { _itunesLastError = r.status; return []; }
_itunesLastError = null;
if (!r.ok) { _deezerLastError = r.status; return []; }
_deezerLastError = null;
const txt = await r.text();
const data = (txt ? JSON.parse(txt) : {}).data || [];
if (entity === 'album') return data.map(_dzAlbum);
@@ -330,12 +330,12 @@ export async function itunesSearch(query, entity='musicTrack', limit=25) {
// (Deezer returns them in an unhelpful order — the real "Joji" isn't first).
if (entity === 'musicArtist') return data.map(_dzArtist).sort((x, y) => (y.fanCount || 0) - (x.fanCount || 0));
return data.map(_dzTrack);
} catch { _itunesLastError = 'network'; return []; }
} catch { _deezerLastError = 'network'; return []; }
}
// Album tracks (for the album detail view + album download). Deezer's /album/{id} embeds tracks +
// album-level release/genre/cover, so one request gives everything.
export async function itunesLookupAlbum(collectionId) {
export async function deezerLookupAlbum(collectionId) {
try {
const r = await fetch(`${_deezerBase()}/album/${collectionId}`);
const a = await r.json();
@@ -351,17 +351,23 @@ export async function itunesLookupAlbum(collectionId) {
} catch { return []; }
}
export async function itunesLookupArtistSongs(artistId, limit=15) {
export async function deezerLookupArtistSongs(artistId, limit=15) {
try {
const r = await fetch(`${_deezerBase()}/artist/${artistId}/top?limit=${limit}`);
return ((await r.json()).data || []).map(_dzTrack);
} catch { return []; }
}
export async function itunesLookupArtistAlbums(artistId, limit=100) {
// Deezer's /artist/{id}/albums list OMITS the artist object, so _dzAlbum yields artistName:''.
// We already know whose page this is → backfill artistName so downstream download/detail cards
// don't fall back to the literal "Unknown Artist" (which poisoned the yt-dlp search).
export async function deezerLookupArtistAlbums(artistId, limit=100, artistName='') {
try {
const r = await fetch(`${_deezerBase()}/artist/${artistId}/albums?limit=${limit}`);
return ((await r.json()).data || []).map(_dzAlbum).filter(a => a.collectionType === 'Album');
return ((await r.json()).data || [])
.map(_dzAlbum)
.filter(a => a.collectionType === 'Album')
.map(a => (a.artistName ? a : { ...a, artistName }));
} catch { return []; }
}
@@ -375,7 +381,7 @@ export async function deezerArtistImage(artistId) {
// "Neu & Beliebt" — Deezer's global chart (Apple's charts RSS needed a proxy and the search
// fallback is 403 from server IPs). Mapped to { name, artistName, artworkUrl, artworkUrl100 }.
export async function itunesTopCharts() {
export async function deezerTopCharts() {
try {
const r = await fetch(`${_deezerBase()}/chart/0/albums?limit=50`);
const d = await r.json();
@@ -386,7 +392,7 @@ export async function itunesTopCharts() {
} catch { return []; }
}
export async function itunesTopSongs() {
export async function deezerTopSongs() {
try {
const r = await fetch(`${_deezerBase()}/chart/0/tracks?limit=50`);
const d = await r.json();