From e982fde73069332af4f08a8cc8125fb1e49fdea7 Mon Sep 17 00:00:00 2001 From: nk Date: Thu, 4 Jun 2026 10:01:37 +0300 Subject: [PATCH] =?UTF-8?q?feat(enrich):=202-=D0=B9=20=D1=82=D0=BE=D0=BA?= =?UTF-8?q?=D0=B5=D0=BD=20Discogs=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20DE-?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=BA=D1=81=D0=B8=20(2=20IP=20=E2=86=92=20~1?= =?UTF-8?q?08/=D0=BC=D0=B8=D0=BD=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 лимитит по IP. token1 идёт напрямую (IP RU), token2 — через форвард-прокси на DE (IP DE, tinyproxy, доступ только с RU). Два IP, у каждого свой слот ~54/мин → суммарно ~108/мин жанров без 429. undici ProxyAgent. Без DISCOGS_PROXY — только token1 (54/мин). Co-Authored-By: Claude Opus 4.8 --- docker-compose.yml | 1 + package-lock.json | 12 ++++++++- package.json | 3 ++- src/enrich/discogs.service.ts | 46 +++++++++++++++++++++++------------ 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8f7b51d..c801c48 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,6 +23,7 @@ services: # Обогащение треков - DISCOGS_TOKEN=${DISCOGS_TOKEN} - DISCOGS_TOKEN2=${DISCOGS_TOKEN2} + - DISCOGS_PROXY=${DISCOGS_PROXY} - COVERS_DIR=/data/covers - PUBLIC_BASE_URL=${PUBLIC_BASE_URL:-http://121.127.37.212:3000} volumes: diff --git a/package-lock.json b/package-lock.json index 183060f..b26a112 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,8 @@ "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "sharp": "^0.33.5" + "sharp": "^0.33.5", + "undici": "^7.27.0" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -11338,6 +11339,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/undici": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.0.tgz", + "integrity": "sha512-+t2Z/GwkZQDtu00813aP66ygViGtPHKhhoFZpQKpKrE+9jIgES+Zw+mFNaDWOVRKiuJjuqKHzD3B1sfGg8+ZOQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", diff --git a/package.json b/package.json index 56a1942..16427c0 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "sharp": "^0.33.5" + "sharp": "^0.33.5", + "undici": "^7.27.0" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", diff --git a/src/enrich/discogs.service.ts b/src/enrich/discogs.service.ts index 8fc5763..a6d255b 100644 --- a/src/enrich/discogs.service.ts +++ b/src/enrich/discogs.service.ts @@ -1,4 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; +import { ProxyAgent } from 'undici'; // Результат обогащения из Discogs export interface DiscogsResult { @@ -38,11 +39,13 @@ export class DiscogsService { process.env.DISCOGS_TOKEN ?? '', process.env.DISCOGS_TOKEN2 ?? '', ].filter((t) => t.length > 0); - // ВАЖНО: Discogs троттлит по IP (не по токену), поэтому с одного сервера - // общий лимит ~54/мин независимо от числа токенов. Токены ротируем вхолостую - // (выгода от 2-го токена — только на ВТОРОМ IP, напр. DE-воркер). - private nextSlot = 0; - private rr = 0; + // Discogs троттлит по IP. Маршруты: [0] token1 напрямую (IP RU), + // [1] token2 через DE-прокси (IP DE) — два разных IP, у каждого свой слот + // ~54/мин → суммарно ~108/мин. Без прокси используем только маршрут 0. + private readonly proxyAgent = process.env.DISCOGS_PROXY + ? new ProxyAgent(process.env.DISCOGS_PROXY) + : null; + private readonly slots: number[] = this.tokens.map(() => 0); private readonly minIntervalMs = 1100; // Без токена обогащение жанрами не работает (поиск требует авторизации) @@ -50,20 +53,29 @@ export class DiscogsService { return this.tokens.length > 0; } - private async pickToken(): Promise { + // Индексы доступных маршрутов (token2 — только если есть прокси/2-й IP) + private routes(): number[] { + return this.tokens.length >= 2 && this.proxyAgent ? [0, 1] : [0]; + } + + // Резервирует слот наименее загруженного маршрута, возвращает токен + dispatcher + private async pickRoute(): Promise<{ token: string; dispatcher: ProxyAgent | undefined }> { + const routes = this.routes(); + let idx = routes[0]; + for (const r of routes) if (this.slots[r] < this.slots[idx]) idx = r; const now = Date.now(); - const start = Math.max(now, this.nextSlot); - this.nextSlot = start + this.minIntervalMs; // глобальный интервал (IP-лимит) - const wait = start - now; - if (wait > 0) await new Promise((r) => setTimeout(r, wait)); - const token = this.tokens[this.rr % this.tokens.length]; - this.rr++; - return token; + const start = Math.max(now, this.slots[idx]); + this.slots[idx] = start + this.minIntervalMs; + if (start > now) await new Promise((res) => setTimeout(res, start - now)); + return { + token: this.tokens[idx], + dispatcher: idx === 1 ? (this.proxyAgent ?? undefined) : undefined, + }; } async lookup(artist: string, song: string): Promise { if (!this.enabled) return null; - const token = await this.pickToken(); + const { token, dispatcher } = await this.pickRoute(); const params = new URLSearchParams({ artist, @@ -74,9 +86,11 @@ export class DiscogsService { }); const url = `https://api.discogs.com/database/search?${params.toString()}`; - const res = await fetch(url, { + const init: Record = { headers: { 'User-Agent': this.userAgent, Accept: 'application/json' }, - }); + }; + if (dispatcher) init.dispatcher = dispatcher; + const res = await fetch(url, init); if (!res.ok) { this.logger.debug(`Discogs ${res.status} для "${artist} — ${song}"`); return null;