perf(enrich): быстрый cover-only проход эфира через iTunes (без Discogs-гейта)

Discogs-лимитер делал Discogs узким местом (54/мин) для ВСЕХ треков, тормозя
обложки. Теперь крон now-playing красит эфир обложками напрямую через iTunes
(4 параллельно, без Discogs), а полное обогащение жанрами идёт фоном. Обложки
живого набора появляются быстро.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nk
2026-06-03 20:56:50 +03:00
parent 5164843824
commit 36043c32b0

View File

@@ -60,14 +60,15 @@ export class EnrichmentService {
for (const t of pending) this.enqueue(t.id); for (const t of pending) this.enqueue(t.id);
} }
// Раз в минуту гарантируем обложку у играющих СЕЙЧАС треков: создаём Track // Раз в минуту обеспечиваем ОБЛОЖКУ у играющих СЕЙЧАС треков — быстрый проход
// при отсутствии (без записи проигрывания) и приоритетно обогащаем тех, у кого // ТОЛЬКО через iTunes (без Discogs, который лимитирован 54/мин и тормозил бы
// ещё нет обложки. Так now-playing-обложки появляются быстро у всех сетей. // обложки). Полное обогащение (жанр/стили) идёт фоном через backfill/enqueue.
@Cron(CronExpression.EVERY_MINUTE) @Cron(CronExpression.EVERY_MINUTE)
async enrichNowPlaying(): Promise<void> { async enrichNowPlaying(): Promise<void> {
const rows = await this.prisma.nowPlaying.findMany({ const rows = await this.prisma.nowPlaying.findMany({
select: { artist: true, song: true }, select: { artist: true, song: true },
}); });
const todo: { id: string; artist: string; song: string; normKey: string }[] = [];
for (const r of rows) { for (const r of rows) {
const artist = (r.artist ?? '').trim(); const artist = (r.artist ?? '').trim();
const song = (r.song ?? '').trim(); const song = (r.song ?? '').trim();
@@ -77,9 +78,41 @@ export class EnrichmentService {
where: { normKey }, where: { normKey },
create: { normKey, artist, song }, create: { normKey, artist, song },
update: {}, update: {},
select: { id: true, coverUrl: true }, select: { id: true, coverUrl: true, enrichStatus: true },
}); });
if (!track.coverUrl) this.enqueue(track.id, { priority: true }); if (!track.coverUrl) todo.push({ id: track.id, artist, song, normKey });
// полное обогащение (жанр) — в общую очередь, если ещё не сделано
if (track.enrichStatus !== 'done') this.enqueue(track.id);
}
// Быстрый cover-only проход, по 4 параллельно (iTunes терпимо)
for (let i = 0; i < todo.length; i += 4) {
await Promise.all(todo.slice(i, i + 4).map((t) => this.coverFast(t)));
}
}
// Только обложка через iTunes (без Discogs) — для быстрого покрытия эфира
private async coverFast(t: {
id: string;
artist: string;
song: string;
normKey: string;
}): Promise<void> {
try {
const itunes = await this.fetchItunes(t.artist, t.song);
const candidate = itunes?.coverUrl;
if (!candidate) return;
const stored = await this.covers.store(candidate, t.normKey);
if (!stored) return;
await this.prisma.track.update({
where: { id: t.id },
data: {
coverUrl: stored,
genre: itunes?.genre ?? undefined,
album: itunes?.album ?? undefined,
},
});
} catch {
// сбой iTunes (429/сеть) — добёрём на следующем тике
} }
} }