diff --git a/src/config/tmdb.js b/src/config/tmdb.js index 2cdada2..cb18ed9 100644 --- a/src/config/tmdb.js +++ b/src/config/tmdb.js @@ -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: { diff --git a/src/index.js b/src/index.js index d38af86..b192f80 100644 --- a/src/index.js +++ b/src/index.js @@ -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`); diff --git a/src/routes/categories.js b/src/routes/categories.js new file mode 100644 index 0000000..9aa172c --- /dev/null +++ b/src/routes/categories.js @@ -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; diff --git a/src/routes/movies.js b/src/routes/movies.js index 5b01187..7c75241 100644 --- a/src/routes/movies.js +++ b/src/routes/movies.js @@ -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: