При первом появлении трека подтягиваем жанр/стиль/лейбл/год из Discogs и сохраняем обложку в едином формате WebP 500x500 у себя (/covers). Дальше пользователю отдаём только из своей БД — внешние сервисы в рантайме не дёргаем. - Track: +genre/styles/label/year/discogsId/enrichStatus (миграция) - EnrichModule: DiscogsService (поиск), CoverStorageService (sharp->webp), EnrichmentService (очередь с троттлингом + бэкафилл-крон каждые 10 мин) - charts: фильтр чартов по жанру (?genre=), GET /charts/genres, жанр/стиль/лейбл/год в выдаче чарта и детальной странице - main: раздача /covers статикой; docker: volume covers_data + env DISCOGS_TOKEN/PUBLIC_BASE_URL/COVERS_DIR - убран MusicBrainz-фолбэк (заменён Discogs) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
83 lines
2.5 KiB
TypeScript
83 lines
2.5 KiB
TypeScript
import {
|
||
Controller,
|
||
Get,
|
||
Post,
|
||
Delete,
|
||
Param,
|
||
Query,
|
||
UseGuards,
|
||
Req,
|
||
HttpCode,
|
||
HttpStatus,
|
||
} from '@nestjs/common';
|
||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||
import { ChartsService, ChartPeriod } from './charts.service';
|
||
import { AuthGuard } from '../auth/auth.guard';
|
||
import { OptionalAuthGuard } from '../auth/optional-auth.guard';
|
||
import type { Request } from 'express';
|
||
|
||
@ApiTags('charts')
|
||
@Controller('charts')
|
||
export class ChartsController {
|
||
constructor(private readonly chartsService: ChartsService) {}
|
||
|
||
@Get('tracks')
|
||
@ApiOperation({ summary: 'Чарт треков за период' })
|
||
async getTopTracks(
|
||
@Query('period') period: string = 'week',
|
||
@Query('limit') limit: string = '100',
|
||
@Query('genre') genre?: string,
|
||
) {
|
||
const validPeriod: ChartPeriod =
|
||
period === 'day' || period === 'week' || period === 'month' || period === 'all'
|
||
? (period as ChartPeriod)
|
||
: 'week';
|
||
const parsedLimit = Math.min(Math.max(parseInt(limit, 10) || 100, 1), 200);
|
||
const genreFilter = genre?.trim() ? genre.trim() : undefined;
|
||
return this.chartsService.getTopTracks(validPeriod, parsedLimit, genreFilter);
|
||
}
|
||
|
||
@Get('genres')
|
||
@ApiOperation({ summary: 'Список доступных жанров для фильтра' })
|
||
async getGenres() {
|
||
return this.chartsService.getGenres();
|
||
}
|
||
|
||
@Get('tracks/:trackId')
|
||
@UseGuards(OptionalAuthGuard)
|
||
@ApiOperation({ summary: 'Детальная страница трека' })
|
||
async getTrackDetail(
|
||
@Param('trackId') trackId: string,
|
||
@Req() req: Request,
|
||
) {
|
||
const user = req['user'] as { sub: string } | undefined;
|
||
return this.chartsService.getTrackDetail(trackId, user?.sub);
|
||
}
|
||
|
||
@Post('tracks/:trackId/like')
|
||
@UseGuards(AuthGuard)
|
||
@ApiBearerAuth()
|
||
@HttpCode(HttpStatus.OK)
|
||
@ApiOperation({ summary: 'Лайкнуть трек' })
|
||
async likeTrack(
|
||
@Param('trackId') trackId: string,
|
||
@Req() req: Request,
|
||
) {
|
||
const user = req['user'] as { sub: string; email: string };
|
||
return this.chartsService.likeTrack(trackId, user.sub);
|
||
}
|
||
|
||
@Delete('tracks/:trackId/like')
|
||
@UseGuards(AuthGuard)
|
||
@ApiBearerAuth()
|
||
@HttpCode(HttpStatus.OK)
|
||
@ApiOperation({ summary: 'Убрать лайк с трека' })
|
||
async unlikeTrack(
|
||
@Param('trackId') trackId: string,
|
||
@Req() req: Request,
|
||
) {
|
||
const user = req['user'] as { sub: string; email: string };
|
||
return this.chartsService.unlikeTrack(trackId, user.sub);
|
||
}
|
||
}
|