Add categories

This commit is contained in:
2025-05-28 10:27:38 +00:00
parent d99a914ac6
commit 25c05d6eb4
4 changed files with 554 additions and 12 deletions

View File

@@ -28,25 +28,37 @@ class TMDBClient {
);
}
async makeRequest(method, endpoint, params = {}) {
async makeRequest(method, endpoint, options = {}) {
try {
const requestParams = {
...params,
language: 'ru-RU',
region: 'RU'
// Здесь была ошибка - если передать {params: {...}} в options,
// то мы создаем вложенный объект params.params
const clientOptions = {
method,
url: endpoint,
...options
};
// Если не передали params, добавляем базовые
if (!clientOptions.params) {
clientOptions.params = {};
}
// Добавляем базовые параметры, если их еще нет
if (!clientOptions.params.language) {
clientOptions.params.language = 'ru-RU';
}
if (!clientOptions.params.region) {
clientOptions.params.region = 'RU';
}
console.log('TMDB Request:', {
method,
endpoint,
params: requestParams
options: clientOptions
});
const response = await this.client({
method,
url: endpoint,
params: requestParams
});
const response = await this.client(clientOptions);
return response;
} catch (error) {
@@ -198,6 +210,60 @@ class TMDBClient {
return response.data;
}
// Получение жанров фильмов
async getMovieGenres() {
console.log('Getting movie genres');
try {
const response = await this.makeRequest('GET', '/genre/movie/list', {
language: 'ru'
});
return response.data;
} catch (error) {
console.error('Error getting movie genres:', error.message);
throw error;
}
}
// Получение жанров сериалов
async getTVGenres() {
console.log('Getting TV genres');
try {
const response = await this.makeRequest('GET', '/genre/tv/list', {
language: 'ru'
});
return response.data;
} catch (error) {
console.error('Error getting TV genres:', error.message);
throw error;
}
}
// Получение всех жанров (фильмы и сериалы)
async getAllGenres() {
console.log('Getting all genres (movies and TV)');
try {
const [movieGenres, tvGenres] = await Promise.all([
this.getMovieGenres(),
this.getTVGenres()
]);
// Объединяем жанры, удаляя дубликаты по ID
const allGenres = [...movieGenres.genres];
// Добавляем жанры сериалов, которых нет в фильмах
tvGenres.genres.forEach(tvGenre => {
if (!allGenres.some(genre => genre.id === tvGenre.id)) {
allGenres.push(tvGenre);
}
});
return { genres: allGenres };
} catch (error) {
console.error('Error getting all genres:', error.message);
throw error;
}
}
async getMoviesByGenre(genreId, page = 1) {
return this.makeRequest('GET', '/discover/movie', {
params: {

View File

@@ -225,10 +225,12 @@ app.get('/search/multi', async (req, res) => {
const moviesRouter = require('./routes/movies');
const tvRouter = require('./routes/tv');
const imagesRouter = require('./routes/images');
const categoriesRouter = require('./routes/categories');
app.use('/movies', moviesRouter);
app.use('/tv', tvRouter);
app.use('/images', imagesRouter);
app.use('/categories', categoriesRouter);
/**
* @swagger
@@ -287,7 +289,11 @@ module.exports = app;
// Start server only in development
if (process.env.NODE_ENV !== 'production') {
const port = process.env.PORT || 3000;
// Проверяем аргументы командной строки
const args = process.argv.slice(2);
// Используем порт из аргументов командной строки, переменной окружения или по умолчанию 3000
const port = args[0] || process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
console.log(`Documentation available at http://localhost:${port}/api-docs`);

378
src/routes/categories.js Normal file
View File

@@ -0,0 +1,378 @@
const express = require('express');
const router = express.Router();
const { formatDate } = require('../utils/date');
// Middleware для логирования запросов
router.use((req, res, next) => {
console.log('Categories API Request:', {
method: req.method,
path: req.path,
query: req.query,
params: req.params
});
next();
});
/**
* @swagger
* /categories:
* get:
* summary: Получение списка категорий
* description: Возвращает список всех доступных категорий фильмов (жанров)
* tags: [categories]
* responses:
* 200:
* description: Список категорий
* 500:
* description: Ошибка сервера
*/
router.get('/', async (req, res) => {
try {
console.log('Fetching categories (genres)...');
// Получаем данные о всех жанрах из TMDB (фильмы и сериалы)
const genresData = await req.tmdb.getAllGenres();
if (!genresData?.genres || !Array.isArray(genresData.genres)) {
console.error('Invalid genres response:', genresData);
return res.status(500).json({
error: 'Invalid response from TMDB',
details: 'Genres data is missing or invalid'
});
}
// Преобразуем жанры в категории
const categories = genresData.genres.map(genre => ({
id: genre.id,
name: genre.name,
slug: genre.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '')
}));
// Сортируем категории по алфавиту
categories.sort((a, b) => a.name.localeCompare(b.name, 'ru'));
console.log('Categories response:', {
count: categories.length,
categories: categories.slice(0, 3) // логируем только первые 3 для краткости
});
res.json({ categories });
} catch (error) {
console.error('Error fetching categories:', {
message: error.message,
response: error.response?.data,
stack: error.stack
});
res.status(500).json({
error: 'Failed to fetch categories',
details: error.response?.data?.status_message || error.message
});
}
});
/**
* @swagger
* /categories/{id}:
* get:
* summary: Получение категории по ID
* description: Возвращает информацию о категории по ее ID
* tags: [categories]
* parameters:
* - in: path
* name: id
* required: true
* description: ID категории (жанра)
* schema:
* type: integer
* responses:
* 200:
* description: Категория найдена
* 404:
* description: Категория не найдена
* 500:
* description: Ошибка сервера
*/
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
console.log(`Fetching category (genre) with ID: ${id}`);
// Получаем данные о всех жанрах (фильмы и сериалы)
const genresData = await req.tmdb.getAllGenres();
if (!genresData?.genres || !Array.isArray(genresData.genres)) {
console.error('Invalid genres response:', genresData);
return res.status(500).json({
error: 'Invalid response from TMDB',
details: 'Genres data is missing or invalid'
});
}
// Находим жанр по ID
const genre = genresData.genres.find(g => g.id === parseInt(id));
if (!genre) {
return res.status(404).json({
error: 'Category not found',
details: `No category with ID ${id}`
});
}
// Преобразуем жанр в категорию
const category = {
id: genre.id,
name: genre.name,
slug: genre.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''),
moviesCount: null // Можно будет дополнительно получить количество фильмов по жанру
};
res.json(category);
} catch (error) {
console.error('Error fetching category by ID:', error);
res.status(500).json({
error: 'Failed to fetch category',
details: error.response?.data?.status_message || error.message
});
}
});
/**
* @swagger
* /categories/{id}/movies:
* get:
* summary: Получение фильмов по категории
* description: Возвращает список фильмов, принадлежащих указанной категории (жанру)
* tags: [categories]
* parameters:
* - in: path
* name: id
* required: true
* description: ID категории (жанра)
* schema:
* type: integer
* - in: query
* name: page
* description: Номер страницы
* schema:
* type: integer
* minimum: 1
* default: 1
* responses:
* 200:
* description: Список фильмов по категории
* 404:
* description: Категория не найдена
* 500:
* description: Ошибка сервера
*/
router.get('/:id/movies', async (req, res) => {
try {
const { id } = req.params;
const { page = 1 } = req.query;
console.log(`Fetching movies for category (genre) ID: ${id}, page: ${page}`);
// Проверяем существование жанра в списке всех жанров
const genresData = await req.tmdb.getAllGenres();
const genreExists = genresData?.genres?.some(g => g.id === parseInt(id));
if (!genreExists) {
return res.status(404).json({
error: 'Category not found',
details: `No category with ID ${id}`
});
}
// Получаем фильмы по жанру напрямую из TMDB
console.log(`Making TMDB request for movies with genre ID: ${id}, page: ${page}`);
// В URL параметрах напрямую указываем жанр, чтобы быть уверенными
const endpoint = `/discover/movie?with_genres=${id}`;
const requestParams = {
page,
language: 'ru-RU',
include_adult: false,
sort_by: 'popularity.desc'
};
// Дополнительно добавляем вариации для разных жанров
if (parseInt(id) % 2 === 0) {
requestParams['vote_count.gte'] = 50;
} else {
requestParams['vote_average.gte'] = 5;
}
console.log('Request params:', requestParams);
console.log('Endpoint with genre:', endpoint);
const response = await req.tmdb.makeRequest('get', endpoint, {
params: requestParams
});
console.log(`TMDB response received, status: ${response.status}, has results: ${!!response?.data?.results}`);
if (response?.data?.results?.length > 0) {
console.log(`First few movie IDs: ${response.data.results.slice(0, 5).map(m => m.id).join(', ')}`);
}
if (!response?.data?.results) {
console.error('Invalid movie response:', response);
return res.status(500).json({
error: 'Invalid response from TMDB',
details: 'Movie data is missing'
});
}
console.log('Movies by category response:', {
page: response.data.page,
total_results: response.data.total_results,
results_count: response.data.results?.length
});
// Форматируем даты в результатах
const formattedResults = response.data.results.map(movie => ({
...movie,
release_date: movie.release_date ? formatDate(movie.release_date) : undefined,
poster_path: req.tmdb.getImageURL(movie.poster_path, 'w500'),
backdrop_path: req.tmdb.getImageURL(movie.backdrop_path, 'original')
}));
res.json({
...response.data,
results: formattedResults
});
} catch (error) {
console.error('Error fetching movies by category:', {
message: error.message,
response: error.response?.data
});
res.status(500).json({
error: 'Failed to fetch movies by category',
details: error.response?.data?.status_message || error.message
});
}
});
/**
* @swagger
* /categories/{id}/tv:
* get:
* summary: Получение сериалов по категории
* description: Возвращает список сериалов, принадлежащих указанной категории (жанру)
* tags: [categories]
* parameters:
* - in: path
* name: id
* required: true
* description: ID категории (жанра)
* schema:
* type: integer
* - in: query
* name: page
* description: Номер страницы
* schema:
* type: integer
* minimum: 1
* default: 1
* responses:
* 200:
* description: Список сериалов по категории
* 404:
* description: Категория не найдена
* 500:
* description: Ошибка сервера
*/
router.get('/:id/tv', async (req, res) => {
try {
const { id } = req.params;
const { page = 1 } = req.query;
console.log(`Fetching TV shows for category (genre) ID: ${id}, page: ${page}`);
// Проверяем существование жанра в списке всех жанров
const genresData = await req.tmdb.getAllGenres();
const genreExists = genresData?.genres?.some(g => g.id === parseInt(id));
if (!genreExists) {
return res.status(404).json({
error: 'Category not found',
details: `No category with ID ${id}`
});
}
// Получаем сериалы по жанру напрямую из TMDB
console.log(`Making TMDB request for TV shows with genre ID: ${id}, page: ${page}`);
// В URL параметрах напрямую указываем жанр, чтобы быть уверенными
const endpoint = `/discover/tv?with_genres=${id}`;
const requestParams = {
page,
language: 'ru-RU',
include_adult: false,
include_null_first_air_dates: false,
sort_by: 'popularity.desc'
};
// Дополнительно добавляем вариации для разных жанров
if (parseInt(id) % 2 === 0) {
requestParams['vote_count.gte'] = 20;
} else {
requestParams['first_air_date.gte'] = '2010-01-01';
}
console.log('TV Request params:', requestParams);
console.log('TV Endpoint with genre:', endpoint);
const response = await req.tmdb.makeRequest('get', endpoint, {
params: requestParams
});
console.log(`TMDB response for TV genre ${id} received, status: ${response.status}, has results: ${!!response?.data?.results}`);
if (response?.data?.results?.length > 0) {
console.log(`First few TV show IDs: ${response.data.results.slice(0, 5).map(show => show.id).join(', ')}`);
}
if (!response?.data?.results) {
console.error('Invalid TV shows response:', response);
return res.status(500).json({
error: 'Invalid response from TMDB',
details: 'TV shows data is missing'
});
}
console.log('TV shows by category response:', {
page: response.data.page,
total_results: response.data.total_results,
results_count: response.data.results?.length
});
// Форматируем даты в результатах
const formattedResults = response.data.results.map(tvShow => ({
...tvShow,
first_air_date: tvShow.first_air_date ? formatDate(tvShow.first_air_date) : undefined,
poster_path: req.tmdb.getImageURL(tvShow.poster_path, 'w500'),
backdrop_path: req.tmdb.getImageURL(tvShow.backdrop_path, 'original')
}));
res.json({
...response.data,
results: formattedResults
});
} catch (error) {
console.error('Error fetching TV shows by category:', {
message: error.message,
response: error.response?.data
});
res.status(500).json({
error: 'Failed to fetch TV shows by category',
details: error.response?.data?.status_message || error.message
});
}
});
module.exports = router;

View File

@@ -13,6 +13,98 @@ router.use((req, res, next) => {
next();
});
/**
* @swagger
* /movies/search:
* get:
* summary: Поиск фильмов
* description: Поиск фильмов по запросу с поддержкой русского языка
* tags: [movies]
* parameters:
* - in: query
* name: query
* required: true
* description: Поисковый запрос
* schema:
* type: string
* example: Матрица
* - in: query
* name: page
* description: Номер страницы (по умолчанию 1)
* schema:
* type: integer
* minimum: 1
* default: 1
* example: 1
* responses:
* 200:
* description: Успешный поиск
* content:
* application/json:
* schema:
* type: object
* properties:
* page:
* type: integer
* description: Текущая страница
* total_pages:
* type: integer
* description: Всего страниц
* total_results:
* type: integer
* description: Всего результатов
* results:
* type: array
* items:
* $ref: '#/components/schemas/Movie'
* 400:
* description: Неверный запрос
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Ошибка сервера
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
router.get('/search', async (req, res) => {
try {
const { query, page = 1 } = req.query;
if (!query) {
return res.status(400).json({ error: 'Query parameter is required' });
}
console.log('Search request:', { query, page });
const data = await req.tmdb.searchMovies(query, page);
console.log('Search response:', {
page: data.page,
total_results: data.total_results,
total_pages: data.total_pages,
results_count: data.results?.length
});
// Форматируем даты в результатах
const formattedResults = data.results.map(movie => ({
...movie,
release_date: formatDate(movie.release_date)
}));
res.json({
...data,
results: formattedResults
});
} catch (error) {
console.error('Error searching movies:', error);
res.status(500).json({ error: error.message });
}
});
/**
* @swagger
* /search/multi: