diff --git a/PROJECT_SUMMARY.md b/PROJECT_SUMMARY.md index 57b2143..2d278b7 100644 --- a/PROJECT_SUMMARY.md +++ b/PROJECT_SUMMARY.md @@ -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 шаблоны ``` ## 🎯 Ключевые особенности diff --git a/README.md b/README.md index d6559a7..0c41586 100644 --- a/README.md +++ b/README.md @@ -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 -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 < 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 - -# Пересборка контейнеров -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 через интернет. --- -**Создано с ❤️ для удобного поиска и загрузки фильмов** \ No newline at end of file +## Архитектура + +``` +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 / ... +``` diff --git a/app/.env b/app/.env new file mode 100644 index 0000000..2dc8d8a --- /dev/null +++ b/app/.env @@ -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 diff --git a/app/.env.example b/app/.env.example new file mode 100644 index 0000000..55d604b --- /dev/null +++ b/app/.env.example @@ -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=ваш_па... diff --git a/Dockerfile b/app/Dockerfile similarity index 100% rename from Dockerfile rename to app/Dockerfile diff --git a/Dockerfile.telegram b/app/Dockerfile.telegram similarity index 100% rename from Dockerfile.telegram rename to app/Dockerfile.telegram diff --git a/MANAGEMENT.md b/app/MANAGEMENT.md similarity index 100% rename from MANAGEMENT.md rename to app/MANAGEMENT.md diff --git a/SAMBA_ACCESS.md b/app/SAMBA_ACCESS.md similarity index 100% rename from SAMBA_ACCESS.md rename to app/SAMBA_ACCESS.md diff --git a/TELEGRAM_BOT_README.md b/app/TELEGRAM_BOT_README.md similarity index 100% rename from TELEGRAM_BOT_README.md rename to app/TELEGRAM_BOT_README.md diff --git a/UBUNTU_DEPLOYMENT.md b/app/UBUNTU_DEPLOYMENT.md similarity index 100% rename from UBUNTU_DEPLOYMENT.md rename to app/UBUNTU_DEPLOYMENT.md diff --git a/app.py b/app/app.py similarity index 75% rename from app.py rename to app/app.py index cfa2d4d..4e3225b 100644 --- a/app.py +++ b/app/app.py @@ -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() diff --git a/app/docker-compose.yml b/app/docker-compose.yml new file mode 100644 index 0000000..c117659 --- /dev/null +++ b/app/docker-compose.yml @@ -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 diff --git a/requirements.txt b/app/requirements.txt similarity index 100% rename from requirements.txt rename to app/requirements.txt diff --git a/run_telegram_bot.py b/app/run_telegram_bot.py similarity index 100% rename from run_telegram_bot.py rename to app/run_telegram_bot.py diff --git a/telegram_bot.py b/app/telegram_bot.py similarity index 100% rename from telegram_bot.py rename to app/telegram_bot.py diff --git a/templates/error.html b/app/templates/error.html similarity index 100% rename from templates/error.html rename to app/templates/error.html diff --git a/templates/index.html b/app/templates/index.html similarity index 100% rename from templates/index.html rename to app/templates/index.html diff --git a/templates/results.html b/app/templates/results.html similarity index 100% rename from templates/results.html rename to app/templates/results.html diff --git a/templates/torrents.html b/app/templates/torrents.html similarity index 53% rename from templates/torrents.html rename to app/templates/torrents.html index d1afc0a..6247203 100644 --- a/templates/torrents.html +++ b/app/templates/torrents.html @@ -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; }
← Назад к поиску - +

🔍 Торренты для: "{{ movie_title }}"

- +
Фильм: {{ movie_title }} {% if year %}
Год: {{ year }} {% endif %}
- + {% if torrents %}
{% for torrent in torrents %}
{{ torrent.title }}
- +
Размер
{{ torrent.size_readable }}
-
Разрешение
{{ torrent.resolution }}
-
Качество
{{ torrent.quality }}
-
Сиды
{{ torrent.seeds }}
-
Пиры
{{ torrent.peers }}
- {% if torrent.category %}
Категория
{{ torrent.category }}
{% endif %} - {% if torrent.date %}
Дата
{{ torrent.date }}
{% endif %} - {% if torrent.provider %}
Провайдер
@@ -211,11 +212,26 @@
{% endif %}
- +
- Magnet - Скачать .torrent - + {% if torrent.magnet and torrent.magnet.startswith('magnet:') %} + 🧲 Magnet + + {% else %} + + {% endif %} + + {% if torrent.download_url and torrent.download_url.startswith('http') %} + 💾 Скачать .torrent + {% else %} + + {% endif %} + + {% if torrent.magnet and torrent.magnet.startswith('magnet:') %} + + {% else %} + + {% endif %}
{% endfor %} @@ -227,18 +243,68 @@ {% endif %}
+
+