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:
@@ -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:
|
||||
|
||||
@@ -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()}`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user