fix(images): allow the CDNs artwork actually lives on, add Deezer artist photos
Typecheck / typecheck (backend) (push) Has been cancelled
Typecheck / typecheck (workers) (push) Has been cancelled

The image proxy allowlist covered none of the hosts enrichment writes, so every
album cover answered 403: 305 covers sit on coverartarchive.org (which 302s to
archive.org and on to an ia*.us.archive.org node), 126 on mzstatic, 115 on
dzcdn, 26 on discogs.

Artist images were empty for a different reason — no working source. Fanart
needs a key the worker does not have, TheAudioDB and Discogs 404 on most names,
Wikidata needs an MBID that 638 of 734 artists lack, and Last.fm stopped
serving photos. Deezer needs no auth and its host is already allowlisted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kami
2026-08-06 00:23:03 +04:00
parent 93619824d8
commit d371bd97f3
3 changed files with 61 additions and 0 deletions
+35
View File
@@ -33,6 +33,10 @@ interface DeezerSearchResponse {
data?: DeezerSearchResult[];
}
/** Lowercase and strip diacritics, so a query for "Bjork" matches "Björk". */
const foldName = (name: string) =>
name.trim().toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '');
export class DeezerClient {
private readonly baseUrl = 'https://api.deezer.com';
private readonly userAgent: string;
@@ -81,4 +85,35 @@ export class DeezerClient {
return null;
}
}
/**
* Artist photo (1000×1000 `picture_xl`) by name, or null.
*
* ponytail: exact case-insensitive name match only. Deezer's artist search is
* fuzzy and happily returns a tribute band for a near miss, so a wrong photo
* is worse than the placeholder. Loosen it only if real artists get skipped.
*/
async searchArtistImage(artist: string): Promise<string | null> {
if (artist.trim() === '') return null;
const qs = new URLSearchParams({ q: artist });
try {
const data = await requestJson<{ data?: { name?: string; picture_xl?: string; picture_big?: string }[] }>(
`${this.baseUrl}/search/artist?${qs.toString()}`,
{ userAgent: this.userAgent, minIntervalMs: this.minIntervalMs }
);
const want = foldName(artist);
for (const a of data.data ?? []) {
if (foldName(a.name ?? '') !== want) continue;
// Deezer serves a blank grey tile for artists it has no photo for: the
// path is either empty or the md5 of the empty string. Both are useless,
// and the same name can appear twice with only the second one real.
const url = a.picture_xl ?? a.picture_big ?? '';
if (url && !url.includes('d41d8cd98f00b204e9800998ecf8427e') && !url.includes('/artist//')) return url;
}
return null;
} catch (err) {
console.warn('[Deezer] searchArtistImage failed:', (err as Error).message);
return null;
}
}
}