perf(enrich): ротация двух токенов Discogs (~108/мин жанров)

Discogs лимит ~60/мин на токен. Поддержка нескольких токенов (DISCOGS_TOKEN,
DISCOGS_TOKEN2) — у каждого свой слот, берём наименее загруженный → суммарно
вдвое быстрее жанры/стили/лейблы, без 429. Токены — в env (не в гите).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nk
2026-06-04 09:45:35 +03:00
parent ed94bd73d7
commit 94e7f46b39
2 changed files with 20 additions and 10 deletions

View File

@@ -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:

View File

@@ -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<void> {
// Резервирует слот наименее загруженного токена, ждёт его и возвращает токен
private async pickToken(): Promise<string> {
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<DiscogsResult | null> {
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()}`;