feat(shazam): глобальный лимит распознаваний (защита баланса коинов)

Скользящее окно 60с, максимум 30 реальных вызовов Shazam/мин (кэш-хиты не в счёт).
Превышение → 429. Защищает платный баланс от перебора станций.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nk
2026-06-07 18:47:25 +03:00
parent 059ebc9c45
commit 791156f814

View File

@@ -4,6 +4,8 @@ import {
NotFoundException,
BadRequestException,
ServiceUnavailableException,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { ShazamClient } from './shazam.client';
@@ -30,6 +32,10 @@ export class ShazamService {
// когда несколько клиентов распознают одну станцию почти одновременно.
private readonly cache = new Map<number, CacheEntry>();
private readonly CACHE_TTL_MS = 15000;
// Глобальный лимит реальных вызовов Shazam (платные коины) — защита баланса
// от перебора станций. Кэш-хиты сюда не считаются.
private readonly recentCalls: number[] = [];
private readonly MAX_PER_MIN = 30;
constructor(
private readonly prisma: PrismaService,
@@ -58,6 +64,8 @@ export class ShazamService {
throw new BadRequestException('На этой станции нет музыки');
}
this.checkRateLimit(now);
let result: RecognizeResponse;
try {
const audio = await fetchStreamChunk(station.streamUrl);
@@ -87,6 +95,21 @@ export class ShazamService {
return result;
}
// Скользящее окно 60с по реальным (не кэшированным) вызовам Shazam.
private checkRateLimit(now: number): void {
const cutoff = now - 60000;
while (this.recentCalls.length && this.recentCalls[0] < cutoff) {
this.recentCalls.shift();
}
if (this.recentCalls.length >= this.MAX_PER_MIN) {
throw new HttpException(
'Слишком много запросов на распознавание, попробуйте позже',
HttpStatus.TOO_MANY_REQUESTS,
);
}
this.recentCalls.push(now);
}
// Если у распознанного трека нет обложки от Shazam — пробуем взять обложку
// уже обогащённого трека из нашей БД (по тому же normKey, что и чарты).
private async resolveCover(