2025-01-03 19:46:10 +00:00
|
|
|
|
require('dotenv').config();
|
|
|
|
|
|
const express = require('express');
|
|
|
|
|
|
const cors = require('cors');
|
|
|
|
|
|
const swaggerJsdoc = require('swagger-jsdoc');
|
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
const TMDBClient = require('./config/tmdb');
|
|
|
|
|
|
const healthCheck = require('./utils/health');
|
2025-01-16 15:44:05 +00:00
|
|
|
|
const { formatDate } = require('./utils/date');
|
2025-01-03 19:46:10 +00:00
|
|
|
|
|
|
|
|
|
|
const app = express();
|
|
|
|
|
|
|
|
|
|
|
|
// Определяем базовый URL для документации
|
2025-01-03 20:08:13 +00:00
|
|
|
|
const BASE_URL = process.env.NODE_ENV === 'production'
|
|
|
|
|
|
? 'https://neomovies-api.vercel.app'
|
2025-01-03 20:14:34 +00:00
|
|
|
|
: 'http://localhost:3000';
|
2025-01-03 19:46:10 +00:00
|
|
|
|
|
|
|
|
|
|
// Swagger configuration
|
|
|
|
|
|
const swaggerOptions = {
|
|
|
|
|
|
definition: {
|
|
|
|
|
|
openapi: '3.0.0',
|
|
|
|
|
|
info: {
|
|
|
|
|
|
title: 'Neo Movies API',
|
|
|
|
|
|
version: '1.0.0',
|
|
|
|
|
|
description: 'API для поиска и получения информации о фильмах с поддержкой русского языка',
|
|
|
|
|
|
contact: {
|
|
|
|
|
|
name: 'API Support',
|
|
|
|
|
|
url: 'https://gitlab.com/foxixus/neomovies-api'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
servers: [
|
|
|
|
|
|
{
|
|
|
|
|
|
url: BASE_URL,
|
2025-01-03 20:14:34 +00:00
|
|
|
|
description: process.env.NODE_ENV === 'production' ? 'Production server' : 'Development server'
|
|
|
|
|
|
}
|
2025-01-03 19:46:10 +00:00
|
|
|
|
],
|
2025-07-07 18:08:42 +03:00
|
|
|
|
security: [{ bearerAuth: [] }],
|
2025-01-03 19:46:10 +00:00
|
|
|
|
tags: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'movies',
|
|
|
|
|
|
description: 'Операции с фильмами'
|
|
|
|
|
|
},
|
2025-01-16 15:44:05 +00:00
|
|
|
|
{
|
|
|
|
|
|
name: 'tv',
|
|
|
|
|
|
description: 'Операции с сериалами'
|
|
|
|
|
|
},
|
2025-01-03 19:46:10 +00:00
|
|
|
|
{
|
|
|
|
|
|
name: 'health',
|
|
|
|
|
|
description: 'Проверка работоспособности API'
|
2025-07-07 18:08:42 +03:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'auth',
|
|
|
|
|
|
description: 'Операции авторизации'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'favorites',
|
|
|
|
|
|
description: 'Операции с избранным'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'players',
|
|
|
|
|
|
description: 'Плееры Alloha и Lumex'
|
2025-01-03 19:46:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
components: {
|
2025-07-07 18:08:42 +03:00
|
|
|
|
securitySchemes: {
|
|
|
|
|
|
bearerAuth: {
|
|
|
|
|
|
type: 'http',
|
|
|
|
|
|
scheme: 'bearer',
|
|
|
|
|
|
bearerFormat: 'JWT'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-01-03 19:46:10 +00:00
|
|
|
|
schemas: {
|
|
|
|
|
|
Movie: {
|
|
|
|
|
|
type: 'object',
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
id: {
|
|
|
|
|
|
type: 'integer',
|
|
|
|
|
|
description: 'ID фильма'
|
|
|
|
|
|
},
|
|
|
|
|
|
title: {
|
|
|
|
|
|
type: 'string',
|
|
|
|
|
|
description: 'Название фильма'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-01-03 20:14:34 +00:00
|
|
|
|
apis: [path.join(__dirname, 'routes', '*.js'), __filename]
|
2025-01-03 19:46:10 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const swaggerDocs = swaggerJsdoc(swaggerOptions);
|
|
|
|
|
|
|
2025-01-03 19:58:01 +00:00
|
|
|
|
// CORS configuration
|
2025-01-16 16:28:35 +00:00
|
|
|
|
const corsOptions = {
|
|
|
|
|
|
origin: [
|
|
|
|
|
|
'http://localhost:3000',
|
|
|
|
|
|
'https://neo-movies.vercel.app',
|
|
|
|
|
|
/\.vercel\.app$/
|
|
|
|
|
|
],
|
|
|
|
|
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
|
|
|
|
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
|
|
|
|
credentials: true,
|
|
|
|
|
|
optionsSuccessStatus: 200
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
app.use(cors(corsOptions));
|
2025-01-03 19:58:01 +00:00
|
|
|
|
|
2025-01-03 20:08:13 +00:00
|
|
|
|
// Handle preflight requests
|
2025-01-16 16:28:35 +00:00
|
|
|
|
app.options('*', cors(corsOptions));
|
2025-01-03 20:08:13 +00:00
|
|
|
|
|
2025-01-03 19:46:10 +00:00
|
|
|
|
// Middleware
|
|
|
|
|
|
app.use(express.json());
|
|
|
|
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
|
|
|
|
|
|
|
|
|
|
// TMDB client middleware
|
|
|
|
|
|
app.use((req, res, next) => {
|
2025-01-04 12:54:12 +00:00
|
|
|
|
try {
|
|
|
|
|
|
const token = process.env.TMDB_ACCESS_TOKEN;
|
|
|
|
|
|
if (!token) {
|
2025-01-16 16:28:35 +00:00
|
|
|
|
console.error('TMDB_ACCESS_TOKEN is not set');
|
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
|
error: 'Server configuration error',
|
|
|
|
|
|
details: 'API token is not configured'
|
|
|
|
|
|
});
|
2025-01-04 12:54:12 +00:00
|
|
|
|
}
|
2025-01-16 16:28:35 +00:00
|
|
|
|
|
|
|
|
|
|
console.log('Initializing TMDB client...');
|
2025-01-04 12:54:12 +00:00
|
|
|
|
req.tmdb = new TMDBClient(token);
|
|
|
|
|
|
next();
|
|
|
|
|
|
} catch (error) {
|
2025-01-16 16:28:35 +00:00
|
|
|
|
console.error('Failed to initialize TMDB client:', error);
|
2025-01-04 12:54:12 +00:00
|
|
|
|
res.status(500).json({
|
2025-01-16 16:28:35 +00:00
|
|
|
|
error: 'Server initialization error',
|
|
|
|
|
|
details: error.message
|
2025-01-04 12:54:12 +00:00
|
|
|
|
});
|
2025-01-03 19:46:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-01-03 19:58:01 +00:00
|
|
|
|
// API Documentation routes
|
|
|
|
|
|
app.get('/api-docs', (req, res) => {
|
|
|
|
|
|
res.sendFile(path.join(__dirname, 'public', 'api-docs', 'index.html'));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-01-03 19:46:10 +00:00
|
|
|
|
app.get('/api-docs/swagger.json', (req, res) => {
|
|
|
|
|
|
res.setHeader('Content-Type', 'application/json');
|
|
|
|
|
|
res.send(swaggerDocs);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-01-16 15:44:05 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* @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
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-01-03 19:58:01 +00:00
|
|
|
|
// API routes
|
2025-01-16 15:44:05 +00:00
|
|
|
|
const moviesRouter = require('./routes/movies');
|
|
|
|
|
|
const tvRouter = require('./routes/tv');
|
|
|
|
|
|
const imagesRouter = require('./routes/images');
|
2025-05-28 10:27:38 +00:00
|
|
|
|
const categoriesRouter = require('./routes/categories');
|
2025-07-07 18:08:42 +03:00
|
|
|
|
const favoritesRouter = require('./routes/favorites');
|
|
|
|
|
|
const playersRouter = require('./routes/players');
|
|
|
|
|
|
require('./utils/cleanup');
|
|
|
|
|
|
const authRouter = require('./routes/auth');
|
2025-01-16 15:44:05 +00:00
|
|
|
|
|
|
|
|
|
|
app.use('/movies', moviesRouter);
|
|
|
|
|
|
app.use('/tv', tvRouter);
|
|
|
|
|
|
app.use('/images', imagesRouter);
|
2025-05-28 10:27:38 +00:00
|
|
|
|
app.use('/categories', categoriesRouter);
|
2025-07-07 18:08:42 +03:00
|
|
|
|
app.use('/favorites', favoritesRouter);
|
|
|
|
|
|
app.use('/players', playersRouter);
|
|
|
|
|
|
app.use('/auth', authRouter);
|
2025-01-03 19:46:10 +00:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* /health:
|
|
|
|
|
|
* get:
|
|
|
|
|
|
* tags: [health]
|
|
|
|
|
|
* summary: Проверка работоспособности API
|
|
|
|
|
|
* description: Возвращает подробную информацию о состоянии API, включая статус TMDB, использование памяти и системную информацию
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: API работает нормально
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
2025-01-03 20:14:34 +00:00
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* status:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* enum: [ok, error]
|
|
|
|
|
|
* tmdb:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* status:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* enum: [ok, error]
|
2025-01-03 19:46:10 +00:00
|
|
|
|
*/
|
|
|
|
|
|
app.get('/health', async (req, res) => {
|
2025-01-03 20:14:34 +00:00
|
|
|
|
try {
|
|
|
|
|
|
const health = await healthCheck.getFullHealth(req.tmdb);
|
|
|
|
|
|
res.json(health);
|
|
|
|
|
|
} catch (error) {
|
2025-01-16 15:44:05 +00:00
|
|
|
|
console.error('Health check error:', error);
|
|
|
|
|
|
res.status(500).json({
|
2025-01-03 20:14:34 +00:00
|
|
|
|
status: 'error',
|
|
|
|
|
|
error: error.message
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-01-03 19:46:10 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Error handling
|
|
|
|
|
|
app.use((err, req, res, next) => {
|
2025-01-03 20:14:34 +00:00
|
|
|
|
console.error('Error:', err);
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
error: 'Internal Server Error',
|
|
|
|
|
|
message: process.env.NODE_ENV === 'development' ? err.message : undefined
|
|
|
|
|
|
});
|
2025-01-03 19:46:10 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-01-03 20:14:34 +00:00
|
|
|
|
// Handle 404
|
|
|
|
|
|
app.use((req, res) => {
|
|
|
|
|
|
res.status(404).json({ error: 'Not Found' });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Export the Express API
|
|
|
|
|
|
module.exports = app;
|
|
|
|
|
|
|
|
|
|
|
|
// Start server only in development
|
|
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
2025-05-28 10:27:38 +00:00
|
|
|
|
// Проверяем аргументы командной строки
|
|
|
|
|
|
const args = process.argv.slice(2);
|
|
|
|
|
|
// Используем порт из аргументов командной строки, переменной окружения или по умолчанию 3000
|
|
|
|
|
|
const port = args[0] || process.env.PORT || 3000;
|
|
|
|
|
|
|
2025-01-03 20:14:34 +00:00
|
|
|
|
app.listen(port, () => {
|
|
|
|
|
|
console.log(`Server is running on port ${port}`);
|
|
|
|
|
|
console.log(`Documentation available at http://localhost:${port}/api-docs`);
|
|
|
|
|
|
});
|
2025-01-16 15:44:05 +00:00
|
|
|
|
}
|