From 9934a305f8340fae9440bdc38cc223b8c40853b2 Mon Sep 17 00:00:00 2001 From: Akamaru Date: Fri, 2 May 2025 20:10:13 +0200 Subject: [PATCH] Erste Version --- .gitignore | 7 + README.md | 63 +++ build.py | 203 +++++++++ main.py | 1068 ++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 + startvdl.bat | 2 + 6 files changed, 1346 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.py create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 startvdl.bat diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34fe2ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build/ +dist/ +presets/ +bin/yt-dlp.exe +config.json +main.spec +*.7z diff --git a/README.md b/README.md new file mode 100644 index 0000000..c74fd21 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Vider Download Helper + +Eine grafische Benutzeroberfläche für yt-dlp mit Preset-Funktionalität für Windows. + +## Downloads + +Die jeweils aktuelle Version (EXE, CLI) findest du hier: + +https://git.ponywave.de/Akamaru/video-download-helper/releases/latest + +## Funktionen + +- Downloadvideos von YouTube und anderen unterstützten Plattformen +- Erstellen und Verwalten von Presets mit vordefinierten Einstellungen +- Auswahl zwischen installiertem yt-dlp im PATH oder im bin-Ordner der Anwendung + +## Installation + +1. Stelle sicher, dass Python 3.8+ installiert ist +2. Installiere die Abhängigkeiten: `pip install -r requirements.txt` +3. Führe die Anwendung aus: `python main.py` + +## Kompilieren der EXE + +``` +pyinstaller --onefile --windowed main.py +``` + +Die erstellte EXE-Datei befindet sich im `dist`-Ordner. + +## Verwendung + +1. Starte die Anwendung +2. Füge die Video-URL ein +3. Wähle ein Preset oder passe die Einstellungen an +4. Klicke auf "Download" + +## Starten der Anwendung + +### Über die Batch-Datei (empfohlen für Python-Umgebungen) + +Du kannst die Anwendung bequem über die mitgelieferte `startvdl.bat` starten. Diese sorgt dafür, dass das Fenster im Hintergrund läuft und keine Konsole angezeigt wird: + +```bat +@echo off +start "" /b pythonw.exe main.py +``` + +Einfach einen Doppelklick auf `startvdl.bat` ausführen. + +### Über die EXE-Datei (ohne Python) + +Wenn du die Anwendung mit PyInstaller gebaut hast, findest du die ausführbare Datei im `dist`-Ordner (z.B. `main.exe`). + +Starte die Anwendung einfach per Doppelklick auf die EXE: + +``` +dist\main.exe +``` + +### Hinweise +- Stelle sicher, dass sich `yt-dlp.exe` im `bin`-Ordner befindet, wenn du die lokale Variante nutzen möchtest. +- Alternativ kannst du auch die Systeminstallation von yt-dlp (im PATH) verwenden. \ No newline at end of file diff --git a/build.py b/build.py new file mode 100644 index 0000000..da2a92a --- /dev/null +++ b/build.py @@ -0,0 +1,203 @@ +import os +import sys +import shutil +import subprocess +import argparse +import importlib.util +import json +import re + +def check_requirements(): + """Überprüft, ob alle erforderlichen Pakete installiert sind.""" + try: + import PyQt5 + import PyInstaller + except ImportError as e: + print(f"Fehler: {e}") + print("Bitte führe 'pip install -r requirements.txt' aus.") + return False + return True + +def find_yt_dlp(): + """Versucht, yt-dlp im PATH zu finden.""" + try: + result = subprocess.run( + ["where", "yt-dlp"] if os.name == "nt" else ["which", "yt-dlp"], + capture_output=True, + text=True + ) + + if result.returncode == 0: + path = result.stdout.strip().split("\n")[0] + return path + return None + except Exception: + return None + +def download_yt_dlp(): + """Lädt die aktuelle Version von yt-dlp herunter.""" + bin_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin") + + if not os.path.exists(bin_dir): + os.makedirs(bin_dir) + + print("Lade yt-dlp herunter...") + + ytdlp_url = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe" + ytdlp_path = os.path.join(bin_dir, "yt-dlp.exe") + + try: + import requests + response = requests.get(ytdlp_url) + with open(ytdlp_path, "wb") as f: + f.write(response.content) + print(f"yt-dlp wurde nach {ytdlp_path} heruntergeladen.") + return True + except Exception as e: + print(f"Fehler beim Herunterladen von yt-dlp: {e}") + print("Bitte lade yt-dlp manuell herunter und platziere es im 'bin' Ordner.") + return False + +def copy_yt_dlp_from_path(): + """Kopiert yt-dlp aus dem PATH in den bin-Ordner.""" + ytdlp_path = find_yt_dlp() + + if not ytdlp_path: + print("yt-dlp wurde nicht im PATH gefunden.") + return False + + bin_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin") + + if not os.path.exists(bin_dir): + os.makedirs(bin_dir) + + try: + dest_path = os.path.join(bin_dir, "yt-dlp.exe" if os.name == "nt" else "yt-dlp") + shutil.copy2(ytdlp_path, dest_path) + print(f"yt-dlp wurde nach {dest_path} kopiert.") + return True + except Exception as e: + print(f"Fehler beim Kopieren von yt-dlp: {e}") + return False + +def build_exe(onedir=False, console=False): + """Erstellt die ausführbare Datei mit PyInstaller.""" + print("Erstelle EXE-Datei...") + # Nur Default-Presets übernehmen + main_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "main.py") + with open(main_path, "r", encoding="utf-8") as f: + main_code = f.read() + # Default-Presets extrahieren (vereinfachte Suche) + match = re.search(r'defaults\s*=\s*\[(.*?)\n\s*\]', main_code, re.DOTALL) + if match: + defaults_code = "[" + match.group(1) + "]" + try: + # eval mit eingeschränktem Namespace + defaults = eval(defaults_code, {"__builtins__": None}, {}) + except Exception as e: + print(f"Fehler beim Parsen der Default-Presets: {e}") + defaults = [] + else: + print("Konnte Default-Presets nicht finden!") + defaults = [] + # Presets-Ordner bereinigen und nur Default-Presets schreiben + bin_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin") + presets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "presets") + if os.path.exists(presets_dir): + for f in os.listdir(presets_dir): + if f.endswith('.json'): + try: + os.remove(os.path.join(presets_dir, f)) + except Exception: + pass + else: + os.makedirs(presets_dir) + for preset in defaults: + filename = f"{preset['name'].lower().replace(' ', '_')}.json" + try: + with open(os.path.join(presets_dir, filename), 'w', encoding='utf-8') as f: + json.dump(preset, f, indent=4, ensure_ascii=False) + except Exception as e: + print(f"Fehler beim Schreiben von Default-Preset {preset['name']}: {e}") + # Rest wie gehabt + cmd = ["pyinstaller"] + if not onedir: + cmd.append("--onefile") + if not console: + cmd.append("--windowed") + if os.path.exists(bin_dir) and os.listdir(bin_dir): + cmd.extend(["--add-data", f"{bin_dir}{os.pathsep}bin"]) + if os.path.exists(presets_dir): + cmd.extend(["--add-data", f"{presets_dir}{os.pathsep}presets"]) + cmd.append("main.py") + print(f"Führe aus: {' '.join(cmd)}") + try: + try: + subprocess.run(cmd, check=True) + except FileNotFoundError: + print("pyinstaller nicht im PATH gefunden, versuche 'python -m PyInstaller' ...") + cmd[0:1] = [sys.executable, "-m", "PyInstaller"] + subprocess.run(cmd, check=True) + if onedir: + print("Die ausführbare Datei wurde im 'dist/main' Ordner erstellt.") + else: + print("Die ausführbare Datei wurde als 'dist/main.exe' erstellt.") + return True + except subprocess.CalledProcessError as e: + print(f"Fehler beim Erstellen der EXE-Datei: {e}") + return False + except Exception as e: + print(f"Unerwarteter Fehler: {e}") + return False + +def parse_arguments(): + """Parst die Kommandozeilenargumente.""" + parser = argparse.ArgumentParser(description="Build-Skript für YT-DLP GUI") + parser.add_argument("--onedir", action="store_true", help="Erstellt einen Ordner statt einer einzelnen Datei") + parser.add_argument("--console", action="store_true", help="Zeigt die Konsole an") + parser.add_argument("--download-ytdlp", action="store_true", help="Lädt yt-dlp herunter") + parser.add_argument("--copy-ytdlp", action="store_true", help="Kopiert yt-dlp aus dem PATH") + + return parser.parse_args() + +def main(): + args = parse_arguments() + + if not check_requirements(): + return 1 + + # Überprüfe, ob yt-dlp bereits im bin-Ordner existiert + bin_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin") + ytdlp_exists = os.path.exists(os.path.join(bin_dir, "yt-dlp.exe" if os.name == "nt" else "yt-dlp")) + + if not ytdlp_exists: + if args.download_ytdlp: + if not download_yt_dlp(): + return 1 + elif args.copy_ytdlp: + if not copy_yt_dlp_from_path(): + print("yt-dlp konnte nicht kopiert werden. Versuche, es herunterzuladen...") + if not download_yt_dlp(): + return 1 + else: + # Frage den Benutzer + print("yt-dlp wurde nicht im bin-Ordner gefunden.") + choice = input("Möchten Sie (1) yt-dlp herunterladen, (2) aus dem PATH kopieren oder (3) ohne fortfahren? [1/2/3]: ") + + if choice == "1": + if not download_yt_dlp(): + return 1 + elif choice == "2": + if not copy_yt_dlp_from_path(): + print("yt-dlp konnte nicht kopiert werden. Versuche, es herunterzuladen...") + if not download_yt_dlp(): + return 1 + + # Erstelle die ausführbare Datei + if not build_exe(onedir=args.onedir, console=args.console): + return 1 + + return 0 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..e868cdc --- /dev/null +++ b/main.py @@ -0,0 +1,1068 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "PyQt5==5.15.9", +# "PyInstaller==5.13.2", +# "requests==2.31.0", +# ] +# /// +import sys +import os +import json +import subprocess +import re +import urllib.request +from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QPushButton, QComboBox, QTextEdit, + QFileDialog, QMessageBox, QListWidget, QDialog, QFormLayout, + QDialogButtonBox, QInputDialog, QGroupBox, QCheckBox, QTabWidget) +from PyQt5.QtCore import QThread, pyqtSignal, Qt + +# Hilfsfunktionen für den Ressourcenpfad +def get_base_path(): + """Gibt den Basispfad für Ressourcen zurück, funktioniert sowohl für PyInstaller als auch für reguläre Ausführung.""" + if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): + # PyInstaller-Bundled-Modus + return sys._MEIPASS + else: + # Regulärer Modus + return os.path.dirname(os.path.abspath(__file__)) + +def get_user_data_dir(): + if getattr(sys, 'frozen', False): + return os.path.dirname(sys.executable) + else: + return os.path.dirname(os.path.abspath(__file__)) + +def get_user_presets_dir(): + # Presets-Ordner neben der EXE (bzw. Script) + base = get_user_data_dir() + path = os.path.join(base, "presets") + if not os.path.exists(path): + os.makedirs(path, exist_ok=True) + return path + +CONFIG_FILE = os.path.join(get_user_data_dir(), "config.json") +PRESETS_DIR = os.path.join(get_base_path(), "presets") # Nur für Lesezugriff auf mitgelieferte Presets +DEFAULT_CONFIG = { + "output_dir": "", + "use_local_ytdlp": True, + "last_preset": "", + "presets": [], + "ytdlp_flags": { + "ignore_config": False, + "remux_mkv": False, + "embed_metadata": False + }, + "hide_default_presets": False +} + +# Template-Variablen für Serien +SERIES_TEMPLATE = "{series} S{season}E{episode}{extension}" + +class DownloadThread(QThread): + update_signal = pyqtSignal(str) + finished_signal = pyqtSignal(bool, str) + + def __init__(self, url, output_dir, cmd_args, use_local_ytdlp, output_filename=None): + super().__init__() + self.url = url + self.output_dir = output_dir + self.cmd_args = cmd_args + self.use_local_ytdlp = use_local_ytdlp + self.output_filename = output_filename + + def run(self): + try: + # Bestimme den Pfad zu yt-dlp + if self.use_local_ytdlp: + ytdlp_path = os.path.join(get_base_path(), "bin", "yt-dlp.exe") + if not os.path.exists(ytdlp_path): + ytdlp_path = "yt-dlp" # Fallback auf PATH + else: + ytdlp_path = "yt-dlp" + + cmd = [ytdlp_path] + + # Debug-Ausgabe + self.update_signal.emit(f"Debug - self.cmd_args: {self.cmd_args}") + self.update_signal.emit(f"Debug - self.output_dir: {self.output_dir}") + self.update_signal.emit(f"Debug - self.output_filename: {self.output_filename}") + + if self.cmd_args: + # Argumente per Split aufteilen, dabei aber Anführungszeichen berücksichtigen + args = [] + in_quotes = False + current_arg = "" + + for char in self.cmd_args: + if char == '"' or char == "'": + in_quotes = not in_quotes + current_arg += char + elif char.isspace() and not in_quotes: + if current_arg: + args.append(current_arg) + current_arg = "" + else: + current_arg += char + + if current_arg: + args.append(current_arg) + + # Debug-Ausgabe + self.update_signal.emit(f"Debug - Parsed args: {args}") + cmd.extend(args) + + if self.output_dir: + if self.output_filename: + output_path = os.path.join(self.output_dir, self.output_filename) + self.update_signal.emit(f"Debug - Vollständiger Ausgabepfad: {output_path}") + cmd.extend(["-o", output_path]) + else: + default_path = os.path.join(self.output_dir, "%(title)s.%(ext)s") + self.update_signal.emit(f"Debug - Standard-Ausgabepfad: {default_path}") + cmd.extend(["-o", default_path]) + elif self.output_filename: + self.update_signal.emit(f"Debug - Nur Ausgabedateiname: {self.output_filename}") + cmd.extend(["-o", self.output_filename]) + + cmd.append(self.url) + + # Debug-Ausgabe vor der Formatierung + self.update_signal.emit(f"Debug - Befehlszeile vor Formatierung: {cmd}") + + # Formatiere die Befehlszeile mit Anführungszeichen für Elemente mit Leerzeichen + formatted_cmd = [] + for arg in cmd: + if " " in arg and not (arg.startswith('"') and arg.endswith('"')) and not (arg.startswith("'") and arg.endswith("'")): + formatted_cmd.append(f'"{arg}"') + else: + formatted_cmd.append(arg) + + self.update_signal.emit(f"Ausführen: {' '.join(formatted_cmd)}") + + # Unterdrücke das CMD-Fenster unter Windows + creationflags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0 + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + universal_newlines=True, + creationflags=creationflags + ) + + for line in process.stdout: + self.update_signal.emit(line.strip()) + + process.wait() + + if process.returncode == 0: + self.finished_signal.emit(True, "Download erfolgreich abgeschlossen!") + else: + self.finished_signal.emit(False, f"Download fehlgeschlagen mit Exitcode {process.returncode}") + + except Exception as e: + self.update_signal.emit(f"Fehler: {str(e)}") + self.finished_signal.emit(False, f"Fehler: {str(e)}") + + +class PresetDialog(QDialog): + def __init__(self, parent=None, preset_data=None): + super().__init__(parent) + self.setWindowTitle("Preset erstellen/bearbeiten") + self.resize(500, 450) + + self.preset_data = preset_data or { + "name": "", + "description": "", + "args": "", + "has_series_template": False, + "series_template": SERIES_TEMPLATE, + "series": "", + "season": "1", + "episode": "1", + "extension": ".mkv", + "custom_path": "", + "is_audio": False + } + + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout() + form_layout = QFormLayout() + + self.name_edit = QLineEdit(self.preset_data["name"]) + self.description_edit = QLineEdit(self.preset_data["description"]) + self.args_edit = QTextEdit(self.preset_data["args"]) + + form_layout.addRow("Name:", self.name_edit) + form_layout.addRow("Beschreibung:", self.description_edit) + form_layout.addRow("yt-dlp Argumente:", self.args_edit) + + # Audio-Preset Checkbox + self.is_audio_cb = QCheckBox("Ist Audio-Preset (kein Remux nach MKV)") + self.is_audio_cb.setChecked(self.preset_data.get("is_audio", False)) + form_layout.addRow(self.is_audio_cb) + + # Referer + self.referer_edit = QLineEdit(self.preset_data.get("referer", "")) + self.referer_edit.setPlaceholderText("Optional: Referer-Link für --referer=") + form_layout.addRow("Referer:", self.referer_edit) + + # HLS-ffmpeg Checkbox + self.hls_ffmpeg_cb = QCheckBox("HLS-Streams mit ffmpeg herunterladen (--downloader ffmpeg --hls-use-mpegts)") + self.hls_ffmpeg_cb.setChecked(self.preset_data.get("hls_ffmpeg", False)) + form_layout.addRow(self.hls_ffmpeg_cb) + + # Untertitel-Optionen + self.sublang_edit = QLineEdit(self.preset_data.get("sublang", "")) + self.sublang_edit.setPlaceholderText("z.B. de, en, de,en ...") + form_layout.addRow("Untertitelsprache:", self.sublang_edit) + self.embed_subs_cb = QCheckBox("Untertitel einbetten (--embed-subs)") + self.embed_subs_cb.setChecked(self.preset_data.get("embed_subs", False)) + form_layout.addRow(self.embed_subs_cb) + + # Untertitel-Format Dropdown + self.subformat_combo = QComboBox() + self.subformat_combo.addItem("(keine Konvertierung)", "") + self.subformat_combo.addItem("srt", "srt") + self.subformat_combo.addItem("ass", "ass") + self.subformat_combo.addItem("tx3g", "tx3g") + # Vorbelegen + subformat = self.preset_data.get("subformat", "") + idx = self.subformat_combo.findData(subformat) + if idx >= 0: + self.subformat_combo.setCurrentIndex(idx) + form_layout.addRow("Untertitel-Format:", self.subformat_combo) + + # Eigener Pfad (immer sichtbar, mit Durchsuchen) + self.custom_path_edit = QLineEdit(self.preset_data.get("custom_path", "")) + self.custom_path_edit.setPlaceholderText("Optional: Eigener Zielordner für Download") + custom_path_hbox = QHBoxLayout() + custom_path_hbox.addWidget(self.custom_path_edit) + self.custom_path_browse_btn = QPushButton("Durchsuchen...") + self.custom_path_browse_btn.clicked.connect(self.browse_custom_path) + custom_path_hbox.addWidget(self.custom_path_browse_btn) + form_layout.addRow("Eigener Pfad:", custom_path_hbox) + + # Login-Felder + self.username_edit = QLineEdit(self.preset_data.get("username", "")) + self.username_edit.setPlaceholderText("Optional: Benutzername für Login (-u)") + form_layout.addRow("Benutzername:", self.username_edit) + pw_hbox = QHBoxLayout() + self.password_edit = QLineEdit(self.preset_data.get("password", "")) + self.password_edit.setEchoMode(QLineEdit.Password) + self.password_edit.setPlaceholderText("Optional: Passwort für Login (-p)") + self.show_pw_cb = QCheckBox("Passwort anzeigen") + self.show_pw_cb.toggled.connect(self.toggle_password_visible) + pw_hbox.addWidget(self.password_edit) + pw_hbox.addWidget(self.show_pw_cb) + form_layout.addRow("Passwort:", pw_hbox) + pw_hint = QLabel("Hinweis: Passwörter werden im Klartext lokal gespeichert!") + pw_hint.setStyleSheet("color: red;") + form_layout.addRow(pw_hint) + + # Serien-Template + self.series_box = QGroupBox("Serien-Template aktivieren") + self.series_box.setCheckable(True) + self.series_box.setChecked(self.preset_data.get("has_series_template", False)) + + series_layout = QFormLayout() + + self.template_edit = QLineEdit(self.preset_data.get("series_template", SERIES_TEMPLATE)) + self.series_edit = QLineEdit(self.preset_data.get("series", "")) + self.season_edit = QLineEdit(self.preset_data.get("season", "1")) + self.episode_edit = QLineEdit(self.preset_data.get("episode", "1")) + + series_layout.addRow("Template:", self.template_edit) + series_layout.addRow("Serie:", self.series_edit) + series_layout.addRow("Staffel:", self.season_edit) + series_layout.addRow("Folge:", self.episode_edit) + + help_text = QLabel("Verwende {series}, {season}, {episode}, %(ext)s und {path} im Template.") + series_layout.addRow(help_text) + + self.series_box.setLayout(series_layout) + + layout.addLayout(form_layout) + layout.addWidget(self.series_box) + + buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + + layout.addWidget(buttons) + self.setLayout(layout) + + def toggle_password_visible(self, checked): + self.password_edit.setEchoMode(QLineEdit.Normal if checked else QLineEdit.Password) + + def get_preset_data(self): + return { + "name": self.name_edit.text(), + "description": self.description_edit.text(), + "args": self.args_edit.toPlainText(), + "has_series_template": self.series_box.isChecked(), + "series_template": self.template_edit.text(), + "series": self.series_edit.text(), + "season": self.season_edit.text(), + "episode": self.episode_edit.text(), + "custom_path": self.custom_path_edit.text(), + "is_audio": self.is_audio_cb.isChecked(), + "username": self.username_edit.text(), + "password": self.password_edit.text(), + "referer": self.referer_edit.text(), + "hls_ffmpeg": self.hls_ffmpeg_cb.isChecked(), + "sublang": self.sublang_edit.text(), + "embed_subs": self.embed_subs_cb.isChecked(), + "subformat": self.subformat_combo.currentData() + } + + def browse_custom_path(self): + directory = QFileDialog.getExistingDirectory(self, "Eigenen Zielordner auswählen", self.custom_path_edit.text() or "") + if directory: + self.custom_path_edit.setText(directory) + + +class YtDlpDownloadThread(QThread): + finished_signal = pyqtSignal(bool, str) + def run(self): + try: + url = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe" + bin_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin") + os.makedirs(bin_dir, exist_ok=True) + dest_path = os.path.join(bin_dir, "yt-dlp.exe") + urllib.request.urlretrieve(url, dest_path) + self.finished_signal.emit(True, f"yt-dlp.exe wurde erfolgreich nach {dest_path} heruntergeladen.") + except Exception as e: + self.finished_signal.emit(False, f"Fehler beim Herunterladen von yt-dlp.exe: {str(e)}") + + +class OptionenDialog(QDialog): + def __init__(self, output_dir, use_local_ytdlp, parent=None, ytdlp_flags=None): + super().__init__(parent) + self.setWindowTitle("Optionen") + self.resize(420, 250) + self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) + self.selected_output_dir = output_dir + self.selected_use_local_ytdlp = use_local_ytdlp + self.selected_flags = ytdlp_flags or { + "ignore_config": False, + "remux_mkv": False, + "embed_metadata": False + } + self.setup_ui() + + def setup_ui(self): + main_layout = QVBoxLayout() + tabs = QTabWidget() + + # Tab 1: Allgemein + tab_allgemein = QWidget() + layout = QFormLayout() + self.output_dir_input = QLineEdit(self.selected_output_dir) + browse_btn = QPushButton("Durchsuchen...") + browse_btn.clicked.connect(self.browse_output_dir) + hbox = QHBoxLayout() + hbox.addWidget(self.output_dir_input) + hbox.addWidget(browse_btn) + layout.addRow("Standardpfad:", hbox) + self.ytdlp_source_combo = QComboBox() + self.ytdlp_source_combo.addItems(["Lokal (bin/yt-dlp.exe)", "System (PATH)"]) + self.ytdlp_source_combo.setCurrentIndex(0 if self.selected_use_local_ytdlp else 1) + layout.addRow("yt-dlp-Quelle:", self.ytdlp_source_combo) + # Default-Presets ausblenden + self.hide_defaults_cb = QCheckBox("Default-Presets ausblenden") + self.hide_defaults_cb.setChecked(self.parent().config.get("hide_default_presets", False) if self.parent() else False) + layout.addRow(self.hide_defaults_cb) + bin_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin") + ytdlp_path = os.path.join(bin_dir, "yt-dlp.exe") + # Button 1: Herunterladen + self.download_btn = QPushButton("yt-dlp.exe herunterladen") + self.download_btn.clicked.connect(self.download_ytdlp) + self.download_btn.setEnabled(not os.path.exists(ytdlp_path)) + layout.addRow(self.download_btn) + # Button 2: Updaten + self.update_btn = QPushButton("yt-dlp.exe updaten") + self.update_btn.clicked.connect(self.update_ytdlp) + layout.addRow(self.update_btn) + tab_allgemein.setLayout(layout) + tabs.addTab(tab_allgemein, "Allgemein") + + # Tab 2: yt-dlp-Flags + tab_flags = QWidget() + flags_layout = QVBoxLayout() + self.cb_ignore_config = QCheckBox("--ignore-config") + self.cb_ignore_config.setChecked(self.selected_flags.get("ignore_config", False)) + flags_layout.addWidget(self.cb_ignore_config) + flags_layout.addWidget(QLabel("Ignoriert die systemweite yt-dlp-Konfiguration.")) + self.cb_remux_mkv = QCheckBox("--remux-video mkv") + self.cb_remux_mkv.setChecked(self.selected_flags.get("remux_mkv", False)) + flags_layout.addWidget(self.cb_remux_mkv) + flags_layout.addWidget(QLabel("Remuxt das Video ins MKV-Format.")) + self.cb_embed_metadata = QCheckBox("--embed-metadata") + self.cb_embed_metadata.setChecked(self.selected_flags.get("embed_metadata", False)) + flags_layout.addWidget(self.cb_embed_metadata) + flags_layout.addWidget(QLabel("Betten Metadaten in die Ausgabedatei ein.")) + tab_flags.setLayout(flags_layout) + tabs.addTab(tab_flags, "yt-dlp-Flags") + + # Tab 3: Info + tab_info = QWidget() + info_layout = QVBoxLayout() + info_text = ( + "Version: 1.0
" + "© 2025 Akamaru
" + "Sourcecode: https://git.ponywave.de/Akamaru/video-download-helper
" + "Erstellt mit Hilfe von Claude, GPT & Gemini" + ) + info_label = QLabel(info_text) + info_label.setOpenExternalLinks(True) + info_label.setTextFormat(Qt.RichText) + info_layout.addWidget(info_label) + info_layout.addStretch(1) + tab_info.setLayout(info_layout) + tabs.addTab(tab_info, "Info") + + main_layout.addWidget(tabs) + # Stelle sicher, dass QDialogButtonBox nur einmal erstellt und verbunden wird + if hasattr(self, 'button_box'): + try: + main_layout.removeWidget(self.button_box) + self.button_box.deleteLater() + except Exception: + pass + self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.reject) + main_layout.addWidget(self.button_box) + self.setLayout(main_layout) + + def browse_output_dir(self): + directory = QFileDialog.getExistingDirectory(self, "Standardpfad auswählen", self.output_dir_input.text() or os.path.expanduser("~")) + if directory: + self.output_dir_input.setText(directory) + + def get_values(self): + return ( + self.output_dir_input.text(), + self.ytdlp_source_combo.currentIndex() == 0, + { + "ignore_config": self.cb_ignore_config.isChecked(), + "remux_mkv": self.cb_remux_mkv.isChecked(), + "embed_metadata": self.cb_embed_metadata.isChecked() + }, + self.hide_defaults_cb.isChecked() + ) + + def download_ytdlp(self): + self.download_btn.setEnabled(False) + self.download_btn.setText("Lade herunter...") + self.thread = YtDlpDownloadThread() + self.thread.finished_signal.connect(self.download_finished) + self.thread.start() + + def download_finished(self, success, message): + bin_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin") + ytdlp_path = os.path.join(bin_dir, "yt-dlp.exe") + self.download_btn.setEnabled(not os.path.exists(ytdlp_path)) + self.download_btn.setText("yt-dlp.exe herunterladen") + if success: + QMessageBox.information(self, "Erfolg", message) + else: + QMessageBox.critical(self, "Fehler", message) + + def update_ytdlp(self): + self.update_btn.setEnabled(False) + self.update_btn.setText("Aktualisiere...") + # Bestimme, ob lokal oder systemweit + use_local = self.ytdlp_source_combo.currentIndex() == 0 + if use_local: + bin_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin") + ytdlp_path = os.path.join(bin_dir, "yt-dlp.exe") + cmd = [ytdlp_path, "-U"] + else: + cmd = ["yt-dlp", "-U"] + try: + creationflags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0 + result = subprocess.run(cmd, capture_output=True, text=True, creationflags=creationflags) + if result.returncode == 0: + QMessageBox.information(self, "Erfolg", f"yt-dlp wurde aktualisiert.\n\n{result.stdout}") + else: + QMessageBox.warning(self, "Fehler", f"Fehler beim Updaten von yt-dlp:\n{result.stderr or result.stdout}") + except Exception as e: + QMessageBox.critical(self, "Fehler", f"Fehler beim Ausführen von yt-dlp -U: {str(e)}") + self.update_btn.setEnabled(True) + self.update_btn.setText("yt-dlp.exe updaten") + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Video Download Helper") + self.resize(800, 600) + + # Stelle sicher, dass Verzeichnisse existieren + self.ensure_directories() + + # Default-Presets automatisch anlegen, falls keine vorhanden + if not any(f.endswith('.json') for f in os.listdir(PRESETS_DIR)): + self.create_default_presets() + + self.config = self.load_config() + self.presets = self.load_presets() + + self.download_thread = None + + self.setup_ui() + + def ensure_directories(self): + """Stellt sicher, dass alle benötigten Verzeichnisse existieren.""" + # Stelle sicher, dass der Presets-Ordner existiert + os.makedirs(PRESETS_DIR, exist_ok=True) + + # Stelle sicher, dass der bin-Ordner existiert + bin_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin") + os.makedirs(bin_dir, exist_ok=True) + + def setup_ui(self): + central_widget = QWidget() + main_layout = QVBoxLayout() + + # URL Input + url_layout = QHBoxLayout() + url_layout.addWidget(QLabel("Video-URL:")) + self.url_input = QLineEdit() + url_layout.addWidget(self.url_input) + main_layout.addLayout(url_layout) + + # Preset Selection + preset_layout = QHBoxLayout() + preset_layout.addWidget(QLabel("Preset:")) + self.preset_combo = QComboBox() + self.update_preset_combo() + preset_layout.addWidget(self.preset_combo) + + # Preset Buttons + self.add_preset_btn = QPushButton("Neu") + self.add_preset_btn.clicked.connect(self.add_preset) + preset_layout.addWidget(self.add_preset_btn) + + self.edit_preset_btn = QPushButton("Bearbeiten") + self.edit_preset_btn.clicked.connect(self.edit_preset) + preset_layout.addWidget(self.edit_preset_btn) + + self.delete_preset_btn = QPushButton("Löschen") + self.delete_preset_btn.clicked.connect(self.delete_preset) + preset_layout.addWidget(self.delete_preset_btn) + + main_layout.addLayout(preset_layout) + + # Serien-Einstellungen + self.series_group = QGroupBox("Serien-Einstellungen") + self.series_group.setVisible(False) + + series_form = QFormLayout() + + self.series_input = QLineEdit() + self.season_input = QLineEdit() + self.episode_input = QLineEdit() + + series_form.addRow("Serie:", self.series_input) + series_form.addRow("Staffel:", self.season_input) + series_form.addRow("Folge:", self.episode_input) + + self.custom_path_input = QLineEdit() + + series_form.addRow("Eigener Pfad:", self.custom_path_input) + + self.series_group.setLayout(series_form) + main_layout.addWidget(self.series_group) + + # Options-Button statt Felder für Standardpfad und yt-dlp-Quelle + optionen_layout = QHBoxLayout() + self.optionen_btn = QPushButton("Optionen...") + self.optionen_btn.clicked.connect(self.open_optionen_dialog) + optionen_layout.addWidget(self.optionen_btn) + main_layout.addLayout(optionen_layout) + + # Command Preview + main_layout.addWidget(QLabel("Befehlsvorschau:")) + self.cmd_preview = QTextEdit() + self.cmd_preview.setReadOnly(True) + self.cmd_preview.setMaximumHeight(60) + main_layout.addWidget(self.cmd_preview) + + # Download Button + self.download_btn = QPushButton("Download starten") + self.download_btn.clicked.connect(self.start_download) + main_layout.addWidget(self.download_btn) + + # Log Output + main_layout.addWidget(QLabel("Ausgabe:")) + self.log_output = QTextEdit() + self.log_output.setReadOnly(True) + main_layout.addWidget(self.log_output) + + # Connect signals + self.url_input.textChanged.connect(self.update_cmd_preview) + self.preset_combo.currentIndexChanged.connect(self.preset_changed) + # self.optionen_btn.clicked.connect(self.open_optionen_dialog) # Entfernt, um doppeltes Öffnen zu verhindern + + # Serie, Staffel, Folge Signals + self.series_input.textChanged.connect(self.update_cmd_preview) + self.season_input.textChanged.connect(self.update_cmd_preview) + self.episode_input.textChanged.connect(self.update_cmd_preview) + self.custom_path_input.textChanged.connect(self.update_cmd_preview) + + central_widget.setLayout(main_layout) + self.setCentralWidget(central_widget) + + # Initial update + self.preset_changed() + self.update_cmd_preview() + + def load_config(self): + if os.path.exists(CONFIG_FILE): + try: + with open(CONFIG_FILE, 'r') as f: + return json.load(f) + except Exception: + return DEFAULT_CONFIG.copy() + return DEFAULT_CONFIG.copy() + + def save_config(self): + self.config["output_dir"] = self.output_dir_input.text() if hasattr(self, 'output_dir_input') else self.config.get("output_dir", "") + self.config["use_local_ytdlp"] = self.ytdlp_source_combo.currentIndex() == 0 if hasattr(self, 'ytdlp_source_combo') else self.config.get("use_local_ytdlp", True) + if self.preset_combo.currentIndex() >= 0: + self.config["last_preset"] = self.preset_combo.currentText() + try: + with open(CONFIG_FILE, 'w') as f: + json.dump(self.config, f, indent=4) + except Exception as e: + self.log_output.append(f"Fehler beim Speichern der Konfiguration: {str(e)}") + + def load_presets(self): + # Default-Presets immer aus dem Code laden + defaults = [ + { + "name": "Audio (MP3)", + "description": "Nur Audio als MP3", + "args": "-x --audio-format mp3", + "has_series_template": False, + "is_audio": True, + "source": "default" + }, + { + "name": "Video (Best)", + "description": "Beste Videoqualität", + "args": "-f bestvideo+bestaudio", + "has_series_template": False, + "is_audio": False, + "source": "default" + } + ] + presets = defaults.copy() + # User-Presets (können mitgelieferte überschreiben) + user_presets_dir = get_user_presets_dir() + for filename in os.listdir(user_presets_dir): + if filename.endswith('.json'): + try: + with open(os.path.join(user_presets_dir, filename), 'r', encoding='utf-8') as f: + preset = json.load(f) + preset["source"] = "user" + # Überschreibe ggf. gleichnamiges Preset + presets = [p for p in presets if p["name"] != preset["name"]] + presets.append(preset) + except Exception as e: + self.log_output.append(f"Fehler beim Laden von User-Preset {filename}: {str(e)}") + return presets + + def create_default_presets(self): + defaults = [ + { + "name": "Audio (MP3)", + "description": "Nur Audio als MP3", + "args": "-x --audio-format mp3", + "has_series_template": False, + "is_audio": True, + "source": "default" + }, + { + "name": "Video (Best)", + "description": "Beste Videoqualität", + "args": "-f bestvideo+bestaudio", + "has_series_template": False, + "is_audio": False, + "source": "default" + } + ] + + for preset in defaults: + filename = f"{preset['name'].lower().replace(' ', '_')}.json" + try: + with open(os.path.join(PRESETS_DIR, filename), 'w') as f: + json.dump(preset, f, indent=4) + except Exception as e: + self.log_output.append(f"Fehler beim Erstellen von Standard-Preset {preset['name']}: {str(e)}") + + def update_preset_combo(self): + self.preset_combo.clear() + hide_defaults = self.config.get("hide_default_presets", False) + for preset in self.presets: + if hide_defaults and preset.get("source") == "default": + continue + self.preset_combo.addItem(preset["name"]) + # Letztes verwendetes Preset auswählen + if self.config["last_preset"]: + index = self.preset_combo.findText(self.config["last_preset"]) + if index >= 0: + self.preset_combo.setCurrentIndex(index) + + def get_current_preset(self): + if self.preset_combo.currentIndex() >= 0: + preset_name = self.preset_combo.currentText() + for preset in self.presets: + if preset["name"] == preset_name: + return preset + return None + + def add_preset(self): + dialog = PresetDialog(self) + if dialog.exec_() == QDialog.Accepted: + preset_data = dialog.get_preset_data() + if not preset_data["name"]: + QMessageBox.warning(self, "Fehler", "Der Preset-Name darf nicht leer sein.") + return + for preset in self.presets: + if preset["name"] == preset_data["name"]: + QMessageBox.warning(self, "Fehler", f"Ein Preset mit dem Namen '{preset_data['name']}' existiert bereits.") + return + preset_data["source"] = "user" + self.presets.append(preset_data) + # Speichern des Presets im User-Presets-Ordner + user_presets_dir = get_user_presets_dir() + filename = f"{preset_data['name'].lower().replace(' ', '_')}.json" + try: + with open(os.path.join(user_presets_dir, filename), 'w', encoding='utf-8') as f: + json.dump(preset_data, f, indent=4, ensure_ascii=False) + self.update_preset_combo() + self.preset_combo.setCurrentText(preset_data["name"]) + self.update_cmd_preview() + except Exception as e: + self.log_output.append(f"Fehler beim Speichern des Presets: {str(e)}") + + def edit_preset(self): + current_preset = self.get_current_preset() + if not current_preset: + QMessageBox.warning(self, "Warnung", "Kein Preset ausgewählt.") + return + dialog = PresetDialog(self, current_preset) + if dialog.exec_() == QDialog.Accepted: + new_preset_data = dialog.get_preset_data() + if not new_preset_data["name"]: + QMessageBox.warning(self, "Fehler", "Der Preset-Name darf nicht leer sein.") + return + if new_preset_data["name"] != current_preset["name"]: + for preset in self.presets: + if preset["name"] == new_preset_data["name"] and preset != current_preset: + QMessageBox.warning(self, "Fehler", f"Ein Preset mit dem Namen '{new_preset_data['name']}' existiert bereits.") + return + if new_preset_data["name"] != current_preset["name"]: + old_filename = f"{current_preset['name'].lower().replace(' ', '_')}.json" + try: + os.remove(os.path.join(get_user_presets_dir(), old_filename)) + except Exception as e: + self.log_output.append(f"Warnung: Konnte alte Preset-Datei nicht löschen: {str(e)}") + for i, preset in enumerate(self.presets): + if preset["name"] == current_preset["name"]: + new_preset_data["source"] = "user" + self.presets[i] = new_preset_data + break + filename = f"{new_preset_data['name'].lower().replace(' ', '_')}.json" + try: + with open(os.path.join(get_user_presets_dir(), filename), 'w', encoding='utf-8') as f: + json.dump(new_preset_data, f, indent=4, ensure_ascii=False) + old_current_index = self.preset_combo.currentIndex() + self.update_preset_combo() + new_index = self.preset_combo.findText(new_preset_data["name"]) + if new_index >= 0: + self.preset_combo.setCurrentIndex(new_index) + else: + self.preset_combo.setCurrentIndex(old_current_index) + self.update_cmd_preview() + except Exception as e: + self.log_output.append(f"Fehler beim Speichern des Presets: {str(e)}") + + def delete_preset(self): + current_preset = self.get_current_preset() + if not current_preset: + QMessageBox.warning(self, "Warnung", "Kein Preset ausgewählt.") + return + reply = QMessageBox.question( + self, + "Preset löschen", + f"Möchten Sie das Preset '{current_preset['name']}' wirklich löschen?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + if reply == QMessageBox.Yes: + self.presets = [p for p in self.presets if p["name"] != current_preset["name"]] + filename = f"{current_preset['name'].lower().replace(' ', '_')}.json" + try: + os.remove(os.path.join(get_user_presets_dir(), filename)) + self.update_preset_combo() + self.update_cmd_preview() + except Exception as e: + self.log_output.append(f"Fehler beim Löschen des Presets: {str(e)}") + + def open_optionen_dialog(self): + dialog = OptionenDialog( + self.config["output_dir"], + self.config["use_local_ytdlp"], + self, + self.config.get("ytdlp_flags", DEFAULT_CONFIG["ytdlp_flags"]) + ) + if dialog.exec_() == QDialog.Accepted: + output_dir, use_local_ytdlp, ytdlp_flags, hide_defaults = dialog.get_values() + self.config["output_dir"] = output_dir + self.config["use_local_ytdlp"] = use_local_ytdlp + self.config["ytdlp_flags"] = ytdlp_flags + self.config["hide_default_presets"] = hide_defaults + self.update_preset_combo() + self.update_cmd_preview() + + def preset_changed(self): + preset = self.get_current_preset() + if not preset: + self.series_group.setVisible(False) + return + + # Aktualisiere Serien-UI basierend auf dem Preset + has_series = preset.get("has_series_template", False) + self.series_group.setVisible(has_series) + + if has_series: + self.series_input.setText(preset.get("series", "")) + self.season_input.setText(preset.get("season", "1")) + self.episode_input.setText(preset.get("episode", "1")) + self.custom_path_input.setText(preset.get("custom_path", "")) + + self.update_cmd_preview() + + def get_output_filename(self, preset): + if preset.get("has_series_template", False): + template = preset.get("series_template", SERIES_TEMPLATE) + series = self.series_input.text() or preset.get("series", "") + season = self.season_input.text() or preset.get("season", "1") + episode = self.episode_input.text() or preset.get("episode", "1") + custom_path = self.custom_path_input.text() or preset.get("custom_path", "") + # Stelle sicher, dass der Pfad mit einem Trennzeichen endet, wenn er nicht leer ist + if custom_path and not custom_path.endswith("/") and not custom_path.endswith("\\"): + custom_path += "/" # Verwende / als universellen Pfadtrenner + output_name = template + output_name = output_name.replace("{series}", series) + output_name = output_name.replace("{season}", season) + output_name = output_name.replace("{episode}", episode) + output_name = output_name.replace("{path}", custom_path) + # {extension} durch .%(ext)s ersetzen (auch rückwärtskompatibel) + output_name = output_name.replace("{extension}", ".%(ext)s") + # Falls jemand %(ext)s nicht im Template hat, ergänzen wir es am Ende + if "%(ext)s" not in output_name: + output_name += ".%(ext)s" + # Entferne doppelte Schrägstriche + while "//" in output_name: + output_name = output_name.replace("//", "/") + while "\\\\" in output_name: + output_name = output_name.replace("\\\\", "\\") + self.log_output.append("Ausgabedatei-Komponenten:") + self.log_output.append(f" Template: {template}") + self.log_output.append(f" Serie: {series}") + self.log_output.append(f" Staffel: {season}") + self.log_output.append(f" Folge: {episode}") + self.log_output.append(f" Eigener Pfad: {custom_path}") + self.log_output.append(f"Generierter Dateiname: {output_name}") + return output_name + # Debug-Ausgabe auch im Nicht-Serienfall + output_name = "%(title)s.%(ext)s" + custom_path = self.custom_path_input.text() or preset.get("custom_path", "") + if custom_path and not custom_path.endswith("/") and not custom_path.endswith("\\"): + custom_path += "/" + if custom_path: + output_name = custom_path + output_name + self.log_output.append("Ausgabedatei-Komponenten:") + self.log_output.append(f" Template: {output_name}") + self.log_output.append(f" Eigener Pfad: {custom_path}") + self.log_output.append(f"Generierter Dateiname: {output_name}") + return output_name + + def update_cmd_preview(self): + preset = self.get_current_preset() + if not preset: + self.cmd_preview.setText("Kein Preset ausgewählt.") + return + if self.config["use_local_ytdlp"]: + ytdlp_path = os.path.join(get_base_path(), "bin", "yt-dlp.exe") + if not os.path.exists(ytdlp_path): + ytdlp_path = "yt-dlp" # Fallback auf PATH + else: + ytdlp_path = "yt-dlp" + cmd = [ytdlp_path] + flags = self.config.get("ytdlp_flags", {}) + is_audio = preset.get("is_audio", False) + # Login-Optionen + if preset.get("username"): + cmd.extend(["-u", preset["username"]]) + if preset.get("password"): + cmd.extend(["-p", preset["password"]]) + # Referer + if preset.get("referer"): + cmd.append(f"--referer={preset['referer']}") + # HLS-ffmpeg + if preset.get("hls_ffmpeg"): + cmd.extend(["--downloader", "ffmpeg", "--hls-use-mpegts"]) + # Untertitel + if preset.get("sublang"): + cmd.extend(["--sub-lang", preset["sublang"]]) + if preset.get("embed_subs"): + cmd.append("--embed-subs") + if preset.get("subformat"): + cmd.extend(["--convert-subs", preset["subformat"]]) + if flags.get("ignore_config"): + cmd.append("--ignore-config") + if flags.get("remux_mkv") and not is_audio: + cmd.extend(["--remux-video", "mkv"]) + if flags.get("embed_metadata"): + cmd.append("--embed-metadata") + if preset["args"]: + args = [] + in_quotes = False + current_arg = "" + for char in preset["args"]: + if char == '"' or char == "'": + in_quotes = not in_quotes + current_arg += char + elif char.isspace() and not in_quotes: + if current_arg: + args.append(current_arg) + current_arg = "" + else: + current_arg += char + if current_arg: + args.append(current_arg) + cmd.extend(args) + output_dir = self.config["output_dir"] + output_filename = self.get_output_filename(preset) + if output_dir: + output_path = os.path.join(output_dir, output_filename) + cmd.extend(["-o", output_path]) + else: + cmd.extend(["-o", output_filename]) + url = self.url_input.text() or "[URL]" + cmd.append(url) + formatted_cmd = [] + for arg in cmd: + if " " in arg and not (arg.startswith('"') and arg.endswith('"')) and not (arg.startswith("'") and arg.endswith("'")): + formatted_cmd.append(f'"{arg}"') + else: + formatted_cmd.append(arg) + self.cmd_preview.setText(" ".join(formatted_cmd)) + + def start_download(self): + url = self.url_input.text() + if not url: + QMessageBox.warning(self, "Fehler", "Bitte geben Sie eine URL ein.") + return + preset = self.get_current_preset() + if not preset: + QMessageBox.warning(self, "Fehler", "Bitte wählen Sie ein Preset aus.") + return + output_dir = self.config["output_dir"] + use_local_ytdlp = self.config["use_local_ytdlp"] + flags = self.config.get("ytdlp_flags", {}) + is_audio = preset.get("is_audio", False) + extra_args = [] + if preset.get("username"): + extra_args.extend(["-u", preset["username"]]) + if preset.get("password"): + extra_args.extend(["-p", preset["password"]]) + if preset.get("referer"): + extra_args.append(f"--referer={preset['referer']}") + if preset.get("hls_ffmpeg"): + extra_args.extend(["--downloader", "ffmpeg", "--hls-use-mpegts"]) + if preset.get("sublang"): + extra_args.extend(["--sub-lang", preset["sublang"]]) + if preset.get("embed_subs"): + extra_args.append("--embed-subs") + if preset.get("subformat"): + extra_args.extend(["--convert-subs", preset["subformat"]]) + if flags.get("ignore_config"): + extra_args.append("--ignore-config") + if flags.get("remux_mkv") and not is_audio: + extra_args.extend(["--remux-video", "mkv"]) + if flags.get("embed_metadata"): + extra_args.append("--embed-metadata") + full_args = (" ".join(extra_args) + " " + preset["args"]).strip() if extra_args else preset["args"] + self.save_config() + self.log_output.clear() + self.log_output.append(f"Starte Download von: {url}") + output_filename = None + if preset.get("has_series_template", False): + output_filename = self.get_output_filename(preset) + self.log_output.append(f"Verwende Ausgabedateiname: {output_filename}") + self.log_output.append("Download-Parameter:") + self.log_output.append(f"URL: {url}") + self.log_output.append(f"Ausgabeverzeichnis: {output_dir}") + self.log_output.append(f"Argumente: {preset['args']}") + self.log_output.append(f"Lokales yt-dlp: {use_local_ytdlp}") + self.log_output.append(f"yt-dlp-Flags: {flags}") + self.log_output.append(f"Audio-Preset: {is_audio}") + self.log_output.append(f"Benutzername: {preset.get('username','')}") + self.log_output.append(f"Referer: {preset.get('referer','')}") + self.log_output.append(f"HLS-ffmpeg: {preset.get('hls_ffmpeg',False)}") + self.log_output.append(f"Untertitelsprache: {preset.get('sublang','')}") + self.log_output.append(f"Embed-Subs: {preset.get('embed_subs',False)}") + self.log_output.append(f"Untertitel-Format: {preset.get('subformat','')}") + self.log_output.append(f"Ausgabedateiname: {output_filename}") + self.download_thread = DownloadThread( + url=url, + output_dir=output_dir, + cmd_args=full_args, + use_local_ytdlp=use_local_ytdlp, + output_filename=output_filename + ) + self.download_thread.update_signal.connect(self.update_log) + self.download_thread.finished_signal.connect(self.download_finished) + self.download_btn.setEnabled(False) + self.download_thread.start() + + def update_log(self, text): + self.log_output.append(text) + # Scroll zum Ende + scrollbar = self.log_output.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + def download_finished(self, success, message): + self.download_btn.setEnabled(True) + + if success: + self.log_output.append(message) + QMessageBox.information(self, "Erfolg", message) + else: + self.log_output.append(f"Fehler: {message}") + QMessageBox.warning(self, "Fehler", message) + + def closeEvent(self, event): + self.save_config() + event.accept() + + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..43a6648 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +PyQt5==5.15.9 +PyInstaller==5.13.2 +requests==2.31.0 \ No newline at end of file diff --git a/startvdl.bat b/startvdl.bat new file mode 100644 index 0000000..9c2c326 --- /dev/null +++ b/startvdl.bat @@ -0,0 +1,2 @@ +@echo off +start "" /b pythonw.exe main.py \ No newline at end of file