feat(shazam): глобальный лимит распознаваний (защита баланса коинов)
Скользящее окно 60с, максимум 30 реальных вызовов Shazam/мин (кэш-хиты не в счёт). Превышение → 429. Защищает платный баланс от перебора станций. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
ServiceUnavailableException,
|
ServiceUnavailableException,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { ShazamClient } from './shazam.client';
|
import { ShazamClient } from './shazam.client';
|
||||||
@@ -30,6 +32,10 @@ export class ShazamService {
|
|||||||
// когда несколько клиентов распознают одну станцию почти одновременно.
|
// когда несколько клиентов распознают одну станцию почти одновременно.
|
||||||
private readonly cache = new Map<number, CacheEntry>();
|
private readonly cache = new Map<number, CacheEntry>();
|
||||||
private readonly CACHE_TTL_MS = 15000;
|
private readonly CACHE_TTL_MS = 15000;
|
||||||
|
// Глобальный лимит реальных вызовов Shazam (платные коины) — защита баланса
|
||||||
|
// от перебора станций. Кэш-хиты сюда не считаются.
|
||||||
|
private readonly recentCalls: number[] = [];
|
||||||
|
private readonly MAX_PER_MIN = 30;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
@@ -58,6 +64,8 @@ export class ShazamService {
|
|||||||
throw new BadRequestException('На этой станции нет музыки');
|
throw new BadRequestException('На этой станции нет музыки');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.checkRateLimit(now);
|
||||||
|
|
||||||
let result: RecognizeResponse;
|
let result: RecognizeResponse;
|
||||||
try {
|
try {
|
||||||
const audio = await fetchStreamChunk(station.streamUrl);
|
const audio = await fetchStreamChunk(station.streamUrl);
|
||||||
@@ -87,6 +95,21 @@ export class ShazamService {
|
|||||||
return result;
|
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 — пробуем взять обложку
|
// Если у распознанного трека нет обложки от Shazam — пробуем взять обложку
|
||||||
// уже обогащённого трека из нашей БД (по тому же normKey, что и чарты).
|
// уже обогащённого трека из нашей БД (по тому же normKey, что и чарты).
|
||||||
private async resolveCover(
|
private async resolveCover(
|
||||||
|
|||||||
Reference in New Issue
Block a user