Files
radiola-backend/src/charts/charts.controller.ts
nk 0efba7c691 feat(enrich): обогащение треков через Discogs + самохостинг обложек (WebP)
При первом появлении трека подтягиваем жанр/стиль/лейбл/год из 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>
2026-06-03 13:28:22 +03:00

83 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}