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:
1 parent
bc87a04da5
commit
1e206e8df3
13 files changed
+118
-108
No files matched your search
+20
-14
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user