From 5bd7bfb923bf7f0f5b1ae9d5038409b9b785a042 Mon Sep 17 00:00:00 2001 From: nk Date: Wed, 3 Jun 2026 15:00:02 +0300 Subject: [PATCH] =?UTF-8?q?feat(enrich):=20iTunes-=D1=84=D0=BE=D0=BB=D0=B1?= =?UTF-8?q?=D1=8D=D0=BA=20=D0=B4=D0=BB=D1=8F=20=D0=B6=D0=B0=D0=BD=D1=80?= =?UTF-8?q?=D0=B0/=D0=B0=D0=BB=D1=8C=D0=B1=D0=BE=D0=BC=D0=B0/=D0=B3=D0=BE?= =?UTF-8?q?=D0=B4=D0=B0=20(=D0=B3=D0=B8=D0=B1=D1=80=D0=B8=D0=B4=20=D1=81?= =?UTF-8?q?=20Discogs)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iTunes-запрос (уже делается ради обложки) теперь отдаёт и альбом/год/жанр. Жанр: Discogs (тонкий) → iTunes (грубый фолбэк) — поднимает покрытие жанров в чартах. Альбом и дата релиза заполняются из iTunes. Стили и лейбл — по-прежнему только Discogs. Co-Authored-By: Claude Opus 4.8 --- src/enrich/enrichment.service.ts | 75 +++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/src/enrich/enrichment.service.ts b/src/enrich/enrichment.service.ts index 85f5952..ffac9cd 100644 --- a/src/enrich/enrichment.service.ts +++ b/src/enrich/enrichment.service.ts @@ -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 { + ): 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; }