mirror of
https://gitlab.com/foxixus/neomovies-api.git
synced 2025-10-27 17:38:51 +05:00
Update 11 files
- /src/index.js - /src/routes/movies.js - /src/config/tmdb.js - /src/utils/health.js - /src/utils/date.js - /clean.sh - /package.json - /package-lock.json - /vercel.json - /build.sh - /README.md
This commit is contained in:
138
README.md
138
README.md
@@ -1,87 +1,85 @@
|
|||||||
# Neo Movies API
|
# Neo Movies API
|
||||||
|
|
||||||
API для поиска фильмов и сериалов с поддержкой русского языка.
|
REST API для поиска и получения информации о фильмах, использующий TMDB API.
|
||||||
|
|
||||||
## Деплой на AlwaysData
|
## Особенности
|
||||||
|
|
||||||
1. Создайте аккаунт на [AlwaysData](https://www.alwaysdata.com)
|
- Поиск фильмов
|
||||||
|
- Информация о фильмах
|
||||||
|
- Популярные фильмы
|
||||||
|
- Топ рейтинговые фильмы
|
||||||
|
- Предстоящие фильмы
|
||||||
|
- Swagger документация
|
||||||
|
- Поддержка русского языка
|
||||||
|
|
||||||
2. Настройте SSH ключ:
|
## Установка
|
||||||
```bash
|
|
||||||
# Создайте SSH ключ если его нет
|
|
||||||
ssh-keygen -t rsa -b 4096
|
|
||||||
|
|
||||||
# Скопируйте публичный ключ
|
|
||||||
cat ~/.ssh/id_rsa.pub
|
|
||||||
```
|
|
||||||
Добавьте ключ в настройках AlwaysData (SSH Keys)
|
|
||||||
|
|
||||||
3. Подключитесь по SSH:
|
1. Клонируйте репозиторий:
|
||||||
```bash
|
|
||||||
# Замените username на ваш логин
|
|
||||||
ssh username@ssh-username.alwaysdata.net
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Установите Go:
|
|
||||||
```bash
|
|
||||||
# Создайте директорию для Go
|
|
||||||
mkdir -p $HOME/go/bin
|
|
||||||
|
|
||||||
# Скачайте и установите Go
|
|
||||||
wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz
|
|
||||||
tar -C $HOME -xzf go1.21.5.linux-amd64.tar.gz
|
|
||||||
|
|
||||||
# Добавьте Go в PATH
|
|
||||||
echo 'export PATH=$HOME/go/bin:$HOME/go/bin:$PATH' >> ~/.bashrc
|
|
||||||
source ~/.bashrc
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Клонируйте репозиторий:
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/ваш-username/neomovies-api.git
|
|
||||||
cd neomovies-api
|
|
||||||
```
|
|
||||||
|
|
||||||
6. Соберите приложение:
|
|
||||||
```bash
|
|
||||||
chmod +x build.sh
|
|
||||||
./build.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
7. Настройте сервис в панели AlwaysData:
|
|
||||||
- Type: Site
|
|
||||||
- Name: neomovies-api
|
|
||||||
- Address: api.your-name.alwaysdata.net
|
|
||||||
- Command: $HOME/neomovies-api/run.sh
|
|
||||||
- Working directory: $HOME/neomovies-api
|
|
||||||
|
|
||||||
8. Добавьте переменные окружения:
|
|
||||||
- `TMDB_ACCESS_TOKEN`: Ваш токен TMDB API
|
|
||||||
- `PORT`: 8080 (или порт по умолчанию)
|
|
||||||
|
|
||||||
После деплоя ваше API будет доступно по адресу: https://api.your-name.alwaysdata.net
|
|
||||||
|
|
||||||
## Локальная разработка
|
|
||||||
|
|
||||||
1. Установите зависимости:
|
|
||||||
```bash
|
```bash
|
||||||
go mod download
|
git clone https://github.com/yourusername/neomovies-api.git
|
||||||
|
cd neomovies-api
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Запустите сервер:
|
2. Установите зависимости:
|
||||||
```bash
|
```bash
|
||||||
go run main.go
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
API будет доступно по адресу: http://localhost:8080
|
3. Создайте файл `.env` на основе `.env.example`:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Добавьте ваш TMDB Access Token в `.env` файл:
|
||||||
|
```
|
||||||
|
TMDB_ACCESS_TOKEN=your_tmdb_access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
Для разработки:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Для продакшена:
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Развертывание на Vercel
|
||||||
|
|
||||||
|
1. Установите Vercel CLI:
|
||||||
|
```bash
|
||||||
|
npm i -g vercel
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Войдите в ваш аккаунт Vercel:
|
||||||
|
```bash
|
||||||
|
vercel login
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Разверните приложение:
|
||||||
|
```bash
|
||||||
|
vercel
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Добавьте переменные окружения в Vercel:
|
||||||
|
- Перейдите в настройки проекта на Vercel
|
||||||
|
- Добавьте `TMDB_ACCESS_TOKEN` в раздел Environment Variables
|
||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
- `GET /movies/search` - Поиск фильмов
|
|
||||||
- `GET /movies/popular` - Популярные фильмы
|
|
||||||
- `GET /movies/top-rated` - Лучшие фильмы
|
|
||||||
- `GET /movies/upcoming` - Предстоящие фильмы
|
|
||||||
- `GET /movies/:id` - Информация о фильме
|
|
||||||
- `GET /health` - Проверка работоспособности API
|
- `GET /health` - Проверка работоспособности API
|
||||||
|
- `GET /movies/search?query=<search_term>&page=<page_number>` - Поиск фильмов
|
||||||
|
- `GET /movies/:id` - Получить информацию о фильме
|
||||||
|
- `GET /movies/popular` - Получить список популярных фильмов
|
||||||
|
- `GET /movies/top-rated` - Получить список топ рейтинговых фильмов
|
||||||
|
- `GET /movies/upcoming` - Получить список предстоящих фильмов
|
||||||
|
- `GET /movies/:id/external-ids` - Получить внешние ID фильма
|
||||||
|
|
||||||
Полная документация API доступна по адресу: `/swagger/index.html`
|
## Документация API
|
||||||
|
|
||||||
|
После запуска API, документация Swagger доступна по адресу:
|
||||||
|
```
|
||||||
|
http://localhost:3000/api-docs
|
||||||
|
|||||||
27
build.sh
27
build.sh
@@ -1,7 +1,26 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Переходим в директорию с приложением
|
# Создаем директорию для сборки
|
||||||
cd "$HOME/neomovies-api"
|
BUILD_DIR="$HOME/build_tmp"
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
# Собираем приложение
|
# Скачиваем и устанавливаем Go во временную директорию
|
||||||
go build -o app
|
curl -L https://go.dev/dl/go1.21.5.linux-amd64.tar.gz | tar -C "$BUILD_DIR" -xz
|
||||||
|
|
||||||
|
# Настраиваем переменные окружения для Go
|
||||||
|
export PATH="$BUILD_DIR/go/bin:$PATH"
|
||||||
|
export GOPATH="$BUILD_DIR/go_path"
|
||||||
|
export GOCACHE="$BUILD_DIR/go-build"
|
||||||
|
export GOMODCACHE="$BUILD_DIR/go-mod"
|
||||||
|
|
||||||
|
# Создаем необходимые директории
|
||||||
|
mkdir -p "$GOPATH"
|
||||||
|
mkdir -p "$GOCACHE"
|
||||||
|
mkdir -p "$GOMODCACHE"
|
||||||
|
|
||||||
|
# Собираем приложение с отключенным CGO и уменьшенным бинарником
|
||||||
|
cd "$HOME/neomovies-api"
|
||||||
|
CGO_ENABLED=0 go build -ldflags="-s -w" -o app
|
||||||
|
|
||||||
|
# Очищаем после сборки
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
|||||||
13
clean.sh
Normal file
13
clean.sh
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Очищаем кэш Go
|
||||||
|
rm -rf $HOME/go/pkg/*
|
||||||
|
rm -rf $HOME/.cache/go-build/*
|
||||||
|
|
||||||
|
# Удаляем временные файлы
|
||||||
|
rm -f go1.21.5.linux-amd64.tar.gz
|
||||||
|
rm -rf $HOME/go/src/*
|
||||||
|
|
||||||
|
# Очищаем ненужные файлы в проекте
|
||||||
|
rm -rf vendor/
|
||||||
|
rm -f app
|
||||||
1629
package-lock.json
generated
Normal file
1629
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "neomovies-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Neo Movies API with TMDB integration",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nodemon src/index.js",
|
||||||
|
"start": "node src/index.js",
|
||||||
|
"vercel-build": "echo hello"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"swagger-jsdoc": "^6.2.8",
|
||||||
|
"swagger-ui-express": "^5.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/config/tmdb.js
Normal file
105
src/config/tmdb.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
class TMDBClient {
|
||||||
|
constructor(accessToken) {
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: 'https://api.themoviedb.org/3',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async makeRequest(method, endpoint, params = {}) {
|
||||||
|
try {
|
||||||
|
const response = await this.client.request({
|
||||||
|
method,
|
||||||
|
url: endpoint,
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
language: 'ru-RU',
|
||||||
|
region: 'RU'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`TMDB API Error: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getImageURL(path, size = 'original') {
|
||||||
|
if (!path) return null;
|
||||||
|
return `https://image.tmdb.org/t/p/${size}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchMovies(query, page = 1) {
|
||||||
|
const data = await this.makeRequest('GET', '/search/movie', {
|
||||||
|
query,
|
||||||
|
page,
|
||||||
|
include_adult: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Фильтруем результаты
|
||||||
|
data.results = data.results.filter(movie =>
|
||||||
|
movie.poster_path &&
|
||||||
|
movie.overview &&
|
||||||
|
movie.vote_average > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Добавляем полные URL для изображений
|
||||||
|
data.results = data.results.map(movie => ({
|
||||||
|
...movie,
|
||||||
|
poster_path: this.getImageURL(movie.poster_path, 'w500'),
|
||||||
|
backdrop_path: this.getImageURL(movie.backdrop_path, 'w1280')
|
||||||
|
}));
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMovie(id) {
|
||||||
|
const movie = await this.makeRequest('GET', `/movie/${id}`);
|
||||||
|
return {
|
||||||
|
...movie,
|
||||||
|
poster_path: this.getImageURL(movie.poster_path, 'w500'),
|
||||||
|
backdrop_path: this.getImageURL(movie.backdrop_path, 'w1280')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPopularMovies(page = 1) {
|
||||||
|
const data = await this.makeRequest('GET', '/movie/popular', { page });
|
||||||
|
data.results = data.results.map(movie => ({
|
||||||
|
...movie,
|
||||||
|
poster_path: this.getImageURL(movie.poster_path, 'w500'),
|
||||||
|
backdrop_path: this.getImageURL(movie.backdrop_path, 'w1280')
|
||||||
|
}));
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTopRatedMovies(page = 1) {
|
||||||
|
const data = await this.makeRequest('GET', '/movie/top_rated', { page });
|
||||||
|
data.results = data.results.map(movie => ({
|
||||||
|
...movie,
|
||||||
|
poster_path: this.getImageURL(movie.poster_path, 'w500'),
|
||||||
|
backdrop_path: this.getImageURL(movie.backdrop_path, 'w1280')
|
||||||
|
}));
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUpcomingMovies(page = 1) {
|
||||||
|
const data = await this.makeRequest('GET', '/movie/upcoming', { page });
|
||||||
|
data.results = data.results.map(movie => ({
|
||||||
|
...movie,
|
||||||
|
poster_path: this.getImageURL(movie.poster_path, 'w500'),
|
||||||
|
backdrop_path: this.getImageURL(movie.backdrop_path, 'w1280')
|
||||||
|
}));
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMovieExternalIDs(id) {
|
||||||
|
return await this.makeRequest('GET', `/movie/${id}/external_ids`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TMDBClient;
|
||||||
257
src/index.js
Normal file
257
src/index.js
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const swaggerJsdoc = require('swagger-jsdoc');
|
||||||
|
const swaggerUi = require('swagger-ui-express');
|
||||||
|
const TMDBClient = require('./config/tmdb');
|
||||||
|
const healthCheck = require('./utils/health');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// 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://github.com/yourusername/neomovies-api'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: `http://localhost:${port}`,
|
||||||
|
description: 'Development server',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
name: 'movies',
|
||||||
|
description: 'Операции с фильмами'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'health',
|
||||||
|
description: 'Проверка работоспособности API'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
schemas: {
|
||||||
|
Movie: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'ID фильма'
|
||||||
|
},
|
||||||
|
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: 'Сообщение об ошибке'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Health: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['healthy', 'unhealthy'],
|
||||||
|
description: 'Общий статус API'
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Версия API'
|
||||||
|
},
|
||||||
|
uptime: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
seconds: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Время работы в секундах'
|
||||||
|
},
|
||||||
|
formatted: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Отформатированное время работы'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tmdb: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['ok', 'error'],
|
||||||
|
description: 'Статус подключения к TMDB'
|
||||||
|
},
|
||||||
|
responseTime: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Время ответа TMDB в мс'
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Сообщение об ошибке, если есть'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
heapTotal: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Общий размер кучи (MB)'
|
||||||
|
},
|
||||||
|
heapUsed: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Использованный размер кучи (MB)'
|
||||||
|
},
|
||||||
|
rss: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Resident Set Size (MB)'
|
||||||
|
},
|
||||||
|
memoryUsage: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Процент использования памяти'
|
||||||
|
},
|
||||||
|
system: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
total: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Общая память системы (MB)'
|
||||||
|
},
|
||||||
|
free: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Свободная память системы (MB)'
|
||||||
|
},
|
||||||
|
usage: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Процент использования системной памяти'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
system: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
platform: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Операционная система'
|
||||||
|
},
|
||||||
|
arch: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Архитектура процессора'
|
||||||
|
},
|
||||||
|
nodeVersion: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Версия Node.js'
|
||||||
|
},
|
||||||
|
cpuUsage: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Загрузка CPU'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
description: 'Время проверки'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
apis: ['./src/routes/*.js', './src/index.js'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const swaggerDocs = swaggerJsdoc(swaggerOptions);
|
||||||
|
|
||||||
|
// Custom CSS для Swagger UI
|
||||||
|
const swaggerCustomOptions = {
|
||||||
|
customCss: '.swagger-ui .topbar { display: none }',
|
||||||
|
customSiteTitle: "Neo Movies API Documentation",
|
||||||
|
customfavIcon: "https://www.themoviedb.org/favicon.ico"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// TMDB client middleware
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (!process.env.TMDB_ACCESS_TOKEN) {
|
||||||
|
return res.status(500).json({ error: 'TMDB_ACCESS_TOKEN is not set' });
|
||||||
|
}
|
||||||
|
req.tmdb = new TMDBClient(process.env.TMDB_ACCESS_TOKEN);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs, swaggerCustomOptions));
|
||||||
|
app.use('/movies', require('./routes/movies'));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /health:
|
||||||
|
* get:
|
||||||
|
* tags: [health]
|
||||||
|
* summary: Проверка работоспособности API
|
||||||
|
* description: Возвращает подробную информацию о состоянии API, включая статус TMDB, использование памяти и системную информацию
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: API работает нормально
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Health'
|
||||||
|
*/
|
||||||
|
app.get('/health', async (req, res) => {
|
||||||
|
const health = await healthCheck.getFullHealth(req.tmdb);
|
||||||
|
res.json(health);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).json({ error: 'Something went wrong!' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Server is running on port ${port}`);
|
||||||
|
console.log(`Documentation available at http://localhost:${port}/api-docs`);
|
||||||
|
});
|
||||||
325
src/routes/movies.js
Normal file
325
src/routes/movies.js
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { formatDate } = require('../utils/date');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await req.tmdb.searchMovies(query, page);
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
page: results.page,
|
||||||
|
total_pages: results.total_pages,
|
||||||
|
total_results: results.total_results,
|
||||||
|
results: results.results.map(movie => ({
|
||||||
|
id: movie.id,
|
||||||
|
title: movie.title,
|
||||||
|
overview: movie.overview,
|
||||||
|
release_date: formatDate(movie.release_date),
|
||||||
|
vote_average: movie.vote_average,
|
||||||
|
poster_path: movie.poster_path,
|
||||||
|
backdrop_path: movie.backdrop_path
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /movies/{id}:
|
||||||
|
* get:
|
||||||
|
* summary: Получить информацию о фильме
|
||||||
|
* description: Получает детальную информацию о фильме по ID
|
||||||
|
* tags: [movies]
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* description: ID фильма
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* example: 603
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Информация о фильме
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Movie'
|
||||||
|
* 500:
|
||||||
|
* description: Ошибка сервера
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
router.get('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const movie = await req.tmdb.getMovie(req.params.id);
|
||||||
|
res.json({
|
||||||
|
...movie,
|
||||||
|
release_date: formatDate(movie.release_date)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /movies/popular:
|
||||||
|
* get:
|
||||||
|
* summary: Популярные фильмы
|
||||||
|
* description: Получает список популярных фильмов с русскими названиями и описаниями
|
||||||
|
* tags: [movies]
|
||||||
|
* 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/Movie'
|
||||||
|
* 500:
|
||||||
|
* description: Ошибка сервера
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
router.get('/popular', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { page = 1 } = req.query;
|
||||||
|
const movies = await req.tmdb.getPopularMovies(page);
|
||||||
|
res.json(movies);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /movies/top-rated:
|
||||||
|
* get:
|
||||||
|
* summary: Лучшие фильмы
|
||||||
|
* description: Получает список лучших фильмов с русскими названиями и описаниями
|
||||||
|
* tags: [movies]
|
||||||
|
* 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/Movie'
|
||||||
|
* 500:
|
||||||
|
* description: Ошибка сервера
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
router.get('/top-rated', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { page = 1 } = req.query;
|
||||||
|
const movies = await req.tmdb.getTopRatedMovies(page);
|
||||||
|
res.json(movies);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /movies/upcoming:
|
||||||
|
* get:
|
||||||
|
* summary: Предстоящие фильмы
|
||||||
|
* description: Получает список предстоящих фильмов с русскими названиями и описаниями
|
||||||
|
* tags: [movies]
|
||||||
|
* 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/Movie'
|
||||||
|
* 500:
|
||||||
|
* description: Ошибка сервера
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
router.get('/upcoming', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { page = 1 } = req.query;
|
||||||
|
const movies = await req.tmdb.getUpcomingMovies(page);
|
||||||
|
res.json(movies);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /movies/{id}/external-ids:
|
||||||
|
* get:
|
||||||
|
* summary: Внешние ID фильма
|
||||||
|
* description: Получает внешние идентификаторы фильма (IMDb и др.)
|
||||||
|
* tags: [movies]
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* description: ID фильма
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* example: 603
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Внешние ID фильма
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* imdb_id:
|
||||||
|
* type: string
|
||||||
|
* description: ID на IMDb
|
||||||
|
* facebook_id:
|
||||||
|
* type: string
|
||||||
|
* description: ID на Facebook
|
||||||
|
* instagram_id:
|
||||||
|
* type: string
|
||||||
|
* description: ID на Instagram
|
||||||
|
* twitter_id:
|
||||||
|
* type: string
|
||||||
|
* description: ID на Twitter
|
||||||
|
* 500:
|
||||||
|
* description: Ошибка сервера
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
router.get('/:id/external-ids', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const externalIds = await req.tmdb.getMovieExternalIDs(req.params.id);
|
||||||
|
res.json(externalIds);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
13
src/utils/date.js
Normal file
13
src/utils/date.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
function formatDate(dateString) {
|
||||||
|
if (!dateString) return null;
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('ru-RU', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
formatDate
|
||||||
|
};
|
||||||
103
src/utils/health.js
Normal file
103
src/utils/health.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
const os = require('os');
|
||||||
|
const process = require('process');
|
||||||
|
|
||||||
|
class HealthCheck {
|
||||||
|
constructor() {
|
||||||
|
this.startTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
getUptime() {
|
||||||
|
return Math.floor((Date.now() - this.startTime) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMemoryUsage() {
|
||||||
|
const used = process.memoryUsage();
|
||||||
|
return {
|
||||||
|
heapTotal: Math.round(used.heapTotal / 1024 / 1024), // MB
|
||||||
|
heapUsed: Math.round(used.heapUsed / 1024 / 1024), // MB
|
||||||
|
rss: Math.round(used.rss / 1024 / 1024), // MB
|
||||||
|
memoryUsage: Math.round((used.heapUsed / used.heapTotal) * 100) // %
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getSystemInfo() {
|
||||||
|
return {
|
||||||
|
platform: process.platform,
|
||||||
|
arch: process.arch,
|
||||||
|
nodeVersion: process.version,
|
||||||
|
cpuUsage: Math.round(os.loadavg()[0] * 100) / 100,
|
||||||
|
totalMemory: Math.round(os.totalmem() / 1024 / 1024), // MB
|
||||||
|
freeMemory: Math.round(os.freemem() / 1024 / 1024) // MB
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkTMDBConnection(tmdbClient) {
|
||||||
|
try {
|
||||||
|
const startTime = Date.now();
|
||||||
|
await tmdbClient.makeRequest('GET', '/configuration');
|
||||||
|
const endTime = Date.now();
|
||||||
|
return {
|
||||||
|
status: 'ok',
|
||||||
|
responseTime: endTime - startTime
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatUptime(seconds) {
|
||||||
|
const days = Math.floor(seconds / (24 * 60 * 60));
|
||||||
|
const hours = Math.floor((seconds % (24 * 60 * 60)) / (60 * 60));
|
||||||
|
const minutes = Math.floor((seconds % (60 * 60)) / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
if (days > 0) parts.push(`${days}d`);
|
||||||
|
if (hours > 0) parts.push(`${hours}h`);
|
||||||
|
if (minutes > 0) parts.push(`${minutes}m`);
|
||||||
|
if (remainingSeconds > 0 || parts.length === 0) parts.push(`${remainingSeconds}s`);
|
||||||
|
|
||||||
|
return parts.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFullHealth(tmdbClient) {
|
||||||
|
const uptime = this.getUptime();
|
||||||
|
const tmdbStatus = await this.checkTMDBConnection(tmdbClient);
|
||||||
|
const memory = this.getMemoryUsage();
|
||||||
|
const system = this.getSystemInfo();
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: tmdbStatus.status === 'ok' ? 'healthy' : 'unhealthy',
|
||||||
|
version: process.env.npm_package_version || '1.0.0',
|
||||||
|
uptime: {
|
||||||
|
seconds: uptime,
|
||||||
|
formatted: this.formatUptime(uptime)
|
||||||
|
},
|
||||||
|
tmdb: {
|
||||||
|
status: tmdbStatus.status,
|
||||||
|
responseTime: tmdbStatus.responseTime,
|
||||||
|
error: tmdbStatus.error
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
...memory,
|
||||||
|
system: {
|
||||||
|
total: system.totalMemory,
|
||||||
|
free: system.freeMemory,
|
||||||
|
usage: Math.round(((system.totalMemory - system.freeMemory) / system.totalMemory) * 100)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
system: {
|
||||||
|
platform: system.platform,
|
||||||
|
arch: system.arch,
|
||||||
|
nodeVersion: system.nodeVersion,
|
||||||
|
cpuUsage: system.cpuUsage
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new HealthCheck();
|
||||||
15
vercel.json
Normal file
15
vercel.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"builds": [
|
||||||
|
{
|
||||||
|
"src": "src/index.js",
|
||||||
|
"use": "@vercel/node"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"src": "/(.*)",
|
||||||
|
"dest": "src/index.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user