2025-10-05 22:20:49 +03:00
import os
import re
2025-10-06 01:36:39 +03:00
import asyncio
2025-10-05 22:20:49 +03:00
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 " )
2026-01-02 16:59:05 +03:00
# URL прокси-сервиса TMDB
TMDB_PROXY_URL = os . getenv ( " TMDB_PROXY_URL " , " http://localhost:8001 " )
2025-10-05 22:20:49 +03:00
# 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 :
2026-01-02 16:59:05 +03:00
""" Поиск фильмов через TMDB Proxy """
2025-10-05 22:20:49 +03:00
async with httpx . AsyncClient ( ) as client :
try :
response = await client . get (
2026-01-02 16:59:05 +03:00
f " { TMDB_PROXY_URL } /search/movie " ,
2025-10-05 22:20:49 +03:00
params = {
" query " : query ,
" language " : " ru-RU " ,
" include_adult " : False
}
)
response . raise_for_status ( )
return response . json ( )
except httpx . HTTPError as e :
2026-01-02 16:59:05 +03:00
raise HTTPException ( status_code = 500 , detail = f " TMDB Proxy error: { str ( e ) } " )
2025-10-05 22:20:49 +03:00
async def get_movie_details ( movie_id : int ) - > dict :
2026-01-02 16:59:05 +03:00
""" Получение детальной информации о фильме из TMDB через прокси """
2025-10-05 22:20:49 +03:00
async with httpx . AsyncClient ( ) as client :
try :
response = await client . get (
2026-01-02 16:59:05 +03:00
f " { TMDB_PROXY_URL } /movie/ { movie_id } " ,
2025-10-05 22:20:49 +03:00
params = {
" language " : " ru-RU " ,
" append_to_response " : " external_ids "
}
)
response . raise_for_status ( )
return response . json ( )
except httpx . HTTPError as e :
2026-01-02 16:59:05 +03:00
raise HTTPException ( status_code = 500 , detail = f " TMDB Proxy error: { str ( e ) } " )
2025-10-05 22:20:49 +03:00
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 } " )
# Поиск на конкретном провайдере - исправленный эндпоинт
2026-01-02 17:50:10 +03:00
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
2025-10-05 22:20:49 +03:00
if search_response . status_code == 200 :
2026-01-02 17:50:10 +03:00
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
2025-10-05 22:20:49 +03:00
if not isinstance ( results , list ) :
print ( f " Unexpected response format from { provider_name } : { type ( results ) } " )
2026-01-02 17:50:10 +03:00
if isinstance ( results , dict ) :
print ( f " Dict keys: { list ( results . keys ( ) ) } " )
2025-10-05 22:20:49 +03:00
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 ] } ... " )
2025-10-31 11:43:43 +03:00
# Логируем доступные поля для отладки
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 ] } ... " )
2025-10-05 22:20:49 +03:00
torrent = parse_torrent_result ( result , movie_title , year )
if torrent :
2025-10-31 11:43:43 +03:00
# Если 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 ] } ... " )
2025-10-05 22:20:49 +03:00
# Скорим торрент
score = score_torrent ( torrent , movie_title , original_title , year )
torrent [ ' relevance_score ' ] = score
size_mb = torrent . get ( ' size_bytes ' , 0 ) / ( 1024 * 1024 )
2025-10-31 11:43:43 +03:00
print ( f " Torrent score: { score : .2f } , size: { size_mb : .1f } MB, seeds: { torrent . get ( ' seeds ' , 0 ) } , ID: { torrent . get ( ' id ' , ' None ' ) [ : 20 ] } ... " )
2025-10-05 22:20:49 +03:00
# Добавляем только если скор больше 0.1 и размер больше 100MB
if score > 0.1 and torrent . get ( ' size_bytes ' , 0 ) > 100 * 1024 * 1024 :
2025-10-31 11:43:43 +03:00
# Проверяем, что 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
2025-10-05 22:20:49 +03:00
torrents . append ( torrent )
2025-10-31 11:43:43 +03:00
print ( f " ✅ Added to results with ID: { torrent . get ( ' id ' , ' None ' ) [ : 20 ] } ... " )
2025-10-05 22:20:49 +03:00
else :
print ( f " ❌ Filtered out (score: { score : .2f } , size: { size_mb : .1f } MB) " )
# Если нашли достаточно результатов, переходим к следующему провайдеру
if len ( torrents ) > = 20 :
break
except Exception as e :
2026-01-02 17:50:10 +03:00
print ( f " Error searching on { provider_name } : { type ( e ) . __name__ } : { e } " )
import traceback
traceback . print_exc ( )
2025-10-05 22:20:49 +03:00
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 } " )
2025-10-13 21:11:19 +03:00
# Получаем список доступных провайдеров
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
2025-10-05 22:20:49 +03:00
2025-10-13 21:11:19 +03:00
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 } " )
2025-10-31 11:43:43 +03:00
# Пробуем разные варианты API для поиска по ID
search_urls = [
2025-10-13 21:11:19 +03:00
f " { TORRENT_SEARCH_URL } /api/search/id/ { provider_name } " ,
2025-10-31 11:43:43 +03:00
f " { TORRENT_SEARCH_URL } /api/search/ { provider_name } " ,
]
response = None
results = None
2025-10-05 22:20:49 +03:00
2025-10-31 11:43:43 +03:00
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 :
2025-10-13 21:11:19 +03:00
# Берем первый результат
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
2025-10-31 11:43:43 +03:00
# Если хэша нет в 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 ] } ... " )
2025-10-13 21:11:19 +03:00
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 ' , ' ' )
2025-10-31 11:43:43 +03:00
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 = " "
2026-06-03 09:29:09 +00:00
# Пробуем локальный torapi-qbit если хэш пустой
if not hash_value or not magnet or ' urn:btih: ' not in magnet or len ( magnet ) < 30 :
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 = fb_result . get ( ' Hash ' , ' ' )
if not fb_hash :
import re as re2
fb_magnet = fb_result . get ( ' Magnet ' , ' ' )
hm2 = re2 . search ( r ' urn:btih:([a-fA-F0-9] {40} ) ' , fb_magnet )
if hm2 :
fb_hash = hm2 . group ( 1 )
if fb_hash :
magnet = generate_clean_magnet ( fb_hash , torrent_name )
print ( f " torapi-qbit fallback: got magnet hash { fb_hash [ : 10 ] } ... " )
except Exception as fbe :
print ( f " torapi-qbit fallback failed: { fbe } " )
2025-10-13 21:11:19 +03:00
# Парсим результат в стандартный формат
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 } " )
2025-10-06 01:36:39 +03:00
else :
2025-10-13 21:11:19 +03:00
print ( f " Error searching by ID on { provider_name } : { response . status_code } - { response . text } " )
except Exception as e :
2026-01-02 17:50:10 +03:00
print ( f " Error searching on { provider_name } : { type ( e ) . __name__ } : { e } " )
import traceback
traceback . print_exc ( )
2025-10-13 21:11:19 +03:00
continue
2026-06-03 09:29:09 +00:00
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 } " )
2025-10-13 21:11:19 +03:00
return None
2025-10-05 22:20:49 +03:00
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 ' ] )
2025-10-06 01:36:39 +03:00
# Создаем чистую magnet-ссылку с публичными трекерами
2025-10-05 22:20:49 +03:00
magnet = None
2025-10-06 01:36:39 +03:00
if ' Hash ' in result and result [ ' Hash ' ] :
# Генерируем чистую magnet-ссылку из хэша с публичными трекерами
2025-10-05 22:20:49 +03:00
hash_value = result [ ' Hash ' ]
2025-10-06 01:36:39 +03:00
torrent_title = result [ ' Name ' ]
magnet = generate_clean_magnet ( hash_value , torrent_title )
elif ' Magnet ' in result and result [ ' Magnet ' ] :
# Если нет хэша, используем оригинальную magnet-ссылку
magnet = result [ ' Magnet ' ]
2025-10-05 22:20:49 +03:00
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 '
2025-10-31 11:43:43 +03:00
# Извлекаем 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 ' , ' ' )
2025-10-05 22:20:49 +03:00
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 ,
2025-10-31 11:43:43 +03:00
" id " : torrent_id
2025-10-05 22:20:49 +03:00
}
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
2025-10-06 01:36:39 +03:00
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
2025-10-05 22:20:49 +03:00
@app.get ( " / " , response_class = HTMLResponse )
async def home ( request : Request ) :
""" Главная страница с формой поиска """
2025-10-13 21:11:19 +03:00
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 ( 135 deg , #667eea 0%, #764ba2 100%);
min - height : 100 vh ;
display : flex ;
align - items : center ;
justify - content : center ;
padding : 20 px ;
}
. container {
background : rgba ( 255 , 255 , 255 , 0.95 ) ;
backdrop - filter : blur ( 10 px ) ;
border - radius : 20 px ;
box - shadow : 0 20 px 40 px rgba ( 0 , 0 , 0 , 0.1 ) ;
padding : 40 px ;
max - width : 600 px ;
width : 100 % ;
text - align : center ;
}
. logo {
font - size : 4 rem ;
margin - bottom : 20 px ;
background : linear - gradient ( 45 deg , #667eea, #764ba2);
- webkit - background - clip : text ;
- webkit - text - fill - color : transparent ;
background - clip : text ;
}
h1 {
color : #333;
font - size : 2.5 rem ;
margin - bottom : 10 px ;
font - weight : 700 ;
}
. subtitle {
color : #666;
font - size : 1.2 rem ;
margin - bottom : 40 px ;
line - height : 1.6 ;
}
. search - form {
display : flex ;
flex - direction : column ;
gap : 20 px ;
margin - bottom : 30 px ;
}
. search - input {
padding : 15 px 20 px ;
border : 2 px solid #e1e5e9;
border - radius : 50 px ;
font - size : 1.1 rem ;
outline : none ;
transition : all 0.3 s ease ;
background : #fff;
}
. search - input : focus {
border - color : #667eea;
box - shadow : 0 0 0 3 px rgba ( 102 , 126 , 234 , 0.1 ) ;
}
. search - button {
padding : 15 px 30 px ;
background : linear - gradient ( 45 deg , #667eea, #764ba2);
color : white ;
border : none ;
border - radius : 50 px ;
font - size : 1.1 rem ;
font - weight : 600 ;
cursor : pointer ;
transition : all 0.3 s ease ;
text - transform : uppercase ;
letter - spacing : 1 px ;
}
. search - button : hover {
transform : translateY ( - 2 px ) ;
box - shadow : 0 10 px 20 px rgba ( 102 , 126 , 234 , 0.3 ) ;
}
. search - button : active {
transform : translateY ( 0 ) ;
}
. features {
display : grid ;
grid - template - columns : repeat ( auto - fit , minmax ( 150 px , 1 fr ) ) ;
gap : 20 px ;
margin - top : 40 px ;
}
. feature {
padding : 20 px ;
background : rgba ( 102 , 126 , 234 , 0.1 ) ;
border - radius : 15 px ;
transition : all 0.3 s ease ;
}
. feature : hover {
transform : translateY ( - 5 px ) ;
background : rgba ( 102 , 126 , 234 , 0.2 ) ;
}
. feature - icon {
font - size : 2 rem ;
margin - bottom : 10 px ;
}
. feature - title {
font - weight : 600 ;
color : #333;
margin - bottom : 5 px ;
}
. feature - desc {
font - size : 0.9 rem ;
color : #666;
}
. loading {
display : none ;
margin - top : 20 px ;
}
. spinner {
border : 3 px solid #f3f3f3;
border - top : 3 px solid #667eea;
border - radius : 50 % ;
width : 30 px ;
height : 30 px ;
animation : spin 1 s linear infinite ;
margin : 0 auto ;
}
@keyframes spin {
0 % { transform : rotate ( 0 deg ) ; }
100 % { transform : rotate ( 360 deg ) ; }
}
. error {
color : #e74c3c;
background : #fdf2f2;
padding : 10 px 15 px ;
border - radius : 10 px ;
margin - top : 20 px ;
display : none ;
}
. success {
color : #27ae60;
background : #f0f9f0;
padding : 10 px 15 px ;
border - radius : 10 px ;
margin - top : 20 px ;
display : none ;
}
@media ( max - width : 768 px ) {
. container {
padding : 30 px 20 px ;
}
h1 {
font - size : 2 rem ;
}
. logo {
font - size : 3 rem ;
}
}
< / 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 ) } " )
2025-10-05 22:20:49 +03:00
@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 ) } " )
2025-10-31 11:43:43 +03:00
@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 ) } "
}
2025-10-05 22:20:49 +03:00
@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 } " )
2025-10-31 11:43:43 +03:00
if not torrent_id or torrent_id . strip ( ) == ' ' :
return { " status " : " error " , " message " : " ID торрента не указан " }
2025-10-06 01:36:39 +03:00
# Получаем информацию о торренте по ID
2025-10-05 22:20:49 +03:00
torrent_info = await search_torrent_by_id ( torrent_id )
if not torrent_info :
2025-10-31 11:43:43 +03:00
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 ' } " )
2025-10-05 22:20:49 +03:00
# Получаем учетные данные из переменных окружения
qb_username = os . getenv ( " QBITTORRENT_USERNAME " , " admin " )
qb_password = os . getenv ( " QBITTORRENT_PASSWORD " , " vrubel07 " )
2025-10-06 01:36:39 +03:00
qb_host = os . getenv ( " QBITTORRENT_HOST " , " localhost " )
2025-10-13 21:11:19 +03:00
qb_port = os . getenv ( " QBITTORRENT_PORT " , " 8082 " )
2025-10-05 22:20:49 +03:00
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 " )
2025-10-06 01:36:39 +03:00
# Пробуем сначала добавить через magnet-ссылку (более надежно)
2025-10-31 11:43:43 +03:00
magnet = torrent_info . get ( ' magnet ' , ' ' ) . strip ( )
# Проверяем, что magnet ссылка валидна (содержит хэш)
if magnet and ' urn:btih: ' in magnet and len ( magnet ) > 20 :
2025-10-06 01:36:39 +03:00
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 ) # Ждем немного
2025-10-31 11:43:43 +03:00
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 ' )
}
2025-10-06 01:36:39 +03:00
2025-10-31 11:43:43 +03:00
# Если не нашли по хэшу, но ответ был Ok, считаем успешным
2025-10-09 12:53:06 +03:00
return {
" status " : " success " ,
" message " : f " Торрент ' { torrent_info . get ( ' title ' , ' Unknown ' ) [ : 50 ] } ... ' добавлен в qBittorrent через magnet-ссылку! " ,
" torrent_hash " : torrent_hash ,
" torrent_name " : torrent_info . get ( ' title ' , ' Unknown ' )
}
2025-10-06 01:36:39 +03:00
else :
2025-10-31 11:43:43 +03:00
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... " )
2025-10-05 22:20:49 +03:00
2025-10-06 01:36:39 +03:00
# Если magnet не сработал, пробуем .torrent файл
2025-10-31 11:43:43 +03:00
torrent_url = torrent_info . get ( ' torrent_url ' , ' ' )
if not torrent_url :
# Пробуем также поле url
torrent_url = torrent_info . get ( ' url ' , ' ' )
2025-10-06 01:36:39 +03:00
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. " :
2025-10-31 11:43:43 +03:00
# Проверяем, что торрент действительно добавился
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, считаем успешным
2025-10-09 12:53:06 +03:00
return {
" status " : " success " ,
2025-10-31 11:43:43 +03:00
" message " : f " Торрент ' { torrent_info . get ( ' title ' , ' Unknown ' ) [ : 50 ] } ... ' добавлен в qBittorrent через .torrent файл (проверьте список торрентов в qBittorrent)! " ,
2025-10-09 12:53:06 +03:00
" torrent_hash " : torrent_info . get ( ' hash ' , ' ' ) ,
" torrent_name " : torrent_info . get ( ' title ' , ' Unknown ' )
}
2025-10-06 01:36:39 +03:00
else :
2025-10-31 11:43:43 +03:00
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 } " }
2025-10-05 22:20:49 +03:00
else :
2025-10-31 11:43:43 +03:00
return { " status " : " error " , " message " : f " Н е найдена ни валидная magnet-ссылка, ни .torrent файл для торрента { torrent_id } . Проверьте, что торрент доступен. " }
2025-10-05 22:20:49 +03:00
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
2025-10-13 21:11:19 +03:00
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 )