Fix DLC package splitting with local decryption for real release names
Some checks are pending
Build and Release / build (push) Waiting to run
Some checks are pending
Build and Release / build (push) Waiting to run
DLC imports now use JDownloader's DLC service to decrypt containers locally, preserving the original package names (e.g. "Gluehendes.Feuer.S01.German.DL.720p.WEB.x264-WvF") instead of inferring abbreviated names from obfuscated filenames. Falls back to dcrypt.it with improved re-grouping when local decryption is unavailable. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
da18e3847e
commit
f1e132b2ed
@ -1,3 +1,4 @@
|
|||||||
|
import base64
|
||||||
import json
|
import json
|
||||||
import html
|
import html
|
||||||
import queue
|
import queue
|
||||||
@ -7,7 +8,9 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
|
import urllib.request
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
import zipfile
|
import zipfile
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
||||||
@ -31,14 +34,22 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
send2trash = None
|
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"
|
API_BASE_URL = "https://api.real-debrid.com/rest/1.0"
|
||||||
CONFIG_FILE = Path(__file__).with_name("rd_downloader_config.json")
|
CONFIG_FILE = Path(__file__).with_name("rd_downloader_config.json")
|
||||||
CHUNK_SIZE = 1024 * 512
|
CHUNK_SIZE = 1024 * 512
|
||||||
APP_NAME = "Real-Debrid Downloader GUI"
|
APP_NAME = "Real-Debrid Downloader GUI"
|
||||||
APP_VERSION = "1.1.0"
|
APP_VERSION = "1.1.1"
|
||||||
DEFAULT_UPDATE_REPO = "Sucukdeluxe/real-debrid-downloader"
|
DEFAULT_UPDATE_REPO = "Sucukdeluxe/real-debrid-downloader"
|
||||||
DEFAULT_RELEASE_ASSET = "Real-Debrid-Downloader-win64.zip"
|
DEFAULT_RELEASE_ASSET = "Real-Debrid-Downloader-win64.zip"
|
||||||
DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload"
|
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
|
REQUEST_RETRIES = 3
|
||||||
RETRY_BACKOFF_SECONDS = 1.2
|
RETRY_BACKOFF_SECONDS = 1.2
|
||||||
RETRY_HTTP_STATUS = {408, 429, 500, 502, 503, 504}
|
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)")
|
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]:
|
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:
|
with file_path.open("rb") as handle:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
DCRYPT_UPLOAD_URL,
|
DCRYPT_UPLOAD_URL,
|
||||||
@ -1048,6 +1070,16 @@ class DownloaderApp(tk.Tk):
|
|||||||
raise RuntimeError("; ".join(details) if details else "DLC konnte nicht entschluesselt werden")
|
raise RuntimeError("; ".join(details) if details else "DLC konnte nicht entschluesselt werden")
|
||||||
|
|
||||||
packages = self._extract_packages_from_payload(payload)
|
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:
|
if not packages:
|
||||||
links = self._extract_urls_recursive(payload)
|
links = self._extract_urls_recursive(payload)
|
||||||
packages = self._group_links_by_inferred_name(links)
|
packages = self._group_links_by_inferred_name(links)
|
||||||
@ -1058,6 +1090,65 @@ class DownloaderApp(tk.Tk):
|
|||||||
|
|
||||||
return packages
|
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>(.*?)</rc>", 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:
|
def _decode_dcrypt_payload(self, response_text: str) -> object:
|
||||||
text = response_text.strip()
|
text = response_text.strip()
|
||||||
match = re.search(r"<textarea[^>]*>(.*?)</textarea>", text, flags=re.IGNORECASE | re.DOTALL)
|
match = re.search(r"<textarea[^>]*>(.*?)</textarea>", text, flags=re.IGNORECASE | re.DOTALL)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user