refactor: split into two stacks - searchFilms/ (NL) and app/ (RU)
This commit is contained in:
parent
6ef3a10d0d
commit
51348a9d23
36 changed files with 326 additions and 1271 deletions
12
app/.env.example
Normal file
12
app/.env.example
Normal 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
14
app/Dockerfile
Normal 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
22
app/Dockerfile.telegram
Normal 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
126
app/MANAGEMENT.md
Normal 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
172
app/SAMBA_ACCESS.md
Normal 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
226
app/TELEGRAM_BOT_README.md
Normal 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
129
app/UBUNTU_DEPLOYMENT.md
Normal 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
1289
app/app.py
Normal file
File diff suppressed because it is too large
Load diff
92
app/docker-compose.yml
Normal file
92
app/docker-compose.yml
Normal 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
10
app/requirements.txt
Normal 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
37
app/run_telegram_bot.py
Normal 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
786
app/telegram_bot.py
Normal 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
63
app/templates/error.html
Normal 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
79
app/templates/index.html
Normal 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
177
app/templates/results.html
Normal 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
266
app/templates/torrents.html
Normal 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
76
app/test_telegram_bot.py
Normal 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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue