From 368d57012d635361e73c581779047d8e01f97f24 Mon Sep 17 00:00:00 2001 From: vrubel Date: Fri, 31 Oct 2025 11:43:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B1=D0=BE=D0=BB=D0=B5=D0=B5=20=D0=BD=D0=B0?= =?UTF-8?q?=D0=B4=D0=B5=D0=B6=D0=BD=D1=8B=D0=B9=20=D0=BF=D0=BE=D0=B8=D1=81?= =?UTF-8?q?=D0=BA=20=D0=BC=D0=B0=D0=B3=D0=BD=D0=B5=D1=82=20=D1=81=D1=81?= =?UTF-8?q?=D1=8B=D0=BB=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 239 ++++++++++++++++++++++++++++++++++++++------- docker-compose.yml | 6 +- 2 files changed, 209 insertions(+), 36 deletions(-) diff --git a/app.py b/app.py index 8169282..db5721e 100644 --- a/app.py +++ b/app.py @@ -301,18 +301,40 @@ async def search_torrents(movie_title: str, year: str = None, original_title: st # Обрабатываем все результаты и скорим их 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)} - {result['Name'][:100]}...") + 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") + 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)") @@ -355,15 +377,28 @@ async def search_torrent_by_id(torrent_id: str) -> dict: try: print(f"Searching ID {torrent_id} on {provider_name}") - response = await client.get( + # Пробуем разные варианты API для поиска по ID + search_urls = [ f"{TORRENT_SEARCH_URL}/api/search/id/{provider_name}", - params={"query": torrent_id} - ) + f"{TORRENT_SEARCH_URL}/api/search/{provider_name}", + ] - if response.status_code == 200: - results = response.json() - print(f"Response from {provider_name}: {type(results)} - {len(results) if isinstance(results, list) else 'not a list'}") - if results and isinstance(results, list) and len(results) > 0: + 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 пустое @@ -374,6 +409,16 @@ async def search_torrent_by_id(torrent_id: str) -> dict: 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) @@ -381,8 +426,9 @@ async def search_torrent_by_id(torrent_id: str) -> dict: else: # Fallback на оригинальную magnet-ссылку если нет хэша magnet = result.get('Magnet', '') - if magnet and not magnet.startswith('magnet:'): - magnet = f"magnet:?xt=urn:btih:{hash_value}" + 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 = { @@ -486,6 +532,25 @@ def parse_torrent_result(result: dict, movie_title: str, year: str = None) -> di 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, @@ -499,7 +564,7 @@ def parse_torrent_result(result: dict, movie_title: str, year: str = None) -> di "category": result.get('Category', ''), "date": result.get('Date', ''), "provider": provider, - "id": result.get('Id', '') + "id": torrent_id } except Exception as e: print(f"Error parsing torrent result: {e}") @@ -911,6 +976,60 @@ async def api_search_torrent_by_id(torrent_id: str): 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): """Страница с результатами поиска торрентов""" @@ -940,10 +1059,16 @@ async def add_torrent_to_client(torrent_id: str = Form(...)): 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: - return {"status": "error", "message": f"Торрент с ID {torrent_id} не найден"} + 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") @@ -965,8 +1090,10 @@ async def add_torrent_to_client(torrent_id: str = Form(...)): print("Successfully authenticated with qBittorrent") # Пробуем сначала добавить через magnet-ссылку (более надежно) - magnet = torrent_info.get('magnet') - if 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", @@ -979,20 +1106,29 @@ async def add_torrent_to_client(torrent_id: str = Form(...)): if add_response.status_code == 200 and add_response.text.strip() == "Ok.": # Проверяем, что торрент действительно добавился await asyncio.sleep(2) # Ждем немного - torrents_response = await client.get(f"{qb_url}/api/v2/torrents/info") - if torrents_response.status_code == 200: - torrents = torrents_response.json() - # Ищем торрент по хэшу - torrent_hash = torrent_info.get('hash', '').upper() - 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') - } + 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-ссылку!", @@ -1000,10 +1136,16 @@ async def add_torrent_to_client(torrent_id: str = Form(...)): "torrent_name": torrent_info.get('title', 'Unknown') } else: - print(f"Magnet link failed, trying .torrent file...") + 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') + 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( @@ -1015,16 +1157,47 @@ async def add_torrent_to_client(torrent_id: str = Form(...)): 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 файл!", + "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: - return {"status": "error", "message": f"Ошибка добавления торрента (HTTP {add_response.status_code}): {add_response.text}"} + 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}"} + return {"status": "error", "message": f"Не найдена ни валидная magnet-ссылка, ни .torrent файл для торрента {torrent_id}. Проверьте, что торрент доступен."} except httpx.ConnectError as e: print(f"Connection error: {e}") diff --git a/docker-compose.yml b/docker-compose.yml index 699ac04..c316a40 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: - TORAPI_URL=http://torrent-api:8000 - TORRENT_SEARCH_URL=http://host.docker.internal:8443 - TORRENT_ADD_URL=http://host.docker.internal:8088 - - QBITTORRENT_USERNAME=admin + - QBITTORRENT_USERNAME=vrubelroman - QBITTORRENT_PASSWORD=vrubel07 - QBITTORRENT_HOST=host.docker.internal - QBITTORRENT_PORT=8082 @@ -39,7 +39,7 @@ services: image: lifailon/torapi:latest container_name: TorAPI-qBittorrent environment: - - USERNAME=admin + - USERNAME=vrubelroman - PASSWORD=vrubel07 - PROXY_ADDRESS=host.docker.internal - PROXY_PORT=8082 @@ -62,7 +62,7 @@ services: - TELEGRAM_BOT_TOKEN=7662650066:AAFgsfYJNYgpcSHaSe6fspsjqmhMkOBT1s4 - TORRENT_SEARCH_URL=http://host.docker.internal:8443 - TORRENT_ADD_URL=http://host.docker.internal:8088 - - QBITTORRENT_USERNAME=admin + - QBITTORRENT_USERNAME=vrubelroman - QBITTORRENT_PASSWORD=vrubel07 - QBITTORRENT_HOST=host.docker.internal - QBITTORRENT_PORT=8082