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:
2025-01-03 19:10:34 +00:00
parent 612e49817c
commit 1f525a80d6
11 changed files with 2573 additions and 74 deletions

138
README.md
View File

@@ -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

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

22
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,15 @@
{
"version": 2,
"builds": [
{
"src": "src/index.js",
"use": "@vercel/node"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "src/index.js"
}
]
}