feat(enrich): iTunes-фолбэк для жанра/альбома/года (гибрид с Discogs)
iTunes-запрос (уже делается ради обложки) теперь отдаёт и альбом/год/жанр. Жанр: Discogs (тонкий) → iTunes (грубый фолбэк) — поднимает покрытие жанров в чартах. Альбом и дата релиза заполняются из iTunes. Стили и лейбл — по-прежнему только Discogs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -115,40 +115,49 @@ export class EnrichmentService {
|
||||
? await this.discogs.lookup(track.artist, track.song)
|
||||
: null;
|
||||
|
||||
// Обложка: iTunes (покрытие почти как у Record — Apple-арт) → Discogs →
|
||||
// уже имеющаяся. Приводим к WebP и кладём к себе (если ещё не наша).
|
||||
const itunesCover = await this.fetchItunesCover(track.artist, track.song);
|
||||
// iTunes: обложка (покрытие почти как у Record) + альбом/год/жанр как
|
||||
// фолбэк к Discogs. Гибрид: стили и лейбл — только Discogs.
|
||||
const itunes = await this.fetchItunes(track.artist, track.song);
|
||||
|
||||
// Обложка → WebP к себе (если ещё не наша)
|
||||
let coverUrl = track.coverUrl;
|
||||
const candidate = itunesCover ?? data?.coverImageUrl ?? track.coverUrl;
|
||||
const candidate = itunes?.coverUrl ?? data?.coverImageUrl ?? track.coverUrl;
|
||||
if (candidate && !this.isSelfHosted(candidate)) {
|
||||
const stored = await this.covers.store(candidate, track.normKey);
|
||||
if (stored) coverUrl = stored;
|
||||
}
|
||||
|
||||
// Без токена Discogs жанры не получим — оставляем статус pending,
|
||||
// чтобы добрать позже (когда токен появится), но обложку уже сохранили.
|
||||
// Жанр: Discogs приоритетнее (тонкий), затем iTunes (грубый фолбэк)
|
||||
const genre = data?.genre ?? itunes?.genre ?? track.genre;
|
||||
const year = data?.year ?? itunes?.year ?? track.year;
|
||||
const album = track.album ?? itunes?.album ?? null;
|
||||
const releaseDate =
|
||||
track.releaseDate ??
|
||||
itunes?.releaseDate ??
|
||||
(data?.year ? new Date(Date.UTC(data.year, 0, 1)) : null);
|
||||
|
||||
// Без токена Discogs стили/лейбл не получим — оставляем pending, чтобы
|
||||
// добрать позже (но обложку/жанр-iTunes уже сохранили).
|
||||
const enriched = this.discogs.enabled;
|
||||
|
||||
await this.prisma.track.update({
|
||||
where: { id: trackId },
|
||||
data: {
|
||||
genre: data?.genre ?? track.genre,
|
||||
genre,
|
||||
styles: data?.styles?.length ? data.styles : track.styles,
|
||||
label: data?.label ?? track.label,
|
||||
year: data?.year ?? track.year,
|
||||
year,
|
||||
album,
|
||||
discogsId: data?.discogsId ?? track.discogsId,
|
||||
coverUrl,
|
||||
releaseDate:
|
||||
!track.releaseDate && data?.year
|
||||
? new Date(Date.UTC(data.year, 0, 1))
|
||||
: track.releaseDate,
|
||||
releaseDate,
|
||||
enrichStatus: enriched ? 'done' : 'pending',
|
||||
enrichedAt: enriched ? new Date() : track.enrichedAt,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.debug(
|
||||
`Обогащён "${track.artist} — ${track.song}": genre=${data?.genre ?? '—'}, label=${data?.label ?? '—'}`,
|
||||
`Обогащён "${track.artist} — ${track.song}": genre=${genre ?? '—'}, label=${data?.label ?? '—'}`,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.debug(`Обогащение ${trackId} не удалось: ${(e as Error).message}`);
|
||||
@@ -158,12 +167,18 @@ export class EnrichmentService {
|
||||
}
|
||||
}
|
||||
|
||||
// Обложка из iTunes Search API (без ключа, высокое покрытие).
|
||||
// artworkUrl100 апскейлим до 600×600.
|
||||
private async fetchItunesCover(
|
||||
// iTunes Search API (без ключа, высокое покрытие): обложка (600×600) +
|
||||
// альбом/год/жанр/дата релиза.
|
||||
private async fetchItunes(
|
||||
artist: string,
|
||||
song: string,
|
||||
): Promise<string | null> {
|
||||
): Promise<{
|
||||
coverUrl: string | null;
|
||||
album: string | null;
|
||||
year: number | null;
|
||||
releaseDate: Date | null;
|
||||
genre: string | null;
|
||||
} | null> {
|
||||
try {
|
||||
const term = encodeURIComponent(`${artist} ${song}`.trim());
|
||||
const url = `https://itunes.apple.com/search?term=${term}&entity=song&limit=1`;
|
||||
@@ -172,11 +187,29 @@ export class EnrichmentService {
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json()) as {
|
||||
results?: Array<{ artworkUrl100?: string }>;
|
||||
results?: Array<{
|
||||
artworkUrl100?: string;
|
||||
collectionName?: string;
|
||||
releaseDate?: string;
|
||||
primaryGenreName?: string;
|
||||
}>;
|
||||
};
|
||||
const r = data.results?.[0];
|
||||
if (!r) return null;
|
||||
|
||||
const cover = r.artworkUrl100
|
||||
? r.artworkUrl100.replace(/\/\d+x\d+bb\./, '/600x600bb.')
|
||||
: null;
|
||||
const rd = r.releaseDate ? new Date(r.releaseDate) : null;
|
||||
const validDate = rd && !isNaN(rd.getTime()) ? rd : null;
|
||||
|
||||
return {
|
||||
coverUrl: cover,
|
||||
album: r.collectionName ?? null,
|
||||
year: validDate ? validDate.getUTCFullYear() : null,
|
||||
releaseDate: validDate,
|
||||
genre: r.primaryGenreName ?? null,
|
||||
};
|
||||
const art = data.results?.[0]?.artworkUrl100;
|
||||
if (!art) return null;
|
||||
return art.replace(/\/\d+x\d+bb\./, '/600x600bb.');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user