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}
|
- FRONTEND_URL=${FRONTEND_URL:-https://radiola.app}
|
||||||
# Обогащение треков
|
# Обогащение треков
|
||||||
- DISCOGS_TOKEN=${DISCOGS_TOKEN}
|
- DISCOGS_TOKEN=${DISCOGS_TOKEN}
|
||||||
|
- DISCOGS_TOKEN2=${DISCOGS_TOKEN2}
|
||||||
- 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}
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -30,37 +30,46 @@ interface DiscogsSearchItem {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class DiscogsService {
|
export class DiscogsService {
|
||||||
private readonly logger = new Logger(DiscogsService.name);
|
private readonly logger = new Logger(DiscogsService.name);
|
||||||
private readonly token = process.env.DISCOGS_TOKEN ?? '';
|
|
||||||
private readonly userAgent = 'radiOLA/1.0 +https://radiola.app';
|
private readonly userAgent = 'radiOLA/1.0 +https://radiola.app';
|
||||||
|
|
||||||
// Глобальный rate-limiter: Discogs ~60 запросов/мин. Разносим вызовы ≥1.1с
|
// Несколько токенов Discogs — лимит ~60/мин НА ТОКЕН. У каждого свой слот,
|
||||||
// независимо от параллельности обогащения (иначе 429).
|
// выбираем наименее загруженный → суммарно N×~54/мин без 429.
|
||||||
private nextSlot = 0;
|
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;
|
private readonly minIntervalMs = 1100;
|
||||||
|
|
||||||
// Без токена обогащение жанрами не работает (поиск требует авторизации)
|
// Без токена обогащение жанрами не работает (поиск требует авторизации)
|
||||||
get enabled(): boolean {
|
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 now = Date.now();
|
||||||
const start = Math.max(now, this.nextSlot);
|
const start = Math.max(now, this.slots[idx]);
|
||||||
this.nextSlot = start + this.minIntervalMs;
|
this.slots[idx] = start + this.minIntervalMs;
|
||||||
const wait = start - now;
|
const wait = start - now;
|
||||||
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
||||||
|
return this.tokens[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
async lookup(artist: string, song: string): Promise<DiscogsResult | null> {
|
async lookup(artist: string, song: string): Promise<DiscogsResult | null> {
|
||||||
if (!this.enabled) return null;
|
if (!this.enabled) return null;
|
||||||
await this.rateLimit();
|
const token = await this.pickToken();
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
artist,
|
artist,
|
||||||
track: song,
|
track: song,
|
||||||
type: 'release',
|
type: 'release',
|
||||||
per_page: '5',
|
per_page: '5',
|
||||||
token: this.token,
|
token,
|
||||||
});
|
});
|
||||||
const url = `https://api.discogs.com/database/search?${params.toString()}`;
|
const url = `https://api.discogs.com/database/search?${params.toString()}`;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user