mirror of
https://gitlab.com/foxixus/neomovies-api.git
synced 2025-10-27 17:38:51 +05:00
Update 6 files
- /src/index.js - /src/config/tmdb.js - /src/routes/movies.js - /src/routes/images.js - /src/routes/tv.js - /package-lock.json
This commit is contained in:
2900
package-lock.json
generated
2900
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,6 @@ class TMDBClient {
|
||||
|
||||
async makeRequest(method, endpoint, params = {}) {
|
||||
try {
|
||||
// Убедимся, что параметры запроса корректны
|
||||
const requestParams = {
|
||||
...params,
|
||||
language: 'ru-RU',
|
||||
@@ -49,15 +48,6 @@ class TMDBClient {
|
||||
params: requestParams
|
||||
});
|
||||
|
||||
console.log('TMDB Response:', {
|
||||
endpoint,
|
||||
requestParams,
|
||||
status: response.status,
|
||||
page: response.data.page,
|
||||
totalPages: response.data.total_pages,
|
||||
resultsCount: response.data.results?.length
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('TMDB Error:', {
|
||||
@@ -66,10 +56,7 @@ class TMDBClient {
|
||||
error: error.message,
|
||||
response: error.response?.data
|
||||
});
|
||||
if (error.response) {
|
||||
throw new Error(`TMDB API Error: ${error.response.data.status_message || error.message}`);
|
||||
}
|
||||
throw new Error(`Network Error: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +65,69 @@ class TMDBClient {
|
||||
return `https://image.tmdb.org/t/p/${size}${path}`;
|
||||
}
|
||||
|
||||
isReleased(releaseDate) {
|
||||
if (!releaseDate) return false;
|
||||
|
||||
// Если дата в будущем формате (с "г."), пропускаем фильм
|
||||
if (releaseDate.includes(' г.')) {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const yearStr = releaseDate.split(' ')[2];
|
||||
const year = parseInt(yearStr, 10);
|
||||
return year <= currentYear;
|
||||
}
|
||||
|
||||
// Для ISO дат
|
||||
const date = new Date(releaseDate);
|
||||
if (isNaN(date.getTime())) return true; // Если не смогли распарсить, пропускаем
|
||||
|
||||
const currentDate = new Date();
|
||||
return date <= currentDate;
|
||||
}
|
||||
|
||||
filterAndProcessResults(results, type = 'movie') {
|
||||
if (!Array.isArray(results)) {
|
||||
console.error('Expected results to be an array, got:', typeof results);
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`Filtering ${type}s, total before:`, results.length);
|
||||
|
||||
const filteredResults = results.filter(item => {
|
||||
if (!item || typeof item !== 'object') {
|
||||
console.log('Skipping invalid item object');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем название (для фильмов - title, для сериалов - name)
|
||||
const title = type === 'movie' ? item.title : item.name;
|
||||
const isNumericTitle = /^\d+$/.test(title || '');
|
||||
const hasCyrillic = /[а-яА-ЯёЁ]/.test(title || '');
|
||||
const hasValidTitle = isNumericTitle || hasCyrillic;
|
||||
|
||||
if (!hasValidTitle) {
|
||||
console.log(`Skipping ${type} - invalid title:`, title);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем рейтинг
|
||||
const hasValidRating = item.vote_average > 0;
|
||||
if (!hasValidRating) {
|
||||
console.log(`Skipping ${type} - no rating:`, title);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
console.log(`${type}s after filtering:`, filteredResults.length);
|
||||
|
||||
return filteredResults.map(item => ({
|
||||
...item,
|
||||
poster_path: this.getImageURL(item.poster_path, 'w500'),
|
||||
backdrop_path: this.getImageURL(item.backdrop_path, 'original')
|
||||
}));
|
||||
}
|
||||
|
||||
async searchMovies(query, page = 1) {
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
console.log('Searching movies:', { query, page: pageNum });
|
||||
@@ -89,14 +139,42 @@ class TMDBClient {
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
data.results = data.results
|
||||
.filter(movie => movie.poster_path && movie.overview && movie.vote_average > 0)
|
||||
.map(movie => ({
|
||||
...movie,
|
||||
poster_path: this.getImageURL(movie.poster_path, 'w500'),
|
||||
backdrop_path: this.getImageURL(movie.backdrop_path, 'original')
|
||||
}));
|
||||
data.results = this.filterAndProcessResults(data.results, 'movie');
|
||||
return data;
|
||||
}
|
||||
|
||||
async getPopularMovies(page = 1) {
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
console.log('Getting popular movies:', { page: pageNum });
|
||||
|
||||
const response = await this.makeRequest('GET', '/movie/popular', {
|
||||
page: pageNum
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
data.results = this.filterAndProcessResults(data.results, 'movie');
|
||||
return data;
|
||||
}
|
||||
|
||||
async getTopRatedMovies(page = 1) {
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
const response = await this.makeRequest('GET', '/movie/top_rated', {
|
||||
page: pageNum
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
data.results = this.filterAndProcessResults(data.results, 'movie');
|
||||
return data;
|
||||
}
|
||||
|
||||
async getUpcomingMovies(page = 1) {
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
const response = await this.makeRequest('GET', '/movie/upcoming', {
|
||||
page: pageNum
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
data.results = this.filterAndProcessResults(data.results, 'movie');
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -110,68 +188,44 @@ class TMDBClient {
|
||||
};
|
||||
}
|
||||
|
||||
async getPopularMovies(page = 1) {
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
console.log('Getting popular movies:', { page: pageNum });
|
||||
|
||||
const response = await this.makeRequest('GET', '/movie/popular', {
|
||||
page: pageNum
|
||||
});
|
||||
|
||||
console.log('Popular movies response:', {
|
||||
requestedPage: pageNum,
|
||||
returnedPage: response.data.page,
|
||||
totalPages: response.data.total_pages,
|
||||
resultsCount: response.data.results.length
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
data.results = data.results.map(movie => ({
|
||||
...movie,
|
||||
poster_path: this.getImageURL(movie.poster_path, 'w500'),
|
||||
backdrop_path: this.getImageURL(movie.backdrop_path, 'original')
|
||||
}));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getTopRatedMovies(page = 1) {
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
const response = await this.makeRequest('GET', '/movie/top_rated', {
|
||||
page: pageNum
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
data.results = data.results.map(movie => ({
|
||||
...movie,
|
||||
poster_path: this.getImageURL(movie.poster_path, 'w500'),
|
||||
backdrop_path: this.getImageURL(movie.backdrop_path, 'original')
|
||||
}));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getUpcomingMovies(page = 1) {
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
const response = await this.makeRequest('GET', '/movie/upcoming', {
|
||||
page: pageNum
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
data.results = data.results.map(movie => ({
|
||||
...movie,
|
||||
poster_path: this.getImageURL(movie.poster_path, 'w500'),
|
||||
backdrop_path: this.getImageURL(movie.backdrop_path, 'original')
|
||||
}));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getMovieExternalIDs(id) {
|
||||
const response = await this.makeRequest('GET', `/movie/${id}/external_ids`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getMovieVideos(id) {
|
||||
const response = await this.makeRequest('GET', `/movie/${id}/videos`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getMoviesByGenre(genreId, page = 1) {
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
const response = await this.makeRequest('GET', '/discover/movie', {
|
||||
page: pageNum,
|
||||
with_genres: genreId,
|
||||
sort_by: 'popularity.desc'
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
data.results = this.filterAndProcessResults(data.results, 'movie');
|
||||
return data;
|
||||
}
|
||||
|
||||
// TV Show methods
|
||||
async getPopularTVShows(page = 1) {
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
console.log('Getting popular TV shows:', { page: pageNum });
|
||||
|
||||
const response = await this.makeRequest('GET', '/tv/popular', {
|
||||
page: pageNum
|
||||
});
|
||||
|
||||
return {
|
||||
...response.data,
|
||||
results: this.filterAndProcessResults(response.data.results, 'tv')
|
||||
};
|
||||
}
|
||||
|
||||
async searchTVShows(query, page = 1) {
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
console.log('Searching TV shows:', { query, page: pageNum });
|
||||
@@ -182,59 +236,36 @@ class TMDBClient {
|
||||
include_adult: false
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
data.results = data.results
|
||||
.filter(show => show.poster_path && show.overview && show.vote_average > 0)
|
||||
.map(show => ({
|
||||
...show,
|
||||
poster_path: this.getImageURL(show.poster_path, 'w500'),
|
||||
backdrop_path: this.getImageURL(show.backdrop_path, 'original')
|
||||
}));
|
||||
|
||||
return data;
|
||||
return {
|
||||
...response.data,
|
||||
results: this.filterAndProcessResults(response.data.results, 'tv')
|
||||
};
|
||||
}
|
||||
|
||||
async getTVShow(id) {
|
||||
const response = await this.makeRequest('GET', `/tv/${id}`);
|
||||
const response = await this.makeRequest('GET', `/tv/${id}`, {
|
||||
append_to_response: 'credits,videos,similar,external_ids'
|
||||
});
|
||||
|
||||
const show = response.data;
|
||||
return {
|
||||
...show,
|
||||
poster_path: this.getImageURL(show.poster_path, 'w500'),
|
||||
backdrop_path: this.getImageURL(show.backdrop_path, 'original')
|
||||
backdrop_path: this.getImageURL(show.backdrop_path, 'original'),
|
||||
credits: show.credits || { cast: [], crew: [] },
|
||||
videos: show.videos || { results: [] }
|
||||
};
|
||||
}
|
||||
|
||||
async getPopularTVShows(page = 1) {
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
const response = await this.makeRequest('GET', '/tv/popular', {
|
||||
page: pageNum
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
data.results = data.results.map(show => ({
|
||||
...show,
|
||||
poster_path: this.getImageURL(show.poster_path, 'w500'),
|
||||
backdrop_path: this.getImageURL(show.backdrop_path, 'original')
|
||||
}));
|
||||
|
||||
return data;
|
||||
async getTVShowExternalIDs(id) {
|
||||
const response = await this.makeRequest('GET', `/tv/${id}/external_ids`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getTopRatedTVShows(page = 1) {
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
const response = await this.makeRequest('GET', '/tv/top_rated', {
|
||||
page: pageNum
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
data.results = data.results.map(show => ({
|
||||
...show,
|
||||
poster_path: this.getImageURL(show.poster_path, 'w500'),
|
||||
backdrop_path: this.getImageURL(show.backdrop_path, 'original')
|
||||
}));
|
||||
|
||||
return data;
|
||||
async getTVShowVideos(id) {
|
||||
const response = await this.makeRequest('GET', `/tv/${id}/videos`);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TMDBClient;
|
||||
module.exports = TMDBClient;
|
||||
143
src/index.js
143
src/index.js
@@ -5,6 +5,7 @@ const swaggerJsdoc = require('swagger-jsdoc');
|
||||
const path = require('path');
|
||||
const TMDBClient = require('./config/tmdb');
|
||||
const healthCheck = require('./utils/health');
|
||||
const { formatDate } = require('./utils/date');
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -37,6 +38,10 @@ const swaggerOptions = {
|
||||
name: 'movies',
|
||||
description: 'Операции с фильмами'
|
||||
},
|
||||
{
|
||||
name: 'tv',
|
||||
description: 'Операции с сериалами'
|
||||
},
|
||||
{
|
||||
name: 'health',
|
||||
description: 'Проверка работоспособности API'
|
||||
@@ -54,36 +59,6 @@ const swaggerOptions = {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Название фильма'
|
||||
},
|
||||
overview: {
|
||||
type: 'string',
|
||||
description: 'Описание фильма'
|
||||
},
|
||||
release_date: {
|
||||
type: 'string',
|
||||
format: 'date',
|
||||
description: 'Дата выхода'
|
||||
},
|
||||
vote_average: {
|
||||
type: 'number',
|
||||
description: 'Средняя оценка'
|
||||
},
|
||||
poster_path: {
|
||||
type: 'string',
|
||||
description: 'URL постера'
|
||||
},
|
||||
backdrop_path: {
|
||||
type: 'string',
|
||||
description: 'URL фонового изображения'
|
||||
}
|
||||
}
|
||||
},
|
||||
Error: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
error: {
|
||||
type: 'string',
|
||||
description: 'Сообщение об ошибке'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,8 +113,109 @@ app.get('/api-docs/swagger.json', (req, res) => {
|
||||
res.send(swaggerDocs);
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /search/multi:
|
||||
* get:
|
||||
* summary: Мультипоиск
|
||||
* description: Поиск фильмов и сериалов по запросу
|
||||
* tags: [search]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: query
|
||||
* required: true
|
||||
* description: Поисковый запрос
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: page
|
||||
* description: Номер страницы
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* default: 1
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Успешный поиск
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* page:
|
||||
* type: integer
|
||||
* results:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* title:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* media_type:
|
||||
* type: string
|
||||
* enum: [movie, tv]
|
||||
*/
|
||||
app.get('/search/multi', async (req, res) => {
|
||||
try {
|
||||
const { query, page = 1 } = req.query;
|
||||
|
||||
if (!query) {
|
||||
return res.status(400).json({ error: 'Query parameter is required' });
|
||||
}
|
||||
|
||||
console.log('Multi-search request:', { query, page });
|
||||
|
||||
const response = await req.tmdb.makeRequest('get', '/search/multi', {
|
||||
query,
|
||||
page,
|
||||
include_adult: false,
|
||||
language: 'ru-RU'
|
||||
});
|
||||
|
||||
if (!response.data || !response.data.results) {
|
||||
console.error('Invalid response from TMDB:', response);
|
||||
return res.status(500).json({ error: 'Invalid response from TMDB API' });
|
||||
}
|
||||
|
||||
console.log('Multi-search response:', {
|
||||
page: response.data.page,
|
||||
total_results: response.data.total_results,
|
||||
total_pages: response.data.total_pages,
|
||||
results_count: response.data.results?.length
|
||||
});
|
||||
|
||||
// Форматируем даты в результатах
|
||||
const formattedResults = response.data.results.map(item => ({
|
||||
...item,
|
||||
release_date: item.release_date ? formatDate(item.release_date) : undefined,
|
||||
first_air_date: item.first_air_date ? formatDate(item.first_air_date) : undefined
|
||||
}));
|
||||
|
||||
res.json({
|
||||
...response.data,
|
||||
results: formattedResults
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in multi-search:', error.response?.data || error.message);
|
||||
res.status(500).json({
|
||||
error: 'Failed to search',
|
||||
details: error.response?.data?.status_message || error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// API routes
|
||||
app.use('/movies', require('./routes/movies'));
|
||||
const moviesRouter = require('./routes/movies');
|
||||
const tvRouter = require('./routes/tv');
|
||||
const imagesRouter = require('./routes/images');
|
||||
|
||||
app.use('/movies', moviesRouter);
|
||||
app.use('/tv', tvRouter);
|
||||
app.use('/images', imagesRouter);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
@@ -171,7 +247,8 @@ app.get('/health', async (req, res) => {
|
||||
const health = await healthCheck.getFullHealth(req.tmdb);
|
||||
res.json(health);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
console.error('Health check error:', error);
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
error: error.message
|
||||
});
|
||||
@@ -202,4 +279,4 @@ if (process.env.NODE_ENV !== 'production') {
|
||||
console.log(`Server is running on port ${port}`);
|
||||
console.log(`Documentation available at http://localhost:${port}/api-docs`);
|
||||
});
|
||||
}
|
||||
}
|
||||
75
src/routes/images.js
Normal file
75
src/routes/images.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const axios = require('axios');
|
||||
const path = require('path');
|
||||
|
||||
// Базовый URL для изображений TMDB
|
||||
const TMDB_IMAGE_BASE_URL = 'https://image.tmdb.org/t/p';
|
||||
|
||||
// Путь к placeholder изображению
|
||||
const PLACEHOLDER_PATH = path.join(__dirname, '..', 'public', 'images', 'placeholder.jpg');
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /images/{size}/{path}:
|
||||
* get:
|
||||
* summary: Прокси для изображений TMDB
|
||||
* description: Получает изображения с TMDB и отдает их клиенту
|
||||
* tags: [images]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: size
|
||||
* required: true
|
||||
* description: Размер изображения (w500, original и т.д.)
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: path
|
||||
* name: path
|
||||
* required: true
|
||||
* description: Путь к изображению
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Изображение
|
||||
* content:
|
||||
* image/*:
|
||||
* schema:
|
||||
* type: string
|
||||
* format: binary
|
||||
*/
|
||||
router.get('/:size/:path(*)', async (req, res) => {
|
||||
try {
|
||||
const { size, path: imagePath } = req.params;
|
||||
|
||||
// Если запрашивается placeholder, возвращаем локальный файл
|
||||
if (imagePath === 'placeholder.jpg') {
|
||||
return res.sendFile(PLACEHOLDER_PATH);
|
||||
}
|
||||
|
||||
// Проверяем размер изображения
|
||||
const validSizes = ['w92', 'w154', 'w185', 'w342', 'w500', 'w780', 'original'];
|
||||
const imageSize = validSizes.includes(size) ? size : 'original';
|
||||
|
||||
// Формируем URL изображения
|
||||
const imageUrl = `${TMDB_IMAGE_BASE_URL}/${imageSize}/${imagePath}`;
|
||||
|
||||
// Получаем изображение
|
||||
const response = await axios.get(imageUrl, {
|
||||
responseType: 'stream',
|
||||
validateStatus: status => status === 200
|
||||
});
|
||||
|
||||
// Устанавливаем заголовки
|
||||
res.set('Content-Type', response.headers['content-type']);
|
||||
res.set('Cache-Control', 'public, max-age=31536000'); // кэшируем на 1 год
|
||||
|
||||
// Передаем изображение клиенту
|
||||
response.data.pipe(res);
|
||||
} catch (error) {
|
||||
console.error('Image proxy error:', error.message);
|
||||
res.sendFile(PLACEHOLDER_PATH);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -72,40 +72,129 @@ router.use((req, res, next) => {
|
||||
*/
|
||||
router.get('/search', async (req, res) => {
|
||||
try {
|
||||
const { query, page } = req.query;
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
const { query, page = 1 } = req.query;
|
||||
|
||||
if (!query) {
|
||||
return res.status(400).json({ error: 'Query parameter is required' });
|
||||
}
|
||||
|
||||
if (pageNum < 1) {
|
||||
return res.status(400).json({ error: 'Page must be greater than 0' });
|
||||
}
|
||||
console.log('Search request:', { query, page });
|
||||
|
||||
const response = await req.tmdb.searchMovies(query, pageNum);
|
||||
|
||||
if (!response || !response.data) {
|
||||
throw new Error('Failed to fetch data from TMDB');
|
||||
}
|
||||
const data = await req.tmdb.searchMovies(query, page);
|
||||
|
||||
const { results, ...rest } = response.data;
|
||||
|
||||
const formattedResults = results.map(movie => ({
|
||||
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({
|
||||
...rest,
|
||||
...data,
|
||||
results: formattedResults
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Search movies error:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to search movies',
|
||||
details: error.message
|
||||
console.error('Error searching movies:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /search/multi:
|
||||
* get:
|
||||
* summary: Мультипоиск
|
||||
* description: Поиск фильмов и сериалов по запросу
|
||||
* tags: [search]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: query
|
||||
* required: true
|
||||
* description: Поисковый запрос
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: page
|
||||
* description: Номер страницы
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* default: 1
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Успешный поиск
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* page:
|
||||
* type: integer
|
||||
* results:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* title:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* media_type:
|
||||
* type: string
|
||||
* enum: [movie, tv]
|
||||
*/
|
||||
router.get('/search/multi', async (req, res) => {
|
||||
try {
|
||||
const { query, page = 1 } = req.query;
|
||||
|
||||
if (!query) {
|
||||
return res.status(400).json({ error: 'Query parameter is required' });
|
||||
}
|
||||
|
||||
console.log('Multi search request:', { query, page });
|
||||
|
||||
// Параллельный поиск фильмов и сериалов
|
||||
const [moviesData, tvData] = await Promise.all([
|
||||
req.tmdb.searchMovies(query, page),
|
||||
req.tmdb.searchTVShows(query, page)
|
||||
]);
|
||||
|
||||
// Объединяем и сортируем результаты по популярности
|
||||
const combinedResults = [
|
||||
...moviesData.results.map(movie => ({
|
||||
...movie,
|
||||
media_type: 'movie',
|
||||
release_date: formatDate(movie.release_date)
|
||||
})),
|
||||
...tvData.results.map(show => ({
|
||||
...show,
|
||||
media_type: 'tv',
|
||||
first_air_date: formatDate(show.first_air_date)
|
||||
}))
|
||||
].sort((a, b) => b.popularity - a.popularity);
|
||||
|
||||
// Пагинация результатов
|
||||
const itemsPerPage = 20;
|
||||
const startIndex = (parseInt(page) - 1) * itemsPerPage;
|
||||
const paginatedResults = combinedResults.slice(startIndex, startIndex + itemsPerPage);
|
||||
|
||||
res.json({
|
||||
page: parseInt(page),
|
||||
results: paginatedResults,
|
||||
total_pages: Math.ceil(combinedResults.length / itemsPerPage),
|
||||
total_results: combinedResults.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in multi search:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -452,57 +541,129 @@ router.get('/upcoming', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// TV Shows Routes
|
||||
router.get('/tv/search', async (req, res) => {
|
||||
try {
|
||||
const { query, page } = req.query;
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
|
||||
if (!query) {
|
||||
return res.status(400).json({ error: 'Query parameter is required' });
|
||||
}
|
||||
|
||||
if (pageNum < 1) {
|
||||
return res.status(400).json({ error: 'Page must be greater than 0' });
|
||||
}
|
||||
|
||||
const response = await req.tmdb.searchTVShows(query, pageNum);
|
||||
|
||||
if (!response || !response.data) {
|
||||
throw new Error('Failed to fetch data from TMDB');
|
||||
}
|
||||
|
||||
const { results, ...rest } = response.data;
|
||||
|
||||
const formattedResults = results.map(show => ({
|
||||
...show,
|
||||
first_air_date: formatDate(show.first_air_date)
|
||||
}));
|
||||
|
||||
res.json({
|
||||
...rest,
|
||||
results: formattedResults
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error searching TV shows:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/tv/:id', async (req, res) => {
|
||||
/**
|
||||
* @swagger
|
||||
* /movies/{id}/videos:
|
||||
* get:
|
||||
* summary: Видео фильма
|
||||
* description: Получает список видео для фильма (трейлеры, тизеры и т.д.)
|
||||
* tags: [movies]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* description: ID фильма
|
||||
* schema:
|
||||
* type: integer
|
||||
* example: 550
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Список видео
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* results:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* key:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* site:
|
||||
* type: string
|
||||
* type:
|
||||
* type: string
|
||||
* 404:
|
||||
* description: Видео не найдены
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 500:
|
||||
* description: Ошибка сервера
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
router.get('/:id/videos', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const show = await req.tmdb.getTVShow(id);
|
||||
show.first_air_date = formatDate(show.first_air_date);
|
||||
res.json(show);
|
||||
const videos = await req.tmdb.getMovieVideos(id);
|
||||
|
||||
if (!videos || !videos.results) {
|
||||
return res.status(404).json({ error: 'Videos not found' });
|
||||
}
|
||||
|
||||
res.json(videos);
|
||||
} catch (error) {
|
||||
console.error('Error fetching TV show:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
console.error('Get videos error:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch videos',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/tv/popular', async (req, res) => {
|
||||
/**
|
||||
* @swagger
|
||||
* /movies/genre/{id}:
|
||||
* get:
|
||||
* summary: Фильмы по жанру
|
||||
* description: Получает список фильмов определенного жанра
|
||||
* tags: [movies]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* description: ID жанра
|
||||
* schema:
|
||||
* type: integer
|
||||
* example: 28
|
||||
* - in: query
|
||||
* name: page
|
||||
* description: Номер страницы
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* default: 1
|
||||
* example: 1
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Список фильмов
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* page:
|
||||
* type: integer
|
||||
* results:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Movie'
|
||||
* 404:
|
||||
* description: Жанр не найден
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 500:
|
||||
* description: Ошибка сервера
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
router.get('/genre/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { page } = req.query;
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
|
||||
@@ -510,59 +671,28 @@ router.get('/tv/popular', async (req, res) => {
|
||||
return res.status(400).json({ error: 'Page must be greater than 0' });
|
||||
}
|
||||
|
||||
const response = await req.tmdb.getPopularTVShows(pageNum);
|
||||
const movies = await req.tmdb.getMoviesByGenre(id, pageNum);
|
||||
|
||||
if (!response || !response.data) {
|
||||
throw new Error('Failed to fetch data from TMDB');
|
||||
if (!movies || !movies.results) {
|
||||
return res.status(404).json({ error: 'Movies not found for this genre' });
|
||||
}
|
||||
|
||||
const { results, ...rest } = response.data;
|
||||
|
||||
const formattedResults = results.map(show => ({
|
||||
...show,
|
||||
first_air_date: formatDate(show.first_air_date)
|
||||
const formattedResults = movies.results.map(movie => ({
|
||||
...movie,
|
||||
release_date: formatDate(movie.release_date)
|
||||
}));
|
||||
|
||||
res.json({
|
||||
...rest,
|
||||
...movies,
|
||||
results: formattedResults
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching popular TV shows:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/tv/top-rated', async (req, res) => {
|
||||
try {
|
||||
const { page } = req.query;
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
|
||||
if (pageNum < 1) {
|
||||
return res.status(400).json({ error: 'Page must be greater than 0' });
|
||||
}
|
||||
|
||||
const response = await req.tmdb.getTopRatedTVShows(pageNum);
|
||||
|
||||
if (!response || !response.data) {
|
||||
throw new Error('Failed to fetch data from TMDB');
|
||||
}
|
||||
|
||||
const { results, ...rest } = response.data;
|
||||
|
||||
const formattedResults = results.map(show => ({
|
||||
...show,
|
||||
first_air_date: formatDate(show.first_air_date)
|
||||
}));
|
||||
|
||||
res.json({
|
||||
...rest,
|
||||
results: formattedResults
|
||||
console.error('Get movies by genre error:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch movies by genre',
|
||||
details: error.message
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching top rated TV shows:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
310
src/routes/tv.js
Normal file
310
src/routes/tv.js
Normal file
@@ -0,0 +1,310 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { formatDate } = require('../utils/date');
|
||||
|
||||
// Middleware для логирования запросов
|
||||
router.use((req, res, next) => {
|
||||
console.log('TV Shows API Request:', {
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
query: req.query,
|
||||
params: req.params
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /tv/popular:
|
||||
* get:
|
||||
* summary: Популярные сериалы
|
||||
* description: Получает список популярных сериалов с русскими названиями и описаниями
|
||||
* tags: [tv]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* description: Номер страницы
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* default: 1
|
||||
* example: 1
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Список популярных сериалов
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* page:
|
||||
* type: integer
|
||||
* results:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/TVShow'
|
||||
* 500:
|
||||
* description: Ошибка сервера
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
router.get('/popular', async (req, res) => {
|
||||
try {
|
||||
const { page } = req.query;
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
|
||||
if (pageNum < 1) {
|
||||
return res.status(400).json({ error: 'Page must be greater than 0' });
|
||||
}
|
||||
|
||||
const response = await req.tmdb.getPopularTVShows(pageNum);
|
||||
|
||||
if (!response || !response.results) {
|
||||
throw new Error('Invalid response from TMDB');
|
||||
}
|
||||
|
||||
const formattedResults = response.results.map(show => ({
|
||||
...show,
|
||||
first_air_date: formatDate(show.first_air_date)
|
||||
}));
|
||||
|
||||
res.json({
|
||||
...response,
|
||||
results: formattedResults
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Popular TV shows error:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch popular TV shows',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /tv/search:
|
||||
* get:
|
||||
* summary: Поиск сериалов
|
||||
* description: Поиск сериалов по запросу с поддержкой русского языка
|
||||
* tags: [tv]
|
||||
* 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
|
||||
* results:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/TVShow'
|
||||
* 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 } = req.query;
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
|
||||
if (!query) {
|
||||
return res.status(400).json({ error: 'Query parameter is required' });
|
||||
}
|
||||
|
||||
if (pageNum < 1) {
|
||||
return res.status(400).json({ error: 'Page must be greater than 0' });
|
||||
}
|
||||
|
||||
const response = await req.tmdb.searchTVShows(query, pageNum);
|
||||
|
||||
if (!response || !response.results) {
|
||||
throw new Error('Failed to fetch data from TMDB');
|
||||
}
|
||||
|
||||
const formattedResults = response.results.map(show => ({
|
||||
...show,
|
||||
first_air_date: formatDate(show.first_air_date)
|
||||
}));
|
||||
|
||||
res.json({
|
||||
...response,
|
||||
results: formattedResults
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Search TV shows error:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to search TV shows',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /tv/{id}:
|
||||
* get:
|
||||
* summary: Детали сериала
|
||||
* description: Получает подробную информацию о сериале по его ID
|
||||
* tags: [tv]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* description: ID сериала
|
||||
* schema:
|
||||
* type: integer
|
||||
* example: 1399
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Детали сериала
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/TVShow'
|
||||
* 404:
|
||||
* description: Сериал не найден
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 500:
|
||||
* description: Ошибка сервера
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const show = await req.tmdb.getTVShow(id);
|
||||
|
||||
if (!show) {
|
||||
return res.status(404).json({ error: 'TV show not found' });
|
||||
}
|
||||
|
||||
// Ensure all required fields are present and formatted correctly
|
||||
const formattedShow = {
|
||||
id: show.id,
|
||||
name: show.name,
|
||||
overview: show.overview,
|
||||
poster_path: show.poster_path,
|
||||
backdrop_path: show.backdrop_path,
|
||||
first_air_date: formatDate(show.first_air_date),
|
||||
vote_average: show.vote_average,
|
||||
vote_count: show.vote_count,
|
||||
number_of_seasons: show.number_of_seasons,
|
||||
number_of_episodes: show.number_of_episodes,
|
||||
genres: show.genres || [],
|
||||
genre_ids: show.genre_ids || show.genres?.map(g => g.id) || [],
|
||||
credits: show.credits || { cast: [], crew: [] },
|
||||
videos: show.videos || { results: [] }
|
||||
};
|
||||
|
||||
res.json(formattedShow);
|
||||
} catch (error) {
|
||||
console.error('Get TV show error:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch TV show details',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /tv/{id}/external-ids:
|
||||
* get:
|
||||
* summary: Внешние ID сериала
|
||||
* description: Получает внешние идентификаторы сериала (IMDb, и т.д.)
|
||||
* tags: [tv]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* description: ID сериала
|
||||
* schema:
|
||||
* type: integer
|
||||
* example: 1399
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Внешние ID сериала
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* imdb_id:
|
||||
* type: string
|
||||
* tvdb_id:
|
||||
* type: integer
|
||||
* facebook_id:
|
||||
* type: string
|
||||
* instagram_id:
|
||||
* type: string
|
||||
* twitter_id:
|
||||
* type: string
|
||||
* 404:
|
||||
* description: Сериал не найден
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 500:
|
||||
* description: Ошибка сервера
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
router.get('/:id/external-ids', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const externalIds = await req.tmdb.getTVShowExternalIDs(id);
|
||||
|
||||
if (!externalIds) {
|
||||
return res.status(404).json({ error: 'External IDs not found' });
|
||||
}
|
||||
|
||||
res.json(externalIds);
|
||||
} catch (error) {
|
||||
console.error('Get external IDs error:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch external IDs',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user