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)
|
? await this.discogs.lookup(track.artist, track.song)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Обложка: iTunes (покрытие почти как у Record — Apple-арт) → Discogs →
|
// iTunes: обложка (покрытие почти как у Record) + альбом/год/жанр как
|
||||||
// уже имеющаяся. Приводим к WebP и кладём к себе (если ещё не наша).
|
// фолбэк к Discogs. Гибрид: стили и лейбл — только Discogs.
|
||||||
const itunesCover = await this.fetchItunesCover(track.artist, track.song);
|
const itunes = await this.fetchItunes(track.artist, track.song);
|
||||||
|
|
||||||
|
// Обложка → WebP к себе (если ещё не наша)
|
||||||
let coverUrl = track.coverUrl;
|
let coverUrl = track.coverUrl;
|
||||||
const candidate = itunesCover ?? data?.coverImageUrl ?? track.coverUrl;
|
const candidate = itunes?.coverUrl ?? data?.coverImageUrl ?? track.coverUrl;
|
||||||
if (candidate && !this.isSelfHosted(candidate)) {
|
if (candidate && !this.isSelfHosted(candidate)) {
|
||||||
const stored = await this.covers.store(candidate, track.normKey);
|
const stored = await this.covers.store(candidate, track.normKey);
|
||||||
if (stored) coverUrl = stored;
|
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;
|
const enriched = this.discogs.enabled;
|
||||||
|
|
||||||
await this.prisma.track.update({
|
await this.prisma.track.update({
|
||||||
where: { id: trackId },
|
where: { id: trackId },
|
||||||
data: {
|
data: {
|
||||||
genre: data?.genre ?? track.genre,
|
genre,
|
||||||
styles: data?.styles?.length ? data.styles : track.styles,
|
styles: data?.styles?.length ? data.styles : track.styles,
|
||||||
label: data?.label ?? track.label,
|
label: data?.label ?? track.label,
|
||||||
year: data?.year ?? track.year,
|
year,
|
||||||
|
album,
|
||||||
discogsId: data?.discogsId ?? track.discogsId,
|
discogsId: data?.discogsId ?? track.discogsId,
|
||||||
coverUrl,
|
coverUrl,
|
||||||
releaseDate:
|
releaseDate,
|
||||||
!track.releaseDate && data?.year
|
|
||||||
? new Date(Date.UTC(data.year, 0, 1))
|
|
||||||
: track.releaseDate,
|
|
||||||
enrichStatus: enriched ? 'done' : 'pending',
|
enrichStatus: enriched ? 'done' : 'pending',
|
||||||
enrichedAt: enriched ? new Date() : track.enrichedAt,
|
enrichedAt: enriched ? new Date() : track.enrichedAt,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Обогащён "${track.artist} — ${track.song}": genre=${data?.genre ?? '—'}, label=${data?.label ?? '—'}`,
|
`Обогащён "${track.artist} — ${track.song}": genre=${genre ?? '—'}, label=${data?.label ?? '—'}`,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.debug(`Обогащение ${trackId} не удалось: ${(e as Error).message}`);
|
this.logger.debug(`Обогащение ${trackId} не удалось: ${(e as Error).message}`);
|
||||||
@@ -158,12 +167,18 @@ export class EnrichmentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обложка из iTunes Search API (без ключа, высокое покрытие).
|
// iTunes Search API (без ключа, высокое покрытие): обложка (600×600) +
|
||||||
// artworkUrl100 апскейлим до 600×600.
|
// альбом/год/жанр/дата релиза.
|
||||||
private async fetchItunesCover(
|
private async fetchItunes(
|
||||||
artist: string,
|
artist: string,
|
||||||
song: string,
|
song: string,
|
||||||
): Promise<string | null> {
|
): Promise<{
|
||||||
|
coverUrl: string | null;
|
||||||
|
album: string | null;
|
||||||
|
year: number | null;
|
||||||
|
releaseDate: Date | null;
|
||||||
|
genre: string | null;
|
||||||
|
} | null> {
|
||||||
try {
|
try {
|
||||||
const term = encodeURIComponent(`${artist} ${song}`.trim());
|
const term = encodeURIComponent(`${artist} ${song}`.trim());
|
||||||
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`;
|
||||||
@@ -172,11 +187,29 @@ export class EnrichmentService {
|
|||||||
});
|
});
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
const data = (await res.json()) as {
|
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 {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user