1227 lines
57 KiB
Python
1227 lines
57 KiB
Python
import os
|
||
import re
|
||
import asyncio
|
||
import httpx
|
||
import requests
|
||
from bs4 import BeautifulSoup
|
||
from fastapi import FastAPI, Request, Form, HTTPException
|
||
from fastapi.responses import HTMLResponse
|
||
from fastapi.templating import Jinja2Templates
|
||
from fastapi.staticfiles import StaticFiles
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
|
||
app = FastAPI(title="Movie Search API", version="1.0.0")
|
||
|
||
# Настройка CORS
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["*"], # В продакшене лучше указать конкретные домены
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
# Настройка шаблонов
|
||
templates = Jinja2Templates(directory="templates")
|
||
|
||
# URL прокси-сервиса TMDB
|
||
TMDB_PROXY_URL = os.getenv("TMDB_PROXY_URL", "http://localhost:8001")
|
||
|
||
# URL torAPI
|
||
TORAPI_URL = os.getenv("TORAPI_URL", "http://localhost:8088")
|
||
|
||
# URL Torrent Search API
|
||
TORRENT_SEARCH_URL = os.getenv("TORRENT_SEARCH_URL", "http://localhost:8443")
|
||
TORRENT_ADD_URL = os.getenv("TORRENT_ADD_URL", "http://localhost:8444")
|
||
|
||
async def search_movies(query: str) -> dict:
|
||
"""Поиск фильмов через TMDB Proxy"""
|
||
async with httpx.AsyncClient() as client:
|
||
try:
|
||
response = await client.get(
|
||
f"{TMDB_PROXY_URL}/search/movie",
|
||
params={
|
||
"query": query,
|
||
"language": "ru-RU",
|
||
"include_adult": False
|
||
}
|
||
)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
except httpx.HTTPError as e:
|
||
raise HTTPException(status_code=500, detail=f"TMDB Proxy error: {str(e)}")
|
||
|
||
async def get_movie_details(movie_id: int) -> dict:
|
||
"""Получение детальной информации о фильме из TMDB через прокси"""
|
||
async with httpx.AsyncClient() as client:
|
||
try:
|
||
response = await client.get(
|
||
f"{TMDB_PROXY_URL}/movie/{movie_id}",
|
||
params={
|
||
"language": "ru-RU",
|
||
"append_to_response": "external_ids"
|
||
}
|
||
)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
except httpx.HTTPError as e:
|
||
raise HTTPException(status_code=500, detail=f"TMDB Proxy error: {str(e)}")
|
||
|
||
def parse_size(size_str: str) -> tuple:
|
||
"""Парсинг размера файла и возврат в байтах и читаемом виде"""
|
||
if not size_str:
|
||
return 0, "Неизвестно"
|
||
|
||
size_str = size_str.upper().strip()
|
||
multipliers = {
|
||
'KB': 1024,
|
||
'MB': 1024**2,
|
||
'GB': 1024**3,
|
||
'TB': 1024**4
|
||
}
|
||
|
||
for unit, multiplier in multipliers.items():
|
||
if unit in size_str:
|
||
try:
|
||
number = float(re.findall(r'[\d.]+', size_str)[0])
|
||
bytes_size = int(number * multiplier)
|
||
return bytes_size, size_str
|
||
except (ValueError, IndexError):
|
||
pass
|
||
|
||
return 0, size_str
|
||
|
||
def extract_resolution(title: str) -> str:
|
||
"""Извлечение разрешения из названия торрента"""
|
||
title_upper = title.upper()
|
||
|
||
# Поиск разрешений
|
||
resolutions = ['2160P', '4K', '1080P', '720P', '480P', '360P']
|
||
for res in resolutions:
|
||
if res in title_upper:
|
||
if res == '4K':
|
||
return '2160p'
|
||
return res.lower()
|
||
|
||
# Поиск по паттернам
|
||
if re.search(r'\b(2160|4k)\b', title_upper):
|
||
return '2160p'
|
||
elif re.search(r'\b1080\b', title_upper):
|
||
return '1080p'
|
||
elif re.search(r'\b720\b', title_upper):
|
||
return '720p'
|
||
elif re.search(r'\b480\b', title_upper):
|
||
return '480p'
|
||
|
||
return 'Неизвестно'
|
||
|
||
def normalize_search_term(term: str) -> str:
|
||
"""Нормализация поискового термина для сцены"""
|
||
if not term:
|
||
return ""
|
||
|
||
# Убираем артикли
|
||
term = re.sub(r'\b(the|a|an)\b', '', term, flags=re.IGNORECASE)
|
||
|
||
# Заменяем пробелы на точки и дефисы
|
||
term = re.sub(r'\s+', '.', term.strip())
|
||
|
||
# Убираем лишние точки
|
||
term = re.sub(r'\.+', '.', term)
|
||
|
||
# Убираем точки в начале и конце
|
||
term = term.strip('.')
|
||
|
||
return term
|
||
|
||
def generate_search_variants(title: str, original_title: str = None, year: str = None) -> list:
|
||
"""Генерация вариантов названий для поиска"""
|
||
variants = []
|
||
|
||
# Базовые варианты
|
||
variants.extend([title, original_title] if original_title else [title])
|
||
|
||
# Варианты с годом
|
||
if year:
|
||
variants.extend([f"{title} {year}", f"{original_title} {year}"] if original_title else [f"{title} {year}"])
|
||
|
||
# Нормализованные варианты
|
||
normalized_variants = [normalize_search_term(v) for v in variants if v]
|
||
variants.extend(normalized_variants)
|
||
|
||
# Специальные варианты для известных фильмов
|
||
title_lower = title.lower()
|
||
if 'терминатор' in title_lower or 'terminator' in title_lower:
|
||
variants.extend(['T2', 'T2.Judgment.Day', 'T2.Judgement.Day'])
|
||
if '2' in title_lower or 'второй' in title_lower:
|
||
variants.extend(['Terminator.2', 'Терминатор.2'])
|
||
|
||
# Убираем дубликаты и пустые значения
|
||
return list(set([v for v in variants if v and v.strip()]))
|
||
|
||
def score_torrent(torrent: dict, movie_title: str, original_title: str = None, year: str = None) -> float:
|
||
"""Скоринг торрента по релевантности"""
|
||
score = 0.0
|
||
torrent_name = torrent.get('title', '').lower()
|
||
movie_title_lower = movie_title.lower()
|
||
original_title_lower = original_title.lower() if original_title else ""
|
||
|
||
# Скоринг по названию (0-1)
|
||
if movie_title_lower in torrent_name:
|
||
score += 0.8
|
||
if original_title_lower and original_title_lower in torrent_name:
|
||
score += 0.9
|
||
|
||
# Скоринг по году (0-0.5)
|
||
if year and year in torrent_name:
|
||
score += 0.5
|
||
|
||
# Скоринг по качеству (0-0.3)
|
||
quality = torrent.get('quality', '').lower()
|
||
if 'bluray' in quality or 'remux' in quality:
|
||
score += 0.3
|
||
elif 'web-dl' in quality or 'webdl' in quality:
|
||
score += 0.2
|
||
elif 'hdtv' in quality:
|
||
score += 0.1
|
||
|
||
# Скоринг по разрешению (0-0.2)
|
||
resolution = torrent.get('resolution', '').lower()
|
||
if '2160p' in resolution or '4k' in resolution:
|
||
score += 0.2
|
||
elif '1080p' in resolution:
|
||
score += 0.15
|
||
elif '720p' in resolution:
|
||
score += 0.1
|
||
|
||
# Скоринг по сидам (0-0.1)
|
||
seeds = torrent.get('seeds', 0)
|
||
if seeds > 100:
|
||
score += 0.1
|
||
elif seeds > 50:
|
||
score += 0.05
|
||
|
||
return min(score, 1.0) # Ограничиваем максимум 1.0
|
||
|
||
def generate_demo_torrents(movie_title: str, year: str = None, original_title: str = None) -> list:
|
||
"""Генерация демо-данных для тестирования (когда API не работает)"""
|
||
torrents = []
|
||
|
||
# Создаем реалистичные варианты торрентов
|
||
quality_variants = [
|
||
{"quality": "BluRay", "resolution": "1080p", "size": "8.5 GB", "seeds": 150, "peers": 25},
|
||
{"quality": "WEB-DL", "resolution": "1080p", "size": "6.2 GB", "seeds": 120, "peers": 20},
|
||
{"quality": "BluRay", "resolution": "2160p", "size": "25.3 GB", "seeds": 200, "peers": 35},
|
||
{"quality": "WEBRip", "resolution": "720p", "size": "3.8 GB", "seeds": 80, "peers": 15},
|
||
{"quality": "HDTV", "resolution": "720p", "size": "2.1 GB", "seeds": 50, "peers": 8}
|
||
]
|
||
|
||
# Генерируем варианты названий
|
||
title_variants = [movie_title]
|
||
if original_title and original_title != movie_title:
|
||
title_variants.append(original_title)
|
||
|
||
for i, variant in enumerate(quality_variants):
|
||
# Выбираем случайное название
|
||
base_title = title_variants[i % len(title_variants)]
|
||
|
||
# Создаем реалистичное название торрента
|
||
torrent_title = f"{base_title} ({year or '2020'}) - {variant['resolution']} - {variant['quality']}"
|
||
|
||
# Создаем fake magnet hash
|
||
fake_hash = f"08ada5a7a6183aae1e09d831df6748d566095a10{i:02d}"
|
||
|
||
torrent = {
|
||
"title": torrent_title,
|
||
"size_bytes": parse_size_to_bytes(variant['size']),
|
||
"size_readable": variant['size'],
|
||
"resolution": variant['resolution'],
|
||
"quality": variant['quality'],
|
||
"seeds": variant['seeds'],
|
||
"peers": variant['peers'],
|
||
"magnet": f"magnet:?xt=urn:btih:{fake_hash}&dn={base_title.replace(' ', '+')}&tr=udp://tracker.openbittorrent.com:80",
|
||
"download_url": f"https://example.com/torrent/{fake_hash}/",
|
||
"category": "Фильмы",
|
||
"date": "2025-01-05",
|
||
"provider": "Demo",
|
||
"relevance_score": 0.9 - (i * 0.1) # Убывающий скор
|
||
}
|
||
torrents.append(torrent)
|
||
|
||
return torrents
|
||
|
||
async def search_torrents(movie_title: str, year: str = None, original_title: str = None, genres: list = None, imdb_id: str = None) -> list:
|
||
"""Поиск торрентов для фильма через Torrent Search API"""
|
||
torrents = []
|
||
|
||
try:
|
||
# Генерируем варианты поисковых запросов
|
||
search_queries = generate_search_variants(movie_title, original_title, year)
|
||
print(f"Generated search variants: {search_queries}")
|
||
|
||
# Добавляем IMDB ID если есть
|
||
if imdb_id:
|
||
search_queries.append(imdb_id)
|
||
|
||
# Получаем список доступных провайдеров
|
||
async with httpx.AsyncClient() as client:
|
||
providers_response = await client.get(f"{TORRENT_SEARCH_URL}/api/provider/list")
|
||
if providers_response.status_code != 200:
|
||
print(f"Failed to get providers: {providers_response.status_code}")
|
||
return torrents
|
||
|
||
providers = providers_response.json()
|
||
print(f"Available providers: {[p['Provider'] for p in providers]}")
|
||
|
||
# Ищем торренты на всех доступных провайдерах
|
||
for provider in providers:
|
||
provider_name = provider['Provider'].lower()
|
||
|
||
try:
|
||
# Пробуем каждый вариант поискового запроса
|
||
for search_query in search_queries:
|
||
print(f"Searching '{search_query}' on {provider_name}")
|
||
|
||
# Поиск на конкретном провайдере - исправленный эндпоинт
|
||
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:
|
||
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}")
|
||
|
||
# Обрабатываем все результаты и скорим их
|
||
for result in results[:20]: # Берем больше результатов для скоринга
|
||
print(f"Processing torrent: {result['Name'][:100]}...")
|
||
# Логируем доступные поля для отладки
|
||
torrent_id = result.get('Id') or result.get('id') or result.get('ID') or result.get('Hash', '')[:8] or ''
|
||
if torrent_id:
|
||
print(f" Found ID: {torrent_id[:20]}...")
|
||
torrent = parse_torrent_result(result, movie_title, year)
|
||
if torrent:
|
||
# Если ID не был найден, пробуем использовать Hash как ID
|
||
if not torrent.get('id') or torrent.get('id') == '':
|
||
torrent_id_from_hash = result.get('Hash', '')
|
||
if torrent_id_from_hash:
|
||
torrent['id'] = torrent_id_from_hash
|
||
print(f" Using Hash as ID: {torrent_id_from_hash[:20]}...")
|
||
|
||
# Скорим торрент
|
||
score = score_torrent(torrent, movie_title, original_title, year)
|
||
torrent['relevance_score'] = score
|
||
size_mb = torrent.get('size_bytes', 0) / (1024 * 1024)
|
||
print(f"Torrent score: {score:.2f}, size: {size_mb:.1f}MB, seeds: {torrent.get('seeds', 0)}, ID: {torrent.get('id', 'None')[:20]}...")
|
||
|
||
# Добавляем только если скор больше 0.1 и размер больше 100MB
|
||
if score > 0.1 and torrent.get('size_bytes', 0) > 100 * 1024 * 1024:
|
||
# Проверяем, что ID есть, иначе используем Hash
|
||
if not torrent.get('id') or torrent.get('id') == '':
|
||
if result.get('Hash'):
|
||
torrent['id'] = result['Hash']
|
||
elif result.get('Url'):
|
||
# Пытаемся извлечь ID из URL
|
||
url_id_match = re.search(r'/(\d+)/?$', result.get('Url', ''))
|
||
if url_id_match:
|
||
torrent['id'] = url_id_match.group(1)
|
||
else:
|
||
torrent['id'] = result.get('Url', '')[-20:] # Используем последние 20 символов URL
|
||
torrents.append(torrent)
|
||
print(f" ✅ Added to results with ID: {torrent.get('id', 'None')[:20]}...")
|
||
else:
|
||
print(f" ❌ Filtered out (score: {score:.2f}, size: {size_mb:.1f}MB)")
|
||
|
||
# Если нашли достаточно результатов, переходим к следующему провайдеру
|
||
if len(torrents) >= 20:
|
||
break
|
||
|
||
except Exception as e:
|
||
print(f"Error searching on {provider_name}: {type(e).__name__}: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
continue
|
||
|
||
# Сортируем по количеству сидов и ограничиваем общее количество
|
||
torrents.sort(key=lambda x: x.get('seeds', 0), reverse=True)
|
||
return torrents[:30] # Возвращаем топ-30 результатов
|
||
|
||
except Exception as e:
|
||
print(f"Error searching torrents: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return torrents
|
||
|
||
async def search_torrent_by_id(torrent_id: str) -> dict:
|
||
"""Поиск торрента по ID через Torrent Search API"""
|
||
try:
|
||
async with httpx.AsyncClient() as client:
|
||
print(f"Searching torrent by ID: {torrent_id}")
|
||
|
||
# Получаем список доступных провайдеров
|
||
providers_response = await client.get(f"{TORRENT_SEARCH_URL}/api/provider/list")
|
||
if providers_response.status_code != 200:
|
||
print(f"Failed to get providers: {providers_response.status_code}")
|
||
return None
|
||
|
||
providers = providers_response.json()
|
||
print(f"Available providers: {[p['Provider'] for p in providers]}")
|
||
|
||
# Пробуем найти торрент на всех доступных провайдерах
|
||
for provider in providers:
|
||
provider_name = provider['Provider'].lower()
|
||
|
||
try:
|
||
print(f"Searching ID {torrent_id} on {provider_name}")
|
||
# Пробуем разные варианты API для поиска по ID
|
||
search_urls = [
|
||
f"{TORRENT_SEARCH_URL}/api/search/id/{provider_name}",
|
||
f"{TORRENT_SEARCH_URL}/api/search/{provider_name}",
|
||
]
|
||
|
||
response = None
|
||
results = None
|
||
|
||
for search_url in search_urls:
|
||
try:
|
||
response = await client.get(search_url, params={"query": torrent_id}, timeout=30.0)
|
||
if response.status_code == 200:
|
||
results = response.json()
|
||
print(f"Response from {provider_name} ({search_url}): {type(results)} - {len(results) if isinstance(results, list) else 'not a list'}")
|
||
break
|
||
except Exception as e:
|
||
print(f"Error trying {search_url}: {e}")
|
||
continue
|
||
|
||
if response and response.status_code == 200 and results:
|
||
if isinstance(results, list) and len(results) > 0:
|
||
# Берем первый результат
|
||
result = results[0]
|
||
# Используем Original_Name если Name пустое
|
||
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]}...")
|
||
|
||
if hash_value:
|
||
# Генерируем чистую magnet-ссылку с публичными трекерами
|
||
magnet = generate_clean_magnet(hash_value, torrent_title)
|
||
print(f"Generated clean magnet with public trackers: {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]}")
|
||
magnet = ""
|
||
|
||
# Парсим результат в стандартный формат
|
||
torrent = {
|
||
"title": torrent_name,
|
||
"url": result.get('Url', ''),
|
||
"hash": result.get('Hash', ''),
|
||
"magnet": magnet,
|
||
"torrent_url": result.get('Torrent', ''),
|
||
"imdb_url": result.get('IMDb_link', ''),
|
||
"kinopoisk_url": result.get('Kinopoisk_link', ''),
|
||
"poster": result.get('Poster', ''),
|
||
"year": result.get('Year', ''),
|
||
"release": result.get('Release', ''),
|
||
"type": result.get('Type', ''),
|
||
"duration": result.get('Duration', ''),
|
||
"audio": result.get('Audio', ''),
|
||
"director": result.get('Directer', ''),
|
||
"actors": result.get('Actors', ''),
|
||
"description": result.get('Description', ''),
|
||
"quality": result.get('Quality', ''),
|
||
"video": result.get('Video', ''),
|
||
"files": result.get('Files', []),
|
||
"provider": provider_name,
|
||
"id": torrent_id
|
||
}
|
||
return torrent
|
||
else:
|
||
print(f"No results found for ID {torrent_id} on {provider_name}")
|
||
else:
|
||
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}: {type(e).__name__}: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
continue
|
||
|
||
print(f"No results found for ID {torrent_id} on any provider")
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"Error searching torrent by ID: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return None
|
||
|
||
def is_movie_torrent(torrent_name: str, movie_title: str, original_title: str = None) -> bool:
|
||
"""Проверяет, является ли торрент фильмом"""
|
||
torrent_name_lower = torrent_name.lower()
|
||
movie_title_lower = movie_title.lower()
|
||
original_title_lower = original_title.lower() if original_title else ""
|
||
|
||
# Исключаем только явно не-фильмы
|
||
exclude_keywords = ['игра', 'game', 'музыка', 'music', 'программа', 'program', 'софт', 'software']
|
||
for keyword in exclude_keywords:
|
||
if keyword in torrent_name_lower:
|
||
print(f"Excluding due to keyword '{keyword}': {torrent_name[:100]}...")
|
||
return False
|
||
|
||
# Проверяем, что название содержит название фильма (русское или оригинальное)
|
||
contains_title = movie_title_lower in torrent_name_lower
|
||
if original_title_lower and not contains_title:
|
||
contains_title = original_title_lower in torrent_name_lower
|
||
|
||
print(f"Title '{movie_title_lower}' or '{original_title_lower}' in torrent: {contains_title}")
|
||
return contains_title
|
||
|
||
def parse_torrent_result(result: dict, movie_title: str, year: str = None) -> dict:
|
||
"""Парсит результат поиска торрентов"""
|
||
try:
|
||
# Извлекаем разрешение из названия
|
||
resolution = extract_resolution(result['Name'])
|
||
|
||
# Извлекаем качество из названия
|
||
quality = extract_quality(result['Name'])
|
||
|
||
# Парсим размер
|
||
size_bytes = parse_size_to_bytes(result['Size'])
|
||
|
||
# Создаем чистую magnet-ссылку с публичными трекерами
|
||
magnet = None
|
||
if 'Hash' in result and result['Hash']:
|
||
# Генерируем чистую magnet-ссылку из хэша с публичными трекерами
|
||
hash_value = result['Hash']
|
||
torrent_title = result['Name']
|
||
magnet = generate_clean_magnet(hash_value, torrent_title)
|
||
elif 'Magnet' in result and result['Magnet']:
|
||
# Если нет хэша, используем оригинальную magnet-ссылку
|
||
magnet = result['Magnet']
|
||
elif 'Torrent' in result and result['Torrent']:
|
||
# Используем URL на .torrent файл как fallback
|
||
magnet = result['Torrent']
|
||
|
||
# Определяем провайдера по URL
|
||
provider = 'Unknown'
|
||
torrent_url = result.get('Torrent', '')
|
||
if 'rutracker.org' in torrent_url:
|
||
provider = 'rutracker'
|
||
elif 'kinozal.tv' in torrent_url:
|
||
provider = 'kinozal'
|
||
elif 'rutor.info' in torrent_url:
|
||
provider = 'rutor'
|
||
elif 'nnmclub.to' in torrent_url:
|
||
provider = 'nonameclub'
|
||
|
||
# Извлекаем ID из разных возможных полей
|
||
torrent_id = (
|
||
result.get('Id') or
|
||
result.get('id') or
|
||
result.get('ID') or
|
||
result.get('Hash', '')[:8] or # Используем первые 8 символов хэша как fallback
|
||
''
|
||
)
|
||
|
||
# Если ID не найден, пытаемся извлечь из URL
|
||
if not torrent_id and result.get('Url'):
|
||
url_id_match = re.search(r'/(\d+)/?$', result.get('Url', ''))
|
||
if url_id_match:
|
||
torrent_id = url_id_match.group(1)
|
||
|
||
# Если все еще нет ID, используем Hash полностью
|
||
if not torrent_id:
|
||
torrent_id = result.get('Hash', '')
|
||
|
||
return {
|
||
"title": result['Name'],
|
||
"size_bytes": size_bytes,
|
||
"size_readable": result['Size'],
|
||
"resolution": resolution,
|
||
"quality": quality,
|
||
"seeds": int(result.get('Seeds', 0)),
|
||
"peers": int(result.get('Peers', 0)),
|
||
"magnet": magnet,
|
||
"download_url": result.get('Torrent', ''),
|
||
"category": result.get('Category', ''),
|
||
"date": result.get('Date', ''),
|
||
"provider": provider,
|
||
"id": torrent_id
|
||
}
|
||
except Exception as e:
|
||
print(f"Error parsing torrent result: {e}")
|
||
return None
|
||
|
||
def extract_quality(title: str) -> str:
|
||
"""Извлекает качество из названия торрента"""
|
||
title_upper = title.upper()
|
||
|
||
if 'BLURAY' in title_upper or 'BDRIP' in title_upper:
|
||
return 'BluRay'
|
||
elif 'WEB-DL' in title_upper or 'WEBDL' in title_upper:
|
||
return 'WEB-DL'
|
||
elif 'WEBRIP' in title_upper or 'WEB-RIP' in title_upper:
|
||
return 'WEBRip'
|
||
elif 'HDTV' in title_upper:
|
||
return 'HDTV'
|
||
elif 'DVDRIP' in title_upper or 'DVD-RIP' in title_upper:
|
||
return 'DVDRip'
|
||
else:
|
||
return 'Unknown'
|
||
|
||
def parse_size_to_bytes(size_str: str) -> int:
|
||
"""Конвертирует строку размера в байты"""
|
||
size_str = size_str.upper().strip()
|
||
multipliers = {
|
||
'KB': 1024,
|
||
'MB': 1024**2,
|
||
'GB': 1024**3,
|
||
'TB': 1024**4
|
||
}
|
||
|
||
for unit, multiplier in multipliers.items():
|
||
if unit in size_str:
|
||
try:
|
||
number = float(re.findall(r'[\d.]+', size_str)[0])
|
||
return int(number * multiplier)
|
||
except (ValueError, IndexError):
|
||
pass
|
||
|
||
return 0
|
||
|
||
def generate_clean_magnet(hash_value: str, title: str = None) -> str:
|
||
"""Генерирует чистую magnet-ссылку с публичными трекерами"""
|
||
if not hash_value:
|
||
return ""
|
||
|
||
# Проверенные рабочие трекеры (минимальный набор)
|
||
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"
|
||
]
|
||
|
||
# Создаем базовую magnet-ссылку с хэшем
|
||
magnet = f"magnet:?xt=urn:btih:{hash_value}"
|
||
|
||
# НЕ добавляем название файла - это может вызывать проблемы с кириллицей
|
||
# DHT сам найдет название по хэшу
|
||
|
||
# Добавляем публичные трекеры
|
||
for tracker in public_trackers:
|
||
magnet += f"&tr={tracker}"
|
||
|
||
return magnet
|
||
|
||
@app.get("/", response_class=HTMLResponse)
|
||
async def home(request: Request):
|
||
"""Главная страница с формой поиска"""
|
||
try:
|
||
print(f"Home page requested from {request.client.host}")
|
||
return HTMLResponse("""
|
||
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>🎬 Поиск фильмов и сериалов</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
backdrop-filter: blur(10px);
|
||
border-radius: 20px;
|
||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||
padding: 40px;
|
||
max-width: 600px;
|
||
width: 100%;
|
||
text-align: center;
|
||
}
|
||
|
||
.logo {
|
||
font-size: 4rem;
|
||
margin-bottom: 20px;
|
||
background: linear-gradient(45deg, #667eea, #764ba2);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
|
||
h1 {
|
||
color: #333;
|
||
font-size: 2.5rem;
|
||
margin-bottom: 10px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.subtitle {
|
||
color: #666;
|
||
font-size: 1.2rem;
|
||
margin-bottom: 40px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.search-form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.search-input {
|
||
padding: 15px 20px;
|
||
border: 2px solid #e1e5e9;
|
||
border-radius: 50px;
|
||
font-size: 1.1rem;
|
||
outline: none;
|
||
transition: all 0.3s ease;
|
||
background: #fff;
|
||
}
|
||
|
||
.search-input:focus {
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||
}
|
||
|
||
.search-button {
|
||
padding: 15px 30px;
|
||
background: linear-gradient(45deg, #667eea, #764ba2);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 50px;
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.search-button:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||
}
|
||
|
||
.search-button:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.features {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
gap: 20px;
|
||
margin-top: 40px;
|
||
}
|
||
|
||
.feature {
|
||
padding: 20px;
|
||
background: rgba(102, 126, 234, 0.1);
|
||
border-radius: 15px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.feature:hover {
|
||
transform: translateY(-5px);
|
||
background: rgba(102, 126, 234, 0.2);
|
||
}
|
||
|
||
.feature-icon {
|
||
font-size: 2rem;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.feature-title {
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.feature-desc {
|
||
font-size: 0.9rem;
|
||
color: #666;
|
||
}
|
||
|
||
.loading {
|
||
display: none;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.spinner {
|
||
border: 3px solid #f3f3f3;
|
||
border-top: 3px solid #667eea;
|
||
border-radius: 50%;
|
||
width: 30px;
|
||
height: 30px;
|
||
animation: spin 1s linear infinite;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.error {
|
||
color: #e74c3c;
|
||
background: #fdf2f2;
|
||
padding: 10px 15px;
|
||
border-radius: 10px;
|
||
margin-top: 20px;
|
||
display: none;
|
||
}
|
||
|
||
.success {
|
||
color: #27ae60;
|
||
background: #f0f9f0;
|
||
padding: 10px 15px;
|
||
border-radius: 10px;
|
||
margin-top: 20px;
|
||
display: none;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.container {
|
||
padding: 30px 20px;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 2rem;
|
||
}
|
||
|
||
.logo {
|
||
font-size: 3rem;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="logo">🎬</div>
|
||
<h1>Поиск фильмов и сериалов</h1>
|
||
<p class="subtitle">
|
||
Найдите любой фильм или сериал и скачайте его через торрент.<br>
|
||
Быстро, удобно и бесплатно!
|
||
</p>
|
||
|
||
<form class="search-form" action="/search" method="post" id="searchForm">
|
||
<input
|
||
type="text"
|
||
name="movie_title"
|
||
class="search-input"
|
||
placeholder="Введите название фильма или сериала..."
|
||
required
|
||
autocomplete="off"
|
||
>
|
||
<button type="submit" class="search-button">
|
||
🔍 Найти
|
||
</button>
|
||
</form>
|
||
|
||
<div class="loading" id="loading">
|
||
<div class="spinner"></div>
|
||
<p>Ищем фильмы...</p>
|
||
</div>
|
||
|
||
<div class="error" id="error"></div>
|
||
<div class="success" id="success"></div>
|
||
|
||
<div class="features">
|
||
<div class="feature">
|
||
<div class="feature-icon">🎯</div>
|
||
<div class="feature-title">Точный поиск</div>
|
||
<div class="feature-desc">Находим именно то, что вы ищете</div>
|
||
</div>
|
||
<div class="feature">
|
||
<div class="feature-icon">⚡</div>
|
||
<div class="feature-title">Быстро</div>
|
||
<div class="feature-desc">Результаты за секунды</div>
|
||
</div>
|
||
<div class="feature">
|
||
<div class="feature-icon">🆓</div>
|
||
<div class="feature-title">Бесплатно</div>
|
||
<div class="feature-desc">Полностью бесплатный сервис</div>
|
||
</div>
|
||
<div class="feature">
|
||
<div class="feature-icon">📱</div>
|
||
<div class="feature-title">Удобно</div>
|
||
<div class="feature-desc">Работает на всех устройствах</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.getElementById('searchForm').addEventListener('submit', function(e) {
|
||
const loading = document.getElementById('loading');
|
||
const error = document.getElementById('error');
|
||
const success = document.getElementById('success');
|
||
|
||
// Скрываем предыдущие сообщения
|
||
error.style.display = 'none';
|
||
success.style.display = 'none';
|
||
|
||
// Показываем загрузку
|
||
loading.style.display = 'block';
|
||
|
||
// Проверяем, что поле не пустое
|
||
const input = document.querySelector('input[name="movie_title"]');
|
||
if (!input.value.trim()) {
|
||
e.preventDefault();
|
||
loading.style.display = 'none';
|
||
error.textContent = 'Пожалуйста, введите название фильма';
|
||
error.style.display = 'block';
|
||
return;
|
||
}
|
||
});
|
||
|
||
// Автофокус на поле ввода
|
||
document.querySelector('input[name="movie_title"]').focus();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
""")
|
||
except Exception as e:
|
||
print(f"Error in home page: {e}")
|
||
raise HTTPException(status_code=500, detail=f"Error loading home page: {str(e)}")
|
||
|
||
@app.post("/search", response_class=HTMLResponse)
|
||
async def search(request: Request, movie_title: str = Form(...)):
|
||
"""Обработка поиска фильмов"""
|
||
try:
|
||
search_results = await search_movies(movie_title)
|
||
return templates.TemplateResponse(
|
||
"results.html",
|
||
{
|
||
"request": request,
|
||
"query": movie_title,
|
||
"movies": search_results.get("results", []),
|
||
"total_results": search_results.get("total_results", 0)
|
||
}
|
||
)
|
||
except HTTPException as e:
|
||
return templates.TemplateResponse(
|
||
"error.html",
|
||
{
|
||
"request": request,
|
||
"error": str(e.detail)
|
||
}
|
||
)
|
||
|
||
@app.get("/api/search/{movie_title}")
|
||
async def api_search(movie_title: str):
|
||
"""API endpoint для поиска фильмов (возвращает JSON)"""
|
||
try:
|
||
search_results = await search_movies(movie_title)
|
||
return search_results
|
||
except HTTPException as e:
|
||
raise e
|
||
|
||
@app.get("/api/torrents/{movie_title}")
|
||
async def api_search_torrents(movie_title: str, year: str = None, original_title: str = None, genres: str = None, imdb_id: str = None):
|
||
"""API endpoint для поиска торрентов фильма"""
|
||
try:
|
||
# Парсим жанры из строки
|
||
genres_list = None
|
||
if genres:
|
||
genres_list = [g.strip() for g in genres.split(',')]
|
||
|
||
torrents = await search_torrents(movie_title, year, original_title, genres_list, imdb_id)
|
||
return {"torrents": torrents, "movie_title": movie_title, "year": year}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"Torrent search error: {str(e)}")
|
||
|
||
@app.get("/api/torrent/id/{torrent_id}")
|
||
async def api_search_torrent_by_id(torrent_id: str):
|
||
"""API endpoint для поиска торрента по ID"""
|
||
try:
|
||
torrent = await search_torrent_by_id(torrent_id)
|
||
if torrent:
|
||
return {"status": "success", "torrent": torrent}
|
||
else:
|
||
return {"status": "error", "message": f"Торрент с ID {torrent_id} не найден"}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"Torrent ID search error: {str(e)}")
|
||
|
||
@app.get("/api/check-qbittorrent")
|
||
async def check_qbittorrent_connection():
|
||
"""Проверка доступности qBittorrent"""
|
||
try:
|
||
qb_username = os.getenv("QBITTORRENT_USERNAME", "admin")
|
||
qb_password = os.getenv("QBITTORRENT_PASSWORD", "vrubel07")
|
||
qb_host = os.getenv("QBITTORRENT_HOST", "localhost")
|
||
qb_port = os.getenv("QBITTORRENT_PORT", "8082")
|
||
qb_url = f"http://{qb_host}:{qb_port}"
|
||
|
||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||
try:
|
||
auth_response = await client.post(
|
||
f"{qb_url}/api/v2/auth/login",
|
||
data={"username": qb_username, "password": qb_password}
|
||
)
|
||
|
||
if auth_response.status_code == 200 and auth_response.text.strip() == "Ok.":
|
||
# Получаем версию qBittorrent
|
||
version_response = await client.get(f"{qb_url}/api/v2/app/version")
|
||
version = version_response.text if version_response.status_code == 200 else "Unknown"
|
||
|
||
# Получаем список торрентов
|
||
torrents_response = await client.get(f"{qb_url}/api/v2/torrents/info")
|
||
torrents_count = 0
|
||
if torrents_response.status_code == 200:
|
||
torrents_count = len(torrents_response.json())
|
||
|
||
return {
|
||
"status": "success",
|
||
"message": "qBittorrent доступен",
|
||
"url": qb_url,
|
||
"version": version,
|
||
"torrents_count": torrents_count
|
||
}
|
||
else:
|
||
return {
|
||
"status": "error",
|
||
"message": f"Ошибка аутентификации: {auth_response.text}",
|
||
"url": qb_url
|
||
}
|
||
except httpx.ConnectError as e:
|
||
return {
|
||
"status": "error",
|
||
"message": f"Не удалось подключиться к qBittorrent: {str(e)}",
|
||
"url": qb_url,
|
||
"hint": "Проверьте, что qBittorrent запущен и доступен по адресу выше. Если используется VPN, убедитесь, что он настроен правильно."
|
||
}
|
||
except Exception as e:
|
||
return {
|
||
"status": "error",
|
||
"message": f"Ошибка при проверке: {str(e)}"
|
||
}
|
||
|
||
@app.get("/torrents/{movie_title}", response_class=HTMLResponse)
|
||
async def torrents_page(request: Request, movie_title: str, year: str = None):
|
||
"""Страница с результатами поиска торрентов"""
|
||
try:
|
||
torrents = await search_torrents(movie_title, year)
|
||
return templates.TemplateResponse(
|
||
"torrents.html",
|
||
{
|
||
"request": request,
|
||
"movie_title": movie_title,
|
||
"year": year,
|
||
"torrents": torrents
|
||
}
|
||
)
|
||
except Exception as e:
|
||
return templates.TemplateResponse(
|
||
"error.html",
|
||
{
|
||
"request": request,
|
||
"error": f"Ошибка поиска торрентов: {str(e)}"
|
||
}
|
||
)
|
||
|
||
@app.post("/api/add-torrent")
|
||
async def add_torrent_to_client(torrent_id: str = Form(...)):
|
||
"""Добавление торрента в qBittorrent через прямое API"""
|
||
try:
|
||
print(f"Attempting to add torrent with ID: {torrent_id}")
|
||
|
||
if not torrent_id or torrent_id.strip() == '':
|
||
return {"status": "error", "message": "ID торрента не указан"}
|
||
|
||
# Получаем информацию о торренте по ID
|
||
torrent_info = await search_torrent_by_id(torrent_id)
|
||
if not torrent_info:
|
||
print(f"Torrent info is None 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'}")
|
||
|
||
# Получаем учетные данные из переменных окружения
|
||
qb_username = os.getenv("QBITTORRENT_USERNAME", "admin")
|
||
qb_password = os.getenv("QBITTORRENT_PASSWORD", "vrubel07")
|
||
qb_host = os.getenv("QBITTORRENT_HOST", "localhost")
|
||
qb_port = os.getenv("QBITTORRENT_PORT", "8082")
|
||
qb_url = f"http://{qb_host}:{qb_port}"
|
||
|
||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||
# Аутентификация в qBittorrent
|
||
auth_response = await client.post(
|
||
f"{qb_url}/api/v2/auth/login",
|
||
data={"username": qb_username, "password": qb_password}
|
||
)
|
||
|
||
if auth_response.status_code != 200 or auth_response.text.strip() != "Ok.":
|
||
return {"status": "error", "message": f"Ошибка аутентификации в qBittorrent: {auth_response.text}"}
|
||
|
||
print("Successfully authenticated with qBittorrent")
|
||
|
||
# Пробуем сначала добавить через magnet-ссылку (более надежно)
|
||
magnet = torrent_info.get('magnet', '').strip()
|
||
|
||
# Проверяем, что magnet ссылка валидна (содержит хэш)
|
||
if magnet and 'urn:btih:' in magnet and len(magnet) > 20:
|
||
print(f"Trying to add via magnet link: {magnet[:100]}...")
|
||
add_response = await client.post(
|
||
f"{qb_url}/api/v2/torrents/add",
|
||
data={"urls": magnet}
|
||
)
|
||
|
||
print(f"Add via magnet response status: {add_response.status_code}")
|
||
print(f"Add via magnet response text: {add_response.text}")
|
||
|
||
if add_response.status_code == 200 and add_response.text.strip() == "Ok.":
|
||
# Проверяем, что торрент действительно добавился
|
||
await asyncio.sleep(2) # Ждем немного
|
||
torrent_hash = torrent_info.get('hash', '').upper()
|
||
|
||
# Если хэш не был в torrent_info, извлекаем из magnet ссылки
|
||
if not torrent_hash and magnet:
|
||
hash_match = re.search(r'urn:btih:([a-fA-F0-9]{40}|[a-zA-Z0-9]{32})', magnet)
|
||
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')
|
||
}
|
||
|
||
# Если не нашли по хэшу, но ответ был 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')
|
||
}
|
||
else:
|
||
print(f"Magnet link failed (status: {add_response.status_code}, response: {add_response.text}), trying .torrent file...")
|
||
else:
|
||
print(f"Magnet link invalid or empty: '{magnet[:50] if magnet else 'None'}...', trying .torrent file...")
|
||
|
||
# Если magnet не сработал, пробуем .torrent файл
|
||
torrent_url = torrent_info.get('torrent_url', '')
|
||
if not torrent_url:
|
||
# Пробуем также поле url
|
||
torrent_url = torrent_info.get('url', '')
|
||
|
||
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}")
|
||
|
||
if add_response.status_code == 200 and add_response.text.strip() == "Ok.":
|
||
# Проверяем, что торрент действительно добавился
|
||
await asyncio.sleep(2) # Ждем немного
|
||
|
||
# Пытаемся найти торрент в списке по названию (так как хэш может быть неизвестен)
|
||
torrent_title = torrent_info.get('title', '').lower()
|
||
torrents_response = await client.get(f"{qb_url}/api/v2/torrents/info")
|
||
|
||
if torrents_response.status_code == 200:
|
||
torrents = torrents_response.json()
|
||
# Ищем торрент по названию (первые 20 символов для точного совпадения)
|
||
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:
|
||
added_torrent = torrent
|
||
break
|
||
|
||
if added_torrent:
|
||
return {
|
||
"status": "success",
|
||
"message": f"Торрент '{torrent_info.get('title', 'Unknown')[:50]}...' успешно добавлен в qBittorrent через .torrent файл!",
|
||
"torrent_hash": added_torrent.get('hash', ''),
|
||
"torrent_name": added_torrent.get('name', torrent_info.get('title', 'Unknown'))
|
||
}
|
||
|
||
# Если не нашли по названию, но ответ был Ok, считаем успешным
|
||
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')
|
||
}
|
||
else:
|
||
error_msg = add_response.text.strip()
|
||
if "Fails" in error_msg or "Bad Request" in error_msg:
|
||
return {"status": "error", "message": f"qBittorrent отклонил торрент. Возможно, проблема с VPN или файл недоступен. Ответ: {error_msg}"}
|
||
return {"status": "error", "message": f"Ошибка добавления торрента (HTTP {add_response.status_code}): {error_msg}"}
|
||
else:
|
||
return {"status": "error", "message": f"Не найдена ни валидная magnet-ссылка, ни .torrent файл для торрента {torrent_id}. Проверьте, что торрент доступен."}
|
||
|
||
except httpx.ConnectError as e:
|
||
print(f"Connection error: {e}")
|
||
raise HTTPException(status_code=503, detail=f"qBittorrent недоступен по адресу {qb_url}. Убедитесь, что сервис запущен")
|
||
except Exception as e:
|
||
print(f"Unexpected error: {e}")
|
||
raise HTTPException(status_code=500, detail=f"Ошибка при добавлении торрента: {str(e)}")
|
||
|
||
if __name__ == "__main__":
|
||
import uvicorn
|
||
print("Starting server on 0.0.0.0:8000")
|
||
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="debug", access_log=True)
|