perf(enrich): 3-й IP Discogs (token3 через форс-IPv4 RU) + concurrency 12
token3 ходит напрямую с RU, но форсирован IPv4 (121.127.37.212) — для Discogs это 3-й IP (token1=RU-v6, token2=DE-v4, token3=RU-v4). ~162/мин потолок. Без доп. инфры. concurrency 12 чтобы задействовать 3 IP. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ services:
|
|||||||
# Обогащение треков
|
# Обогащение треков
|
||||||
- DISCOGS_TOKEN=${DISCOGS_TOKEN}
|
- DISCOGS_TOKEN=${DISCOGS_TOKEN}
|
||||||
- DISCOGS_TOKEN2=${DISCOGS_TOKEN2}
|
- DISCOGS_TOKEN2=${DISCOGS_TOKEN2}
|
||||||
|
- DISCOGS_TOKEN3=${DISCOGS_TOKEN3}
|
||||||
- DISCOGS_PROXY=${DISCOGS_PROXY}
|
- DISCOGS_PROXY=${DISCOGS_PROXY}
|
||||||
- COVERS_DIR=/data/covers
|
- COVERS_DIR=/data/covers
|
||||||
- PUBLIC_BASE_URL=${PUBLIC_BASE_URL:-http://121.127.37.212:3000}
|
- PUBLIC_BASE_URL=${PUBLIC_BASE_URL:-http://121.127.37.212:3000}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ProxyAgent } from 'undici';
|
import { ProxyAgent, Agent } from 'undici';
|
||||||
|
|
||||||
// Результат обогащения из Discogs
|
// Результат обогащения из Discogs
|
||||||
export interface DiscogsResult {
|
export interface DiscogsResult {
|
||||||
@@ -33,44 +33,43 @@ export class DiscogsService {
|
|||||||
private readonly logger = new Logger(DiscogsService.name);
|
private readonly logger = new Logger(DiscogsService.name);
|
||||||
private readonly userAgent = 'radiOLA/1.0 +https://radiola.app';
|
private readonly userAgent = 'radiOLA/1.0 +https://radiola.app';
|
||||||
|
|
||||||
// Несколько токенов Discogs — лимит ~60/мин НА ТОКЕН. У каждого свой слот,
|
// Discogs троттлит ПО IP. Делаем несколько маршрутов с РАЗНЫМИ IP, каждый со
|
||||||
// выбираем наименее загруженный → суммарно N×~54/мин без 429.
|
// своим токеном и слотом ~54/мин → суммарно N×54/мин без 429:
|
||||||
private readonly tokens = [
|
// • token1 — напрямую (RU, IPv6 по умолчанию)
|
||||||
process.env.DISCOGS_TOKEN ?? '',
|
// • token2 — через DE-прокси (выход с IP DE)
|
||||||
process.env.DISCOGS_TOKEN2 ?? '',
|
// • token3 — напрямую, но форсируем IPv4 (RU, другой IP, чем IPv6)
|
||||||
].filter((t) => t.length > 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;
|
private readonly minIntervalMs = 1100;
|
||||||
|
private readonly routeList = this.buildRoutes();
|
||||||
|
private readonly slots: number[] = this.routeList.map(() => 0);
|
||||||
|
|
||||||
|
private buildRoutes(): { token: string; dispatcher: unknown }[] {
|
||||||
|
const routes: { token: string; dispatcher: unknown }[] = [];
|
||||||
|
const t1 = process.env.DISCOGS_TOKEN ?? '';
|
||||||
|
const t2 = process.env.DISCOGS_TOKEN2 ?? '';
|
||||||
|
const t3 = process.env.DISCOGS_TOKEN3 ?? '';
|
||||||
|
const proxy = process.env.DISCOGS_PROXY ?? '';
|
||||||
|
if (t1) routes.push({ token: t1, dispatcher: undefined });
|
||||||
|
if (t2 && proxy) routes.push({ token: t2, dispatcher: new ProxyAgent(proxy) });
|
||||||
|
if (t3) routes.push({ token: t3, dispatcher: new Agent({ connect: { family: 4 } }) });
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
// Без токена обогащение жанрами не работает (поиск требует авторизации)
|
// Без токена обогащение жанрами не работает (поиск требует авторизации)
|
||||||
get enabled(): boolean {
|
get enabled(): boolean {
|
||||||
return this.tokens.length > 0;
|
return this.routeList.length > 0;
|
||||||
}
|
|
||||||
|
|
||||||
// Индексы доступных маршрутов (token2 — только если есть прокси/2-й IP)
|
|
||||||
private routes(): number[] {
|
|
||||||
return this.tokens.length >= 2 && this.proxyAgent ? [0, 1] : [0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Резервирует слот наименее загруженного маршрута, возвращает токен + dispatcher
|
// Резервирует слот наименее загруженного маршрута, возвращает токен + dispatcher
|
||||||
private async pickRoute(): Promise<{ token: string; dispatcher: ProxyAgent | undefined }> {
|
private async pickRoute(): Promise<{ token: string; dispatcher: unknown }> {
|
||||||
const routes = this.routes();
|
let idx = 0;
|
||||||
let idx = routes[0];
|
for (let i = 1; i < this.slots.length; i++) {
|
||||||
for (const r of routes) if (this.slots[r] < this.slots[idx]) idx = r;
|
if (this.slots[i] < this.slots[idx]) idx = i;
|
||||||
|
}
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const start = Math.max(now, this.slots[idx]);
|
const start = Math.max(now, this.slots[idx]);
|
||||||
this.slots[idx] = start + this.minIntervalMs;
|
this.slots[idx] = start + this.minIntervalMs;
|
||||||
if (start > now) await new Promise((res) => setTimeout(res, start - now));
|
if (start > now) await new Promise((res) => setTimeout(res, start - now));
|
||||||
return {
|
return this.routeList[idx];
|
||||||
token: this.tokens[idx],
|
|
||||||
dispatcher: idx === 1 ? (this.proxyAgent ?? undefined) : undefined,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async lookup(artist: string, song: string): Promise<DiscogsResult | null> {
|
async lookup(artist: string, song: string): Promise<DiscogsResult | null> {
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export class EnrichmentService {
|
|||||||
private running = false;
|
private running = false;
|
||||||
// Discogs сам себя лимитирует (rate-limiter в DiscogsService), поэтому можно
|
// Discogs сам себя лимитирует (rate-limiter в DiscogsService), поэтому можно
|
||||||
// выше параллельность: обложки (iTunes, без лимита) льются быстрее.
|
// выше параллельность: обложки (iTunes, без лимита) льются быстрее.
|
||||||
private readonly throttleMs = 200;
|
private readonly throttleMs = 150;
|
||||||
private readonly concurrency = 8;
|
private readonly concurrency = 12;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
|
|||||||
Reference in New Issue
Block a user