fix(enrich): не помечать done при сбое запроса iTunes (промах не застывает)

Отличаем сбой запроса (сеть/HTTP-ошибка → ретраить, оставляем pending) от чистого
'не найдено' (done). Раньше транзиентный сбой iTunes под нагрузкой навсегда лишал
трек обложки. fetchItunes теперь бросает при !res.ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nk
2026-06-03 18:09:54 +03:00
parent 40a9f3968f
commit 338f189f33

View File

@@ -117,7 +117,14 @@ export class EnrichmentService {
// iTunes: обложка (покрытие почти как у Record) + альбом/год/жанр как // iTunes: обложка (покрытие почти как у Record) + альбом/год/жанр как
// фолбэк к Discogs. Гибрид: стили и лейбл — только Discogs. // фолбэк к Discogs. Гибрид: стили и лейбл — только Discogs.
const itunes = await this.fetchItunes(track.artist, track.song); // Отличаем сбой запроса (ретраить) от чистого «не найдено» (done).
let itunes: Awaited<ReturnType<typeof this.fetchItunes>> = null;
let itunesFailed = false;
try {
itunes = await this.fetchItunes(track.artist, track.song);
} catch {
itunesFailed = true;
}
// Обложка → WebP к себе (если ещё не наша) // Обложка → WebP к себе (если ещё не наша)
let coverUrl = track.coverUrl; let coverUrl = track.coverUrl;
@@ -136,9 +143,10 @@ export class EnrichmentService {
itunes?.releaseDate ?? itunes?.releaseDate ??
(data?.year ? new Date(Date.UTC(data.year, 0, 1)) : null); (data?.year ? new Date(Date.UTC(data.year, 0, 1)) : null);
// Без токена Discogs стили/лейбл не получим — оставляем pending, чтобы // Помечаем done, если обогатились. НЕ помечаем (оставляем pending для
// добрать позже (но обложку/жанр-iTunes уже сохранили). // ретрая), если: нет токена Discogs, ИЛИ запрос к iTunes упал И обложку
const enriched = this.discogs.enabled; // так и не получили (транзиентный сбой — промах не должен застывать).
const enriched = this.discogs.enabled && !(itunesFailed && !coverUrl);
await this.prisma.track.update({ await this.prisma.track.update({
where: { id: trackId }, where: { id: trackId },
@@ -179,46 +187,43 @@ export class EnrichmentService {
releaseDate: Date | null; releaseDate: Date | null;
genre: string | null; genre: string | null;
} | null> { } | null> {
try { // Пунктуация в названии («St.Thomas», «feat.») ломает поиск iTunes —
// Пунктуация в названии («St.Thomas», «feat.») ломает поиск iTunes — // заменяем все не-буквенно-цифровые символы на пробел и схлопываем.
// заменяем все не-буквенно-цифровые символы на пробел и схлопываем. const clean = `${artist} ${song}`
const clean = `${artist} ${song}` .replace(/[^\p{L}\p{N}]+/gu, ' ')
.replace(/[^\p{L}\p{N}]+/gu, ' ') .replace(/\s+/g, ' ')
.replace(/\s+/g, ' ') .trim();
.trim(); const term = encodeURIComponent(clean);
const term = encodeURIComponent(clean); const url = `https://itunes.apple.com/search?term=${term}&entity=song&limit=1`;
const url = `https://itunes.apple.com/search?term=${term}&entity=song&limit=1`; // Бросаем при сетевой/HTTP-ошибке — это сбой, а не «не найдено».
const res = await fetch(url, { const res = await fetch(url, {
headers: { 'User-Agent': 'radiOLA/1.0 +https://radiola.app' }, headers: { 'User-Agent': 'radiOLA/1.0 +https://radiola.app' },
}); });
if (!res.ok) return null; if (!res.ok) throw new Error(`iTunes ${res.status}`);
const data = (await res.json()) as { const data = (await res.json()) as {
results?: Array<{ results?: Array<{
artworkUrl100?: string; artworkUrl100?: string;
collectionName?: string; collectionName?: string;
releaseDate?: string; releaseDate?: string;
primaryGenreName?: string; primaryGenreName?: string;
}>; }>;
}; };
const r = data.results?.[0]; const r = data.results?.[0];
if (!r) return null; if (!r) return null; // чистое «не найдено»
const cover = r.artworkUrl100 const cover = r.artworkUrl100
? r.artworkUrl100.replace(/\/\d+x\d+bb\./, '/600x600bb.') ? r.artworkUrl100.replace(/\/\d+x\d+bb\./, '/600x600bb.')
: null; : null;
const rd = r.releaseDate ? new Date(r.releaseDate) : null; const rd = r.releaseDate ? new Date(r.releaseDate) : null;
const validDate = rd && !isNaN(rd.getTime()) ? rd : null; const validDate = rd && !isNaN(rd.getTime()) ? rd : null;
return { return {
coverUrl: cover, coverUrl: cover,
album: r.collectionName ?? null, album: r.collectionName ?? null,
year: validDate ? validDate.getUTCFullYear() : null, year: validDate ? validDate.getUTCFullYear() : null,
releaseDate: validDate, releaseDate: validDate,
genre: r.primaryGenreName ?? null, genre: r.primaryGenreName ?? null,
}; };
} catch {
return null;
}
} }
private isSelfHosted(url: string): boolean { private isSelfHosted(url: string): boolean {