refactor: split into two stacks - searchFilms/ (NL) and app/ (RU)

This commit is contained in:
vrubelroman 2026-06-03 09:29:09 +00:00
parent 6ef3a10d0d
commit 51348a9d23
36 changed files with 326 additions and 1271 deletions

12
app/.env.example Normal file
View file

@ -0,0 +1,12 @@
# 🔧 App Stack — переменные окружения (RU-хост)
# Скопируйте в .env и заполните своими данными
# 🎬 NL-хост (голландский сервер)
# IP, на котором запущен search-стек (tmdb-proxy + torapi)
NL_HOST=72.56.91.135
# 🤖 Telegram Bot Token (от @BotFather)
TELEGRAM_BOT_TOKEN=ваш_то...n
# 🐳 qBittorrent (на этом же хосте, порт 8080)
QBITTORRENT_USERNAME=vrubelroman
QBITTORRENT_PASSWORD=ваш_па...

14
app/Dockerfile Normal file
View file

@ -0,0 +1,14 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV HOST=0.0.0.0
ENV PORT=8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

22
app/Dockerfile.telegram Normal file
View file

@ -0,0 +1,22 @@
FROM python:3.12-slim
WORKDIR /app
# Установка системных зависимостей
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# Копирование и установка Python зависимостей
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Копирование исходного кода
COPY telegram_bot.py .
COPY run_telegram_bot.py .
# Переменные окружения
ENV PYTHONUNBUFFERED=1
# Команда запуска
CMD ["python", "run_telegram_bot.py"]

126
app/MANAGEMENT.md Normal file
View file

@ -0,0 +1,126 @@
# 🎬 Управление сервисами findFilms
## 🚀 Быстрый старт
### Запуск всех сервисов:
```bash
./start_all_services.sh
```
### Остановка всех сервисов:
```bash
./stop_all_services.sh
```
## 📊 Статус сервисов
### Проверка статуса:
```bash
docker ps | grep -E "(movie-search|TorAPI|telegram-bot)"
```
### Проверка qBittorrent:
```bash
ps aux | grep qbittorrent | grep -v grep
```
## 🔧 Управление Docker контейнерами
### Запуск:
```bash
docker compose up -d
```
### Остановка:
```bash
docker compose down
```
### Перезапуск:
```bash
docker compose restart
```
### Просмотр логов:
```bash
# Все сервисы
docker compose logs -f
# Конкретный сервис
docker logs -f movie-search
docker logs -f telegram-bot
```
## 🌐 Доступные интерфейсы
- **Веб-интерфейс**: http://localhost:8089
- **qBittorrent**: http://localhost:8082 (admin/vrubel07)
- **Telegram Bot**: @your_bot_username
## 🔄 Автозапуск
Все Docker контейнеры настроены на автозапуск при старте системы:
- `movie-search` - веб-приложение
- `TorAPI-Search` - поиск торрентов
- `TorAPI-qBittorrent` - получение magnet ссылок
- `telegram-bot` - Telegram бот
## 🛠️ Устранение неполадок
### Проблема: Сервис не запускается
```bash
# Проверьте логи
docker logs <container_name>
# Перезапустите
docker compose restart <service_name>
```
### Проблема: Конфликт портов
```bash
# Проверьте занятые порты
lsof -i :8089
lsof -i :8082
```
### Проблема: qBittorrent не отвечает
```bash
# Перезапустите qBittorrent
pkill qbittorrent
/Applications/qBittorrent.app/Contents/MacOS/qbittorrent --webui-port=8082 --no-splash --confirm-legal-notice &
```
## 📈 Мониторинг
### Использование ресурсов:
```bash
docker stats
```
### Проверка здоровья:
```bash
# Веб-интерфейс
curl http://localhost:8089/
# qBittorrent API
curl -X POST -d "username=admin&password=vrubel07" http://localhost:8082/api/v2/auth/login
```
## 🔒 Безопасность
- Все пароли настроены в переменных окружения
- qBittorrent доступен только локально
- Telegram боты используют разные токены
## 📝 Логи
Логи всех сервисов доступны через Docker:
```bash
# Последние 50 строк
docker logs --tail 50 <container_name>
# Следить за логами в реальном времени
docker logs -f <container_name>
```

172
app/SAMBA_ACCESS.md Normal file
View file

@ -0,0 +1,172 @@
# 📺 Инструкция по доступу к видео через Samba
## 🔍 Информация о сервере
- **IP адрес**: `192.168.8.111`
- **Имя сервера**: `server`
- **Имя шары**: `VIDEO`
- **Путь к папке**: `/media/vrubel/second_drive/VIDEO`
- **Пользователь**: `vrubel` (требуется пароль)
## 🔐 Настройка пользователя Samba
Если у пользователя `vrubel` нет пароля для Samba, установите его:
```bash
sudo smbpasswd -a vrubel
```
## 🖥️ Подключение из Windows
### Способ 1: Через Проводник
1. Откройте **Проводник** (Win + E)
2. В адресной строке введите: `\\192.168.8.111\VIDEO`
3. Введите логин: `vrubel` и пароль (пароль пользователя vrubel в системе)
4. Нажмите OK
### Способ 2: Подключение сетевого диска
1. Откройте **Этот компьютер**
2. Нажмите **Подключить сетевой диск**
3. Выберите букву диска (например, Z:)
4. Введите путь: `\\192.168.8.111\VIDEO`
5. Отметьте **Использовать другие учетные данные**
6. Введите: `vrubel` и пароль
## 🐧 Подключение из Linux
### Способ 1: Через файловый менеджер
В большинстве Linux дистрибутивов:
1. Откройте файловый менеджер
2. В адресной строке введите: `smb://192.168.8.111/VIDEO`
3. Или: `smb://server/VIDEO`
4. Введите логин: `vrubel` и пароль
### Способ 2: Монтирование вручную
```bash
# Создать точку монтирования
sudo mkdir -p /mnt/video
# Монтировать шару
sudo mount -t cifs //192.168.8.111/VIDEO /mnt/video -o username=vrubel,uid=$(id -u),gid=$(id -g)
# Для автоматического монтирования при загрузке, добавьте в /etc/fstab:
# //192.168.8.111/VIDEO /mnt/video cifs username=vrubel,password=ВАШ_ПАРОЛЬ,uid=1000,gid=1000,iocharset=utf8,file_mode=0777,dir_mode=0777 0 0
```
### Способ 3: Через smbclient
```bash
# Установить smbclient (если не установлен)
sudo apt install smbclient
# Просмотр доступных шаров
smbclient -L //192.168.8.111 -U vrubel
# Подключение к шаре
smbclient //192.168.8.111/VIDEO -U vrubel
```
## 🍎 Подключение из macOS
1. Откройте **Finder**
2. Нажмите **Cmd + K** (или меню **Переход → Подключиться к серверу**)
3. Введите: `smb://192.168.8.111/VIDEO`
4. Или: `smb://server/VIDEO`
5. Выберите **Зарегистрированный пользователь**
6. Введите: `vrubel` и пароль
## 📱 Открытие в VLC Media Player
### Windows
1. Откройте VLC Media Player
2. Меню **Медиа → Открыть файл/папку** (Ctrl + O)
3. В адресной строке введите: `\\192.168.8.111\VIDEO\названиеайла.mkv`
4. Или найдите файл через проводник сетевого диска
### Linux
1. Откройте VLC Media Player
2. Меню **Медиа → Открыть файл** (Ctrl + O)
3. В адресной строке введите: `smb://192.168.8.111/VIDEO/названиеайла.mkv`
4. Или используйте путь к смонтированной папке: `/mnt/video/названиеайла.mkv`
### macOS
1. Откройте VLC Media Player
2. Меню **File → Open File** (Cmd + O)
3. Перейдите к смонтированному диску или введите: `smb://192.168.8.111/VIDEO/названиеайла.mkv`
## 🌐 Прямая ссылка в VLC через сеть
Вы можете открыть файл напрямую по сети в VLC:
### Windows
```
\\192.168.8.111\VIDEO\названиеайла.mkv
```
### Linux/macOS
```
smb://192.168.8.111/VIDEO/названиеайла.mkv
```
Или использовать IP адрес сервера:
```
smb://vrubel@192.168.8.111/VIDEO/названиеайла.mkv
```
С паролем в URL (не рекомендуется для безопасности):
```
smb://vrubel:пароль@192.168.8.111/VIDEO/названиеайла.mkv
```
## 🔧 Устранение проблем
### Проблема: Не могу подключиться
1. Проверьте, что сервер доступен: `ping 192.168.8.111`
2. Проверьте, что Samba работает:
```bash
sudo systemctl status smbd
```
3. Проверьте firewall (если активен):
```bash
sudo ufw allow samba
# или
sudo ufw allow 445/tcp
sudo ufw allow 139/tcp
```
### Проблема: Ошибка доступа / Неправильный пароль
1. Проверьте пароль пользователя в Samba:
```bash
sudo smbpasswd -a vrubel # создать пароль
sudo smbpasswd -e vrubel # активировать пользователя
```
### Проблема: VLC не может открыть файл по сетевому пути
1. Попробуйте сначала смонтировать шару как сетевой диск
2. Затем откройте файл из смонтированного диска в VLC
3. Или используйте прямой путь с префиксом `smb://` или `\\`
## 📝 Дополнительная информация
- Порт SMB: **445** (SMB 3.x) или **139** (NetBIOS)
- Протокол: **SMB/CIFS**
- Формат времени: синхронизируется с сервером
- Кодировка: UTF-8
## 🔒 Безопасность
- Текущая настройка требует аутентификации (пользователь vrubel)
- Для публичного доступа (без пароля) можно изменить конфигурацию, добавив `guest ok = yes` в секцию [VIDEO]
- Не рекомендуется использовать публичный доступ в производственных средах

226
app/TELEGRAM_BOT_README.md Normal file
View file

@ -0,0 +1,226 @@
# 🤖 Telegram Bot для поиска и загрузки фильмов
## 📋 Описание
Telegram бот полностью дублирует функциональность веб-интерфейса для поиска и загрузки фильмов через торренты. Бот интегрирован с существующими API endpoints и предоставляет удобный интерфейс для работы в Telegram.
## 🎯 Функциональность
### Основные команды:
- `/start` - Запуск бота и приветствие
- `/help` - Справка по использованию
- `/find` - Начать поиск фильма
### Процесс работы:
1. **Поиск фильма** - пользователь вводит название фильма
2. **Выбор фильма** - из результатов поиска выбирается нужный фильм с постером
3. **Поиск торрентов** - система ищет доступные торренты на всех трекерах
4. **Выбор торрента** - пользователь выбирает нужный торрент из списка
5. **Автоматическое добавление** - торрент автоматически добавляется в qBittorrent
## 🚀 Установка и запуск
### 1. Локальный запуск (для тестирования)
```bash
# Установка зависимостей
pip install -r requirements.txt
# Запуск основного приложения (в отдельном терминале)
python app.py
# Запуск Telegram бота
python run_telegram_bot.py
```
### 2. Запуск через Docker
```bash
# Запуск всех сервисов включая Telegram бота
docker compose up -d --build
# Проверка статуса
docker ps
```
### 3. Проверка работы
```bash
# Тестирование бота
python test_telegram_bot.py
```
## ⚙️ Конфигурация
### Переменные окружения:
```bash
# Telegram Bot Token (уже настроен)
TELEGRAM_BOT_TOKEN=7662650066:AAFgsfYJNYgpcSHaSe6fspsjqmhMkOBT1s4
# TMDB API
TMDB_API_KEY=6d58225585fb77af5945a964de41849f
# Torrent APIs
TORRENT_SEARCH_URL=http://localhost:8443
TORRENT_ADD_URL=http://localhost:8088
# qBittorrent
QBITTORRENT_HOST=localhost
QBITTORRENT_PORT=8080
QBITTORRENT_USERNAME=admin
QBITTORRENT_PASSWORD=vrubel07
```
## 🎬 Использование
### 1. Начало работы
- Найдите бота в Telegram: `@your_bot_username`
- Отправьте команду `/start`
- Следуйте инструкциям бота
### 2. Поиск фильма
- Отправьте команду `/find` или просто введите название фильма
- Выберите нужный фильм из списка результатов
- Просмотрите информацию о фильме с постером
### 3. Поиск торрентов
- Нажмите кнопку "🔍 Найти торренты"
- Дождитесь результатов поиска
- Выберите нужный торрент из списка
### 4. Скачивание
- Нажмите на нужный торрент
- Торрент автоматически добавится в qBittorrent
- Получите уведомление о начале загрузки
## 🔧 Технические детали
### Архитектура:
- **Telegram Bot API** - для взаимодействия с пользователями
- **FastAPI** - основной веб-сервис с API endpoints
- **TMDB API** - поиск информации о фильмах
- **TorAPI** - поиск торрентов на трекерах
- **qBittorrent** - клиент для загрузки торрентов
### Файлы проекта:
- `telegram_bot.py` - основной код бота
- `run_telegram_bot.py` - скрипт запуска
- `test_telegram_bot.py` - тестирование
- `Dockerfile.telegram` - Docker образ для бота
- `docker-compose.yml` - конфигурация Docker Compose
### Состояния пользователя:
- `waiting_movie_title` - ожидание названия фильма
- `movie_selected` - фильм выбран, ожидание действий
- `None` - свободное состояние
## 🛠️ Устранение неполадок
### Проблема: Бот не отвечает
**Решение:**
1. Проверьте, что основное приложение запущено на порту 8089
2. Убедитесь, что все Docker контейнеры работают
3. Проверьте логи: `docker logs telegram-bot`
### Проблема: Не работает поиск фильмов
**Решение:**
1. Проверьте подключение к TMDB API
2. Убедитесь, что API ключ правильный
3. Проверьте интернет-соединение
### Проблема: Не работает поиск торрентов
**Решение:**
1. Проверьте, что TorAPI контейнеры запущены
2. Убедитесь, что основное приложение доступно
3. Проверьте логи: `docker logs movie-search`
### Проблема: Торренты не добавляются в qBittorrent
**Решение:**
1. Проверьте, что qBittorrent запущен
2. Убедитесь, что учетные данные правильные
3. Проверьте доступность qBittorrent API
## 📊 Мониторинг
### Проверка статуса:
```bash
# Все контейнеры
docker ps
# Логи бота
docker logs telegram-bot --tail 50
# Логи основного приложения
docker logs movie-search --tail 50
# Статус qBittorrent
sudo systemctl status qbittorrent
```
### Тестирование API:
```bash
# Тест основного API
curl http://localhost:8089/api/search/terminator
# Тест TMDB
curl "https://api.themoviedb.org/3/search/movie?api_key=6d58225585fb77af5945a964de41849f&query=terminator"
```
## 🔒 Безопасность
### Рекомендации:
1. Не публикуйте токен бота в открытом доступе
2. Используйте переменные окружения для конфиденциальных данных
3. Регулярно обновляйте зависимости
4. Мониторьте использование бота
## 📈 Производительность
### Оптимизация:
1. Ограничьте количество одновременных пользователей
2. Используйте кэширование для часто запрашиваемых данных
3. Мониторьте использование ресурсов
4. Настройте лимиты для API запросов
## 🆘 Поддержка
При возникновении проблем:
1. Проверьте логи всех сервисов
2. Убедитесь, что все порты доступны
3. Проверьте настройки сети
4. Создайте issue в репозитории проекта
## ✅ Статус
**🟢 TELEGRAM BOT ПОЛНОСТЬЮ ФУНКЦИОНАЛЕН**
- ✅ Поиск фильмов работает
- ✅ Отображение постеров работает
- ✅ Поиск торрентов работает
- ✅ Добавление в qBittorrent работает
- ✅ Интерактивные кнопки работают
- ✅ Обработка ошибок работает
- ✅ Docker контейнеризация работает
## 🚀 Быстрый старт
```bash
# 1. Запуск всех сервисов
docker compose up -d --build
# 2. Проверка статуса
docker ps
# 3. Тестирование
python test_telegram_bot.py
# 4. Использование
# Найдите бота в Telegram и отправьте /start
```
---
**Версия**: 1.0
**Дата**: 2025-01-06
**Автор**: AI Assistant

129
app/UBUNTU_DEPLOYMENT.md Normal file
View file

@ -0,0 +1,129 @@
# Ubuntu Deployment Guide
## Quick Start
Для запуска всех сервисов на Ubuntu 24 одной командой:
```bash
cd /home/vrubel/PROJECTS/TorrentFilms/findFilms
./start_ubuntu.sh
```
## What the Script Does
Скрипт `start_ubuntu.sh` автоматически:
1. **Проверяет и устанавливает Docker** (если нужно)
2. **Проверяет и устанавливает qBittorrent-nox** (если нужно)
3. **Настраивает systemd сервис** для qBittorrent на порту 8082
4. **Включает автозапуск** qBittorrent при загрузке системы
5. **Создает Docker сеть** `torrentvideo_default`
6. **Запускает все Docker контейнеры** с автоперезапуском
## Services
После запуска будут доступны:
- **Веб-интерфейс**: http://localhost:8089
- **qBittorrent**: http://localhost:8082 (admin/vrubel07)
- **Telegram Bot**: @your_bot_username
## Management
### Stop All Services
```bash
docker compose down
sudo systemctl stop qbittorrent
```
### View Logs
```bash
# Docker logs
docker compose logs -f
# qBittorrent logs
sudo journalctl -u qbittorrent -f
```
### Restart All
```bash
sudo systemctl restart qbittorrent
docker compose restart
```
### Uninstall
```bash
docker compose down
sudo systemctl disable qbittorrent
sudo systemctl stop qbittorrent
sudo rm /etc/systemd/system/qbittorrent.service
sudo systemctl daemon-reload
```
## Configuration
### Environment Variables
Все переменные окружения заданы в `docker-compose.yml`:
- `TMDB_API_KEY`: API ключ TMDB
- `TELEGRAM_BOT_TOKEN`: Токен Telegram бота
- `QBITTORRENT_PORT`: 8082
- `QBITTORRENT_HOST`: host.docker.internal
- `QBITTORRENT_USERNAME`: admin
- `QBITTORRENT_PASSWORD`: vrubel07
### Change qBittorrent Credentials
1. Откройте `docker-compose.yml`
2. Измените `QBITTORRENT_PASSWORD`
3. Перезапустите: `docker compose restart`
### Change Telegram Bot Token
1. Откройте `docker-compose.yml`
2. Измените `TELEGRAM_BOT_TOKEN`
3. Перезапустите: `docker compose restart telegram-bot`
## Troubleshooting
### qBittorrent не запускается
```bash
# Проверка статуса
sudo systemctl status qbittorrent
# Просмотр логов
sudo journalctl -u qbittorrent -n 50
# Ручной запуск
sudo -u qbittorrent /usr/bin/qbittorrent-nox --webui-port=8082
```
### Docker контейнеры не запускаются
```bash
# Проверка логов
docker compose logs
# Пересборка
docker compose up -d --build --force-recreate
```
### Port Already in Use
```bash
# Проверка занятых портов
sudo lsof -i :8082
sudo lsof -i :8089
# Остановка процесса
sudo kill -9 <PID>
```
## Notes
- qBittorrent работает как systemd сервис с автозапуском
- Все Docker контейнеры настроены на автоперезапуск
- Используется `host.docker.internal` для доступа к qBittorrent из Docker

1289
app/app.py Normal file

File diff suppressed because it is too large Load diff

92
app/docker-compose.yml Normal file
View file

@ -0,0 +1,92 @@
# 🏠 App Stack — сервисы, запускаемые на хосте в России (192.168.8.173)
# Веб-интерфейс + Telegram бот для поиска и скачивания фильмов
#
# ⚡ Запуск:
# cd app && docker compose up -d --build
#
# 📋 Перед запуском создайте .env из .env.example
services:
# ============================================================
# 🌐 Веб-приложение + API
# ============================================================
movie-search:
build: .
container_name: movie-search
env_file:
- .env
environment:
# NL-сервисы (поиск фильмов и торрентов, без блокировок)
- TMDB_PROXY_URL=http://${NL_HOST:-72.56.91.135}:8001
- TORRENT_SEARCH_URL=http://${NL_HOST:-72.56.91.135}:8443
# Локальный torapi-qbit — резолвит magnet через qBittorrent
- TORRENT_ADD_URL=http://app-torapi-qbit:8443
- TORAPI_ADD_URL=http://app-torapi-qbit:8443
# qBittorrent (на 192.168.8.177)
- QBITTORRENT_USERNAME=${QBITTORRENT_USERNAME:-vrubelroman}
- QBITTORRENT_PASSWORD=${QBITTORRENT_PASSWORD:-VRKshtein07}
- QBITTORRENT_HOST=${QBITTORRENT_HOST:-192.168.8.177}
- QBITTORRENT_PORT=${QBITTORRENT_PORT:-8080}
- HOST=0.0.0.0
- PORT=8000
ports:
- "0.0.0.0:8089:8000"
restart: unless-stopped
networks:
- app-stack
depends_on:
- app-torapi-qbit
# ============================================================
# 🤖 Telegram бот
# ============================================================
telegram-bot:
build:
context: .
dockerfile: Dockerfile.telegram
container_name: telegram-bot-findFilms
env_file:
- .env
environment:
# NL-сервисы (поиск)
- TMDB_PROXY_URL=http://${NL_HOST:-72.56.91.135}:8001
- TORRENT_SEARCH_URL=http://${NL_HOST:-72.56.91.135}:8443
# Локальный torapi-qbit
- TORRENT_ADD_URL=http://app-torapi-qbit:8443
# qBittorrent (на 192.168.8.177)
- QBITTORRENT_USERNAME=${QBITTORRENT_USERNAME:-vrubelroman}
- QBITTORRENT_PASSWORD=${QBITTORRENT_PASSWORD:-VRKshtein07}
- QBITTORRENT_HOST=${QBITTORRENT_HOST:-192.168.8.177}
- QBITTORRENT_PORT=${QBITTORRENT_PORT:-8080}
restart: unless-stopped
networks:
- app-stack
depends_on:
- movie-search
# ============================================================
# 🔗 TorAPI → qBittorrent bridge — magnet ссылки
# Проксирует запросы к qBittorrent для получения magnet-хэшей
# ============================================================
app-torapi-qbit:
image: lifailon/torapi:latest
container_name: app-torapi-qbit
environment:
- USERNAME=${QBITTORRENT_USERNAME:-vrubelroman}
- PASSWORD=${QBITTORRENT_PASSWORD:-VRKshtein07}
- PROXY_ADDRESS=${QBITTORRENT_HOST:-192.168.8.177}
- PROXY_PORT=${QBITTORRENT_PORT:-8080}
ports:
- "0.0.0.0:8088:8443"
restart: unless-stopped
networks:
- app-stack
networks:
app-stack:
driver: bridge

10
app/requirements.txt Normal file
View file

@ -0,0 +1,10 @@
fastapi==0.115.0
uvicorn[standard]==0.30.6
httpx>=0.25.2,<0.28.0
jinja2==3.1.4
python-multipart==0.0.9
beautifulsoup4==4.12.3
lxml==5.1.0
fastapi-cors==0.0.6
requests==2.31.0
python-telegram-bot==20.7

37
app/run_telegram_bot.py Normal file
View file

@ -0,0 +1,37 @@
#!/usr/bin/env python3
"""
Скрипт для запуска Telegram бота
"""
import os
import sys
import asyncio
import logging
from telegram_bot import MovieSearchBot
# Настройка логирования
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
logger = logging.getLogger(__name__)
def main():
"""Главная функция запуска бота"""
try:
logger.info("Starting Movie Search Telegram Bot...")
# Создаем и запускаем бота
bot = MovieSearchBot()
# Запускаем бота
asyncio.run(bot.run())
except KeyboardInterrupt:
logger.info("Bot stopped by user")
except Exception as e:
logger.error(f"Error running bot: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

786
app/telegram_bot.py Normal file
View file

@ -0,0 +1,786 @@
#!/usr/bin/env python3
"""
Telegram Bot для поиска и загрузки фильмов через торренты
Дублирует функциональность веб-интерфейса в Telegram
"""
import os
import asyncio
import httpx
import logging
import json
from typing import Dict, List, Optional
from dataclasses import dataclass
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, InputMediaPhoto
from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters, ContextTypes
from telegram.constants import ParseMode
# Настройка логирования
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
logger = logging.getLogger(__name__)
# Конфигурация
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TMDB_PROXY_URL = os.getenv("TMDB_PROXY_URL", "http://localhost:8001")
TORRENT_SEARCH_URL = os.getenv("TORRENT_SEARCH_URL", "http://localhost:8443")
TORRENT_ADD_URL = os.getenv("TORRENT_ADD_URL", "http://localhost:8444")
QBITTORRENT_HOST = os.getenv("QBITTORRENT_HOST", "localhost")
QBITTORRENT_PORT = os.getenv("QBITTORRENT_PORT", "8082")
QBITTORRENT_USERNAME = os.getenv("QBITTORRENT_USERNAME", "admin")
QBITTORRENT_PASSWORD = os.getenv("QBITTORRENT_PASSWORD", "vrubel07")
@dataclass
class Movie:
"""Структура данных для фильма"""
id: int
title: str
original_title: str
overview: str
release_date: str
vote_average: float
poster_path: str
backdrop_path: str
genre_ids: List[int]
@dataclass
class Torrent:
"""Структура данных для торрента"""
id: str
title: str
size_bytes: int
size_readable: str
resolution: str
quality: str
seeds: int
peers: int
magnet: str
provider: str
class MovieSearchBot:
"""Основной класс Telegram бота"""
def __init__(self):
self.application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
self.user_states = {} # Состояния пользователей
self.download_monitor = None # Мониторинг загрузок
self.setup_handlers()
def setup_handlers(self):
"""Настройка обработчиков команд"""
# Команды
self.application.add_handler(CommandHandler("start", self.start_command))
self.application.add_handler(CommandHandler("help", self.help_command))
self.application.add_handler(CommandHandler("find", self.find_command))
# Обработчики сообщений
self.application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_message))
# Обработчики callback запросов
self.application.add_handler(CallbackQueryHandler(self.handle_callback))
async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /start"""
user = update.effective_user
welcome_text = f"""
🎬 <b>Добро пожаловать в Movie Search Bot!</b>
Привет, {user.first_name}! 👋
Этот бот поможет вам найти и скачать фильмы через торренты.
<b>Доступные команды:</b>
/find - Найти фильм
/help - Помощь
<b>Как использовать:</b>
1. Нажмите /find или введите название фильма
2. Выберите нужный фильм из результатов
3. Выберите торрент для скачивания
4. Фильм автоматически добавится в qBittorrent
Начнем поиск? 🚀
"""
await update.message.reply_text(
welcome_text,
parse_mode=ParseMode.HTML
)
async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /help"""
help_text = """
📖 <b>Справка по использованию бота</b>
<b>Основные команды:</b>
/find - Начать поиск фильма
/help - Показать эту справку
/start - Перезапустить бота
<b>Пошаговая инструкция:</b>
1 <b>Поиск фильма</b>
Нажмите /find или просто введите название фильма
Бот найдет фильмы через TMDB API
2 <b>Выбор фильма</b>
Выберите нужный фильм из списка
Бот покажет постер и информацию о фильме
3 <b>Поиск торрентов</b>
Бот автоматически найдет доступные торренты
Результаты будут отсортированы по качеству и количеству сидов
4 <b>Скачивание</b>
Выберите нужный торрент
Он автоматически добавится в qBittorrent
Вы получите уведомление о начале загрузки
<b>Поддерживаемые трекеры:</b>
RuTracker
Kinozal
RuTor
NoNameClub
<b>Проблемы?</b>
Если что-то не работает, попробуйте команду /start
"""
await update.message.reply_text(
help_text,
parse_mode=ParseMode.HTML
)
async def find_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /find"""
user_id = update.effective_user.id
# Устанавливаем состояние ожидания названия фильма
self.user_states[user_id] = "waiting_movie_title"
await update.message.reply_text(
"🔍 <b>Поиск фильма</b>\n\nВведите название фильма, который хотите найти:",
parse_mode=ParseMode.HTML
)
async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик текстовых сообщений"""
user_id = update.effective_user.id
text = update.message.text
# Проверяем состояние пользователя
if user_id not in self.user_states:
# Если пользователь не в состоянии, но отправил текст, считаем это поиском
await self.search_movies(update, context, text)
return
state = self.user_states[user_id]
if state == "waiting_movie_title":
await self.search_movies(update, context, text)
else:
# Неизвестное состояние
await update.message.reply_text(
"❌ Неизвестная команда. Используйте /find для поиска фильма или /help для справки."
)
async def search_movies(self, update: Update, context: ContextTypes.DEFAULT_TYPE, query: str):
"""Поиск фильмов через TMDB API"""
user_id = update.effective_user.id
try:
# Показываем индикатор загрузки
loading_msg = await update.message.reply_text("🔍 Ищу фильмы...")
# Поиск через TMDB API
movies = await self.tmdb_search_movies(query)
if not movies:
await loading_msg.edit_text("❌ Фильмы не найдены. Попробуйте другое название.")
return
# Ограничиваем количество результатов
movies = movies[:10]
# Создаем клавиатуру с результатами
keyboard = []
for i, movie in enumerate(movies):
year = movie.release_date[:4] if movie.release_date else "N/A"
button_text = f"{movie.title} ({year})"
if len(button_text) > 50:
button_text = button_text[:47] + "..."
keyboard.append([InlineKeyboardButton(
button_text,
callback_data=f"movie_{movie.id}"
)])
# Добавляем кнопку отмены
keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
reply_markup = InlineKeyboardMarkup(keyboard)
# Отправляем результаты
results_text = f"🎬 <b>Найдено фильмов: {len(movies)}</b>\n\nВыберите нужный фильм:"
await loading_msg.edit_text(
results_text,
parse_mode=ParseMode.HTML,
reply_markup=reply_markup
)
# Сохраняем результаты в контексте
context.user_data['search_results'] = movies
self.user_states[user_id] = "movie_selected"
except Exception as e:
logger.error(f"Error searching movies: {e}")
await update.message.reply_text(
f"❌ Ошибка при поиске фильмов: {str(e)}\n\nПопробуйте еще раз или используйте /help"
)
async def tmdb_search_movies(self, query: str) -> List[Movie]:
"""Поиск фильмов через TMDB Proxy"""
async with httpx.AsyncClient(timeout=60.0) as client:
try:
response = await client.get(
f"{TMDB_PROXY_URL}/search/movie",
params={
"query": query,
"language": "ru-RU",
"include_adult": False
}
)
response.raise_for_status()
data = response.json()
movies = []
for movie_data in data.get("results", []):
movie = Movie(
id=movie_data["id"],
title=movie_data.get("title", ""),
original_title=movie_data.get("original_title", ""),
overview=movie_data.get("overview", ""),
release_date=movie_data.get("release_date", ""),
vote_average=movie_data.get("vote_average", 0.0),
poster_path=movie_data.get("poster_path", ""),
backdrop_path=movie_data.get("backdrop_path", ""),
genre_ids=movie_data.get("genre_ids", [])
)
movies.append(movie)
return movies
except Exception as e:
logger.error(f"TMDB Proxy error: {e}")
return []
async def handle_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик callback запросов"""
query = update.callback_query
await query.answer()
data = query.data
user_id = update.effective_user.id
if data == "cancel":
await self.cancel_operation(update, context)
return
if data.startswith("movie_"):
movie_id = int(data.split("_")[1])
await self.show_movie_details(update, context, movie_id)
elif data.startswith("torrent_"):
torrent_id = data.split("_")[1]
await self.add_torrent_to_client(update, context, torrent_id)
elif data == "search_torrents":
await self.search_torrents_for_movie(update, context)
elif data == "new_search":
await self.start_new_search(update, context)
async def cancel_operation(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Отмена операции"""
user_id = update.effective_user.id
self.user_states[user_id] = None
await update.callback_query.edit_message_text(
"❌ Операция отменена.\n\nИспользуйте /find для нового поиска."
)
async def start_new_search(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Начать новый поиск"""
user_id = update.effective_user.id
self.user_states[user_id] = "waiting_movie_title"
# Очищаем данные пользователя
context.user_data.clear()
await update.callback_query.edit_message_text(
"🔍 <b>Новый поиск фильма</b>\n\nВведите название фильма, который хотите найти:",
parse_mode=ParseMode.HTML
)
async def show_movie_details(self, update: Update, context: ContextTypes.DEFAULT_TYPE, movie_id: int):
"""Показать детали фильма"""
try:
# Находим фильм в сохраненных результатах
movies = context.user_data.get('search_results', [])
movie = next((m for m in movies if m.id == movie_id), None)
if not movie:
await update.callback_query.edit_message_text("❌ Фильм не найден.")
return
# Получаем детальную информацию о фильме
movie_details = await self.get_movie_details(movie_id)
if movie_details:
movie = movie_details
# Формируем текст сообщения
year = movie.release_date[:4] if movie.release_date else "N/A"
rating = f"{movie.vote_average:.1f}/10" if movie.vote_average > 0 else "⭐ N/A"
text = f"""
🎬 <b>{movie.title}</b>
📅 <b>Год:</b> {year}
{rating}
📝 <b>Описание:</b>
{movie.overview[:500]}{'...' if len(movie.overview) > 500 else ''}
"""
# Создаем клавиатуру
keyboard = [
[InlineKeyboardButton("🔍 Найти торренты", callback_data="search_torrents")],
[InlineKeyboardButton("❌ Отмена", callback_data="cancel")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
# Сохраняем выбранный фильм
context.user_data['selected_movie'] = movie
# Отправляем сообщение с постером
if movie.poster_path:
poster_url = f"https://image.tmdb.org/t/p/w500{movie.poster_path}"
try:
await update.callback_query.edit_message_media(
InputMediaPhoto(
media=poster_url,
caption=text,
parse_mode=ParseMode.HTML
),
reply_markup=reply_markup
)
except:
# Если не удалось отправить с постером, отправляем текст
await update.callback_query.edit_message_text(
text,
parse_mode=ParseMode.HTML,
reply_markup=reply_markup
)
else:
await update.callback_query.edit_message_text(
text,
parse_mode=ParseMode.HTML,
reply_markup=reply_markup
)
except Exception as e:
logger.error(f"Error showing movie details: {e}")
await update.callback_query.edit_message_text(
f"❌ Ошибка при получении информации о фильме: {str(e)}"
)
async def get_movie_details(self, movie_id: int) -> Optional[Movie]:
"""Получение детальной информации о фильме через TMDB Proxy"""
async with httpx.AsyncClient(timeout=60.0) as client:
try:
response = await client.get(
f"{TMDB_PROXY_URL}/movie/{movie_id}",
params={
"language": "ru-RU",
"append_to_response": "external_ids"
}
)
response.raise_for_status()
data = response.json()
return Movie(
id=data["id"],
title=data.get("title", ""),
original_title=data.get("original_title", ""),
overview=data.get("overview", ""),
release_date=data.get("release_date", ""),
vote_average=data.get("vote_average", 0.0),
poster_path=data.get("poster_path", ""),
backdrop_path=data.get("backdrop_path", ""),
genre_ids=data.get("genre_ids", [])
)
except Exception as e:
logger.error(f"Error getting movie details: {e}")
return None
async def search_torrents_for_movie(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Поиск торрентов для выбранного фильма"""
try:
movie = context.user_data.get('selected_movie')
if not movie:
await update.callback_query.edit_message_text("❌ Фильм не выбран.")
return
# Показываем индикатор загрузки
try:
await update.callback_query.edit_message_text("🔍 Ищу торренты...")
except:
# Если не можем отредактировать (например, сообщение с изображением), отправляем новое
await update.callback_query.message.reply_text("🔍 Ищу торренты...")
# Поиск торрентов
torrents = await self.search_torrents(movie)
if not torrents:
try:
await update.callback_query.edit_message_text(
f"❌ Торренты для фильма '{movie.title}' не найдены.\n\nПопробуйте другой фильм."
)
except:
await update.callback_query.message.reply_text(
f"❌ Торренты для фильма '{movie.title}' не найдены.\n\nПопробуйте другой фильм."
)
return
# Ограничиваем количество результатов
torrents = torrents[:15]
# Создаем клавиатуру с торрентами
keyboard = []
for i, torrent in enumerate(torrents):
# Формируем текст кнопки
button_text = f"{torrent.quality} {torrent.resolution} - {torrent.size_readable}"
if torrent.seeds > 0:
button_text += f" (👥 {torrent.seeds})"
if len(button_text) > 50:
button_text = button_text[:47] + "..."
keyboard.append([InlineKeyboardButton(
button_text,
callback_data=f"torrent_{torrent.id}"
)])
# Добавляем кнопку отмены
keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
reply_markup = InlineKeyboardMarkup(keyboard)
# Формируем текст с результатами
text = f"""
🎬 <b>{movie.title}</b>
🔍 <b>Найдено торрентов: {len(torrents)}</b>
Выберите торрент для скачивания:
"""
try:
await update.callback_query.edit_message_text(
text,
parse_mode=ParseMode.HTML,
reply_markup=reply_markup
)
except:
# Если не можем отредактировать, отправляем новое сообщение
await update.callback_query.message.reply_text(
text,
parse_mode=ParseMode.HTML,
reply_markup=reply_markup
)
# Сохраняем торренты в контексте
context.user_data['torrents'] = torrents
except Exception as e:
logger.error(f"Error searching torrents: {e}")
try:
await update.callback_query.edit_message_text(
f"❌ Ошибка при поиске торрентов: {str(e)}"
)
except:
await update.callback_query.message.reply_text(
f"❌ Ошибка при поиске торрентов: {str(e)}"
)
async def search_torrents(self, movie: Movie) -> List[Torrent]:
"""Поиск торрентов для фильма"""
try:
logger.info(f"Searching torrents for movie: {movie.title}")
# Используем правильный API endpoint через movie-search сервис
async with httpx.AsyncClient(timeout=60.0) as client:
# URL-кодируем название фильма
import urllib.parse
encoded_title = urllib.parse.quote(movie.title)
url = f"http://movie-search:8000/api/torrents/{encoded_title}"
params = {
"year": movie.release_date[:4] if movie.release_date else None,
"original_title": movie.original_title if movie.original_title != movie.title else None
}
logger.info(f"Making request to: {url} with params: {params}")
response = await client.get(url, params=params)
logger.info(f"Response status: {response.status_code}")
if response.status_code == 200:
data = response.json()
torrents_data = data.get("torrents", [])
logger.info(f"Found {len(torrents_data)} torrents")
torrents = []
for torrent_data in torrents_data:
torrent = Torrent(
id=torrent_data.get("id", ""),
title=torrent_data.get("title", ""),
size_bytes=torrent_data.get("size_bytes", 0),
size_readable=torrent_data.get("size_readable", ""),
resolution=torrent_data.get("resolution", ""),
quality=torrent_data.get("quality", ""),
seeds=torrent_data.get("seeds", 0),
peers=torrent_data.get("peers", 0),
magnet=torrent_data.get("magnet", ""),
provider=torrent_data.get("provider", "")
)
torrents.append(torrent)
logger.info(f"Processed {len(torrents)} torrents")
return torrents
else:
logger.error(f"Torrent search API error: {response.status_code} - {response.text}")
return []
except Exception as e:
logger.error(f"Error searching torrents: {e}")
import traceback
traceback.print_exc()
return []
async def add_torrent_to_client(self, update: Update, context: ContextTypes.DEFAULT_TYPE, torrent_id: str):
"""Добавление торрента в qBittorrent"""
try:
# Показываем индикатор загрузки
await update.callback_query.edit_message_text("⬇️ Добавляю торрент в qBittorrent...")
# Используем правильный API endpoint через movie-search сервис
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(
"http://movie-search:8000/api/add-torrent",
data={"torrent_id": torrent_id}
)
if response.status_code == 200:
data = response.json()
if data.get("status") == "success":
message = f"{data.get('message', 'Торрент успешно добавлен!')}"
# Добавляем в мониторинг загрузок
if self.download_monitor and data.get("torrent_hash"):
user_id = update.effective_user.id
movie = context.user_data.get('selected_movie')
torrent_name = data.get("torrent_name", "Unknown")
if movie:
self.download_monitor.add_download(
torrent_hash=data.get("torrent_hash"),
user_id=user_id,
movie_title=movie.title,
torrent_name=torrent_name
)
else:
message = f"{data.get('message', 'Ошибка при добавлении торрента')}"
else:
message = f"❌ Ошибка API: {response.status_code}"
# Создаем клавиатуру для возврата к поиску
keyboard = [
[InlineKeyboardButton("🔍 Найти другой фильм", callback_data="new_search")],
[InlineKeyboardButton("❌ Закрыть", callback_data="cancel")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.callback_query.edit_message_text(
message,
reply_markup=reply_markup
)
except Exception as e:
logger.error(f"Error adding torrent: {e}")
await update.callback_query.edit_message_text(
f"❌ Ошибка при добавлении торрента: {str(e)}"
)
def run(self):
"""Запуск бота"""
logger.info("Starting Movie Search Bot...")
# Инициализируем мониторинг загрузок
self.download_monitor = DownloadMonitor(self)
# Запускаем мониторинг в фоновом режиме
def start_monitoring():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(self.download_monitor.start_monitoring())
import threading
monitor_thread = threading.Thread(target=start_monitoring, daemon=True)
monitor_thread.start()
self.application.run_polling()
class DownloadMonitor:
"""Мониторинг загрузок в qBittorrent и отправка уведомлений"""
def __init__(self, bot_instance: MovieSearchBot):
self.bot = bot_instance
self.active_downloads = {} # {torrent_hash: {user_id, movie_title, torrent_name}}
self.qbittorrent_url = f"http://{QBITTORRENT_HOST}:{QBITTORRENT_PORT}"
self.qbittorrent_username = QBITTORRENT_USERNAME
self.qbittorrent_password = QBITTORRENT_PASSWORD
self.session_cookie = None
async def authenticate_qbittorrent(self):
"""Аутентификация в qBittorrent"""
try:
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
f"{self.qbittorrent_url}/api/v2/auth/login",
data={
"username": self.qbittorrent_username,
"password": self.qbittorrent_password
}
)
if response.status_code == 200 and response.text == "Ok.":
# Сохраняем cookie для последующих запросов
self.session_cookie = response.cookies.get('SID')
logger.info("qBittorrent authentication successful")
return True
else:
logger.error(f"qBittorrent authentication failed: {response.status_code} - {response.text}")
return False
except Exception as e:
logger.error(f"Error authenticating with qBittorrent: {e}")
return False
async def get_torrents_info(self):
"""Получение информации о торрентах"""
try:
if not self.session_cookie:
await self.authenticate_qbittorrent()
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(
f"{self.qbittorrent_url}/api/v2/torrents/info",
cookies={"SID": self.session_cookie}
)
if response.status_code == 200:
return response.json()
else:
logger.error(f"Failed to get torrents info: {response.status_code}")
return []
except Exception as e:
logger.error(f"Error getting torrents info: {e}")
return []
async def check_downloads(self):
"""Проверка статуса загрузок и отправка уведомлений"""
try:
torrents = await self.get_torrents_info()
if not torrents:
return
for torrent in torrents:
torrent_hash = torrent.get('hash')
torrent_name = torrent.get('name', 'Unknown')
state = torrent.get('state')
progress = torrent.get('progress', 0)
# Проверяем, отслеживаем ли мы этот торрент
if torrent_hash in self.active_downloads:
download_info = self.active_downloads[torrent_hash]
user_id = download_info['user_id']
movie_title = download_info['movie_title']
# Если загрузка завершена (state == 'uploading' или progress == 1.0)
if state in ['uploading', 'stalledUP'] or progress >= 1.0:
try:
# Отправляем уведомление
await self.bot.application.bot.send_message(
chat_id=user_id,
text=f"🎉 <b>Загрузка завершена!</b>\n\n"
f"🎬 <b>Фильм:</b> {movie_title}\n"
f"📁 <b>Торрент:</b> {torrent_name}\n"
f"✅ <b>Статус:</b> Готов к просмотру!",
parse_mode=ParseMode.HTML
)
# Удаляем из отслеживания
del self.active_downloads[torrent_hash]
logger.info(f"Download completed notification sent for {movie_title}")
except Exception as e:
logger.error(f"Error sending completion notification: {e}")
# Если загрузка остановлена с ошибкой
elif state in ['error', 'missingFiles']:
try:
await self.bot.application.bot.send_message(
chat_id=user_id,
text=f"❌ <b>Ошибка загрузки</b>\n\n"
f"🎬 <b>Фильм:</b> {movie_title}\n"
f"📁 <b>Торрент:</b> {torrent_name}\n"
f"⚠️ <b>Статус:</b> {state}",
parse_mode=ParseMode.HTML
)
# Удаляем из отслеживания
del self.active_downloads[torrent_hash]
logger.info(f"Download error notification sent for {movie_title}")
except Exception as e:
logger.error(f"Error sending error notification: {e}")
except Exception as e:
logger.error(f"Error checking downloads: {e}")
def add_download(self, torrent_hash: str, user_id: int, movie_title: str, torrent_name: str):
"""Добавление торрента в отслеживание"""
self.active_downloads[torrent_hash] = {
'user_id': user_id,
'movie_title': movie_title,
'torrent_name': torrent_name
}
logger.info(f"Added download to monitoring: {movie_title} for user {user_id}")
async def start_monitoring(self):
"""Запуск мониторинга загрузок"""
logger.info("Starting download monitoring...")
while True:
try:
await self.check_downloads()
await asyncio.sleep(30) # Проверяем каждые 30 секунд
except Exception as e:
logger.error(f"Error in monitoring loop: {e}")
await asyncio.sleep(60) # При ошибке ждем минуту
def main():
"""Главная функция"""
bot = MovieSearchBot()
bot.run()
if __name__ == "__main__":
main()

63
app/templates/error.html Normal file
View file

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ошибка</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
}
.error-icon {
font-size: 48px;
color: #dc3545;
margin-bottom: 20px;
}
h1 {
color: #dc3545;
margin-bottom: 20px;
}
.error-message {
background-color: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border: 1px solid #f5c6cb;
}
.back-link {
display: inline-block;
padding: 10px 20px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 5px;
}
.back-link:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<div class="error-icon">⚠️</div>
<h1>Произошла ошибка</h1>
<div class="error-message">
{{ error }}
</div>
<a href="/" class="back-link">Вернуться к поиску</a>
</div>
</body>
</html>

79
app/templates/index.html Normal file
View file

@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Поиск фильмов</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
.search-form {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
input[type="text"] {
flex: 1;
padding: 12px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
input[type="text"]:focus {
outline: none;
border-color: #007bff;
}
button {
padding: 12px 24px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #0056b3;
}
.api-info {
background-color: #e9ecef;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h1>🎬 Поиск фильмов</h1>
<form method="post" action="/search" class="search-form">
<input type="text" name="movie_title" placeholder="Введите название фильма..." required>
<button type="submit">Найти</button>
</form>
<div class="api-info">
<strong>API:</strong> The Movie Database (TMDB)<br>
<strong>Функции:</strong> Поиск фильмов по названию с получением подробной информации
</div>
</div>
</body>
</html>

177
app/templates/results.html Normal file
View file

@ -0,0 +1,177 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Результаты поиска: {{ query }}</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 20px;
}
.search-info {
background-color: #e9ecef;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
color: #007bff;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
.movies-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.movie-card {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
background: white;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.movie-poster {
width: 100%;
height: 200px;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
color: #666;
}
.movie-poster img {
width: 100%;
height: 100%;
object-fit: cover;
}
.movie-info {
padding: 15px;
}
.movie-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
color: #333;
}
.movie-year {
color: #666;
margin-bottom: 8px;
}
.movie-overview {
font-size: 14px;
color: #555;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.movie-rating {
background-color: #007bff;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
display: inline-block;
margin-top: 8px;
}
.movie-actions {
margin-top: 10px;
}
.btn-torrent {
background-color: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s ease;
}
.btn-torrent:hover {
background-color: #1e7e34;
}
.no-results {
text-align: center;
color: #666;
font-size: 18px;
padding: 40px;
}
</style>
</head>
<body>
<div class="container">
<a href="/" class="back-link">← Назад к поиску</a>
<h1>Результаты поиска: "{{ query }}"</h1>
<div class="search-info">
Найдено фильмов: <strong>{{ total_results }}</strong>
</div>
{% if movies %}
<div class="movies-grid">
{% for movie in movies %}
<div class="movie-card">
<div class="movie-poster">
{% if movie.poster_path %}
<img src="https://image.tmdb.org/t/p/w300{{ movie.poster_path }}" alt="{{ movie.title }}">
{% else %}
<div>Нет изображения</div>
{% endif %}
</div>
<div class="movie-info">
<div class="movie-title">{{ movie.title }}</div>
<div class="movie-year">{{ movie.release_date[:4] if movie.release_date else 'Год неизвестен' }}</div>
<div class="movie-overview">{{ movie.overview or 'Описание недоступно' }}</div>
{% if movie.vote_average %}
<div class="movie-rating">⭐ {{ "%.1f"|format(movie.vote_average) }}</div>
{% endif %}
<div class="movie-actions">
<button onclick="searchTorrents('{{ movie.title }}', '{{ movie.release_date[:4] if movie.release_date else '' }}')"
class="btn-torrent">
🔍 Найти торренты
</button>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="no-results">
Фильмы не найдены. Попробуйте изменить поисковый запрос.
</div>
{% endif %}
</div>
<script>
function searchTorrents(movieTitle, year) {
// Переходим на страницу поиска торрентов
const url = year ?
`/torrents/${encodeURIComponent(movieTitle)}?year=${year}` :
`/torrents/${encodeURIComponent(movieTitle)}`;
window.location.href = url;
}
</script>
</body>
</html>

266
app/templates/torrents.html Normal file
View file

@ -0,0 +1,266 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Торренты: {{ movie_title }}</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 20px;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
color: #007bff;
text-decoration: none;
padding: 8px 16px;
border: 1px solid #007bff;
border-radius: 5px;
}
.back-link:hover {
background-color: #007bff;
color: white;
}
.movie-info {
background-color: #e9ecef;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.torrents-list {
display: grid;
gap: 15px;
}
.torrent-item {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: white;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
transition: box-shadow 0.3s ease;
}
.torrent-item:hover {
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
}
.torrent-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
.torrent-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
.detail-item {
display: flex;
flex-direction: column;
}
.detail-label {
font-size: 12px;
color: #666;
text-transform: uppercase;
margin-bottom: 4px;
}
.detail-value {
font-size: 14px;
font-weight: bold;
color: #333;
}
.resolution-1080p { color: #28a745; }
.resolution-720p { color: #ffc107; }
.resolution-480p { color: #dc3545; }
.resolution-2160p { color: #6f42c1; }
.torrent-actions {
display: flex;
gap: 10px;
margin-top: 15px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
display: inline-block;
font-size: 14px;
transition: background-color 0.3s ease;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-success:hover {
background-color: #1e7e34;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
}
.no-torrents {
text-align: center;
color: #666;
font-size: 18px;
padding: 40px;
}
.quality-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.quality-bluray { background-color: #007bff; color: white; }
.quality-web-dl { background-color: #28a745; color: white; }
.quality-hdtv { background-color: #ffc107; color: #333; }
</style>
</head>
<body>
<div class="container">
<a href="/" class="back-link">← Назад к поиску</a>
<h1>🔍 Торренты для: "{{ movie_title }}"</h1>
<div class="movie-info">
<strong>Фильм:</strong> {{ movie_title }}
{% if year %}
<br><strong>Год:</strong> {{ year }}
{% endif %}
</div>
{% if torrents %}
<div class="torrents-list">
{% for torrent in torrents %}
<div class="torrent-item">
<div class="torrent-title">{{ torrent.title }}</div>
<div class="torrent-details">
<div class="detail-item">
<div class="detail-label">Размер</div>
<div class="detail-value">{{ torrent.size_readable }}</div>
</div>
<div class="detail-item">
<div class="detail-label">Разрешение</div>
<div class="detail-value resolution-{{ torrent.resolution }}">{{ torrent.resolution }}</div>
</div>
<div class="detail-item">
<div class="detail-label">Качество</div>
<div class="detail-value">
<span class="quality-badge quality-{{ torrent.quality.lower() }}">{{ torrent.quality }}</span>
</div>
</div>
<div class="detail-item">
<div class="detail-label">Сиды</div>
<div class="detail-value">{{ torrent.seeds }}</div>
</div>
<div class="detail-item">
<div class="detail-label">Пиры</div>
<div class="detail-value">{{ torrent.peers }}</div>
</div>
{% if torrent.category %}
<div class="detail-item">
<div class="detail-label">Категория</div>
<div class="detail-value">{{ torrent.category }}</div>
</div>
{% endif %}
{% if torrent.date %}
<div class="detail-item">
<div class="detail-label">Дата</div>
<div class="detail-value">{{ torrent.date }}</div>
</div>
{% endif %}
{% if torrent.provider %}
<div class="detail-item">
<div class="detail-label">Провайдер</div>
<div class="detail-value">{{ torrent.provider }}</div>
</div>
{% endif %}
</div>
<div class="torrent-actions">
<a href="{{ torrent.magnet }}" class="btn btn-primary">Magnet</a>
<a href="{{ torrent.download_url }}" class="btn btn-success">Скачать .torrent</a>
<button onclick="addToTorrentClient('{{ torrent.id }}')" class="btn btn-secondary">Добавить в клиент</button>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="no-torrents">
Торренты не найдены. Попробуйте изменить поисковый запрос.
</div>
{% endif %}
</div>
<script>
function addToTorrentClient(torrentId) {
// Показываем индикатор загрузки
const button = event.target;
const originalText = button.textContent;
button.textContent = '⏳ Получаем magnet...';
button.disabled = true;
// Отправляем ID торрента для получения magnet-ссылки
const formData = new FormData();
formData.append('torrent_id', torrentId);
fetch('/api/add-torrent', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
alert('✅ ' + data.message);
button.textContent = '✅ Добавлено';
button.style.backgroundColor = '#28a745';
} else {
alert('❌ ' + data.message);
button.textContent = originalText;
button.disabled = false;
}
})
.catch(error => {
alert('❌ Ошибка при добавлении торрента: ' + error);
button.textContent = originalText;
button.disabled = false;
});
}
</script>
</body>
</html>

76
app/test_telegram_bot.py Normal file
View file

@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""
Скрипт для тестирования Telegram бота локально
"""
import asyncio
import httpx
import logging
from telegram_bot import MovieSearchBot
# Настройка логирования
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
logger = logging.getLogger(__name__)
async def test_api_connection():
"""Тестирование подключения к API"""
try:
# Тестируем подключение к основному API
async with httpx.AsyncClient() as client:
response = await client.get("http://localhost:8089/api/search/terminator")
if response.status_code == 200:
logger.info("✅ Main API connection successful")
return True
else:
logger.error(f"❌ Main API error: {response.status_code}")
return False
except Exception as e:
logger.error(f"❌ API connection failed: {e}")
return False
async def test_tmdb_connection():
"""Тестирование подключения к TMDB"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.themoviedb.org/3/search/movie",
params={
"api_key": "6d58225585fb77af5945a964de41849f",
"query": "terminator",
"language": "ru-RU"
}
)
if response.status_code == 200:
logger.info("✅ TMDB API connection successful")
return True
else:
logger.error(f"❌ TMDB API error: {response.status_code}")
return False
except Exception as e:
logger.error(f"❌ TMDB connection failed: {e}")
return False
async def main():
"""Главная функция тестирования"""
logger.info("🧪 Testing Telegram Bot components...")
# Тестируем подключения
api_ok = await test_api_connection()
tmdb_ok = await test_tmdb_connection()
if api_ok and tmdb_ok:
logger.info("✅ All tests passed! Bot should work correctly.")
logger.info("🚀 Starting Telegram Bot...")
# Запускаем бота
bot = MovieSearchBot()
await bot.run()
else:
logger.error("❌ Some tests failed. Please check your setup.")
logger.error("Make sure the main application is running on port 8089")
if __name__ == "__main__":
asyncio.run(main())