diff --git a/real_debrid_downloader_gui.py b/real_debrid_downloader_gui.py index ae77331..845e8f4 100644 --- a/real_debrid_downloader_gui.py +++ b/real_debrid_downloader_gui.py @@ -1,3 +1,4 @@ +import base64 import json import html import queue @@ -7,7 +8,9 @@ import subprocess import sys import tempfile import threading +import urllib.request import webbrowser +import xml.etree.ElementTree as ET import zipfile from collections import deque from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait @@ -31,14 +34,22 @@ try: except ImportError: send2trash = None +try: + from Cryptodome.Cipher import AES as _AES +except ImportError: + _AES = None + API_BASE_URL = "https://api.real-debrid.com/rest/1.0" CONFIG_FILE = Path(__file__).with_name("rd_downloader_config.json") CHUNK_SIZE = 1024 * 512 APP_NAME = "Real-Debrid Downloader GUI" -APP_VERSION = "1.1.0" +APP_VERSION = "1.1.1" DEFAULT_UPDATE_REPO = "Sucukdeluxe/real-debrid-downloader" DEFAULT_RELEASE_ASSET = "Real-Debrid-Downloader-win64.zip" DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload" +DLC_SERVICE_URL = "http://service.jdownloader.org/dlcrypt/service.php?srcType=dlc&destType=pylo&data={}" +DLC_AES_KEY = b"cb99b5cbc24db398" +DLC_AES_IV = b"9bc24cb995cb8db3" REQUEST_RETRIES = 3 RETRY_BACKOFF_SECONDS = 1.2 RETRY_HTTP_STATUS = {408, 429, 500, 502, 503, 504} @@ -1025,6 +1036,17 @@ class DownloaderApp(tk.Tk): self.status_var.set(f"DLC importiert: {len(packages)} Paket(e), {total_links} Link(s)") def _decrypt_dlc_file(self, file_path: Path) -> list[DownloadPackage]: + # Primary: local decryption via JDownloader DLC service (preserves + # real package names like JDownloader does). + if _AES is not None: + try: + packages = self._decrypt_dlc_local(file_path) + if packages: + return packages + except Exception: + pass # fall through to dcrypt.it + + # Fallback: dcrypt.it (no package structure, only flat link list). with file_path.open("rb") as handle: response = requests.post( DCRYPT_UPLOAD_URL, @@ -1048,6 +1070,16 @@ class DownloaderApp(tk.Tk): raise RuntimeError("; ".join(details) if details else "DLC konnte nicht entschluesselt werden") packages = self._extract_packages_from_payload(payload) + + # When the payload contains a single flat link list (e.g. dcrypt.it + # ``{"success": {"links": [...]}}``), _extract_packages_from_payload + # will lump everything into one package. Re-group by filename so that + # distinct releases end up in separate packages. + if len(packages) == 1: + regrouped = self._group_links_by_inferred_name(packages[0].links) + if len(regrouped) > 1: + packages = regrouped + if not packages: links = self._extract_urls_recursive(payload) packages = self._group_links_by_inferred_name(links) @@ -1058,6 +1090,65 @@ class DownloaderApp(tk.Tk): return packages + def _decrypt_dlc_local(self, file_path: Path) -> list[DownloadPackage]: + """Decrypt a DLC container locally via JDownloader's DLC service. + + Returns a list of DownloadPackage with the real release names that + are embedded in the DLC container's XML structure. + """ + content = file_path.read_text(encoding="ascii", errors="ignore").strip() + if len(content) < 89: + return [] + + dlc_key = content[-88:] + dlc_data = content[:-88] + + # Ask JDownloader service for the RC token. + url = DLC_SERVICE_URL.format(dlc_key) + with urllib.request.urlopen(url, timeout=30) as resp: + rc_response = resp.read().decode("utf-8") + + rc_match = re.search(r"(.*?)", rc_response) + if not rc_match: + return [] + + # Decrypt RC to obtain the real AES key. + rc_bytes = base64.b64decode(rc_match.group(1)) + cipher = _AES.new(DLC_AES_KEY, _AES.MODE_CBC, DLC_AES_IV) + real_key = cipher.decrypt(rc_bytes)[:16] + + # Decrypt the main payload. + encrypted = base64.b64decode(dlc_data) + cipher2 = _AES.new(real_key, _AES.MODE_CBC, real_key) + decrypted = cipher2.decrypt(encrypted) + # Strip PKCS7 padding. + pad = decrypted[-1] + if 1 <= pad <= 16 and decrypted[-pad:] == bytes([pad]) * pad: + decrypted = decrypted[:-pad] + + xml_data = base64.b64decode(decrypted).decode("utf-8") + root = ET.fromstring(xml_data) + content_node = root.find("content") + if content_node is None: + return [] + + packages: list[DownloadPackage] = [] + for pkg_el in content_node.findall("package"): + name_b64 = pkg_el.get("name", "") + name = base64.b64decode(name_b64).decode("utf-8") if name_b64 else "" + + urls: list[str] = [] + for file_el in pkg_el.findall("file"): + url_el = file_el.find("url") + if url_el is not None and url_el.text: + urls.append(base64.b64decode(url_el.text.strip()).decode("utf-8")) + + if urls: + package_name = sanitize_filename(name or infer_package_name_from_links(urls) or f"Paket-{len(packages) + 1:03d}") + packages.append(DownloadPackage(name=package_name, links=self._unique_preserve_order(urls))) + + return packages + def _decode_dcrypt_payload(self, response_text: str) -> object: text = response_text.strip() match = re.search(r"]*>(.*?)", text, flags=re.IGNORECASE | re.DOTALL)