Compare commits

..

10 commits

Author SHA1 Message Date
vrubelroman
127060d023 fix: пропал декоратор @app.post(/api/add-torrent), follow_redirects в tmdb-proxy
- Возвращён @app.post('/api/add-torrent') — был съеден при вставке
  proxy-torrent-download, из-за чего кнопка 'Добавить в клиент' всегда
  возвращала 404
- tmdb-proxy /proxy-torrent: добавлен follow_redirects=True — rutracker
  и kinozal отдают 302 перед .torrent файлом
2026-06-03 19:32:41 +00:00
vrubelroman
a5497eef26 fix(app): исправление скачивания торрентов
- generate_clean_magnet: убраны мёртвые трекеры (coppersurfer.tk, leechers-paradise.org),
  добавлены рабочие (tamersunion.org, exodus.desync.com, moeking.me),
  включено &dn= с URL-кодированием кириллицы
- extract_hash_from_result: новая единая функция извлечения хэша из 5 источников
  (Hash, InfoHash, Magnet, btih: в URL, Id)
- /api/add-torrent: убран ложный success — после Ok. от qBittorrent идёт реальная
  верификация (торрент появился в списке по хэшу или названию). Если не появился — error.
- /api/proxy-torrent-download: новый endpoint для скачивания .torrent файлов
  через NL-прокси (обходит DPI-блокировку)
- torrents.html: кнопка Копировать magnet (Clipboard API + fallback),
  proxy-ссылки для .torrent, disabled-состояния для пустых magnet/torrent_url
- tmdb-proxy: добавлен /proxy-torrent endpoint
- urlencode filter для Jinja2
- test_app.py: 47 тестов на чистые функции
2026-06-03 19:27:14 +00:00
vrubelroman
fb2aa5a60a fix: pass magnet link directly to add-torrent API, avoid search_by_id lookup 2026-06-03 10:49:29 +00:00
vrubelroman
b971294909 fix: improve torapi-qbit fallback check for empty magnets 2026-06-03 10:06:29 +00:00
vrubelroman
135d51a00e fix .env with full tokens 2026-06-03 09:49:43 +00:00
vrubelroman
f5c1193dba add .env with production keys (private repo) 2026-06-03 09:40:58 +00:00
vrubelroman
51348a9d23 refactor: split into two stacks - searchFilms/ (NL) and app/ (RU) 2026-06-03 09:29:09 +00:00
vrubel
6ef3a10d0d Добавлена сеть npm_default для доступа Nginx Proxy Manager к movie-search 2026-01-02 18:16:03 +03:00
vrubel
a41f1251bc Добавлен .env файл с настройками 2026-01-02 17:53:29 +03:00
vrubel
15406c732b Улучшено логирование ошибок при поиске торрентов 2026-01-02 17:50:10 +03:00
38 changed files with 1008 additions and 1379 deletions

View file

@ -54,29 +54,35 @@
## 📊 Технические детали
### Архитектура:
### Архитектура (два стека):
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Telegram Bot │ │ Web Interface │ │ qBittorrent │
│ (Docker) │ │ (Docker) │ │ (Host) │
└─────────┬───────┘ └─────────┬───────┘ └─────────┬───────┘
│ │ │
└──────────────────────┼──────────────────────┘
┌─────────────┴─────────────┐
│ FastAPI App │
│ (Movie Search API) │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ TMDB API │
│ (Movie Information) │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ TorAPI │
│ (Torrent Search) │
└───────────────────────────┘
┌─────────────────────────────────────────┐
│ searchFilms/ — NL-хост (72.56.91.135) │
│ ┌──────────┐ ┌──────────────────────┐ │
│ │ tmdb- │ │ torapi-search │ │
│ │ proxy │ │ (rutracker, kinozal, │ │
│ │ (:8001) │ │ rutor, nnmclub) │ │
│ └────┬─────┘ └──────────┬───────────┘ │
│ │ │ │
│ ┌────┴───────────────────┴───────────┐ │
│ │ torapi-qbittorrent (:8444) │ │
│ │ bridge → magnet ссылки │ │
│ └────────────┬────────────────────────┘ │
└───────────────┼──────────────────────────┘
│ интернет
┌───────────────┼──────────────────────────┐
│ │ app/ — RU-хост │
│ ┌────────────┴──────────────────────┐ │
│ │ movie-search (:8089) FastAPI API │ │
│ │ веб-интерфейс + API endpoints │ │
│ └────┬──────────────────────┬───────┘ │
│ │ │ │
│ ┌────┴──────────┐ ┌──────┴────────┐ │
│ │ qBittorrent │ │ telegram-bot │ │
│ │ (192.168.8.177│ │ │ │
│ │ :8080) │ │ @your_bot │ │
│ └───────────────┘ └───────────────┘ │
└─────────────────────────────────────────┘
```
### Поддерживаемые трекеры:
@ -136,24 +142,26 @@ docker ps
## 📁 Структура файлов
```
searchTorrentDownl/
├── app.py # Основное веб-приложение
├── telegram_bot.py # Telegram бот
├── run_telegram_bot.py # Скрипт запуска бота
├── test_telegram_bot.py # Тестирование бота
├── start_all.sh # Скрипт запуска всего проекта
├── requirements.txt # Python зависимости
├── Dockerfile # Docker образ веб-приложения
├── Dockerfile.telegram # Docker образ Telegram бота
├── docker-compose.yml # Docker Compose конфигурация
├── templates/ # HTML шаблоны
│ ├── index.html # Главная страница
│ ├── results.html # Результаты поиска фильмов
│ ├── torrents.html # Результаты поиска торрентов
│ └── error.html # Страница ошибок
├── README.md # Основная документация
├── TELEGRAM_BOT_README.md # Документация Telegram бота
└── PROJECT_SUMMARY.md # Эта сводка
findFilms/
├── README.md # Инструкция по запуску
├── PROJECT_SUMMARY.md # Архитектура проекта
├── .gitignore
├── searchFilms/ # 🌍 NL-стек (Голландия, 72.56.91.135)
│ ├── docker-compose.yml # tmdb-proxy + torapi-search + bridge
│ ├── .env.example
│ └── tmdb-proxy/ # build-зависимость
│ ├── Dockerfile
│ └── tmdb_proxy.py
└── app/ # 🏠 RU-стек (Россия, 192.168.8.173)
├── docker-compose.yml # movie-search + telegram-bot
├── .env.example
├── app.py # FastAPI приложение
├── telegram_bot.py # Telegram бот
├── Dockerfile
├── Dockerfile.telegram
└── templates/ # HTML шаблоны
```
## 🎯 Ключевые особенности

540
README.md
View file

@ -1,469 +1,79 @@
# 🎬 searchTorrentDownl
# findFilms — поиск и скачивание фильмов
**Полнофункциональная система для поиска и загрузки фильмов через торренты с Telegram ботом**
[![Docker](https://img.shields.io/badge/Docker-Ready-blue?logo=docker)](https://www.docker.com/)
[![Python](https://img.shields.io/badge/Python-3.12+-green?logo=python)](https://python.org/)
[![FastAPI](https://img.shields.io/badge/FastAPI-0.115.0-red?logo=fastapi)](https://fastapi.tiangolo.com/)
[![Telegram](https://img.shields.io/badge/Telegram-Bot-blue?logo=telegram)](https://telegram.org/)
## 🎯 Описание проекта
**searchTorrentDownl** - это современная система для поиска и загрузки фильмов, которая объединяет:
- 🌐 **Веб-интерфейс** с адаптивным дизайном
- 🤖 **Telegram бот** с полной функциональностью
- 🔍 **Поиск фильмов** через TMDB API
- 🎬 **Поиск торрентов** на популярных трекерах
- ⬇️ **Автоматическое добавление** в qBittorrent
- 🔔 **Уведомления** о завершении загрузки
### ✨ Основные возможности
- **🎬 Поиск фильмов** - интеллектуальный поиск с постерами и описаниями
- **🔍 Поиск торрентов** - на всех популярных трекерах (RuTracker, Kinozal, RuTor, NoNameClub)
- **📱 Telegram бот** - полная функциональность в мессенджере
- **🌐 Веб-интерфейс** - удобный поиск и навигация
- **⬇️ Автоматическая загрузка** - добавление торрентов в qBittorrent одним кликом
- **🔔 Уведомления** - сообщения о завершении загрузки в Telegram
- **🐳 Docker** - полная контейнеризация для простого развертывания
## 🏗️ Архитектура системы
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Telegram Bot │ │ Web Interface │ │ qBittorrent │
│ (Docker) │ │ (Docker) │ │ (Host) │
└─────────┬───────┘ └─────────┬───────┘ └─────────┬───────┘
│ │ │
└──────────────────────┼──────────────────────┘
┌─────────────┴─────────────┐
│ FastAPI App │
│ (Movie Search API) │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ TMDB API │
│ (Movie Information) │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ TorAPI │
│ (Torrent Search) │
└───────────────────────────┘
```
### 🔧 Компоненты системы
1. **FastAPI приложение** (Docker) - основной веб-сервис и API
2. **Telegram Bot** (Docker) - бот для мессенджера
3. **TorAPI-Search** (Docker) - поиск торрентов по названию
4. **TorAPI-qBittorrent** (Docker) - получение magnet ссылок
5. **qBittorrent-nox** (Host) - клиент для загрузки торрентов
## 📋 Системные требования
### Минимальные требования
- **ОС**: Linux (Ubuntu 20.04+, Debian 11+)
- **RAM**: 2GB
- **Диск**: 10GB свободного места
- **CPU**: 2 ядра
### Необходимое ПО
- **Docker**: 20.10+
- **Docker Compose**: 2.0+
- **qBittorrent-nox**: 4.6.7+
## 🚀 Быстрый старт
### 1⃣ Автоматическое развертывание (рекомендуется)
```bash
# Клонируйте репозиторий
git clone <repository-url>
cd searchTorrentDownl
# Запустите скрипт развертывания
chmod +x deploy.sh
./deploy.sh
```
Скрипт автоматически:
- Установит все зависимости
- Настроит qBittorrent
- Запустит все сервисы
- Покажет статус системы
### 2⃣ Ручное развертывание
#### Шаг 1: Установка зависимостей
```bash
# Обновление системы
sudo apt update && sudo apt upgrade -y
# Установка Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
# Установка Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# Установка qBittorrent
sudo apt install -y qbittorrent-nox
```
#### Шаг 2: Настройка qBittorrent
```bash
# Создание пользователя
sudo useradd -r -s /bin/false qbittorrent
# Создание systemd сервиса
sudo tee /etc/systemd/system/qbittorrent.service > /dev/null <<EOF
[Unit]
Description=qBittorrent-nox
After=network.target
[Service]
Type=simple
User=qbittorrent
Group=qbittorrent
ExecStart=/usr/bin/qbittorrent-nox --webui-port=8080
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
# Запуск сервиса
sudo systemctl daemon-reload
sudo systemctl enable qbittorrent
sudo systemctl start qbittorrent
```
#### Шаг 3: Настройка проекта
```bash
# Создание Docker сети
docker network create torrentvideo_default
# Копирование конфигурации
cp env.example .env
# Редактирование конфигурации
nano .env
```
#### Шаг 4: Запуск сервисов
```bash
# Запуск всех сервисов
docker compose up -d --build
# Проверка статуса
docker ps
```
## ⚙️ Конфигурация
### 🔑 Получение API ключей
#### TMDB API Key
1. Зарегистрируйтесь на [themoviedb.org](https://www.themoviedb.org/)
2. Перейдите в [Settings > API](https://www.themoviedb.org/settings/api)
3. Создайте новый API ключ
4. Скопируйте ключ в файл `.env`
#### Telegram Bot Token
1. Найдите [@BotFather](https://t.me/BotFather) в Telegram
2. Отправьте команду `/newbot`
3. Следуйте инструкциям для создания бота
4. Скопируйте токен в файл `.env`
### 📝 Файл конфигурации (.env)
```bash
# TMDB API Key
TMDB_API_KEY=your_tmdb_api_key_here
# Telegram Bot Token
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
# qBittorrent настройки
QBITTORRENT_USERNAME=admin
QBITTORRENT_PASSWORD=admin
QBITTORRENT_HOST=host.docker.internal
QBITTORRENT_PORT=8080
# TorAPI настройки
TORRENT_SEARCH_URL=http://host.docker.internal:8443
TORRENT_ADD_URL=http://host.docker.internal:8088
```
## 🌐 Использование
### Веб-интерфейс
1. Откройте http://localhost:8089
2. Введите название фильма
3. Выберите фильм из результатов
4. Выберите торрент для скачивания
5. Торрент автоматически добавится в qBittorrent
### Telegram Bot
1. Найдите вашего бота в Telegram
2. Отправьте команду `/start` или `/find`
3. Введите название фильма
4. Выберите фильм из списка
5. Нажмите "Найти торренты"
6. Выберите нужный торрент
7. Получите уведомление о завершении загрузки
### qBittorrent
- **Веб-интерфейс**: http://localhost:8080
- **Логин по умолчанию**: admin / admin
- **Настройка папок**: Settings > Downloads
## 📁 Структура проекта
```
searchTorrentDownl/
├── app.py # Основное FastAPI приложение
├── telegram_bot.py # Telegram бот
├── run_telegram_bot.py # Скрипт запуска бота
├── deploy.sh # Скрипт автоматического развертывания
├── start_all.sh # Скрипт запуска (локально)
├── requirements.txt # Python зависимости
├── env.example # Пример конфигурации
├── Dockerfile # Docker образ основного приложения
├── Dockerfile.telegram # Docker образ Telegram бота
├── docker-compose.yml # Docker Compose конфигурация
├── templates/ # HTML шаблоны
│ ├── index.html # Главная страница
│ ├── results.html # Результаты поиска фильмов
│ ├── torrents.html # Результаты поиска торрентов
│ └── error.html # Страница ошибок
├── README.md # Основная документация
├── TELEGRAM_BOT_README.md # Документация Telegram бота
└── PROJECT_SUMMARY.md # Сводка проекта
```
## 🔧 Управление сервисами
### Основные команды
```bash
# Запуск всех сервисов
docker compose up -d
# Остановка всех сервисов
docker compose down
# Перезапуск сервисов
docker compose restart
# Просмотр логов
docker compose logs -f
# Просмотр статуса
docker ps
```
### Управление отдельными сервисами
```bash
# Запуск только веб-приложения
docker compose up -d movie-search
# Запуск только Telegram бота
docker compose up -d telegram-bot
# Перезапуск TorAPI
docker compose restart torapi-search torapi-qbittorrent
```
### Управление qBittorrent
```bash
# Запуск
sudo systemctl start qbittorrent
# Остановка
sudo systemctl stop qbittorrent
# Статус
sudo systemctl status qbittorrent
# Логи
sudo journalctl -u qbittorrent -f
```
## 🐛 Устранение неполадок
### Проблемы с Docker
```bash
# Проверка статуса контейнеров
docker ps -a
# Просмотр логов
docker logs <container_name>
# Пересборка контейнеров
docker compose up -d --build --force-recreate
```
### Проблемы с qBittorrent
```bash
# Проверка статуса
sudo systemctl status qbittorrent
# Перезапуск
sudo systemctl restart qbittorrent
# Проверка портов
netstat -tlnp | grep 8080
```
### Проблемы с сетью
```bash
# Проверка Docker сетей
docker network ls
# Создание сети заново
docker network rm torrentvideo_default
docker network create torrentvideo_default
```
### Проблемы с API
```bash
# Проверка TMDB API
curl "https://api.themoviedb.org/3/movie/550?api_key=YOUR_API_KEY"
# Проверка Telegram Bot
curl "https://api.telegram.org/botYOUR_BOT_TOKEN/getMe"
# Проверка TorAPI
curl "http://localhost:8443/api/provider/list"
```
## 📊 Мониторинг
### Логи сервисов
```bash
# Все сервисы
docker compose logs -f
# Конкретный сервис
docker logs -f movie-search
docker logs -f telegram-bot
docker logs -f TorAPI-Search
docker logs -f TorAPI-qBittorrent
```
### Мониторинг ресурсов
```bash
# Использование ресурсов контейнерами
docker stats
# Использование диска
df -h
# Использование памяти
free -h
```
## 🔒 Безопасность
### Рекомендации
1. **Измените пароли по умолчанию**
```bash
# В файле .env
QBITTORRENT_PASSWORD=your_secure_password
```
2. **Настройте файрвол**
```bash
sudo ufw allow 8080 # qBittorrent
sudo ufw allow 8089 # Web interface
sudo ufw enable
```
3. **Используйте HTTPS в продакшене**
- Настройте reverse proxy (nginx)
- Получите SSL сертификат
4. **Регулярно обновляйте зависимости**
```bash
docker compose pull
docker compose up -d --build
```
## 🚀 Развертывание в продакшене
### Настройка reverse proxy (nginx)
```nginx
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:8089;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
### Использование Docker Swarm
```bash
# Инициализация Swarm
docker swarm init
# Развертывание стека
docker stack deploy -c docker-compose.yml searchtorrentdownl
```
## 🤝 Вклад в проект
1. Форкните репозиторий
2. Создайте ветку для новой функции
3. Внесите изменения
4. Создайте Pull Request
## 📄 Лицензия
Этот проект распространяется под лицензией MIT. См. файл [LICENSE](LICENSE) для подробностей.
## 🆘 Поддержка
При возникновении проблем:
1. Проверьте [раздел устранения неполадок](#-устранение-неполадок)
2. Изучите логи сервисов
3. Создайте issue в репозитории
4. Опишите проблему и приложите логи
## 📈 Планы развития
- [ ] Поддержка дополнительных трекеров
- [ ] Веб-интерфейс для управления ботом
- [ ] Система уведомлений по email
- [ ] API для интеграции с другими приложениями
- [ ] Поддержка сериалов и аниме
- [ ] Мобильное приложение
Двухстековая архитектура: **searchFilms/** на NL-хосте (Голландия, без блокировок), **app/** на RU-хосте (Россия). Между собой общаются по HTTP через интернет.
---
**Создано с ❤️ для удобного поиска и загрузки фильмов**
## Архитектура
```
searchFilms/ (NL, 72.56.91.135) internet app/ (RU, 192.168.8.173)
┌─────────────────────────┐ HTTP ┌─────────────────────────────┐
│ tmdb-proxy (:8001) │ ←──────────→ │ movie-search (:8089) │
│ torapi-search (:8443) │ поиск │ telegram-bot │
│ │ │ qBittorrent (:8080) │
└─────────────────────────┘ └─────────────────────────────┘
```
**Поток данных:**
1. Пользователь ищет фильм → movie-search делает запрос к **tmdb-proxy** (NL)
2. Пользователь ищет торренты → movie-search делает запрос к **torapi-search** (NL)
3. Пользователь добавляет торрент → movie-search отдаёт magnet-ссылку в **qBittorrent** (локально)
NL-хосту **не нужен** доступ к RU — только RU ходит к NL.
---
## Быстрый старт
### 🌍 SearchFilms Stack — на голландском хосте (72.56.91.135)
```bash
cd searchFilms
cp .env.example .env # указать TMDB_API_KEY
docker compose up -d --build
```
Откроет порты:
- `:8001` — TMDB API Proxy
- `:8443` — TorAPI Search
### 🏠 App Stack — на хосте в России
```bash
cd app
cp .env.example .env # указать TELEGRAM_BOT_TOKEN
docker compose up -d --build
```
Откроет:
- `:8089` — веб-интерфейс
- Telegram бот (ждёт команды `/start`)
### Переменные окружения
| Переменная | Где | Назначение |
|---|---|---|
| `NL_HOST` | `app/.env` | IP голландского хоста (по умолч. `72.56.91.135`) |
| `TELEGRAM_BOT_TOKEN` | `app/.env` | Токен бота от @BotFather |
| `TMDB_API_KEY` | `search/.env` | Ключ TMDB API |
| `QBITTORRENT_USERNAME` | `app/.env` | Логин qBittorrent |
| `QBITTORRENT_PASSWORD` | `app/.env` | Пароль qBittorrent |
---
## Структура
```
findFilms/
├── README.md ← этот файл
├── PROJECT_SUMMARY.md ← архитектура
├── searchFilms/ ← NL-стек
│ ├── docker-compose.yml
│ ├── .env.example
│ └── tmdb-proxy/ ← build-зависимость
└── app/ ← RU-стек
├── docker-compose.yml
├── .env.example
├── app.py / telegram_bot.py / templates / Dockerfile / ...
```

6
app/.env Normal file
View file

@ -0,0 +1,6 @@
TELEGRAM_BOT_TOKEN=7662650066:AAFgsfYJNYgpcSHaSe6fspsjqmhMkOBT1s4
NL_HOST=72.56.91.135
QBITTORRENT_USERNAME=vrubelroman
QBITTORRENT_PASSWORD=VRKshtein07
QBITTORRENT_HOST=192.168.8.177
QBITTORRENT_PORT=8080

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=ваш_па...

View file

@ -4,11 +4,12 @@ import asyncio
import httpx
import requests
from bs4 import BeautifulSoup
from fastapi import FastAPI, Request, Form, HTTPException
from fastapi.responses import HTMLResponse
from fastapi import FastAPI, Request, Form, HTTPException, Query
from fastapi.responses import HTMLResponse, Response
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from urllib.parse import quote
app = FastAPI(title="Movie Search API", version="1.0.0")
@ -24,6 +25,13 @@ app.add_middleware(
# Настройка шаблонов
templates = Jinja2Templates(directory="templates")
# Регистрируем фильтр urlencode для шаблонов
def urlencode_filter(s):
if s:
return quote(s, safe='')
return ''
templates.env.filters['urlencode'] = urlencode_filter
# URL прокси-сервиса TMDB
TMDB_PROXY_URL = os.getenv("TMDB_PROXY_URL", "http://localhost:8001")
@ -283,15 +291,29 @@ async def search_torrents(movie_title: str, year: str = None, original_title: st
print(f"Searching '{search_query}' on {provider_name}")
# Поиск на конкретном провайдере - исправленный эндпоинт
search_response = await client.get(
f"{TORRENT_SEARCH_URL}/api/search/title/{provider_name}",
params={"query": search_query}
)
try:
search_response = await client.get(
f"{TORRENT_SEARCH_URL}/api/search/title/{provider_name}",
params={"query": search_query},
timeout=30.0
)
except Exception as request_error:
print(f"Request error on {provider_name} for '{search_query}': {request_error}")
import traceback
traceback.print_exc()
continue
if search_response.status_code == 200:
results = search_response.json()
try:
results = search_response.json()
except Exception as json_error:
print(f"JSON parse error on {provider_name} for '{search_query}': {json_error}")
print(f"Response text (first 500 chars): {search_response.text[:500]}")
continue
if not isinstance(results, list):
print(f"Unexpected response format from {provider_name}: {type(results)}")
if isinstance(results, dict):
print(f"Dict keys: {list(results.keys())}")
continue
print(f"Found {len(results)} results for '{search_query}' on {provider_name}")
@ -340,7 +362,9 @@ async def search_torrents(movie_title: str, year: str = None, original_title: st
break
except Exception as e:
print(f"Error searching on {provider_name}: {e}")
print(f"Error searching on {provider_name}: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
continue
# Сортируем по количеству сидов и ограничиваем общее количество
@ -402,31 +426,45 @@ async def search_torrent_by_id(torrent_id: str) -> dict:
torrent_name = result.get('Name', '') or result.get('Original_Name', '')
print(f"Found torrent by ID on {provider_name}: {torrent_name[:100]}...")
# Получаем хэш и создаем чистую magnet-ссылку с публичными трекерами
hash_value = result.get('Hash', '')
torrent_title = torrent_name
# Если хэша нет в Hash, пытаемся извлечь из Magnet ссылки
if not hash_value:
original_magnet = result.get('Magnet', '')
if original_magnet:
# Извлекаем хэш из magnet ссылки
hash_match = re.search(r'urn:btih:([a-fA-F0-9]{40}|[a-zA-Z0-9]{32})', original_magnet)
if hash_match:
hash_value = hash_match.group(1)
print(f"Extracted hash from magnet link: {hash_value[:10]}...")
# Получаем хэш — используем единую функцию извлечения
hash_value = extract_hash_from_result(result)
if hash_value:
# Генерируем чистую magnet-ссылку с публичными трекерами
magnet = generate_clean_magnet(hash_value, torrent_title)
print(f"Generated clean magnet with public trackers: {magnet[:100]}...")
magnet = generate_clean_magnet(hash_value, torrent_name)
print(f"Generated clean magnet with hash {hash_value[:10]}...: {magnet[:100]}...")
else:
# Fallback на оригинальную magnet-ссылку если нет хэша
magnet = result.get('Magnet', '')
if not magnet or not magnet.startswith('magnet:'):
print(f"Warning: No hash found and no valid magnet link. Hash: {hash_value}, Magnet: {result.get('Magnet', 'None')[:50]}")
print(f"Warning: No hash found and no valid magnet link. Hash fields: Hash={result.get('Hash', 'None')[:20]}, Magnet={result.get('Magnet', 'None')[:50]}")
magnet = ""
# Пробуем локальный torapi-qbit если хэш пустой или битый
if not magnet or not re.search(r'urn:btih:([a-fA-F0-9]{40}|[a-zA-Z0-9]{32})', magnet):
try:
torapi_add_url = os.getenv("TORAPI_ADD_URL", "http://localhost:8444")
fb_resp = await client.get(
f"{torapi_add_url}/api/search/id/{provider_name}",
params={"query": torrent_id},
timeout=15.0
)
if fb_resp.status_code == 200:
fb_data = fb_resp.json()
if isinstance(fb_data, list) and len(fb_data) > 0:
fb_result = fb_data[0]
fb_hash = extract_hash_from_result(fb_result)
if fb_hash:
magnet = generate_clean_magnet(fb_hash, torrent_name)
print(f"torapi-qbit fallback: got hash {fb_hash[:10]}... via extract_hash_from_result")
except Exception as fbe:
print(f"torapi-qbit fallback failed: {fbe}")
# Если хэш всё ещё пустой — пропускаем этого провайдера
if not hash_value and not re.search(r'urn:btih:([a-fA-F0-9]{40}|[a-zA-Z0-9]{32})', magnet):
print(f"Skipping {provider_name}: no valid magnet hash")
continue
# Парсим результат в стандартный формат
torrent = {
"title": torrent_name,
@ -458,10 +496,48 @@ async def search_torrent_by_id(torrent_id: str) -> dict:
print(f"Error searching by ID on {provider_name}: {response.status_code} - {response.text}")
except Exception as e:
print(f"Error searching on {provider_name}: {e}")
print(f"Error searching on {provider_name}: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
continue
print(f"No results found for ID {torrent_id} on any provider")
print(f"No results found for ID {torrent_id} on any provider via search API")
# Fallback: пробуем локальный torapi-qbit (через qBittorrent)
try:
torapi_add_url = os.getenv("TORAPI_ADD_URL", "http://localhost:8444")
fallback_response = await client.get(
f"{torapi_add_url}/api/search/id/rutracker",
params={"query": torrent_id},
timeout=30.0
)
if fallback_response.status_code == 200:
fallback_results = fallback_response.json()
if isinstance(fallback_results, list) and len(fallback_results) > 0:
result = fallback_results[0]
hash_value = result.get('Hash', '')
if not hash_value:
orig_magnet = result.get('Magnet', '')
hm = re.search(r'urn:btih:([a-fA-F0-9]{40})', orig_magnet)
if hm:
hash_value = hm.group(1)
if hash_value:
torrent_name = result.get('Name', '') or result.get('Original_Name', '')
magnet = generate_clean_magnet(hash_value, torrent_name)
print(f"Fallback: got magnet via torapi-qbit: {magnet[:60]}...")
return {
"title": torrent_name,
"url": result.get('Url', ''),
"hash": hash_value,
"magnet": magnet,
"torrent_url": result.get('Torrent', ''),
"provider": "torapi-qbit",
"id": torrent_id
}
except Exception as e:
print(f"Fallback to torapi-qbit failed: {e}")
print(f"All fallbacks exhausted for ID {torrent_id}")
return None
except Exception as e:
@ -604,25 +680,69 @@ def parse_size_to_bytes(size_str: str) -> int:
return 0
def extract_hash_from_result(result: dict) -> str:
"""Извлекает хэш торрента из результата API — пробует все возможные поля"""
hash_value = ""
# 1. Прямое поле Hash
hash_value = result.get('Hash', '') or result.get('hash', '') or ''
if hash_value and re.match(r'^[a-fA-F0-9]{40}$', hash_value):
return hash_value.upper()
# 2. Поле InfoHash
if not hash_value:
hash_value = result.get('InfoHash', '') or result.get('info_hash', '') or ''
if hash_value and re.match(r'^[a-fA-F0-9]{40}$', hash_value):
return hash_value.upper()
# 3. Из Magnet ссылки
magnet = result.get('Magnet', '') or result.get('magnet', '') or ''
if magnet:
hm = re.search(r'urn:btih:([a-fA-F0-9]{40})', magnet)
if hm:
return hm.group(1).upper()
hm32 = re.search(r'urn:btih:([a-zA-Z0-9]{32})', magnet)
if hm32:
return hm32.group(1)
# 4. Из Torrent URL (rutracker: /dl.php?t=XXXXX)
torrent_url = result.get('Torrent', '') or result.get('torrent', '') or result.get('Url', '') or ''
# Некоторые провайдеры кодируют хэш в URL
if 'btih:' in torrent_url:
hm = re.search(r'btih:([a-fA-F0-9]{40})', torrent_url)
if hm:
return hm.group(1).upper()
# 5. Из поля Id/ID (некоторые API возвращают хэш как ID)
tid = result.get('Id') or result.get('id') or result.get('ID') or ''
if tid and re.match(r'^[a-fA-F0-9]{40}$', tid):
return tid.upper()
return hash_value # Может быть пустым
def generate_clean_magnet(hash_value: str, title: str = None) -> str:
"""Генерирует чистую magnet-ссылку с публичными трекерами"""
if not hash_value:
return ""
# Проверенные рабочие трекеры (минимальный набор)
# Рабочие публичные трекеры (2025+)
public_trackers = [
"udp://tracker.opentrackr.org:1337/announce",
"udp://open.stealth.si:80/announce",
"udp://tracker.openbittorrent.com:6969/announce",
"udp://tracker.coppersurfer.tk:6969/announce",
"udp://tracker.leechers-paradise.org:6969/announce"
"https://tracker.tamersunion.org:443/announce",
"udp://exodus.desync.com:6969/announce",
"udp://tracker.moeking.me:6969/announce",
]
# Создаем базовую magnet-ссылку с хэшем
magnet = f"magnet:?xt=urn:btih:{hash_value}"
# НЕ добавляем название файла - это может вызывать проблемы с кириллицей
# DHT сам найдет название по хэшу
# Добавляем название в кодировке RFC 3986 для удобства
if title:
from urllib.parse import quote
magnet += f"&dn={quote(title)}"
# Добавляем публичные трекеры
for tracker in public_trackers:
@ -1050,8 +1170,64 @@ async def torrents_page(request: Request, movie_title: str, year: str = None):
}
)
@app.get("/api/proxy-torrent-download")
async def proxy_torrent_download(url: str = Query(...)):
"""Прокси скачивание .torrent файла через NL-сервер (обходит DPI)"""
try:
proxy_base = os.getenv("TMDB_PROXY_URL", "http://localhost:8001")
print(f"Proxying .torrent download: {url} via {proxy_base}")
async with httpx.AsyncClient(timeout=30.0) as client:
# Пробуем через NL прокси (tmdb-proxy имеет прямой выход в интернет)
try:
proxy_resp = await client.get(
f"{proxy_base}/proxy-torrent",
params={"url": url},
timeout=30.0
)
if proxy_resp.status_code == 200 or (proxy_resp.status_code < 400 and len(proxy_resp.content) > 100):
content_type = proxy_resp.headers.get("content-type", "application/x-bittorrent")
return Response(
content=proxy_resp.content,
media_type=content_type,
headers={
"Content-Disposition": f'attachment; filename="{os.path.basename(url)}.torrent"',
"Content-Length": str(len(proxy_resp.content))
}
)
print(f"NL proxy returned {proxy_resp.status_code}, trying direct download...")
except Exception as proxy_err:
print(f"NL proxy error: {proxy_err}, trying direct download...")
# Fallback: пытаемся скачать напрямую из контейнера (может работать если DNS не блокируется)
try:
direct_resp = await client.get(url, timeout=15.0)
if direct_resp.status_code == 200 and len(direct_resp.content) > 100:
content_type = direct_resp.headers.get("content-type", "application/x-bittorrent")
return Response(
content=direct_resp.content,
media_type=content_type,
headers={
"Content-Disposition": f'attachment; filename="{os.path.basename(url)}.torrent"',
"Content-Length": str(len(direct_resp.content))
}
)
except Exception as direct_err:
print(f"Direct download failed: {direct_err}")
raise HTTPException(
status_code=502,
detail=f"Не удалось скачать .torrent файл. NL-прокси и прямой доступ недоступны."
)
except HTTPException:
raise
except Exception as e:
print(f"Error in proxy-torrent-download: {e}")
raise HTTPException(status_code=500, detail=f"Ошибка прокси: {str(e)}")
@app.post("/api/add-torrent")
async def add_torrent_to_client(torrent_id: str = Form(...)):
async def add_torrent_to_client(torrent_id: str = Form(...), magnet: str = Form(""), torrent_title: str = Form("")):
"""Добавление торрента в qBittorrent через прямое API"""
try:
print(f"Attempting to add torrent with ID: {torrent_id}")
@ -1059,10 +1235,21 @@ async def add_torrent_to_client(torrent_id: str = Form(...)):
if not torrent_id or torrent_id.strip() == '':
return {"status": "error", "message": "ID торрента не указан"}
# Получаем информацию о торренте по ID
torrent_info = await search_torrent_by_id(torrent_id)
# Если magnet передан напрямую (из результатов поиска), используем его
if magnet and magnet.startswith('magnet:'):
print(f"Using direct magnet link: {magnet[:100]}...")
torrent_info = {
"title": torrent_title or torrent_id,
"hash": "",
"magnet": magnet,
"torrent_url": ""
}
else:
# Fallback: ищем по ID через TorAPI
torrent_info = await search_torrent_by_id(torrent_id)
if not torrent_info:
print(f"Torrent info is None for ID: {torrent_id}")
print(f"Torrent info not found for ID: {torrent_id}")
return {"status": "error", "message": f"Торрент с ID {torrent_id} не найден. Проверьте логи для деталей."}
print(f"Found torrent info: title={torrent_info.get('title', 'Unknown')[:50]}, magnet={'present' if torrent_info.get('magnet') else 'missing'}, torrent_url={'present' if torrent_info.get('torrent_url') else 'missing'}")
@ -1111,27 +1298,42 @@ async def add_torrent_to_client(torrent_id: str = Form(...)):
if hash_match:
torrent_hash = hash_match.group(1).upper()
if torrent_hash:
torrents_response = await client.get(f"{qb_url}/api/v2/torrents/info")
if torrents_response.status_code == 200:
torrents = torrents_response.json()
# Ищем торрент по хэшу
for torrent in torrents:
if torrent.get('hash', '').upper() == torrent_hash:
return {
"status": "success",
"message": f"Торрент '{torrent_info.get('title', 'Unknown')[:50]}...' добавлен в qBittorrent через magnet-ссылку!",
"torrent_hash": torrent.get('hash'),
"torrent_name": torrent_info.get('title', 'Unknown')
}
# Реальная верификация: проверяем, что торрент появился в qBittorrent
verified = False
actual_hash = torrent_hash
torrents_response = await client.get(f"{qb_url}/api/v2/torrents/info")
if torrents_response.status_code == 200:
qb_torrents = torrents_response.json()
if torrent_hash:
for qt in qb_torrents:
if qt.get('hash', '').upper() == torrent_hash:
verified = True
actual_hash = qt.get('hash', '')
print(f"Verified: torrent found by hash in qBittorrent")
break
if not verified:
# Пробуем найти по названию
t_title = torrent_info.get('title', '').lower()
for qt in qb_torrents:
qt_name = qt.get('name', '').lower()
if t_title and qt_name and (t_title[:30] in qt_name or qt_name[:30] in t_title):
verified = True
actual_hash = qt.get('hash', '')
print(f"Verified: torrent found by name in qBittorrent: {qt.get('name', '')}")
break
# Если не нашли по хэшу, но ответ был Ok, считаем успешным
return {
"status": "success",
"message": f"Торрент '{torrent_info.get('title', 'Unknown')[:50]}...' добавлен в qBittorrent через magnet-ссылку!",
"torrent_hash": torrent_hash,
"torrent_name": torrent_info.get('title', 'Unknown')
}
if verified:
return {
"status": "success",
"message": f"Торрент '{torrent_info.get('title', 'Unknown')[:50]}...' добавлен в qBittorrent!",
"torrent_hash": actual_hash,
"torrent_name": torrent_info.get('title', 'Unknown')
}
else:
# qBittorrent сказал Ok., но торрент не появился — ложный успех
print(f"WARNING: qBittorrent returned Ok. but torrent was not added (hash={torrent_hash or 'empty'})")
print("Proceeding to .torrent file fallback...")
# Не возвращаем success, а пробуем .torrent fallback ниже
else:
print(f"Magnet link failed (status: {add_response.status_code}, response: {add_response.text}), trying .torrent file...")
else:
@ -1145,13 +1347,42 @@ async def add_torrent_to_client(torrent_id: str = Form(...)):
if torrent_url:
print(f"Trying to add via .torrent file: {torrent_url}")
add_response = await client.post(
f"{qb_url}/api/v2/torrents/add",
data={"urls": torrent_url}
)
print(f"Add via .torrent response status: {add_response.status_code}")
print(f"Add via .torrent response text: {add_response.text}")
# Сначала пробуем скачать .torrent через NL-прокси (обходит DPI)
try:
proxy_base = os.getenv("TMDB_PROXY_URL", "http://localhost:8001")
print(f"Downloading .torrent via NL proxy: {proxy_base}/proxy-torrent")
proxy_resp = await client.get(
f"{proxy_base}/proxy-torrent",
params={"url": torrent_url},
timeout=30.0
)
if proxy_resp.status_code == 200:
torrent_content = proxy_resp.content
print(f"Downloaded .torrent via NL proxy: {len(torrent_content)} bytes")
# Загружаем файл в qBittorrent
add_response = await client.post(
f"{qb_url}/api/v2/torrents/add",
files={"torrents": ("torrent.torrent", torrent_content, "application/x-bittorrent")}
)
print(f"Add via .torrent file upload response: {add_response.status_code} - {add_response.text}")
else:
print(f"NL proxy failed: {proxy_resp.status_code}, sending URL directly to qBittorrent")
add_response = await client.post(
f"{qb_url}/api/v2/torrents/add",
data={"urls": torrent_url}
)
print(f"Add via .torrent response status: {add_response.status_code}")
print(f"Add via .torrent response text: {add_response.text}")
except Exception as proxy_err:
print(f"NL proxy error: {proxy_err}, falling back to direct URL")
add_response = await client.post(
f"{qb_url}/api/v2/torrents/add",
data={"urls": torrent_url}
)
print(f"Add via .torrent response status: {add_response.status_code}")
print(f"Add via .torrent response text: {add_response.text}")
if add_response.status_code == 200 and add_response.text.strip() == "Ok.":
# Проверяем, что торрент действительно добавился
@ -1163,13 +1394,12 @@ async def add_torrent_to_client(torrent_id: str = Form(...)):
if torrents_response.status_code == 200:
torrents = torrents_response.json()
# Ищем торрент по названию (первые 20 символов для точного совпадения)
# Ищем торрент по названию (первые 30 символов)
added_torrent = None
for torrent in torrents:
torrent_name = torrent.get('name', '').lower()
# Проверяем совпадение по началу названия
if torrent_title and torrent_name:
if torrent_title[:20] in torrent_name or torrent_name[:20] in torrent_title:
if torrent_title[:30] in torrent_name or torrent_name[:30] in torrent_title:
added_torrent = torrent
break
@ -1181,12 +1411,11 @@ async def add_torrent_to_client(torrent_id: str = Form(...)):
"torrent_name": added_torrent.get('name', torrent_info.get('title', 'Unknown'))
}
# Если не нашли по названию, но ответ был Ok, считаем успешным
# Торрент не появился — честный error вместо ложного success
print(f"WARNING: qBittorrent returned Ok. for .torrent but torrent was not found in list (title={torrent_title or 'empty'})")
return {
"status": "success",
"message": f"Торрент '{torrent_info.get('title', 'Unknown')[:50]}...' добавлен в qBittorrent через .torrent файл (проверьте список торрентов в qBittorrent)!",
"torrent_hash": torrent_info.get('hash', ''),
"torrent_name": torrent_info.get('title', 'Unknown')
"status": "error",
"message": f"qBittorrent не добавил торрент. Возможно, файл недоступен или DPI-блокировка. Попробуйте magnet-ссылку."
}
else:
error_msg = add_response.text.strip()

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

View file

@ -18,10 +18,7 @@
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 20px;
}
h1 { color: #333; margin-bottom: 20px; }
.back-link {
display: inline-block;
margin-bottom: 20px;
@ -31,20 +28,14 @@
border: 1px solid #007bff;
border-radius: 5px;
}
.back-link:hover {
background-color: #007bff;
color: white;
}
.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;
}
.torrents-list { display: grid; gap: 15px; }
.torrent-item {
border: 1px solid #ddd;
border-radius: 8px;
@ -53,9 +44,7 @@
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-item:hover { box-shadow: 0 4px 10px rgba(0,0,0,0.15); }
.torrent-title {
font-size: 18px;
font-weight: bold;
@ -68,30 +57,24 @@
gap: 15px;
margin-bottom: 15px;
}
.detail-item {
display: flex;
flex-direction: column;
}
.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;
}
.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;
flex-wrap: wrap;
}
.btn {
padding: 8px 16px;
@ -99,31 +82,25 @@
border-radius: 5px;
cursor: pointer;
text-decoration: none;
display: inline-block;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
transition: background-color 0.3s ease;
white-space: nowrap;
}
.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;
}
.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; }
.btn-warning { background-color: #ffc107; color: #333; }
.btn-warning:hover { background-color: #e0a800; }
.btn-info { background-color: #17a2b8; color: white; }
.btn-info:hover { background-color: #138496; }
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
.no-torrents {
text-align: center;
color: #666;
@ -141,69 +118,93 @@
.quality-bluray { background-color: #007bff; color: white; }
.quality-web-dl { background-color: #28a745; color: white; }
.quality-hdtv { background-color: #ffc107; color: #333; }
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: #333;
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.toast.show { opacity: 1; }
.toast.success { background: #28a745; }
.toast.error { background: #dc3545; }
.magnet-link {
word-break: break-all;
font-size: 11px;
color: #666;
margin-top: 8px;
padding: 6px 8px;
background: #f8f9fa;
border-radius: 4px;
display: none;
}
.magnet-link.visible { display: block; }
</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>
@ -211,11 +212,26 @@
</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>
{% if torrent.magnet and torrent.magnet.startswith('magnet:') %}
<a href="{{ torrent.magnet }}" class="btn btn-primary" title="Открыть magnet-ссылку">🧲 Magnet</a>
<button onclick="copyMagnet('{{ torrent.magnet|e }}', this)" class="btn btn-info" title="Копировать magnet-ссылку">📋 Копировать</button>
{% else %}
<button class="btn btn-primary" disabled title="Magnet-ссылка недоступна">🧲 Magnet</button>
{% endif %}
{% if torrent.download_url and torrent.download_url.startswith('http') %}
<a href="/api/proxy-torrent-download?url={{ torrent.download_url|urlencode }}" class="btn btn-success" target="_blank">💾 Скачать .torrent</a>
{% else %}
<button class="btn btn-success" disabled title=".torrent файл недоступен">💾 Скачать .torrent</button>
{% endif %}
{% if torrent.magnet and torrent.magnet.startswith('magnet:') %}
<button onclick="addToTorrentClient('{{ torrent.id }}', '{{ torrent.magnet|e }}', '{{ torrent.title|e }}')" class="btn btn-secondary"> Добавить в клиент</button>
{% else %}
<button class="btn btn-secondary" disabled title="Нет magnet-ссылки для добавления"> Добавить в клиент</button>
{% endif %}
</div>
</div>
{% endfor %}
@ -227,18 +243,68 @@
{% endif %}
</div>
<div id="toast" class="toast"></div>
<script>
function addToTorrentClient(torrentId) {
// Показываем индикатор загрузки
function showToast(message, type) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = 'toast ' + (type || '');
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 3000);
}
function copyMagnet(magnet, button) {
if (!magnet || !magnet.startsWith('magnet:')) {
showToast('❌ Magnet-ссылка недоступна', 'error');
return;
}
// Пробуем Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(magnet).then(() => {
const originalText = button.textContent;
button.textContent = '✅ Скопировано';
setTimeout(() => button.textContent = originalText, 2000);
showToast('✅ Magnet-ссылка скопирована в буфер', 'success');
}).catch(() => {
fallbackCopyMagnet(magnet, button);
});
} else {
fallbackCopyMagnet(magnet, button);
}
}
function fallbackCopyMagnet(magnet, button) {
const textarea = document.createElement('textarea');
textarea.value = magnet;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
const originalText = button.textContent;
button.textContent = '✅ Скопировано';
setTimeout(() => button.textContent = originalText, 2000);
showToast('✅ Magnet-ссылка скопирована в буфер', 'success');
} catch (e) {
showToast('❌ Не удалось скопировать. Выделите ссылку вручную.', 'error');
}
document.body.removeChild(textarea);
}
function addToTorrentClient(torrentId, magnet, title) {
const button = event.target;
const originalText = button.textContent;
button.textContent = '⏳ Получаем magnet...';
button.textContent = '⏳ Добавляем...';
button.disabled = true;
// Отправляем ID торрента для получения magnet-ссылки
const formData = new FormData();
formData.append('torrent_id', torrentId);
formData.append('magnet', magnet || '');
formData.append('torrent_title', title || '');
fetch('/api/add-torrent', {
method: 'POST',
body: formData
@ -246,17 +312,17 @@
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
alert('✅ ' + data.message);
showToast('✅ ' + data.message, 'success');
button.textContent = '✅ Добавлено';
button.style.backgroundColor = '#28a745';
} else {
alert('❌ ' + data.message);
showToast('❌ ' + data.message, 'error');
button.textContent = originalText;
button.disabled = false;
}
})
.catch(error => {
alert('❌ Ошибка при добавлении торрента: ' + error);
showToast('❌ Ошибка при добавлении торрента: ' + error, 'error');
button.textContent = originalText;
button.disabled = false;
});

273
app/test_app.py Normal file
View file

@ -0,0 +1,273 @@
#!/usr/bin/env python3
"""
Тесты для app.py изолированные, без внешних зависимостей.
Запуск: python3 -m unittest test_app -v
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import unittest
from app import (
extract_hash_from_result,
generate_clean_magnet,
parse_size_to_bytes,
parse_size,
extract_resolution,
extract_quality,
generate_search_variants,
score_torrent,
normalize_search_term,
is_movie_torrent,
)
class TestExtractHash(unittest.TestCase):
def test_direct_hash_field(self):
result = {"Hash": "08ada5a7a6183aae1e09d831df6748d566095a10"}
self.assertEqual(extract_hash_from_result(result), "08ADA5A7A6183AAE1E09D831DF6748D566095A10")
def test_hash_from_magnet(self):
result = {
"Hash": "",
"Magnet": "magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=test"
}
self.assertEqual(extract_hash_from_result(result), "08ADA5A7A6183AAE1E09D831DF6748D566095A10")
def test_hash_from_info_hash(self):
result = {"InfoHash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"}
self.assertEqual(
extract_hash_from_result(result),
"A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0"
)
def test_empty_result(self):
self.assertEqual(extract_hash_from_result({}), "")
self.assertEqual(extract_hash_from_result({"Hash": ""}), "")
def test_hash_from_torrent_url_no_btih(self):
result = {"Torrent": "https://rutracker.org/forum/dl.php?t=123456"}
self.assertEqual(extract_hash_from_result(result), "")
def test_case_insensitive_hash(self):
result = {"Hash": "08ada5a7a6183aae1e09d831df6748d566095a10"}
self.assertEqual(extract_hash_from_result(result), "08ADA5A7A6183AAE1E09D831DF6748D566095A10")
def test_id_field_as_hash(self):
result = {"Id": "08ada5a7a6183aae1e09d831df6748d566095a10"}
self.assertEqual(extract_hash_from_result(result), "08ADA5A7A6183AAE1E09D831DF6748D566095A10")
def test_non_hash_id_field(self):
result = {"Id": "12345", "Hash": ""}
self.assertEqual(extract_hash_from_result(result), "")
def test_hash_from_lowercase_field(self):
result = {"hash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"}
self.assertEqual(
extract_hash_from_result(result),
"A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0"
)
class TestGenerateCleanMagnet(unittest.TestCase):
def test_basic_magnet(self):
magnet = generate_clean_magnet("08ada5a7a6183aae1e09d831df6748d566095a10", "Test Movie")
self.assertTrue(magnet.startswith("magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10"))
self.assertIn("&dn=Test%20Movie", magnet)
self.assertIn("&tr=udp://tracker.opentrackr.org:1337/announce", magnet)
self.assertIn("&tr=udp://tracker.openbittorrent.com:6969/announce", magnet)
def test_empty_hash(self):
self.assertEqual(generate_clean_magnet(""), "")
self.assertEqual(generate_clean_magnet(None), "")
def test_no_title(self):
magnet = generate_clean_magnet("08ada5a7a6183aae1e09d831df6748d566095a10")
self.assertTrue(magnet.startswith("magnet:?xt=urn:btih:"))
self.assertNotIn("&dn=", magnet)
def test_cyrillic_title(self):
magnet = generate_clean_magnet("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", "Терминатор 2")
self.assertIn("&dn=%D0%A2%D0%B5%D1%80%D0%BC%D0%B8%D0%BD%D0%B0%D1%82%D0%BE%D1%80%202", magnet)
def test_has_trackers(self):
magnet = generate_clean_magnet("a" * 40)
expected_trackers = [
"tracker.opentrackr.org",
"open.stealth.si",
"tracker.openbittorrent.com",
"tracker.tamersunion.org",
"exodus.desync.com",
"tracker.moeking.me",
]
for tracker in expected_trackers:
self.assertIn(tracker, magnet, f"Missing tracker: {tracker}")
def test_no_dead_trackers(self):
magnet = generate_clean_magnet("a" * 40)
self.assertNotIn("coppersurfer.tk", magnet)
self.assertNotIn("leechers-paradise.org", magnet)
class TestParseSize(unittest.TestCase):
def test_gb(self):
bytes_size, readable = parse_size("25.3 GB")
self.assertEqual(bytes_size, int(25.3 * 1024**3))
self.assertEqual(readable, "25.3 GB")
def test_mb(self):
bytes_size, readable = parse_size("6.2 MB")
self.assertEqual(bytes_size, int(6.2 * 1024**2))
def test_kb(self):
bytes_size, readable = parse_size("512 KB")
self.assertEqual(bytes_size, 512 * 1024)
def test_empty(self):
bytes_size, readable = parse_size("")
self.assertEqual(bytes_size, 0)
self.assertEqual(readable, "Неизвестно")
def test_none(self):
bytes_size, readable = parse_size(None)
self.assertEqual(bytes_size, 0)
self.assertEqual(readable, "Неизвестно")
class TestParseSizeToBytes(unittest.TestCase):
def test_gb(self):
result = parse_size_to_bytes("25.3 GB")
self.assertAlmostEqual(result, 25.3 * 1024**3, delta=1)
def test_mb(self):
result = parse_size_to_bytes("500 MB")
self.assertEqual(result, 500 * 1024**2)
def test_empty(self):
self.assertEqual(parse_size_to_bytes(""), 0)
class TestExtractResolution(unittest.TestCase):
def test_2160p(self):
self.assertEqual(extract_resolution("Movie 2024 UHD 2160p BluRay"), "2160p")
def test_4k(self):
self.assertEqual(extract_resolution("Movie 2024 4K HDR"), "2160p")
def test_1080p(self):
self.assertEqual(extract_resolution("Movie 2024 1080p WEB-DL"), "1080p")
def test_720p(self):
self.assertEqual(extract_resolution("Movie 2024 720p HDTV"), "720p")
def test_unknown(self):
self.assertEqual(extract_resolution("Movie Unknown Quality"), "Неизвестно")
class TestExtractQuality(unittest.TestCase):
def test_bluray(self):
self.assertEqual(extract_quality("Movie 2024 Bluray 1080p"), "BluRay")
def test_bdrip(self):
self.assertEqual(extract_quality("Movie 2024 BDRip 720p"), "BluRay")
def test_webdl(self):
self.assertEqual(extract_quality("Movie 2024 WEB-DL 1080p"), "WEB-DL")
def test_webrip(self):
self.assertEqual(extract_quality("Movie 2024 WEBRip 720p"), "WEBRip")
def test_hdtv(self):
self.assertEqual(extract_quality("Movie 2024 HDTV 720p"), "HDTV")
def test_unknown(self):
self.assertEqual(extract_quality("Movie 2024 Some Quality"), "Unknown")
class TestNormalizeSearchTerm(unittest.TestCase):
def test_basic(self):
self.assertEqual(normalize_search_term("The Terminator"), "Terminator")
def test_with_articles(self):
self.assertEqual(normalize_search_term("The Matrix Revolutions"), "Matrix.Revolutions")
def test_empty(self):
self.assertEqual(normalize_search_term(""), "")
self.assertEqual(normalize_search_term(None), "")
def test_spaces_to_dots(self):
self.assertEqual(normalize_search_term("Harry Potter"), "Harry.Potter")
class TestGenerateSearchVariants(unittest.TestCase):
def test_basic(self):
variants = generate_search_variants("Terminator", "The Terminator", "1984")
self.assertIn("Terminator", variants)
self.assertIn("The Terminator", variants)
self.assertIn("Terminator 1984", variants)
def test_terminator_special(self):
variants = generate_search_variants("Терминатор 2", "Terminator 2", "1991")
self.assertIn("T2", variants)
self.assertIn("T2.Judgment.Day", variants)
def test_no_duplicates(self):
variants = generate_search_variants("Terminator", "Terminator", "1984")
self.assertEqual(len(variants), len(set(variants)))
class TestScoreTorrent(unittest.TestCase):
def test_high_score_exact_match(self):
torrent = {
"title": "The Terminator 1984 1080p BluRay",
"quality": "BluRay",
"resolution": "1080p",
"seeds": 150,
}
score = score_torrent(torrent, "Terminator", "The Terminator", "1984")
self.assertGreater(score, 0.8)
def test_low_score_no_match(self):
torrent = {
"title": "Some Other Movie 2023 720p WEB-DL",
"quality": "WEB-DL",
"resolution": "720p",
"seeds": 5,
}
score = score_torrent(torrent, "Terminator", "The Terminator", "1984")
self.assertAlmostEqual(score, 0.3)
def test_score_capped_at_one(self):
torrent = {
"title": "The Terminator 1984 2160p BluRay",
"quality": "BluRay",
"resolution": "2160p",
"seeds": 200,
}
score = score_torrent(torrent, "The Terminator", "The Terminator", "1984")
self.assertLessEqual(score, 1.0)
class TestIsMovieTorrent(unittest.TestCase):
def test_is_movie(self):
self.assertTrue(is_movie_torrent("The Terminator 1984 BluRay", "Terminator", "The Terminator"))
def test_not_movie_excluded_keyword(self):
self.assertFalse(is_movie_torrent("The Game 1997 1080p BluRay", "The Game", ""))
def test_not_movie_unknown_title(self):
self.assertFalse(is_movie_torrent("Some Other Movie 2023 720p", "Terminator", "The Terminator"))
if __name__ == "__main__":
unittest.main()

263
deploy.sh
View file

@ -1,263 +0,0 @@
#!/bin/bash
# 🚀 Скрипт развертывания searchTorrentDownl на новом компьютере
# Автор: AI Assistant
# Версия: 1.0
set -e # Остановка при ошибке
echo "🎬 searchTorrentDownl - Скрипт развертывания"
echo "=============================================="
echo ""
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Функция для вывода сообщений
log_info() {
echo -e "${BLUE} $1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
# Проверка операционной системы
check_os() {
log_info "Проверка операционной системы..."
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
log_success "Linux обнаружен"
else
log_error "Этот скрипт предназначен для Linux. Обнаружена ОС: $OSTYPE"
exit 1
fi
}
# Проверка прав root
check_root() {
if [[ $EUID -eq 0 ]]; then
log_warning "Скрипт запущен от root. Рекомендуется запускать от обычного пользователя."
read -p "Продолжить? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
}
# Обновление системы
update_system() {
log_info "Обновление системы..."
sudo apt update && sudo apt upgrade -y
log_success "Система обновлена"
}
# Установка Docker
install_docker() {
log_info "Проверка Docker..."
if command -v docker &> /dev/null; then
log_success "Docker уже установлен"
else
log_info "Установка Docker..."
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
rm get-docker.sh
log_success "Docker установлен"
fi
# Проверка Docker Compose
if command -v docker-compose &> /dev/null; then
log_success "Docker Compose уже установлен"
else
log_info "Установка Docker Compose..."
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
log_success "Docker Compose установлен"
fi
}
# Установка qBittorrent
install_qbittorrent() {
log_info "Проверка qBittorrent..."
if command -v qbittorrent-nox &> /dev/null; then
log_success "qBittorrent уже установлен"
else
log_info "Установка qBittorrent-nox..."
sudo apt install -y qbittorrent-nox
log_success "qBittorrent установлен"
fi
}
# Настройка qBittorrent
setup_qbittorrent() {
log_info "Настройка qBittorrent..."
# Создание пользователя qbittorrent если не существует
if ! id "qbittorrent" &>/dev/null; then
sudo useradd -r -s /bin/false qbittorrent
fi
# Создание директории для конфигурации
sudo mkdir -p /home/qbittorrent/.config/qBittorrent
sudo chown -R qbittorrent:qbittorrent /home/qbittorrent
# Создание systemd сервиса
sudo tee /etc/systemd/system/qbittorrent.service > /dev/null <<EOF
[Unit]
Description=qBittorrent-nox
After=network.target
[Service]
Type=simple
User=qbittorrent
Group=qbittorrent
ExecStart=/usr/bin/qbittorrent-nox --webui-port=8080
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
# Перезагрузка systemd и запуск сервиса
sudo systemctl daemon-reload
sudo systemctl enable qbittorrent
sudo systemctl start qbittorrent
log_success "qBittorrent настроен и запущен"
log_info "Веб-интерфейс: http://localhost:8080"
log_info "Логин по умолчанию: admin / admin"
}
# Создание Docker сети
create_docker_network() {
log_info "Создание Docker сети..."
if docker network ls | grep -q "torrentvideo_default"; then
log_success "Сеть torrentvideo_default уже существует"
else
docker network create torrentvideo_default
log_success "Сеть torrentvideo_default создана"
fi
}
# Клонирование репозитория
clone_repository() {
if [ -f "app.py" ]; then
log_success "Проект уже находится в текущей директории"
else
log_error "Проект не найден в текущей директории"
log_info "Пожалуйста, клонируйте репозиторий:"
log_info "git clone <repository-url>"
log_info "cd searchTorrentDownl"
exit 1
fi
}
# Настройка переменных окружения
setup_environment() {
log_info "Настройка переменных окружения..."
if [ ! -f ".env" ]; then
log_info "Создание файла .env..."
cat > .env <<EOF
# TMDB API Key (получите на https://www.themoviedb.org/settings/api)
TMDB_API_KEY=your_tmdb_api_key_here
# Telegram Bot Token (получите у @BotFather)
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
# qBittorrent настройки
QBITTORRENT_USERNAME=admin
QBITTORRENT_PASSWORD=admin
QBITTORRENT_HOST=host.docker.internal
QBITTORRENT_PORT=8080
# TorAPI настройки
TORRENT_SEARCH_URL=http://host.docker.internal:8443
TORRENT_ADD_URL=http://host.docker.internal:8088
EOF
log_warning "Файл .env создан. Пожалуйста, отредактируйте его с вашими настройками!"
else
log_success "Файл .env уже существует"
fi
}
# Запуск сервисов
start_services() {
log_info "Запуск сервисов..."
# Остановка существующих контейнеров
docker compose down 2>/dev/null || true
# Сборка и запуск
docker compose up -d --build
log_success "Сервисы запущены"
}
# Проверка статуса
check_status() {
log_info "Проверка статуса сервисов..."
echo ""
echo "📊 Статус контейнеров:"
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
echo ""
echo "🌐 Доступные сервисы:"
echo " • Веб-интерфейс: http://localhost:8089"
echo " • qBittorrent: http://localhost:8080 (admin/admin)"
echo " • Telegram Bot: @your_bot_username (команда /start)"
echo ""
echo "📝 Следующие шаги:"
echo " 1. Откройте http://localhost:8080 и настройте qBittorrent"
echo " 2. Отредактируйте .env файл с вашими API ключами"
echo " 3. Перезапустите сервисы: docker compose restart"
echo " 4. Откройте http://localhost:8089 для тестирования"
}
# Основная функция
main() {
echo "🚀 Начинаем развертывание searchTorrentDownl..."
echo ""
check_os
check_root
update_system
install_docker
install_qbittorrent
setup_qbittorrent
create_docker_network
clone_repository
setup_environment
start_services
check_status
echo ""
log_success "🎉 Развертывание завершено успешно!"
echo ""
log_info "Для остановки сервисов: docker compose down"
log_info "Для просмотра логов: docker compose logs -f"
log_info "Для перезапуска: docker compose restart"
}
# Запуск
main "$@"

View file

@ -1,81 +0,0 @@
services:
movie-search:
build: .
container_name: movie-search
env_file:
- .env
environment:
- HOST=0.0.0.0
- PORT=8000
- TORAPI_URL=http://torrent-api:8000
- TORRENT_ADD_URL=http://host.docker.internal:8088
- QBITTORRENT_USERNAME=vrubelroman
- QBITTORRENT_PASSWORD=vrubel07
- QBITTORRENT_HOST=host.docker.internal
- QBITTORRENT_PORT=8082
ports:
- "0.0.0.0:8089:8000"
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- torrentvideo_default
- default
torapi-search:
image: lifailon/torapi:latest
container_name: TorAPI-Search
environment:
- USERNAME=
- PASSWORD=
ports:
- "8443:8443"
restart: unless-stopped
networks:
- torrentvideo_default
- default
torapi-qbittorrent:
image: lifailon/torapi:latest
container_name: TorAPI-qBittorrent
environment:
- USERNAME=vrubelroman
- PASSWORD=vrubel07
- PROXY_ADDRESS=host.docker.internal
- PROXY_PORT=8082
ports:
- "8444:8443"
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- torrentvideo_default
- default
telegram-bot:
build:
context: .
dockerfile: Dockerfile.telegram
container_name: telegram-bot-findFilms
env_file:
- .env
environment:
- TORRENT_ADD_URL=http://host.docker.internal:8088
- QBITTORRENT_USERNAME=vrubelroman
- QBITTORRENT_PASSWORD=vrubel07
- QBITTORRENT_HOST=host.docker.internal
- QBITTORRENT_PORT=8082
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- torrentvideo_default
- default
depends_on:
- movie-search
- torapi-search
- torapi-qbittorrent
networks:
torrentvideo_default:
external: true

View file

@ -1,29 +0,0 @@
# 🔧 Конфигурация searchTorrentDownl
# Скопируйте этот файл в .env и заполните своими данными
# 🎬 TMDB Proxy URL
# URL прокси-сервиса TMDB (работает на хосте без VPN)
# Если прокси на другом хосте, укажите его IP: http://<IP>:8001
# Если прокси на том же хосте: http://localhost:8001
TMDB_PROXY_URL=72.56.91.135:8001
# 🤖 Telegram Bot Token
# Получите у @BotFather в Telegram
TELEGRAM_BOT_TOKEN=7662650066:AAFgsfYJNYgpcSHaSe6fspsjqmhMkOBT1s4
# 🐳 qBittorrent настройки
QBITTORRENT_USERNAME=admin
QBITTORRENT_PASSWORD=admin
QBITTORRENT_HOST=host.docker.internal
QBITTORRENT_PORT=8082
# 🔍 TorAPI настройки
# URL TorAPI для поиска торрентов (работает на хосте с VPN)
# Если TorAPI на другом хосте, укажите его IP: http://<IP>:8443
# Если TorAPI на том же хосте: http://localhost:8443
TORRENT_SEARCH_URL=http://72.56.91.135:8443
TORRENT_ADD_URL=http://host.docker.internal:8088
# 🌐 Основное приложение
HOST=0.0.0.0
PORT=8000

1
searchFilms/.env Normal file
View file

@ -0,0 +1 @@
TMDB_API_KEY=6d58225585fb77af5945a964de41849f

5
searchFilms/.env.example Normal file
View file

@ -0,0 +1,5 @@
# Search Stack — NL-хост (72.56.91.135)
# 🎬 TMDB API Key
# Получить: https://www.themoviedb.org/settings/api
TMDB_API_KEY=ваш_кл...

View file

@ -0,0 +1,31 @@
services:
tmdb-proxy:
build: ./tmdb-proxy
container_name: search-tmdb-proxy
environment:
- TMDB_API_KEY=$TMDB_API_KEY
- PORT=8001
ports:
- "0.0.0.0:8001:8001"
restart: unless-stopped
dns:
- 8.8.8.8
- 8.8.4.4
networks:
- search-stack
torapi-search:
image: lifailon/torapi:latest
container_name: search-torapi
environment:
- USERNAME=
- PASSWORD=
ports:
- "0.0.0.0:8443:8443"
restart: unless-stopped
networks:
- search-stack
networks:
search-stack:
driver: bridge

View file

@ -8,6 +8,7 @@ import os
import logging
import httpx
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import Response
from fastapi.middleware.cors import CORSMiddleware
# Настройка логирования
@ -130,6 +131,34 @@ async def get_movie_details(
raise HTTPException(status_code=500, detail=f"Unexpected error: {str(e)}")
@app.get("/proxy-torrent")
async def proxy_torrent(url: str = Query(..., description="URL .torrent файла")):
"""Скачивает .torrent файл через NL-сервер (обходит DPI-блокировки)"""
async with httpx.AsyncClient(timeout=30.0) as client:
try:
logger.info(f"Proxying .torrent download: {url}")
response = await client.get(url, timeout=30.0, follow_redirects=True)
response.raise_for_status()
return Response(
content=response.content,
media_type=response.headers.get("content-type", "application/x-bittorrent"),
headers={
"Content-Disposition": f'attachment; filename="torrent.torrent"',
"Content-Length": str(len(response.content))
}
)
except httpx.HTTPStatusError as e:
logger.error(f".torrent proxy returned {e.response.status_code} for {url}")
raise HTTPException(
status_code=e.response.status_code,
detail=f"Failed to download .torrent (status {e.response.status_code})"
)
except httpx.RequestError as e:
logger.error(f".torrent proxy request failed for {url}: {e}")
raise HTTPException(
status_code=502,
detail=f"Cannot download .torrent: {str(e)}"
)
@app.get("/health")
async def health_check():
"""Проверка работоспособности сервиса"""

View file

@ -1,85 +0,0 @@
#!/bin/bash
# Скрипт для запуска всего проекта searchTorrentDownl
# Включает веб-приложение и Telegram бота
echo "🚀 Запуск проекта searchTorrentDownl..."
# Проверяем, что мы в правильной директории
if [ ! -f "app.py" ]; then
echo "❌ Ошибка: Запустите скрипт из директории проекта"
exit 1
fi
# Создаем виртуальное окружение если его нет
if [ ! -d "venv" ]; then
echo "📦 Создание виртуального окружения..."
python3 -m venv venv
fi
# Активируем виртуальное окружение
echo "🔧 Активация виртуального окружения..."
source venv/bin/activate
# Устанавливаем зависимости
echo "📥 Установка зависимостей..."
pip install -r requirements.txt
# Проверяем, что qBittorrent запущен
echo "🔍 Проверка qBittorrent..."
if ! curl -s http://localhost:8080/api/v2/app/version > /dev/null; then
echo "⚠️ qBittorrent не запущен. Запустите его командой:"
echo " sudo systemctl start qbittorrent"
echo " или"
echo " sudo -u qbittorrent /usr/bin/qbittorrent-nox --webui-port=8080"
echo ""
echo "🔧 Продолжаем без qBittorrent (поиск фильмов будет работать)..."
fi
# Запускаем основное приложение в фоне
echo "🌐 Запуск веб-приложения..."
python3 app.py &
APP_PID=$!
# Ждем немного, чтобы приложение запустилось
sleep 3
# Проверяем, что приложение запустилось
if ! curl -s http://localhost:8089/api/search/terminator > /dev/null; then
echo "❌ Ошибка запуска веб-приложения"
kill $APP_PID 2>/dev/null
exit 1
fi
echo "✅ Веб-приложение запущено на http://localhost:8089"
# Запускаем Telegram бота
echo "🤖 Запуск Telegram бота..."
python3 run_telegram_bot.py &
BOT_PID=$!
echo ""
echo "🎉 Проект успешно запущен!"
echo ""
echo "📱 Доступные интерфейсы:"
echo " • Веб-интерфейс: http://localhost:8089"
echo " • qBittorrent: http://localhost:8080 (admin/vrubel07)"
echo " • Telegram Bot: @your_bot_username (команда /start)"
echo ""
echo "🛑 Для остановки нажмите Ctrl+C"
# Функция для корректного завершения
cleanup() {
echo ""
echo "🛑 Остановка сервисов..."
kill $APP_PID 2>/dev/null
kill $BOT_PID 2>/dev/null
echo "✅ Все сервисы остановлены"
exit 0
}
# Перехватываем сигнал завершения
trap cleanup SIGINT SIGTERM
# Ждем завершения
wait

View file

@ -1,37 +0,0 @@
#!/bin/bash
# Скрипт для запуска всех сервисов findFilms
echo "🚀 Запуск всех сервисов findFilms..."
# Переходим в директорию проекта
cd /Users/admin/Documents/PROJECTS/TorrentFilm/findFilms
# Запускаем qBittorrent локально (если не запущен)
if ! pgrep -f "qbittorrent.*--webui-port=8082" > /dev/null; then
echo "📱 Запуск qBittorrent..."
/Applications/qBittorrent.app/Contents/MacOS/qbittorrent --webui-port=8082 --no-splash --confirm-legal-notice &
sleep 5
fi
# Запускаем Docker сервисы
echo "🐳 Запуск Docker сервисов..."
docker compose up -d
# Проверяем статус
echo "📊 Проверка статуса сервисов..."
sleep 5
echo ""
echo "🎉 Все сервисы запущены!"
echo ""
echo "📱 Доступные интерфейсы:"
echo " • Веб-интерфейс: http://localhost:8089"
echo " • qBittorrent: http://localhost:8082 (admin/vrubel07)"
echo " • Telegram Bot: @your_bot_username"
echo ""
echo "🔧 Управление:"
echo " • Остановить все: docker compose down"
echo " • Перезапустить: docker compose restart"
echo " • Логи: docker compose logs -f"

View file

@ -1,177 +0,0 @@
#!/bin/bash
# Скрипт для запуска всех сервисов findFilms на Ubuntu
# Устанавливает qBittorrent, настраивает systemd и запускает Docker контейнеры
set -e # Остановка при ошибке
echo "🚀 Запуск findFilms для Ubuntu"
echo "=============================================="
echo ""
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log_info() {
echo -e "${BLUE} $1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
# Проверка директории
if [ ! -f "app.py" ]; then
log_error "Запустите скрипт из директории проекта findFilms"
exit 1
fi
# Шаг 1: Проверка и установка Docker
log_info "Проверка Docker..."
if ! command -v docker &> /dev/null; then
log_warning "Docker не найден, установка Docker..."
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
rm get-docker.sh
log_success "Docker установлен"
log_warning "Перезапустите терминал или выполните: newgrp docker"
exit 0
else
log_success "Docker найден"
fi
# Проверка Docker Compose
if ! command -v docker compose &> /dev/null; then
log_warning "Docker Compose не найден, установка..."
sudo apt-get update
sudo apt-get install -y docker-compose-plugin
log_success "Docker Compose установлен"
fi
# Шаг 2: Установка qBittorrent
log_info "Проверка qBittorrent..."
if ! command -v qbittorrent-nox &> /dev/null; then
log_warning "qBittorrent не найден, установка..."
sudo apt-get update
sudo apt-get install -y qbittorrent-nox
log_success "qBittorrent установлен"
else
log_success "qBittorrent найден"
fi
# Шаг 3: Настройка systemd сервиса для qBittorrent
log_info "Настройка qBittorrent systemd сервиса..."
# Создание пользователя qbittorrent если не существует
if ! id "qbittorrent" &>/dev/null; then
sudo useradd -r -s /bin/false qbittorrent
log_info "Создан пользователь qbittorrent"
fi
# Создание systemd сервиса
sudo tee /etc/systemd/system/qbittorrent.service > /dev/null <<'EOF'
[Unit]
Description=qBittorrent-nox
After=network.target
[Service]
Type=simple
User=qbittorrent
Group=qbittorrent
ExecStart=/usr/bin/qbittorrent-nox --webui-port=8082
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
log_success "qBittorrent systemd сервис настроен"
# Перезагрузка systemd и запуск сервиса
sudo systemctl daemon-reload
sudo systemctl enable qbittorrent
sudo systemctl restart qbittorrent
log_success "qBittorrent запущен и настроен на автозапуск"
# Проверка: нужна ли настройка пароля в qBittorrent
sleep 3
echo ""
log_warning "⚠️ ВАЖНО: Настройка qBittorrent"
echo ""
echo "🔐 qBittorrent может использовать временные credentials."
echo "Для корректной работы всех сервисов необходимо установить:"
echo ""
echo " 1. Откройте: http://localhost:8082"
echo " 2. Войдите (используйте admin/admin если это первый запуск)"
echo " 3. Перейдите в: Tools → Options → Web UI"
echo " 4. Установите:"
echo " • Username: admin"
echo " • Password: vrubel07"
echo " 5. Нажмите 'Save' внизу страницы"
echo ""
echo "📝 Эти credentials необходимы для корректной работы всех сервисов"
echo ""
echo -n "Нажмите Enter когда закончите настройку qBittorrent..."
read
log_info "Веб-интерфейс: http://localhost:8082"
log_info "Логин: admin / vrubel07"
# Шаг 4: Создание Docker сети
log_info "Создание Docker сети..."
if ! docker network ls | grep -q "torrentvideo_default"; then
docker network create torrentvideo_default
log_success "Сеть torrentvideo_default создана"
else
log_success "Сеть torrentvideo_default уже существует"
fi
# Шаг 5: Остановка существующих контейнеров
log_info "Остановка существующих контейнеров..."
docker compose down 2>/dev/null || true
# Шаг 6: Запуск Docker сервисов
log_info "Запуск Docker сервисов..."
docker compose up -d --build
# Ждем немного для старта
sleep 5
# Проверка статуса
log_info "Проверка статуса сервисов..."
echo ""
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "(NAMES|movie-search|TorAPI|telegram-bot)" || true
echo ""
log_success "🎉 Все сервисы запущены!"
echo ""
echo "📱 Доступные интерфейсы:"
echo " • Веб-интерфейс: http://localhost:8089"
echo " • qBittorrent: http://localhost:8082 (admin/vrubel07)"
echo " • Telegram Bot: @your_bot_username"
echo ""
echo "🔧 Управление:"
echo " • Остановить Docker: docker compose down"
echo " • Остановить qBittorrent: sudo systemctl stop qbittorrent"
echo " • Перезапустить все: sudo systemctl restart qbittorrent && docker compose restart"
echo " • Логи Docker: docker compose logs -f"
echo " • Логи qBittorrent: sudo journalctl -u qbittorrent -f"
echo ""

View file

@ -1,22 +0,0 @@
#!/bin/bash
# Скрипт для остановки всех сервисов findFilms
echo "🛑 Остановка всех сервисов findFilms..."
# Переходим в директорию проекта
cd /Users/admin/Documents/PROJECTS/TorrentFilm/findFilms
# Останавливаем Docker сервисы
echo "🐳 Остановка Docker сервисов..."
docker compose down
# Останавливаем qBittorrent
echo "📱 Остановка qBittorrent..."
pkill -f "qbittorrent.*--webui-port=8082"
echo ""
echo "✅ Все сервисы остановлены!"
echo ""
echo "🔧 Для запуска используйте: ./start_all_services.sh"

View file

@ -1,25 +0,0 @@
# TorAPI Proxy Service
Прокси-сервис для TorAPI (Torrent Search API), который работает на хосте с VPN.
## Запуск
```bash
docker-compose up -d
```
Сервис будет доступен на порту `8443`.
## Использование
Основной сервис должен обращаться к этому прокси по адресу:
- Если на другом хосте: `http://<IP_ХОСТА>:8443`
- Если на том же хосте: `http://localhost:8443`
## API Endpoints
TorAPI предоставляет следующие эндпоинты:
- `GET /api/provider/list` - список доступных провайдеров
- `GET /api/search/title/{provider}?query=<запрос>` - поиск по названию
- `GET /api/search/id/{provider}?query=<id>` - поиск по ID

View file

@ -1,14 +0,0 @@
services:
torapi-search:
image: lifailon/torapi:latest
container_name: torapi-search-proxy
environment:
- USERNAME=
- PASSWORD=
ports:
- "0.0.0.0:8443:8443"
restart: unless-stopped
dns:
- 8.8.8.8
- 8.8.4.4