diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000..7934aed Binary files /dev/null and b/icon.ico differ diff --git a/main.py b/main.py index dbc8ddb..615e1d6 100644 --- a/main.py +++ b/main.py @@ -20,6 +20,7 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QH QDialogButtonBox, QInputDialog, QGroupBox, QCheckBox, QTabWidget, QListWidgetItem, QMenu, QAction) from PyQt5.QtCore import QThread, pyqtSignal, Qt +from PyQt5.QtGui import QIcon # Hilfsfunktionen für den Ressourcenpfad def get_base_path(): @@ -37,6 +38,14 @@ def get_user_data_dir(): else: return os.path.dirname(os.path.abspath(__file__)) +def get_temp_dir(): + """Gibt den Pfad zum temporären Verzeichnis zurück und erstellt es bei Bedarf.""" + base_dir = get_base_path() + temp_dir = os.path.join(base_dir, "temp") + if not os.path.exists(temp_dir): + os.makedirs(temp_dir, exist_ok=True) + return temp_dir + def get_user_presets_dir(): # Presets-Ordner neben der EXE (bzw. Script) base = get_user_data_dir() @@ -45,6 +54,16 @@ def get_user_presets_dir(): os.makedirs(path, exist_ok=True) return path +def mask_sensitive_data(command_string): + """Maskiert sensible Daten wie Benutzernamen und Passwörter in der Befehlszeile.""" + # Benutzername maskieren (-u "username" oder --username "username") + command_string = re.sub(r'(-u|--username)\s+("[^"]+"|\'[^\']+\'|\S+)', r'\1 "******"', command_string) + + # Passwort maskieren (-p "password" oder --password "password") + command_string = re.sub(r'(-p|--password)\s+("[^"]+"|\'[^\']+\'|\S+)', r'\1 "******"', command_string) + + return command_string + 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 = { @@ -57,7 +76,9 @@ DEFAULT_CONFIG = { "remux_mkv": False, "embed_metadata": False }, - "hide_default_presets": False + "hide_default_presets": False, + "enable_adn_tab": False, + "mkvmerge_path": "C:\\Program Files\\MKVToolNix\\mkvmerge.exe" } # Template-Variablen für Serien @@ -66,8 +87,11 @@ SERIES_TEMPLATE = "{series} S{season}E{episode}{extension}" class DownloadThread(QThread): update_signal = pyqtSignal(str) finished_signal = pyqtSignal(bool, str) + format_selection_signal = pyqtSignal(list) # Signal für Format-Auswahl + format_id_input_signal = pyqtSignal() # Signal für Format-ID Eingabe - def __init__(self, url, output_dir, cmd_args, use_local_ytdlp, output_filename=None): + def __init__(self, url, output_dir, cmd_args, use_local_ytdlp, output_filename=None, + preset_data=None, config=None): super().__init__() self.url = url self.output_dir = output_dir @@ -76,6 +100,10 @@ class DownloadThread(QThread): self.output_filename = output_filename self.process = None self.abort = False + self.format_id = None + self.preset_data = preset_data or {} + self.config = config or {} + self.temp_files = [] # Liste der temporären Dateien für das Muxing def run(self): try: @@ -87,8 +115,36 @@ class DownloadThread(QThread): else: ytdlp_path = "yt-dlp" - cmd = [ytdlp_path] + # Prüfe, ob Format-Auswahl aktiviert ist + if self.preset_data.get("use_format_selection", False): + formats = self.get_available_formats(ytdlp_path) + if self.abort: + return + + # Warte auf Format-ID Eingabe vom Benutzer + self.format_selection_signal.emit(formats) + self.format_id_input_signal.emit() + + # Warte auf Format-ID (wird über set_format_id gesetzt) + while self.format_id is None: + if self.abort: + self.update_signal.emit("Abgebrochen.") + self.finished_signal.emit(False, "Download wurde abgebrochen.") + return + self.msleep(100) + + self.update_signal.emit(f"Format-ID ausgewählt: {self.format_id}") + # Prüfe auf Dual-Audio-Muxing + if self.preset_data.get("use_dual_audio", False): + # Stelle sicher, dass --remux-video mkv nicht in den Argumenten ist + self.cmd_args = re.sub(r'--remux-video\s+mkv\s*', '', self.cmd_args) + self.update_signal.emit("Dual-Audio-Modus aktiv: --remux-video mkv wird ignoriert") + self.perform_dual_audio_download(ytdlp_path) + return + + # Normaler Download-Prozess (bestehender Code) + 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}") @@ -118,6 +174,10 @@ class DownloadThread(QThread): self.update_signal.emit(f"Debug - Parsed args: {args}") cmd.extend(args) + # Bei Format-Auswahl die ID verwenden + if self.format_id and not self.preset_data.get("use_dual_audio", False): + cmd.extend(["-f", self.format_id]) + if self.output_dir: if self.output_filename: output_path = os.path.join(self.output_dir, self.output_filename) @@ -187,6 +247,449 @@ class DownloadThread(QThread): except: pass + def get_available_formats(self, ytdlp_path): + """Führt yt-dlp -F aus, um verfügbare Formate zu erhalten.""" + self.update_signal.emit("Sammle verfügbare Formate...") + + cmd = [ytdlp_path, "-F"] + + # Authentifizierungsdaten hinzufügen, falls vorhanden + if self.preset_data.get("username"): + cmd.extend(["-u", self.preset_data["username"]]) + if self.preset_data.get("password"): + cmd.extend(["-p", self.preset_data["password"]]) + + # Ignore-Config, falls eingestellt + if self.config.get("ytdlp_flags", {}).get("ignore_config", False): + cmd.append("--ignore-config") + + cmd.append(self.url) + + self.update_signal.emit(f"Ausführen: {' '.join(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 + ) + + formats = [] + + for line in process.stdout: + if self.abort: + process.terminate() + return formats + line = line.strip() + formats.append(line) + self.update_signal.emit(line) + + process.wait() + + if process.returncode != 0: + self.update_signal.emit(f"Fehler beim Abrufen der Formate (Exitcode {process.returncode})") + + return formats + + def set_format_id(self, format_id): + """Setzt die vom Benutzer ausgewählte Format-ID.""" + self.format_id = format_id + + def perform_dual_audio_download(self, ytdlp_path): + """Führt Dual-Audio-Download und Muxing durch.""" + try: + self.update_signal.emit("Starte Dual-Audio-Download...") + + # Temporäres Verzeichnis erstellen/sicherstellen + temp_dir = get_temp_dir() + self.update_signal.emit(f"Verwende temporäres Verzeichnis: {temp_dir}") + + # 1. Schritt: Format-Liste abrufen + if not self.format_id: + formats = self.get_available_formats(ytdlp_path) + if self.abort: + self.finished_signal.emit(False, "Download wurde abgebrochen.") + return + + # Warte auf Format-ID Eingabe vom Benutzer + self.format_selection_signal.emit(formats) + self.format_id_input_signal.emit() + + # Warte auf Format-ID (wird über set_format_id gesetzt) + while self.format_id is None: + if self.abort: + self.update_signal.emit("Abgebrochen.") + self.finished_signal.emit(False, "Download wurde abgebrochen.") + return + self.msleep(100) + + self.update_signal.emit(f"Format-ID ausgewählt: {self.format_id}") + + # 2. Schritt: Nummer für Dateinamen bestimmen (aus dem Template) + nummer = "01" # Standard, falls keine Serie + if self.preset_data.get("has_series_template", False): + # Prüfe, ob aktuelle Werte aus dem MainWindow verfügbar sind + # Das MainWindow speichert aktuelle Werte in self.parent() (falls verfügbar) + main_window = None + try: + # Versuche, das MainWindow zu finden (normalerweise über parent-Beziehung) + parent = self.parent() + if parent and hasattr(parent, 'episode_input') and hasattr(parent, 'season_input'): + main_window = parent + except: + self.update_signal.emit("Warnung: Konnte MainWindow nicht finden für aktuelle Serieneinstellungen.") + + if main_window: + # Verwende die Werte aus dem Hauptfenster + self.update_signal.emit("Verwende Serieneinstellungen aus dem Hauptfenster") + nummer = main_window.episode_input.text() or self.preset_data.get("episode", "01") + else: + # Fallback auf Preset-Daten + nummer = self.preset_data.get("episode", "01") + + # 3. Schritt: Untertitel herunterladen + self.update_signal.emit("Downloade Untertitel...") + sub_cmd = [ytdlp_path, "--quiet", "--progress"] + + # Authentifizierungsdaten hinzufügen + if self.preset_data.get("username"): + sub_cmd.extend(["-u", self.preset_data["username"]]) + if self.preset_data.get("password"): + sub_cmd.extend(["-p", self.preset_data["password"]]) + + # Config ignorieren, falls eingestellt + if self.config.get("ytdlp_flags", {}).get("ignore_config", False): + sub_cmd.append("--ignore-config") + + # Untertitel herunterladen in das temporäre Verzeichnis + sub_output_path = os.path.join(temp_dir, "subs.%(ext)s") + sub_cmd.extend(["--all-subs", "--skip-download", "-o", sub_output_path]) + sub_cmd.append(self.url) + + self.update_signal.emit(f"Ausführen: {' '.join(sub_cmd)}") + + # Unterdrücke das CMD-Fenster unter Windows + creationflags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0 + sub_process = subprocess.Popen( + sub_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + universal_newlines=True, + creationflags=creationflags + ) + + for line in sub_process.stdout: + if self.abort: + sub_process.terminate() + self.finished_signal.emit(False, "Download wurde abgebrochen.") + return + self.update_signal.emit(line.strip()) + + sub_process.wait() + + # Untertiteldateien umbenennen (jetzt im temp-Verzeichnis) + de_sub_filename = self.preset_data.get("de_sub_filename", f"subs.de.ass") + de_forced_sub_filename = self.preset_data.get("de_forced_sub_filename", f"subs.de.forced.ass") + + de_sub_file = os.path.join(temp_dir, de_sub_filename) + de_forced_sub_file = os.path.join(temp_dir, de_forced_sub_filename) + + # Prüfe, ob Dateien umbenannt werden müssen + try: + # Direkte Ausgabe der Dateiliste im Temp-Verzeichnis für Debugging + self.update_signal.emit("Dateien im Temp-Verzeichnis:") + for file in os.listdir(temp_dir): + self.update_signal.emit(f" {file}") + + temp_de_ssa = os.path.join(temp_dir, "subs.de.ssa") + temp_vde_ssa = os.path.join(temp_dir, "subs.vde.ssa") + + # Suche nach allen .ssa / .ass Dateien, falls die Namen nicht exakt stimmen + sub_files = [f for f in os.listdir(temp_dir) if f.endswith('.ssa') or f.endswith('.ass')] + self.update_signal.emit(f"Gefundene Untertiteldateien: {sub_files}") + + # Prüfe auf DE Untertitel + for sub_file in sub_files: + if "de.ssa" in sub_file.lower() and not "vde" in sub_file.lower(): + actual_de_file = os.path.join(temp_dir, sub_file) + if not os.path.exists(de_sub_file) or actual_de_file != de_sub_file: + # Richtige Datei gefunden, aber unter anderem Namen -> umbenennen + self.update_signal.emit(f"DE Untertitel gefunden als: {sub_file}") + if os.path.exists(de_sub_file): + os.remove(de_sub_file) # Falls bereits vorhanden, erst löschen + os.rename(actual_de_file, de_sub_file) + self.update_signal.emit(f"Untertitel umbenannt: {actual_de_file} -> {de_sub_file}") + self.temp_files.append(de_sub_file) + elif "vde" in sub_file.lower() and ".ssa" in sub_file.lower(): + actual_vde_file = os.path.join(temp_dir, sub_file) + if not os.path.exists(de_forced_sub_file) or actual_vde_file != de_forced_sub_file: + # Forced Untertitel gefunden, aber unter anderem Namen + self.update_signal.emit(f"VDE Forced Untertitel gefunden als: {sub_file}") + if os.path.exists(de_forced_sub_file): + os.remove(de_forced_sub_file) # Falls bereits vorhanden, erst löschen + os.rename(actual_vde_file, de_forced_sub_file) + self.update_signal.emit(f"Forced Untertitel umbenannt: {actual_vde_file} -> {de_forced_sub_file}") + self.temp_files.append(de_forced_sub_file) + + # Standardprüfung wie bisher, falls die obigen Prüfungen nichts gefunden haben + if os.path.exists(temp_de_ssa) and not os.path.exists(de_sub_file): + os.rename(temp_de_ssa, de_sub_file) + self.update_signal.emit(f"Untertitel umbenannt: {temp_de_ssa} -> {de_sub_file}") + self.temp_files.append(de_sub_file) + + if os.path.exists(temp_vde_ssa) and not os.path.exists(de_forced_sub_file): + os.rename(temp_vde_ssa, de_forced_sub_file) + self.update_signal.emit(f"Forced Untertitel umbenannt: {temp_vde_ssa} -> {de_forced_sub_file}") + self.temp_files.append(de_forced_sub_file) + except Exception as e: + self.update_signal.emit(f"Fehler beim Verarbeiten der Untertitel: {str(e)}") + + # 4. Schritt: Japanische Audio herunterladen + jap_prefix = self.preset_data.get("jap_prefix", "vostde") + format_suffix = self.preset_data.get("format_suffix", "-1") + + # Stelle sicher, dass wir .mp4 als Dateiendung verwenden + temp_jp_filename_base = self.preset_data.get("temp_jp_filename", f"video_jp.mp4") + # Ersetze %(ext)s durch mp4 (ohne Punkt), falls es noch im Dateinamen vorkommt + if "%(ext)s" in temp_jp_filename_base: + temp_jp_filename_base = temp_jp_filename_base.replace("%(ext)s", "mp4") + # Stelle sicher, dass die Datei mit .mp4 endet + if not temp_jp_filename_base.endswith(".mp4"): + temp_jp_filename_base += ".mp4" + + # Kompletter Pfad im temp-Verzeichnis + temp_jp_filename = os.path.join(temp_dir, temp_jp_filename_base) + + self.update_signal.emit("Downloade japanische Audio...") + jap_cmd = [ytdlp_path, "--quiet", "--progress"] + + # Authentifizierungsdaten hinzufügen + if self.preset_data.get("username"): + jap_cmd.extend(["-u", self.preset_data["username"]]) + if self.preset_data.get("password"): + jap_cmd.extend(["-p", self.preset_data["password"]]) + + # Config ignorieren, falls eingestellt + if self.config.get("ytdlp_flags", {}).get("ignore_config", False): + jap_cmd.append("--ignore-config") + + # Format-ID für japanische Audio + jap_format_id = f"{jap_prefix}-{self.format_id}{format_suffix}" + jap_cmd.extend(["-f", jap_format_id, "-o", temp_jp_filename]) + jap_cmd.append(self.url) + + self.update_signal.emit(f"Ausführen: {' '.join(jap_cmd)}") + + jap_process = subprocess.Popen( + jap_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + universal_newlines=True, + creationflags=creationflags + ) + + for line in jap_process.stdout: + if self.abort: + jap_process.terminate() + self.finished_signal.emit(False, "Download wurde abgebrochen.") + return + self.update_signal.emit(line.strip()) + + jap_process.wait() + + # Prüfe, ob die japanische Audiodatei existiert + jp_file = temp_jp_filename + if os.path.exists(jp_file): + self.temp_files.append(jp_file) + else: + self.update_signal.emit(f"Warnung: Japanische Audiodatei {jp_file} nicht gefunden.") + # Fehler zurückgeben, da ohne die Datei kein Muxing möglich ist + self.finished_signal.emit(False, f"Fehler: Japanische Audiodatei {jp_file} nicht gefunden.") + return + + # 5. Schritt: Deutsche Audio herunterladen + ger_prefix = self.preset_data.get("ger_prefix", "vde") + + # Stelle sicher, dass wir .mp4 als Dateiendung verwenden + temp_de_filename_base = self.preset_data.get("temp_de_filename", f"video_de.mp4") + # Ersetze %(ext)s durch mp4 (ohne Punkt), falls es noch im Dateinamen vorkommt + if "%(ext)s" in temp_de_filename_base: + temp_de_filename_base = temp_de_filename_base.replace("%(ext)s", "mp4") + # Stelle sicher, dass die Datei mit .mp4 endet + if not temp_de_filename_base.endswith(".mp4"): + temp_de_filename_base += ".mp4" + + # Kompletter Pfad im temp-Verzeichnis + temp_de_filename = os.path.join(temp_dir, temp_de_filename_base) + + self.update_signal.emit("Downloade deutsche Audio...") + ger_cmd = [ytdlp_path, "--quiet", "--progress"] + + # Authentifizierungsdaten hinzufügen + if self.preset_data.get("username"): + ger_cmd.extend(["-u", self.preset_data["username"]]) + if self.preset_data.get("password"): + ger_cmd.extend(["-p", self.preset_data["password"]]) + + # Config ignorieren, falls eingestellt + if self.config.get("ytdlp_flags", {}).get("ignore_config", False): + ger_cmd.append("--ignore-config") + + # Format-ID für deutsche Audio + ger_format_id = f"{ger_prefix}-{self.format_id}{format_suffix}" + ger_cmd.extend(["-f", ger_format_id, "-o", temp_de_filename]) + ger_cmd.append(self.url) + + self.update_signal.emit(f"Ausführen: {' '.join(ger_cmd)}") + + ger_process = subprocess.Popen( + ger_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + universal_newlines=True, + creationflags=creationflags + ) + + for line in ger_process.stdout: + if self.abort: + ger_process.terminate() + self.finished_signal.emit(False, "Download wurde abgebrochen.") + return + self.update_signal.emit(line.strip()) + + ger_process.wait() + + # Prüfe, ob die deutsche Audiodatei existiert + de_file = temp_de_filename + if os.path.exists(de_file): + self.temp_files.append(de_file) + else: + self.update_signal.emit(f"Warnung: Deutsche Audiodatei {de_file} nicht gefunden.") + # Fehler zurückgeben, da ohne die Datei kein Muxing möglich ist + self.finished_signal.emit(False, f"Fehler: Deutsche Audiodatei {de_file} nicht gefunden.") + return + + # 6. Schritt: MKVMerge ausführen + self.update_signal.emit("Starte MKV-Muxing...") + + # Bestimme MKVMerge-Pfad + mkvmerge_path = self.config.get("mkvmerge_path", "C:\\Program Files\\MKVToolNix\\mkvmerge.exe") + if not os.path.exists(mkvmerge_path): + self.update_signal.emit(f"Fehler: MKVMerge nicht gefunden unter {mkvmerge_path}") + self.finished_signal.emit(False, f"MKVMerge nicht gefunden. Bitte überprüfe den Pfad in den Optionen.") + return + + # Ausgabedateiname bestimmen + output_name = "" + if self.preset_data.get("has_series_template", False): + series = self.preset_data.get("series", "") + output_name = f"{series} {nummer}.mkv" + else: + # Verwende die Standardausgabe des Programms + output_name = self.output_filename + if output_name is None or "%(ext)s" in output_name: + # Fallback-Name oder %(ext)s im Namen ersetzen + output_name = f"output_{nummer}.mkv" + elif not output_name.endswith(".mkv"): + output_name = f"{output_name}.mkv" + + # Ausgabepfad bestimmen (NICHT im temp-Verzeichnis!) + output_path = os.path.join(self.output_dir, output_name) if self.output_dir else output_name + + # MKVMerge-Befehl zusammenstellen + mkvmerge_cmd = [ + mkvmerge_path, + "--ui-language", "de", + "--priority", "lower", + "--output", output_path, + "--language", "0:und", + "--default-track-flag", "0:no", + "--language", "1:ja", + "--default-track-flag", "1:no", + "(", jp_file, ")", + "--no-video", + "--no-global-tags", + "--language", "1:de", + "(", de_file, ")" + ] + + # Untertitel hinzufügen, falls vorhanden + if os.path.exists(de_sub_file): + mkvmerge_cmd.extend(["--language", "0:de", "(", de_sub_file, ")"]) + self.update_signal.emit(f"Untertitel gefunden: {de_sub_file}") + else: + self.update_signal.emit(f"Warnung: Untertitel nicht gefunden: {de_sub_file}") + + if os.path.exists(de_forced_sub_file): + mkvmerge_cmd.extend(["--language", "0:de", "--forced-display-flag", "0:yes", "(", de_forced_sub_file, ")"]) + self.update_signal.emit(f"Forced Untertitel gefunden: {de_forced_sub_file}") + else: + self.update_signal.emit(f"Warnung: Forced Untertitel nicht gefunden: {de_forced_sub_file}") + + # Track-Order (basierend auf den hinzugefügten Tracks) + track_order = "0:0,1:1,0:1" + if os.path.exists(de_sub_file): + track_order += ",2:0" + if os.path.exists(de_forced_sub_file): + track_order += f",{3 if os.path.exists(de_sub_file) else 2}:0" + + mkvmerge_cmd.extend(["--track-order", track_order]) + + # MKVMerge ausführen + self.update_signal.emit(f"Ausführen MKVMerge: {' '.join(mkvmerge_cmd)}") + + mkvmerge_process = subprocess.Popen( + mkvmerge_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + universal_newlines=True, + creationflags=creationflags + ) + + for line in mkvmerge_process.stdout: + if self.abort: + mkvmerge_process.terminate() + self.finished_signal.emit(False, "Muxing abgebrochen.") + return + self.update_signal.emit(line.strip()) + + mkvmerge_process.wait() + + if mkvmerge_process.returncode != 0 and mkvmerge_process.returncode != 1: # 1 ist Warnung, aber OK + self.update_signal.emit(f"MKVMerge fehlgeschlagen mit Exitcode {mkvmerge_process.returncode}") + self.finished_signal.emit(False, f"MKVMerge fehlgeschlagen mit Exitcode {mkvmerge_process.returncode}") + return + + # 7. Schritt: Aufräumen, falls gewünscht + if self.preset_data.get("cleanup_temp", True): + self.update_signal.emit("Aufräumen: Temporäre Dateien werden gelöscht...") + for file in self.temp_files: + try: + if os.path.exists(file): + os.remove(file) + self.update_signal.emit(f"Gelöscht: {file}") + except Exception as e: + self.update_signal.emit(f"Fehler beim Löschen von {file}: {str(e)}") + + self.update_signal.emit(f"Dual-Audio-Download und Muxing erfolgreich abgeschlossen! Ausgabedatei: {output_path}") + self.finished_signal.emit(True, f"Dual-Audio-Download und Muxing erfolgreich abgeschlossen!") + + except Exception as e: + self.update_signal.emit(f"Fehler beim Dual-Audio-Download: {str(e)}") + self.finished_signal.emit(False, f"Fehler beim Dual-Audio-Download: {str(e)}") class PresetDialog(QDialog): def __init__(self, parent=None, preset_data=None): @@ -194,6 +697,11 @@ class PresetDialog(QDialog): self.setWindowTitle("Preset erstellen/bearbeiten") self.resize(500, 450) + # Icon für den Dialog setzen + icon_path = os.path.join(get_base_path(), "icon.ico") + if os.path.exists(icon_path): + self.setWindowIcon(QIcon(icon_path)) + self.preset_data = preset_data or { "name": "", "description": "", @@ -212,6 +720,12 @@ class PresetDialog(QDialog): def setup_ui(self): layout = QVBoxLayout() + + # Tabs hinzufügen + tabs = QTabWidget() + + # Tab 1: Grundeinstellungen + basic_tab = QWidget() form_layout = QFormLayout() self.name_edit = QLineEdit(self.preset_data["name"]) @@ -222,6 +736,16 @@ class PresetDialog(QDialog): form_layout.addRow("Beschreibung:", self.description_edit) form_layout.addRow("yt-dlp Argumente:", self.args_edit) + # Eigener Pfad (immer sichtbar, mit Durchsuchen) - hierher verschoben + 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) + # 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)) @@ -237,13 +761,21 @@ class PresetDialog(QDialog): self.hls_ffmpeg_cb.setChecked(self.preset_data.get("hls_ffmpeg", False)) form_layout.addRow(self.hls_ffmpeg_cb) + basic_tab.setLayout(form_layout) + tabs.addTab(basic_tab, "Grundeinstellungen") + + # Tab 2: Untertitel + subtitle_tab = QWidget() + subtitle_layout = QFormLayout() + # 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) + subtitle_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) + subtitle_layout.addRow(self.embed_subs_cb) # Untertitel-Format Dropdown self.subformat_combo = QComboBox() @@ -256,22 +788,20 @@ class PresetDialog(QDialog): idx = self.subformat_combo.findData(subformat) if idx >= 0: self.subformat_combo.setCurrentIndex(idx) - form_layout.addRow("Untertitel-Format:", self.subformat_combo) + subtitle_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) + subtitle_tab.setLayout(subtitle_layout) + tabs.addTab(subtitle_tab, "Untertitel") + + # Tab 3: Authentifizierung + auth_tab = QWidget() + auth_layout = QFormLayout() # 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) + auth_layout.addRow("Benutzername:", self.username_edit) + pw_hbox = QHBoxLayout() self.password_edit = QLineEdit(self.preset_data.get("password", "")) self.password_edit.setEchoMode(QLineEdit.Password) @@ -280,10 +810,18 @@ class PresetDialog(QDialog): 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) + auth_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) + auth_layout.addRow(pw_hint) + + auth_tab.setLayout(auth_layout) + tabs.addTab(auth_tab, "Authentifizierung") + + # Tab 4: Pfade & Serien + path_tab = QWidget() + path_layout = QFormLayout() # Serien-Template self.series_box = QGroupBox("Serien-Template aktivieren") @@ -306,9 +844,81 @@ class PresetDialog(QDialog): series_layout.addRow(help_text) self.series_box.setLayout(series_layout) + path_layout.addWidget(self.series_box) - layout.addLayout(form_layout) - layout.addWidget(self.series_box) + path_tab.setLayout(path_layout) + tabs.addTab(path_tab, "Serien") + + # Tab 5: ADN (vorher Experte) + adn_tab = QWidget() + adn_layout = QVBoxLayout() + + # F-Option und Format-ID Checkbox + self.use_format_selection_cb = QCheckBox("Format-Auswahl aktivieren") + self.use_format_selection_cb.setChecked(self.preset_data.get("use_format_selection", False)) + self.use_format_selection_cb.toggled.connect(self.toggle_format_selection) + adn_layout.addWidget(self.use_format_selection_cb) + + # Beschreibung der Format-Auswahl + format_desc = QLabel( + "Diese Option führt beim Start des Downloads zuerst 'yt-dlp -F' aus,\n" + "um verfügbare Formate anzuzeigen. Anschließend wird nach einer Format-ID gefragt." + ) + adn_layout.addWidget(format_desc) + + # Dual-Audio (Jap+Ger) und Untertitel Muxing aktivieren + self.use_dual_audio_cb = QCheckBox("Dual-Audio Muxing aktivieren") + self.use_dual_audio_cb.setChecked(self.preset_data.get("use_dual_audio", False)) + self.use_dual_audio_cb.toggled.connect(self.toggle_dual_audio) + adn_layout.addWidget(self.use_dual_audio_cb) + + # Dual-Audio Gruppe + self.dual_audio_group = QGroupBox("Dual-Audio Einstellungen") + self.dual_audio_group.setEnabled(self.preset_data.get("use_dual_audio", False)) + + dual_form = QFormLayout() + + # Format ID Präfixe + self.jap_prefix_edit = QLineEdit(self.preset_data.get("jap_prefix", "vostde")) + dual_form.addRow("Prefix für japanische Audio:", self.jap_prefix_edit) + + self.ger_prefix_edit = QLineEdit(self.preset_data.get("ger_prefix", "vde")) + dual_form.addRow("Prefix für deutsche Audio:", self.ger_prefix_edit) + + # Suffix (Standard: -1) + self.format_suffix_edit = QLineEdit(self.preset_data.get("format_suffix", "-1")) + dual_form.addRow("Format-Suffix:", self.format_suffix_edit) + + # Dateinamen für temporäre Dateien + self.temp_jp_filename_edit = QLineEdit(self.preset_data.get("temp_jp_filename", "video_jp.mp4")) + dual_form.addRow("Temp. Dateiname JP:", self.temp_jp_filename_edit) + + self.temp_de_filename_edit = QLineEdit(self.preset_data.get("temp_de_filename", "video_de.mp4")) + dual_form.addRow("Temp. Dateiname DE:", self.temp_de_filename_edit) + + # Untertitel-Dateien + self.de_sub_filename_edit = QLineEdit(self.preset_data.get("de_sub_filename", "subs.de.ass")) + dual_form.addRow("DE Untertitel-Datei:", self.de_sub_filename_edit) + + self.de_forced_sub_filename_edit = QLineEdit(self.preset_data.get("de_forced_sub_filename", "subs.de.forced.ass")) + dual_form.addRow("DE forced Untertitel:", self.de_forced_sub_filename_edit) + + # Cleanup-Option + self.cleanup_temp_cb = QCheckBox("Temporäre Dateien nach dem Muxing löschen") + self.cleanup_temp_cb.setChecked(self.preset_data.get("cleanup_temp", True)) + dual_form.addRow(self.cleanup_temp_cb) + + self.dual_audio_group.setLayout(dual_form) + adn_layout.addWidget(self.dual_audio_group) + + adn_tab.setLayout(adn_layout) + + # ADN Tab nur hinzufügen, wenn aktiviert + self.adn_tab_index = None + if self.parent() and self.parent().config.get("enable_adn_tab", False): + self.adn_tab_index = tabs.addTab(adn_tab, "ADN") + + layout.addWidget(tabs) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self.accept) @@ -320,6 +930,14 @@ class PresetDialog(QDialog): def toggle_password_visible(self, checked): self.password_edit.setEchoMode(QLineEdit.Normal if checked else QLineEdit.Password) + def toggle_format_selection(self, checked): + # Diese Methode setzt Flags, wenn die Format-Auswahl aktiviert wird + pass + + def toggle_dual_audio(self, checked): + # Aktiviere/Deaktiviere die Dual-Audio-Einstellungen + self.dual_audio_group.setEnabled(checked) + def get_preset_data(self): return { "name": self.name_edit.text(), @@ -338,7 +956,17 @@ class PresetDialog(QDialog): "hls_ffmpeg": self.hls_ffmpeg_cb.isChecked(), "sublang": self.sublang_edit.text(), "embed_subs": self.embed_subs_cb.isChecked(), - "subformat": self.subformat_combo.currentData() + "subformat": self.subformat_combo.currentData(), + "use_format_selection": self.use_format_selection_cb.isChecked(), + "use_dual_audio": self.use_dual_audio_cb.isChecked(), + "jap_prefix": self.jap_prefix_edit.text(), + "ger_prefix": self.ger_prefix_edit.text(), + "format_suffix": self.format_suffix_edit.text(), + "temp_jp_filename": self.temp_jp_filename_edit.text(), + "temp_de_filename": self.temp_de_filename_edit.text(), + "de_sub_filename": self.de_sub_filename_edit.text(), + "de_forced_sub_filename": self.de_forced_sub_filename_edit.text(), + "cleanup_temp": self.cleanup_temp_cb.isChecked() } def browse_custom_path(self): @@ -367,6 +995,12 @@ class OptionenDialog(QDialog): self.setWindowTitle("Optionen") self.resize(420, 250) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) + + # Icon für den Dialog setzen + icon_path = os.path.join(get_base_path(), "icon.ico") + if os.path.exists(icon_path): + self.setWindowIcon(QIcon(icon_path)) + self.selected_output_dir = output_dir self.selected_use_local_ytdlp = use_local_ytdlp self.selected_flags = ytdlp_flags or { @@ -390,14 +1024,31 @@ class OptionenDialog(QDialog): hbox.addWidget(self.output_dir_input) hbox.addWidget(browse_btn) layout.addRow("Standardpfad:", hbox) + + # MKVMerge-Pfad hinzufügen + self.mkvmerge_path_input = QLineEdit(self.parent().config.get("mkvmerge_path", "C:\\Program Files\\MKVToolNix\\mkvmerge.exe") if self.parent() else "C:\\Program Files\\MKVToolNix\\mkvmerge.exe") + mkvmerge_browse_btn = QPushButton("Durchsuchen...") + mkvmerge_browse_btn.clicked.connect(self.browse_mkvmerge_path) + mkvmerge_hbox = QHBoxLayout() + mkvmerge_hbox.addWidget(self.mkvmerge_path_input) + mkvmerge_hbox.addWidget(mkvmerge_browse_btn) + layout.addRow("MKVMerge-Pfad:", mkvmerge_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) + + # ADN Tab aktivieren + self.enable_adn_tab_cb = QCheckBox("ADN Tab aktivieren") + self.enable_adn_tab_cb.setChecked(self.parent().config.get("enable_adn_tab", False) if self.parent() else False) + layout.addRow(self.enable_adn_tab_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 @@ -466,6 +1117,13 @@ class OptionenDialog(QDialog): if directory: self.output_dir_input.setText(directory) + def browse_mkvmerge_path(self): + file_path, _ = QFileDialog.getOpenFileName(self, "MKVMerge-Executable auswählen", + self.mkvmerge_path_input.text() or "C:\\Program Files\\MKVToolNix", + "Executable (*.exe)") + if file_path: + self.mkvmerge_path_input.setText(file_path) + def get_values(self): return ( self.output_dir_input.text(), @@ -475,7 +1133,9 @@ class OptionenDialog(QDialog): "remux_mkv": self.cb_remux_mkv.isChecked(), "embed_metadata": self.cb_embed_metadata.isChecked() }, - self.hide_defaults_cb.isChecked() + self.hide_defaults_cb.isChecked(), + self.mkvmerge_path_input.text(), + self.enable_adn_tab_cb.isChecked() ) def download_ytdlp(self): @@ -581,6 +1241,11 @@ class MainWindow(QMainWindow): self.setWindowTitle("Video Download Helper") self.resize(800, 600) + # Icon für das Hauptfenster setzen + icon_path = os.path.join(get_base_path(), "icon.ico") + if os.path.exists(icon_path): + self.setWindowIcon(QIcon(icon_path)) + # Stelle sicher, dass Verzeichnisse existieren self.ensure_directories() @@ -949,11 +1614,13 @@ class MainWindow(QMainWindow): 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() + output_dir, use_local_ytdlp, ytdlp_flags, hide_defaults, mkvmerge_path, enable_adn_tab = 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.config["mkvmerge_path"] = mkvmerge_path + self.config["enable_adn_tab"] = enable_adn_tab self.update_preset_combo() self.update_cmd_preview() @@ -1092,7 +1759,11 @@ class MainWindow(QMainWindow): formatted_cmd.append(f'"{arg}"') else: formatted_cmd.append(arg) - self.cmd_preview.setText(" ".join(formatted_cmd)) + + # Maskiere sensible Daten in der Befehlsvorschau + command_string = " ".join(formatted_cmd) + masked_command = mask_sensitive_data(command_string) + self.cmd_preview.setText(masked_command) def start_download(self): # Wenn bereits ein Download läuft und der Button als "Abbrechen" angezeigt wird @@ -1117,6 +1788,18 @@ class MainWindow(QMainWindow): use_local_ytdlp = self.config["use_local_ytdlp"] flags = self.config.get("ytdlp_flags", {}) is_audio = preset.get("is_audio", False) + use_dual_audio = preset.get("use_dual_audio", False) + + # Aktualisiere die Serien-Informationen im Preset für den Download + # (nur temporär für diesen Download, nicht dauerhaft speichern) + if preset.get("has_series_template", False): + updated_preset = preset.copy() + updated_preset["series"] = self.series_input.text() or preset.get("series", "") + updated_preset["season"] = self.season_input.text() or preset.get("season", "1") + updated_preset["episode"] = self.episode_input.text() or preset.get("episode", "1") + else: + updated_preset = preset + extra_args = [] if preset.get("username"): extra_args.extend(["-u", preset["username"]]) @@ -1134,17 +1817,20 @@ class MainWindow(QMainWindow): 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: + # --remux-video mkv nicht verwenden, wenn Dual-Audio aktiv ist + if flags.get("remux_mkv") and not is_audio and not use_dual_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) + output_filename = self.get_output_filename(updated_preset) self.log_output.append(f"Verwende Ausgabedateiname: {output_filename}") self.log_output.append("Download-Parameter:") self.log_output.append(f"URL: {url}") @@ -1160,16 +1846,33 @@ class MainWindow(QMainWindow): 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}") + + # Format-Auswahl und Dual-Audio + if preset.get("use_format_selection", False): + self.log_output.append("Format-Auswahl ist aktiviert.") + if preset.get("use_dual_audio", False): + self.log_output.append("Dual-Audio Muxing ist aktiviert.") + if preset.get("use_dual_audio", False) and not self.config.get("mkvmerge_path"): + QMessageBox.warning(self, "Fehler", "Für Dual-Audio Muxing muss der MKVMerge-Pfad in den Optionen angegeben werden.") + return + self.download_thread = DownloadThread( url=url, output_dir=output_dir, cmd_args=full_args, use_local_ytdlp=use_local_ytdlp, - output_filename=output_filename + output_filename=output_filename, + preset_data=updated_preset, # Hier das aktualisierte Preset verwenden + config=self.config ) self.download_thread.update_signal.connect(self.update_log) self.download_thread.finished_signal.connect(self.download_finished) + # Format-Auswahl für Experten-Modus + if preset.get("use_format_selection", False) or preset.get("use_dual_audio", False): + self.download_thread.format_selection_signal.connect(self.show_format_selection) + self.download_thread.format_id_input_signal.connect(self.prompt_format_id) + # Ändere den Button-Text und deaktiviere UI-Elemente während des Downloads self.download_btn.setText("Download abbrechen") self.disable_ui_during_download(True) @@ -1460,18 +2163,61 @@ class MainWindow(QMainWindow): self.log_output.append(f"Preset: {self.current_queue_item.preset_data.get('name', 'Unbekannt')}") self.log_output.append(f"Ausgabeverzeichnis: {self.current_queue_item.output_dir}") + # Spezielle Optionen anzeigen + use_format_selection = self.current_queue_item.preset_data.get("use_format_selection", False) + use_dual_audio = self.current_queue_item.preset_data.get("use_dual_audio", False) + + # Überprüfe und aktualisiere ggf. Serien-Informationen aus dem Hauptfenster + if self.current_queue_item.preset_data.get("has_series_template", False) and self.current_queue_item.series_info: + # Aktualisiere mit aktuellen Werten aus UI wenn verfügbar + updated_preset = self.current_queue_item.preset_data.copy() + + # Aktuelle Werte aus dem Hauptfenster verwenden (falls Felder sichtbar sind) + if hasattr(self, 'series_input') and self.series_input.isVisible(): + self.current_queue_item.series_info["series"] = self.series_input.text() or self.current_queue_item.series_info.get("series", "") + self.current_queue_item.series_info["season"] = self.season_input.text() or self.current_queue_item.series_info.get("season", "1") + self.current_queue_item.series_info["episode"] = self.episode_input.text() or self.current_queue_item.series_info.get("episode", "1") + + updated_preset["series"] = self.current_queue_item.series_info.get("series", "") + updated_preset["season"] = self.current_queue_item.series_info.get("season", "1") + updated_preset["episode"] = self.current_queue_item.series_info.get("episode", "1") + + self.current_queue_item.preset_data = updated_preset + + if use_format_selection: + self.log_output.append("Format-Auswahl ist aktiviert.") + if use_dual_audio: + self.log_output.append("Dual-Audio Muxing ist aktiviert.") + # Überprüfung des MKVMerge-Pfads + if not self.config.get("mkvmerge_path"): + self.log_output.append("Warnung: Kein MKVMerge-Pfad konfiguriert. Dual-Audio kann fehlschlagen.") + + # Falls Dual-Audio aktiviert ist, sollten wir --remux-video mkv aus den Argumenten entfernen + if use_dual_audio: + cmd_args = self.current_queue_item.extra_args + # Entferne --remux-video mkv aus den Argumenten, wenn vorhanden + cmd_args = re.sub(r'--remux-video\s+mkv\s*', '', cmd_args) + self.current_queue_item.extra_args = cmd_args + # Download-Thread erstellen und starten self.download_thread = DownloadThread( url=self.current_queue_item.url, output_dir=self.current_queue_item.output_dir, cmd_args=self.current_queue_item.extra_args, use_local_ytdlp=self.current_queue_item.use_local_ytdlp, - output_filename=self.current_queue_item.output_filename + output_filename=self.current_queue_item.output_filename, + preset_data=self.current_queue_item.preset_data, + config=self.config ) self.download_thread.update_signal.connect(self.update_log) self.download_thread.finished_signal.connect(self.queue_download_finished) + # Format-Auswahl für Experten-Modus + if self.current_queue_item.preset_data.get("use_format_selection", False) or self.current_queue_item.preset_data.get("use_dual_audio", False): + self.download_thread.format_selection_signal.connect(self.show_format_selection) + self.download_thread.format_id_input_signal.connect(self.prompt_format_id) + # Ändere den Button-Text und deaktiviere UI-Elemente während des Downloads self.download_btn.setText("Download abbrechen") self.disable_ui_during_download(True) @@ -1531,9 +2277,49 @@ class MainWindow(QMainWindow): # Queue speichern (leere Liste) self.save_queue() + def show_format_selection(self, formats): + """Zeigt die verfügbaren Formate in der Ausgabe an.""" + self.log_output.append("\n=== Verfügbare Formate ===") + for format_line in formats: + self.log_output.append(format_line) + self.log_output.append("=====================") + + def prompt_format_id(self): + """Fordert den Benutzer zur Eingabe einer Format-ID auf.""" + dialog = QInputDialog(self) + dialog.setWindowTitle("Format-ID eingeben") + dialog.setLabelText("Bitte gib die gewünschte Format-ID ein:") + dialog.setInputMode(QInputDialog.TextInput) + dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint) # Fragezeichen entfernen + + # Icon für den Dialog setzen + icon_path = os.path.join(get_base_path(), "icon.ico") + if os.path.exists(icon_path): + dialog.setWindowIcon(QIcon(icon_path)) + + ok = dialog.exec_() + format_id = dialog.textValue() + + if ok and format_id: + self.log_output.append(f"Format-ID ausgewählt: {format_id}") + # Setze die Format-ID im Download-Thread + if self.download_thread and self.download_thread.isRunning(): + self.download_thread.set_format_id(format_id) + else: + # Bei Abbruch den Download beenden + self.log_output.append("Format-Auswahl abgebrochen.") + if self.download_thread and self.download_thread.isRunning(): + self.download_thread.abort = True + if __name__ == "__main__": app = QApplication(sys.argv) + + # Globales Icon für die Anwendung setzen + icon_path = os.path.join(get_base_path(), "icon.ico") + if os.path.exists(icon_path): + app.setWindowIcon(QIcon(icon_path)) + window = MainWindow() window.show() sys.exit(app.exec_()) \ No newline at end of file