Add drag-and-drop DLC import in links area
Some checks are pending
Build and Release / build (push) Waiting to run

This commit is contained in:
Sucukdeluxe 2026-02-27 02:17:18 +01:00
parent e8aabfdc7e
commit 90fa7e15dd
3 changed files with 81 additions and 12 deletions

View File

@ -7,6 +7,7 @@ ueber Real-Debrid zu unrestricten und direkt auf deinen PC zu laden.
- Mehrere Links auf einmal (ein Link pro Zeile) - Mehrere Links auf einmal (ein Link pro Zeile)
- DLC Import (`.dlc`) ueber dcrypt.it inklusive Paket-Gruppierung - DLC Import (`.dlc`) ueber dcrypt.it inklusive Paket-Gruppierung
- DLC Drag-and-Drop: `.dlc` direkt in den Links-Bereich ziehen
- Nutzt die Real-Debrid API (`/unrestrict/link`) - Nutzt die Real-Debrid API (`/unrestrict/link`)
- Download-Status pro Link - Download-Status pro Link
- Paket-Ansicht: Paket ist aufklappbar, darunter alle Einzel-Links - Paket-Ansicht: Paket ist aufklappbar, darunter alle Einzel-Links

View File

@ -26,6 +26,12 @@ import requests
import tkinter as tk import tkinter as tk
from tkinter import filedialog, messagebox, ttk from tkinter import filedialog, messagebox, ttk
try:
from tkinterdnd2 import DND_FILES, TkinterDnD
except ImportError:
DND_FILES = None
TkinterDnD = None
try: try:
import pyzipper import pyzipper
except ImportError: except ImportError:
@ -52,7 +58,7 @@ MANIFEST_FILE = Path(__file__).with_name("rd_download_manifest.json")
LOG_FILE = Path(__file__).with_name("rd_downloader.log") LOG_FILE = Path(__file__).with_name("rd_downloader.log")
CHUNK_SIZE = 1024 * 512 CHUNK_SIZE = 1024 * 512
APP_NAME = "Real-Debrid Downloader GUI" APP_NAME = "Real-Debrid Downloader GUI"
APP_VERSION = "1.1.4" APP_VERSION = "1.1.5"
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"
@ -147,6 +153,11 @@ def configure_file_logger() -> logging.Logger:
LOGGER = configure_file_logger() LOGGER = configure_file_logger()
if TkinterDnD is not None:
TkBase = TkinterDnD.Tk
else:
TkBase = tk.Tk
def compact_error_text(message: str, max_len: int = 180) -> str: def compact_error_text(message: str, max_len: int = 180) -> str:
text = str(message or "").strip() text = str(message or "").strip()
@ -507,7 +518,7 @@ class RealDebridClient:
return filename, download_url, retries_used, file_size return filename, download_url, retries_used, file_size
class DownloaderApp(tk.Tk): class DownloaderApp(TkBase):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.title(f"{APP_NAME} v{APP_VERSION}") self.title(f"{APP_NAME} v{APP_VERSION}")
@ -570,6 +581,7 @@ class DownloaderApp(tk.Tk):
self.tooltip_label: ttk.Label | None = None self.tooltip_label: ttk.Label | None = None
self.tooltip_row = "" self.tooltip_row = ""
self.tooltip_column = "" self.tooltip_column = ""
self.dnd_ready = False
self._build_ui() self._build_ui()
self._load_config() self._load_config()
@ -693,12 +705,14 @@ class DownloaderApp(tk.Tk):
ttk.Button(links_actions, text="DLC import", command=self._import_dlc_file).pack(side="left", padx=(8, 0)) ttk.Button(links_actions, text="DLC import", command=self._import_dlc_file).pack(side="left", padx=(8, 0))
ttk.Button(links_actions, text="Links speichern", command=self._save_links_to_file).pack(side="left", padx=(8, 0)) ttk.Button(links_actions, text="Links speichern", command=self._save_links_to_file).pack(side="left", padx=(8, 0))
ttk.Button(links_actions, text="Links leeren", command=self._clear_links).pack(side="left", padx=(8, 0)) ttk.Button(links_actions, text="Links leeren", command=self._clear_links).pack(side="left", padx=(8, 0))
ttk.Label(links_actions, text="Tipp: .dlc per Drag-and-Drop hier ablegen").pack(side="right")
self.links_text = tk.Text(links_frame, height=14, wrap="none") self.links_text = tk.Text(links_frame, height=14, wrap="none")
self.links_text.grid(row=1, column=0, sticky="nsew") self.links_text.grid(row=1, column=0, sticky="nsew")
links_scroll = ttk.Scrollbar(links_frame, orient="vertical", command=self.links_text.yview) links_scroll = ttk.Scrollbar(links_frame, orient="vertical", command=self.links_text.yview)
links_scroll.grid(row=1, column=1, sticky="ns") links_scroll.grid(row=1, column=1, sticky="ns")
self.links_text.configure(yscrollcommand=links_scroll.set) self.links_text.configure(yscrollcommand=links_scroll.set)
self._setup_dlc_drag_and_drop()
actions_frame = ttk.Frame(root) actions_frame = ttk.Frame(root)
actions_frame.grid(row=3, column=0, sticky="ew", pady=(10, 0)) actions_frame.grid(row=3, column=0, sticky="ew", pady=(10, 0))
@ -1320,17 +1334,59 @@ class DownloaderApp(tk.Tk):
self.links_text.delete("1.0", "end") self.links_text.delete("1.0", "end")
self.links_text.insert("1.0", content) self.links_text.insert("1.0", content)
def _import_dlc_file(self) -> None: def _setup_dlc_drag_and_drop(self) -> None:
file_path = filedialog.askopenfilename( if DND_FILES is None:
title="DLC importieren",
initialdir=str(Path.home() / "Desktop"),
filetypes=(("DLC Container", "*.dlc"), ("Alle Dateien", "*.*")),
)
if not file_path:
return return
try: try:
packages = self._decrypt_dlc_file(Path(file_path)) self.links_text.drop_target_register(DND_FILES)
self.links_text.dnd_bind("<<Drop>>", self._on_links_drop)
self.dnd_ready = True
except Exception as exc:
self.dnd_ready = False
LOGGER.warning("Drag-and-Drop konnte nicht aktiviert werden: %s", exc)
def _on_links_drop(self, event: tk.Event) -> str:
raw_data = str(getattr(event, "data", "") or "")
paths = self._parse_dropped_paths(raw_data)
if not paths:
self.status_var.set("Drop ignoriert: keine Datei erkannt")
return "break"
dlc_files = [path for path in paths if path.suffix.lower() == ".dlc"]
if not dlc_files:
self.status_var.set("Drop ignoriert: bitte eine .dlc Datei ziehen")
return "break"
if len(dlc_files) > 1:
self.status_var.set(f"Mehrere DLC-Dateien erkannt ({len(dlc_files)}), lade die erste ...")
self._import_dlc_path(dlc_files[0], source="Drag-and-Drop")
return "break"
def _parse_dropped_paths(self, raw_data: str) -> list[Path]:
text = str(raw_data or "").strip()
if not text:
return []
parts: tuple[str, ...]
try:
parts = tuple(self.tk.splitlist(text))
except Exception:
parts = (text,)
result: list[Path] = []
for part in parts:
value = str(part).strip().strip("{}\"")
if not value:
continue
candidate = Path(value)
if candidate.exists() and candidate.is_file():
result.append(candidate)
return result
def _import_dlc_path(self, file_path: Path, source: str = "DLC Import") -> None:
try:
packages = self._decrypt_dlc_file(file_path)
except Exception as exc: except Exception as exc:
messagebox.showerror("DLC Import", f"DLC konnte nicht importiert werden: {exc}") messagebox.showerror("DLC Import", f"DLC konnte nicht importiert werden: {exc}")
return return
@ -1346,7 +1402,18 @@ class DownloaderApp(tk.Tk):
self.package_name_var.set("") self.package_name_var.set("")
total_links = sum(len(package.links) for package in packages) total_links = sum(len(package.links) for package in packages)
self.status_var.set(f"DLC importiert: {len(packages)} Paket(e), {total_links} Link(s)") self.status_var.set(f"{source}: {len(packages)} Paket(e), {total_links} Link(s)")
LOGGER.info("%s: %s Paket(e), %s Link(s)", source, len(packages), total_links)
def _import_dlc_file(self) -> None:
file_path = filedialog.askopenfilename(
title="DLC importieren",
initialdir=str(Path.home() / "Desktop"),
filetypes=(("DLC Container", "*.dlc"), ("Alle Dateien", "*.*")),
)
if not file_path:
return
self._import_dlc_path(Path(file_path), source="DLC importiert")
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 # Primary: local decryption via JDownloader DLC service (preserves

View File

@ -2,3 +2,4 @@ requests>=2.31.0
pyzipper>=0.3.6 pyzipper>=0.3.6
send2trash>=1.8.2 send2trash>=1.8.2
keyring>=25.6.0 keyring>=25.6.0
tkinterdnd2>=0.4.2