From 94e7f46b3991228f37426d0e7e23bfd5fbc67c1f Mon Sep 17 00:00:00 2001 From: nk Date: Thu, 4 Jun 2026 09:45:35 +0300 Subject: [PATCH] =?UTF-8?q?perf(enrich):=20=D1=80=D0=BE=D1=82=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B4=D0=B2=D1=83=D1=85=20=D1=82=D0=BE=D0=BA?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=D0=B2=20Discogs=20(~108/=D0=BC=D0=B8=D0=BD?= =?UTF-8?q?=20=D0=B6=D0=B0=D0=BD=D1=80=D0=BE=D0=B2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discogs лимит ~60/мин на токен. Поддержка нескольких токенов (DISCOGS_TOKEN, DISCOGS_TOKEN2) — у каждого свой слот, берём наименее загруженный → суммарно вдвое быстрее жанры/стили/лейблы, без 429. Токены — в env (не в гите). Co-Authored-By: Claude Opus 4.8 --- docker-compose.yml | 1 + src/enrich/discogs.service.ts | 29 +++++++++++++++++++---------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 771f043..8f7b51d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,7 @@ services: - FRONTEND_URL=${FRONTEND_URL:-https://radiola.app} # Обогащение треков - DISCOGS_TOKEN=${DISCOGS_TOKEN} + - DISCOGS_TOKEN2=${DISCOGS_TOKEN2} - COVERS_DIR=/data/covers - PUBLIC_BASE_URL=${PUBLIC_BASE_URL:-http://121.127.37.212:3000} volumes: diff --git a/src/enrich/discogs.service.ts b/src/enrich/discogs.service.ts index 5b97de9..c406a33 100644 --- a/src/enrich/discogs.service.ts +++ b/src/enrich/discogs.service.ts @@ -30,37 +30,46 @@ interface DiscogsSearchItem { @Injectable() export class DiscogsService { private readonly logger = new Logger(DiscogsService.name); - private readonly token = process.env.DISCOGS_TOKEN ?? ''; private readonly userAgent = 'radiOLA/1.0 +https://radiola.app'; - // Глобальный rate-limiter: Discogs ~60 запросов/мин. Разносим вызовы ≥1.1с - // независимо от параллельности обогащения (иначе 429). - private nextSlot = 0; + // Несколько токенов Discogs — лимит ~60/мин НА ТОКЕН. У каждого свой слот, + // выбираем наименее загруженный → суммарно N×~54/мин без 429. + private readonly tokens = [ + process.env.DISCOGS_TOKEN ?? '', + process.env.DISCOGS_TOKEN2 ?? '', + ].filter((t) => t.length > 0); + private readonly slots: number[] = this.tokens.map(() => 0); private readonly minIntervalMs = 1100; // Без токена обогащение жанрами не работает (поиск требует авторизации) get enabled(): boolean { - return this.token.length > 0; + return this.tokens.length > 0; } - private async rateLimit(): Promise { + // Резервирует слот наименее загруженного токена, ждёт его и возвращает токен + private async pickToken(): Promise { + let idx = 0; + for (let i = 1; i < this.slots.length; i++) { + if (this.slots[i] < this.slots[idx]) idx = i; + } const now = Date.now(); - const start = Math.max(now, this.nextSlot); - this.nextSlot = start + this.minIntervalMs; + const start = Math.max(now, this.slots[idx]); + this.slots[idx] = start + this.minIntervalMs; const wait = start - now; if (wait > 0) await new Promise((r) => setTimeout(r, wait)); + return this.tokens[idx]; } async lookup(artist: string, song: string): Promise { if (!this.enabled) return null; - await this.rateLimit(); + const token = await this.pickToken(); const params = new URLSearchParams({ artist, track: song, type: 'release', per_page: '5', - token: this.token, + token, }); const url = `https://api.discogs.com/database/search?${params.toString()}`;