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:
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user