2497 lines
113 KiB
Python
2497 lines
113 KiB
Python
#!/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
|
|
import uuid
|
|
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QLabel, QLineEdit, QPushButton, QComboBox, QTextEdit,
|
|
QFileDialog, QMessageBox, QListWidget, QDialog, QFormLayout,
|
|
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():
|
|
"""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_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()
|
|
path = os.path.join(base, "presets")
|
|
if not os.path.exists(path):
|
|
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 = {
|
|
"output_dir": "",
|
|
"use_local_ytdlp": True,
|
|
"last_preset": "",
|
|
"presets": [],
|
|
"ytdlp_flags": {
|
|
"ignore_config": False,
|
|
"remux_mkv": False,
|
|
"embed_metadata": False,
|
|
"use_ffmpeg_location": False
|
|
},
|
|
"hide_default_presets": False,
|
|
"enable_adn_tab": False,
|
|
"mkvmerge_path": "C:\\Program Files\\MKVToolNix\\mkvmerge.exe",
|
|
"ffmpeg_path": "C:\\ffmpeg\\bin\\ffmpeg.exe"
|
|
}
|
|
|
|
# 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)
|
|
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,
|
|
preset_data=None, config=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
|
|
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:
|
|
# 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"
|
|
|
|
# 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}")
|
|
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)
|
|
|
|
# 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)
|
|
self.update_signal.emit(f"Debug - Vollständiger Ausgabepfad: {output_path}")
|
|
cmd.extend(["-o", output_path])
|
|
else:
|
|
# Verwende benutzerdefinierten Namen, wenn in den Presets aktiviert
|
|
if self.preset_data.get("custom_output_template", False) and self.preset_data.get("output_template"):
|
|
output_template = self.preset_data.get("output_template")
|
|
output_path = os.path.join(self.output_dir, output_template)
|
|
self.update_signal.emit(f"Debug - Benutzerdefinierte Name: {output_path}")
|
|
else:
|
|
output_path = os.path.join(self.output_dir, "%(title)s.%(ext)s")
|
|
self.update_signal.emit(f"Debug - Standard-Ausgabepfad: {output_path}")
|
|
cmd.extend(["-o", output_path])
|
|
elif self.output_filename:
|
|
self.update_signal.emit(f"Debug - Nur Ausgabedateiname: {self.output_filename}")
|
|
cmd.extend(["-o", self.output_filename])
|
|
elif self.preset_data.get("custom_output_template", False) and self.preset_data.get("output_template"):
|
|
# Wenn kein Ausgabeverzeichnis, aber benutzerdefinierte Vorlage vorhanden
|
|
output_template = self.preset_data.get("output_template")
|
|
self.update_signal.emit(f"Debug - Nur benutzerdefinierte Name: {output_template}")
|
|
cmd.extend(["-o", output_template])
|
|
|
|
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
|
|
self.process = subprocess.Popen(
|
|
cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
bufsize=1,
|
|
universal_newlines=True,
|
|
creationflags=creationflags
|
|
)
|
|
|
|
for line in self.process.stdout:
|
|
if self.abort:
|
|
self.process.terminate()
|
|
self.update_signal.emit("Download abgebrochen.")
|
|
self.finished_signal.emit(False, "Download wurde abgebrochen.")
|
|
return
|
|
self.update_signal.emit(line.strip())
|
|
|
|
self.process.wait()
|
|
|
|
if self.abort:
|
|
self.finished_signal.emit(False, "Download wurde abgebrochen.")
|
|
elif self.process.returncode == 0:
|
|
self.finished_signal.emit(True, "Download erfolgreich abgeschlossen!")
|
|
else:
|
|
self.finished_signal.emit(False, f"Download fehlgeschlagen mit Exitcode {self.process.returncode}")
|
|
|
|
except Exception as e:
|
|
self.update_signal.emit(f"Fehler: {str(e)}")
|
|
self.finished_signal.emit(False, f"Fehler: {str(e)}")
|
|
|
|
def stop(self):
|
|
self.abort = True
|
|
if self.process:
|
|
try:
|
|
self.update_signal.emit("Versuche Download zu beenden...")
|
|
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):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Preset erstellen/bearbeiten")
|
|
self.resize(500, 450)
|
|
|
|
# Entferne den Hilfe-Button (Fragezeichen) in der Titelleiste
|
|
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.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,
|
|
"custom_output_template": False,
|
|
"output_template": "%(title)s.%(ext)s"
|
|
}
|
|
|
|
self.setup_ui()
|
|
|
|
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"])
|
|
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)
|
|
|
|
# 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))
|
|
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)
|
|
|
|
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 ...")
|
|
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))
|
|
subtitle_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)
|
|
subtitle_layout.addRow("Untertitel-Format:", self.subformat_combo)
|
|
|
|
subtitle_tab.setLayout(subtitle_layout)
|
|
tabs.addTab(subtitle_tab, "Untertitel")
|
|
|
|
# Tab 3: Output Template
|
|
output_tab = QWidget()
|
|
output_layout = QFormLayout()
|
|
|
|
# Custom Output Template Checkbox
|
|
self.custom_output_template_cb = QCheckBox("Eigenen Namen verwenden")
|
|
self.custom_output_template_cb.setChecked(self.preset_data.get("custom_output_template", False))
|
|
output_layout.addRow(self.custom_output_template_cb)
|
|
|
|
# Output Template Field
|
|
self.output_template_edit = QLineEdit(self.preset_data.get("output_template", "%(title)s.%(ext)s"))
|
|
self.output_template_edit.setPlaceholderText("z.B. %(title)s.%(ext)s, %(uploader)s/%(title)s.%(ext)s")
|
|
output_layout.addRow("Name:", self.output_template_edit)
|
|
|
|
# Add examples and documentation link
|
|
examples_label = QLabel("<b>Beispiele:</b><br>" +
|
|
"%(title)s-%(id)s.%(ext)s<br>" +
|
|
"%(uploader)s/%(title)s.%(ext)s<br>" +
|
|
"%(playlist)s/%(playlist_index)s-%(title)s.%(ext)s<br><br>" +
|
|
"<a href='https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#output-template'>Mehr Beispiele in der yt-dlp Dokumentation</a>")
|
|
examples_label.setOpenExternalLinks(True)
|
|
examples_label.setTextFormat(Qt.RichText)
|
|
output_layout.addRow(examples_label)
|
|
|
|
output_tab.setLayout(output_layout)
|
|
tabs.addTab(output_tab, "Name")
|
|
|
|
# Tab 4: 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)")
|
|
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)
|
|
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)
|
|
auth_layout.addRow("Passwort:", pw_hbox)
|
|
|
|
pw_hint = QLabel("Hinweis: Passwörter werden im Klartext lokal gespeichert!")
|
|
pw_hint.setStyleSheet("color: red;")
|
|
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")
|
|
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)
|
|
path_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)
|
|
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 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(),
|
|
"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(),
|
|
"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(),
|
|
"custom_output_template": self.custom_output_template_cb.isChecked(),
|
|
"output_template": self.output_template_edit.text()
|
|
}
|
|
|
|
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, 300)
|
|
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 {
|
|
"ignore_config": False,
|
|
"remux_mkv": False,
|
|
"embed_metadata": False,
|
|
"use_ffmpeg_location": 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)
|
|
|
|
# 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)
|
|
|
|
# FFmpeg-Pfad hinzufügen
|
|
self.ffmpeg_path_input = QLineEdit(self.parent().config.get("ffmpeg_path", "C:\\ffmpeg\\bin\\ffmpeg.exe") if self.parent() else "C:\\ffmpeg\\bin\\ffmpeg.exe")
|
|
ffmpeg_browse_btn = QPushButton("Durchsuchen...")
|
|
ffmpeg_browse_btn.clicked.connect(self.browse_ffmpeg_path)
|
|
ffmpeg_hbox = QHBoxLayout()
|
|
ffmpeg_hbox.addWidget(self.ffmpeg_path_input)
|
|
ffmpeg_hbox.addWidget(ffmpeg_browse_btn)
|
|
layout.addRow("FFmpeg-Pfad:", ffmpeg_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
|
|
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."))
|
|
self.cb_use_ffmpeg_location = QCheckBox("--ffmpeg-location")
|
|
self.cb_use_ffmpeg_location.setChecked(self.selected_flags.get("use_ffmpeg_location", False))
|
|
flags_layout.addWidget(self.cb_use_ffmpeg_location)
|
|
flags_layout.addWidget(QLabel("Nutzt den konfigurierten FFmpeg-Pfad für yt-dlp."))
|
|
tab_flags.setLayout(flags_layout)
|
|
tabs.addTab(tab_flags, "yt-dlp-Flags")
|
|
|
|
# Tab 3: Info
|
|
tab_info = QWidget()
|
|
info_layout = QVBoxLayout()
|
|
info_text = (
|
|
"<b>Version:</b> 1.4<br>"
|
|
"<b>© 2025 Akamaru</b><br>"
|
|
"<b>Sourcecode:</b> <a href='https://git.ponywave.de/Akamaru/video-download-helper'>https://git.ponywave.de/Akamaru/video-download-helper</a><br>"
|
|
"<b>Erstellt mit Hilfe von Claude, GPT & Gemini</b>"
|
|
)
|
|
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 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 browse_ffmpeg_path(self):
|
|
file_path, _ = QFileDialog.getOpenFileName(self, "FFmpeg-Executable auswählen",
|
|
self.ffmpeg_path_input.text() or "C:\\ffmpeg\\bin",
|
|
"Executable (*.exe)")
|
|
if file_path:
|
|
self.ffmpeg_path_input.setText(file_path)
|
|
|
|
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(),
|
|
"use_ffmpeg_location": self.cb_use_ffmpeg_location.isChecked()
|
|
},
|
|
self.hide_defaults_cb.isChecked(),
|
|
self.mkvmerge_path_input.text(),
|
|
self.enable_adn_tab_cb.isChecked(),
|
|
self.ffmpeg_path_input.text()
|
|
)
|
|
|
|
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 QueueItem:
|
|
"""Repräsentiert einen Eintrag in der Download-Queue."""
|
|
def __init__(self, url, preset_data, output_dir=None, output_filename=None,
|
|
series_info=None, use_local_ytdlp=True, extra_args=None):
|
|
self.id = str(uuid.uuid4()) # Eindeutige ID für diesen Queue-Eintrag
|
|
self.url = url
|
|
self.preset_data = preset_data.copy() if preset_data else {}
|
|
self.output_dir = output_dir
|
|
self.output_filename = output_filename
|
|
self.series_info = series_info.copy() if series_info else {}
|
|
self.use_local_ytdlp = use_local_ytdlp
|
|
self.extra_args = extra_args or ""
|
|
self.status = "Wartend"
|
|
|
|
def get_display_name(self):
|
|
"""Gibt einen lesbaren Namen für die Queue-Anzeige zurück."""
|
|
preset_name = self.preset_data.get("name", "Unbekannt")
|
|
if self.series_info:
|
|
series = self.series_info.get("series", "")
|
|
season = self.series_info.get("season", "")
|
|
episode = self.series_info.get("episode", "")
|
|
if series and season and episode:
|
|
return f"{self.url} - {series} S{season}E{episode} ({preset_name})"
|
|
return f"{self.url} ({preset_name})"
|
|
|
|
def to_dict(self):
|
|
"""Konvertiert das QueueItem in ein JSON-serialisierbares Dictionary."""
|
|
return {
|
|
"id": self.id,
|
|
"url": self.url,
|
|
"preset_data": self.preset_data,
|
|
"output_dir": self.output_dir,
|
|
"output_filename": self.output_filename,
|
|
"series_info": self.series_info,
|
|
"use_local_ytdlp": self.use_local_ytdlp,
|
|
"extra_args": self.extra_args,
|
|
"status": self.status
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data):
|
|
"""Erstellt ein QueueItem aus einem Dictionary."""
|
|
item = cls(
|
|
url=data["url"],
|
|
preset_data=data["preset_data"],
|
|
output_dir=data["output_dir"],
|
|
output_filename=data["output_filename"],
|
|
series_info=data["series_info"],
|
|
use_local_ytdlp=data["use_local_ytdlp"],
|
|
extra_args=data["extra_args"]
|
|
)
|
|
item.id = data["id"]
|
|
item.status = data["status"]
|
|
return item
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
def __init__(self):
|
|
super().__init__()
|
|
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()
|
|
|
|
# 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.download_queue = [] # Liste für die Download-Queue
|
|
self.current_queue_item = None # Aktuell laufender Download aus der Queue
|
|
|
|
# UI initialisieren
|
|
self.setup_ui()
|
|
|
|
# Queue aus gespeicherter Datei laden (nach UI-Setup)
|
|
self.load_queue()
|
|
|
|
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
|
|
options_layout = QHBoxLayout()
|
|
self.optionen_btn = QPushButton("Optionen...")
|
|
self.optionen_btn.clicked.connect(self.open_optionen_dialog)
|
|
options_layout.addWidget(self.optionen_btn)
|
|
main_layout.addLayout(options_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 Buttons
|
|
download_buttons_layout = QHBoxLayout()
|
|
self.download_btn = QPushButton("Download starten")
|
|
self.download_btn.clicked.connect(self.start_download)
|
|
download_buttons_layout.addWidget(self.download_btn)
|
|
|
|
# Neu: Queue-Button
|
|
self.queue_btn = QPushButton("Zur Queue hinzufügen")
|
|
self.queue_btn.clicked.connect(self.add_to_queue)
|
|
download_buttons_layout.addWidget(self.queue_btn)
|
|
|
|
main_layout.addLayout(download_buttons_layout)
|
|
|
|
# Neu: Tabbed Layout für Ausgabe und Queue
|
|
self.tabs = QTabWidget()
|
|
|
|
# Log Output Tab
|
|
log_tab = QWidget()
|
|
log_layout = QVBoxLayout()
|
|
log_layout.addWidget(QLabel("Ausgabe:"))
|
|
self.log_output = QTextEdit()
|
|
self.log_output.setReadOnly(True)
|
|
log_layout.addWidget(self.log_output)
|
|
log_tab.setLayout(log_layout)
|
|
self.tabs.addTab(log_tab, "Ausgabe")
|
|
|
|
# Queue Tab
|
|
queue_tab = QWidget()
|
|
queue_layout = QVBoxLayout()
|
|
queue_layout.addWidget(QLabel("Download-Queue:"))
|
|
|
|
self.queue_list = QListWidget()
|
|
self.queue_list.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
self.queue_list.customContextMenuRequested.connect(self.show_queue_context_menu)
|
|
queue_layout.addWidget(self.queue_list)
|
|
|
|
queue_buttons = QHBoxLayout()
|
|
self.start_queue_btn = QPushButton("Queue starten")
|
|
self.start_queue_btn.clicked.connect(self.start_queue)
|
|
queue_buttons.addWidget(self.start_queue_btn)
|
|
|
|
self.clear_queue_btn = QPushButton("Queue leeren")
|
|
self.clear_queue_btn.clicked.connect(self.clear_queue)
|
|
queue_buttons.addWidget(self.clear_queue_btn)
|
|
|
|
queue_layout.addLayout(queue_buttons)
|
|
queue_tab.setLayout(queue_layout)
|
|
self.tabs.addTab(queue_tab, "Queue")
|
|
|
|
main_layout.addWidget(self.tabs)
|
|
|
|
# Connect signals
|
|
self.url_input.textChanged.connect(self.update_cmd_preview)
|
|
self.preset_combo.currentIndexChanged.connect(self.preset_changed)
|
|
|
|
# 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()
|
|
self.update_queue_buttons()
|
|
|
|
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, mkvmerge_path, enable_adn_tab, ffmpeg_path = 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.config["ffmpeg_path"] = ffmpeg_path
|
|
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)
|
|
|
|
# Setze den custom_path_input immer zurück, unabhängig vom Preset-Typ
|
|
self.custom_path_input.setText(preset.get("custom_path", ""))
|
|
|
|
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.update_cmd_preview()
|
|
|
|
def update_cmd_preview(self):
|
|
preset = self.get_current_preset()
|
|
if not preset:
|
|
self.cmd_preview.setText("Kein Preset ausgewählt.")
|
|
return
|
|
|
|
# Zeige Preset-Informationen in der Ausgabe an
|
|
self.show_preset_info(preset)
|
|
|
|
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"])
|
|
# FFmpeg-Pfad
|
|
if flags.get("use_ffmpeg_location") and self.config.get("ffmpeg_path"):
|
|
cmd.extend(["--ffmpeg-location", self.config.get("ffmpeg_path")])
|
|
# 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)
|
|
|
|
# Wenn ein eigener Pfad im Preset definiert ist, diesen verwenden
|
|
custom_path = self.custom_path_input.text() or preset.get("custom_path", "")
|
|
output_dir = custom_path if custom_path else self.config["output_dir"]
|
|
|
|
# Generiere den Dateinamen ohne Debug-Ausgaben
|
|
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)
|
|
# Entferne {path} Platzhalter, da custom_path jetzt als output_dir verwendet wird
|
|
output_name = output_name.replace("{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("\\\\", "\\")
|
|
output_filename = output_name
|
|
else:
|
|
# Verwende benutzerdefinierte Ausgabevorlage, wenn in den Presets aktiviert
|
|
if preset.get("custom_output_template", False) and preset.get("output_template"):
|
|
output_filename = preset.get("output_template")
|
|
else:
|
|
output_filename = "%(title)s.%(ext)s"
|
|
|
|
# Füge den Ausgabepfad zum Befehl hinzu
|
|
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)
|
|
|
|
# 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 show_preset_info(self, preset):
|
|
"""Zeigt Informationen zum ausgewählten Preset in der Ausgabe an."""
|
|
self.log_output.clear()
|
|
self.log_output.append(f"Preset: {preset['name']}")
|
|
if preset.get("description"):
|
|
self.log_output.append(f"Beschreibung: {preset['description']}")
|
|
|
|
# Ausgabepfad
|
|
custom_path = self.custom_path_input.text() or preset.get("custom_path", "")
|
|
output_dir = custom_path if custom_path else self.config["output_dir"]
|
|
self.log_output.append(f"Ausgabeverzeichnis: {output_dir or '(Standard)'}")
|
|
|
|
# Serien-Informationen
|
|
if preset.get("has_series_template", False):
|
|
self.log_output.append("\nSerien-Einstellungen:")
|
|
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")
|
|
|
|
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}")
|
|
|
|
# Generiere Beispiel-Dateinamen
|
|
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}", "")
|
|
output_name = output_name.replace("{extension}", ".mp4")
|
|
|
|
self.log_output.append(f" Beispiel-Dateiname: {output_name}")
|
|
|
|
# Preset-Argumente
|
|
self.log_output.append("\nArgumente:")
|
|
self.log_output.append(f" {preset['args']}")
|
|
|
|
# Besondere Eigenschaften
|
|
special_features = []
|
|
if preset.get("is_audio", False):
|
|
special_features.append("Audio-Preset")
|
|
if preset.get("username"):
|
|
special_features.append("Authentifizierung")
|
|
if preset.get("referer"):
|
|
special_features.append("Referer")
|
|
if preset.get("hls_ffmpeg", False):
|
|
special_features.append("HLS-ffmpeg")
|
|
if preset.get("sublang") or preset.get("embed_subs") or preset.get("subformat"):
|
|
special_features.append("Untertitel")
|
|
if preset.get("use_format_selection", False):
|
|
special_features.append("Format-Auswahl")
|
|
if preset.get("use_dual_audio", False):
|
|
special_features.append("Dual-Audio")
|
|
|
|
if special_features:
|
|
self.log_output.append("\nBesondere Eigenschaften:")
|
|
self.log_output.append(f" {', '.join(special_features)}")
|
|
|
|
def get_output_filename(self, preset):
|
|
"""Generiert den Ausgabedateinamen basierend auf dem Preset und den aktuellen Einstellungen."""
|
|
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)
|
|
# Entferne {path} Platzhalter, da custom_path jetzt als output_dir verwendet wird
|
|
output_name = output_name.replace("{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("\\\\", "\\")
|
|
return output_name
|
|
# Im Nicht-Serienfall einfach den Standardnamen zurückgeben
|
|
return "%(title)s.%(ext)s"
|
|
|
|
def start_download(self):
|
|
# Wenn bereits ein Download läuft und der Button als "Abbrechen" angezeigt wird
|
|
if self.download_thread and self.download_thread.isRunning():
|
|
self.abort_download()
|
|
return
|
|
|
|
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
|
|
|
|
# Wenn im Preset ein eigener Pfad definiert ist, diesen bevorzugen
|
|
custom_path = self.custom_path_input.text() or preset.get("custom_path", "")
|
|
# Wenn custom_path gesetzt ist, verwenden wir diesen anstelle des standard output_dir
|
|
output_dir = custom_path if custom_path else 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)
|
|
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"]])
|
|
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"])
|
|
# FFmpeg-Pfad
|
|
if flags.get("use_ffmpeg_location") and self.config.get("ffmpeg_path"):
|
|
extra_args.extend(["--ffmpeg-location", self.config.get("ffmpeg_path")])
|
|
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")
|
|
# --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(updated_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}")
|
|
|
|
# FFmpeg-Pfad
|
|
if flags.get("use_ffmpeg_location") and self.config.get("ffmpeg_path"):
|
|
self.log_output.append(f"FFmpeg-Pfad: {self.config.get('ffmpeg_path')}")
|
|
|
|
# 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,
|
|
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)
|
|
|
|
self.download_thread.start()
|
|
|
|
def abort_download(self):
|
|
"""Bricht den aktuellen Download ab."""
|
|
if self.download_thread and self.download_thread.isRunning():
|
|
self.log_output.append("Abbruch angefordert...")
|
|
self.download_thread.stop()
|
|
# Button wird in download_finished wieder auf "Download starten" gesetzt
|
|
|
|
def disable_ui_during_download(self, disable=True):
|
|
"""Deaktiviert/aktiviert UI-Elemente während des Downloads."""
|
|
self.url_input.setReadOnly(disable)
|
|
self.preset_combo.setEnabled(not disable)
|
|
self.add_preset_btn.setEnabled(not disable)
|
|
self.edit_preset_btn.setEnabled(not disable)
|
|
self.delete_preset_btn.setEnabled(not disable)
|
|
self.optionen_btn.setEnabled(not disable)
|
|
if hasattr(self, 'series_input'):
|
|
self.series_input.setReadOnly(disable)
|
|
if hasattr(self, 'season_input'):
|
|
self.season_input.setReadOnly(disable)
|
|
if hasattr(self, 'episode_input'):
|
|
self.episode_input.setReadOnly(disable)
|
|
if hasattr(self, 'custom_path_input'):
|
|
self.custom_path_input.setReadOnly(disable)
|
|
|
|
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):
|
|
# UI-Elemente wieder aktivieren
|
|
self.disable_ui_during_download(False)
|
|
# Button-Text zurücksetzen
|
|
self.download_btn.setText("Download starten")
|
|
|
|
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)
|
|
|
|
# Queue-Buttons nach Download aktualisieren
|
|
self.update_queue_buttons()
|
|
|
|
def closeEvent(self, event):
|
|
self.save_config()
|
|
self.save_queue() # Queue beim Beenden speichern
|
|
event.accept()
|
|
|
|
def load_queue(self):
|
|
"""Lädt die gespeicherte Download-Queue aus einer Datei."""
|
|
queue_file = os.path.join(get_user_data_dir(), "queue.json")
|
|
if os.path.exists(queue_file):
|
|
try:
|
|
with open(queue_file, 'r', encoding='utf-8') as f:
|
|
queue_data = json.load(f)
|
|
self.download_queue = [QueueItem.from_dict(item) for item in queue_data]
|
|
# Sicherer Log-Aufruf
|
|
if hasattr(self, 'log_output'):
|
|
self.log_output.append(f"Download-Queue mit {len(self.download_queue)} Elementen geladen.")
|
|
# Queue-Liste aktualisieren
|
|
if hasattr(self, 'queue_list'):
|
|
self.update_queue_list()
|
|
except Exception as e:
|
|
if hasattr(self, 'log_output'):
|
|
self.log_output.append(f"Fehler beim Laden der Download-Queue: {str(e)}")
|
|
print(f"Fehler beim Laden der Download-Queue: {str(e)}")
|
|
self.download_queue = []
|
|
|
|
# Aktualisiere Queue-Buttons nach dem Laden
|
|
if hasattr(self, 'start_queue_btn') and hasattr(self, 'clear_queue_btn'):
|
|
self.update_queue_buttons()
|
|
|
|
def save_queue(self):
|
|
"""Speichert die Download-Queue in eine Datei."""
|
|
queue_file = os.path.join(get_user_data_dir(), "queue.json")
|
|
try:
|
|
queue_data = [item.to_dict() for item in self.download_queue]
|
|
with open(queue_file, 'w', encoding='utf-8') as f:
|
|
json.dump(queue_data, f, indent=4, ensure_ascii=False)
|
|
except Exception as e:
|
|
if hasattr(self, 'log_output'):
|
|
self.log_output.append(f"Fehler beim Speichern der Download-Queue: {str(e)}")
|
|
print(f"Fehler beim Speichern der Download-Queue: {str(e)}")
|
|
|
|
def add_to_queue(self):
|
|
"""Fügt den aktuellen Download zur Queue hinzu."""
|
|
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
|
|
|
|
# Wenn im Preset ein eigener Pfad definiert ist, diesen bevorzugen
|
|
custom_path = self.custom_path_input.text() or preset.get("custom_path", "")
|
|
# Wenn custom_path gesetzt ist, verwenden wir diesen anstelle des standard output_dir
|
|
output_dir = custom_path if custom_path else self.config["output_dir"]
|
|
|
|
series_info = None
|
|
output_filename = None
|
|
|
|
# Wenn es ein Serien-Preset ist, die Serien-Infos separat speichern
|
|
if preset.get("has_series_template", False):
|
|
series_info = {
|
|
"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"),
|
|
"template": preset.get("series_template", SERIES_TEMPLATE)
|
|
}
|
|
output_filename = self.get_output_filename(preset)
|
|
|
|
# Flags und Extra-Argumente vorbereiten
|
|
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"])
|
|
# FFmpeg-Pfad
|
|
if flags.get("use_ffmpeg_location") and self.config.get("ffmpeg_path"):
|
|
extra_args.extend(["--ffmpeg-location", self.config.get("ffmpeg_path")])
|
|
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"]
|
|
|
|
# Erstelle ein Queue-Element
|
|
queue_item = QueueItem(
|
|
url=url,
|
|
preset_data=preset,
|
|
output_dir=output_dir,
|
|
output_filename=output_filename,
|
|
series_info=series_info,
|
|
use_local_ytdlp=self.config["use_local_ytdlp"],
|
|
extra_args=full_args
|
|
)
|
|
|
|
# Füge das Element zur Queue hinzu
|
|
self.download_queue.append(queue_item)
|
|
|
|
# Aktualisiere die Queue-Liste
|
|
self.update_queue_list()
|
|
|
|
# Zur Ausgabe wechseln und Hinzufügen bestätigen
|
|
self.tabs.setCurrentIndex(0) # Wechsle zum Ausgabe-Tab
|
|
self.log_output.append(f"Download wurde zur Queue hinzugefügt: {queue_item.get_display_name()}")
|
|
|
|
# Optional: Leere die URL-Box
|
|
self.url_input.clear()
|
|
|
|
# Aktualisiere Queue-Buttons
|
|
self.update_queue_buttons()
|
|
|
|
# Queue speichern
|
|
self.save_queue()
|
|
|
|
def update_queue_list(self):
|
|
"""Aktualisiert die Anzeige der Queue-Liste."""
|
|
self.queue_list.clear()
|
|
for item in self.download_queue:
|
|
list_item = QListWidgetItem(f"{item.status}: {item.get_display_name()}")
|
|
list_item.setData(Qt.UserRole, item.id) # Speichere die ID als Daten
|
|
self.queue_list.addItem(list_item)
|
|
|
|
def update_queue_buttons(self):
|
|
"""Aktualisiert den Status der Queue-Buttons basierend auf dem Zustand der Queue."""
|
|
has_items = len(self.download_queue) > 0
|
|
is_downloading = self.download_thread and self.download_thread.isRunning()
|
|
|
|
self.start_queue_btn.setEnabled(has_items and not is_downloading)
|
|
self.clear_queue_btn.setEnabled(has_items and not is_downloading)
|
|
|
|
def show_queue_context_menu(self, position):
|
|
"""Zeigt das Kontextmenü für die Queue-Liste an."""
|
|
if not self.queue_list.count():
|
|
return
|
|
|
|
menu = QMenu()
|
|
|
|
# Aktionen erstellen
|
|
remove_action = QAction("Entfernen", self)
|
|
remove_action.triggered.connect(self.remove_selected_queue_item)
|
|
|
|
move_up_action = QAction("Nach oben", self)
|
|
move_up_action.triggered.connect(lambda: self.move_queue_item(-1))
|
|
|
|
move_down_action = QAction("Nach unten", self)
|
|
move_down_action.triggered.connect(lambda: self.move_queue_item(1))
|
|
|
|
# Prüfe ob ein Element ausgewählt ist
|
|
if self.queue_list.currentItem():
|
|
menu.addAction(remove_action)
|
|
menu.addAction(move_up_action)
|
|
menu.addAction(move_down_action)
|
|
|
|
# Zeige Menü
|
|
menu.exec_(self.queue_list.mapToGlobal(position))
|
|
|
|
def remove_selected_queue_item(self):
|
|
"""Entfernt das ausgewählte Element aus der Queue."""
|
|
current_item = self.queue_list.currentItem()
|
|
if not current_item:
|
|
return
|
|
|
|
item_id = current_item.data(Qt.UserRole)
|
|
self.download_queue = [item for item in self.download_queue if item.id != item_id]
|
|
self.update_queue_list()
|
|
self.update_queue_buttons()
|
|
|
|
# Queue speichern
|
|
self.save_queue()
|
|
|
|
def move_queue_item(self, direction):
|
|
"""Verschiebt ein Queue-Element nach oben oder unten."""
|
|
current_row = self.queue_list.currentRow()
|
|
if current_row < 0:
|
|
return
|
|
|
|
new_row = current_row + direction
|
|
if new_row < 0 or new_row >= self.queue_list.count():
|
|
return
|
|
|
|
# Tausche Elemente in der Queue
|
|
self.download_queue[current_row], self.download_queue[new_row] = \
|
|
self.download_queue[new_row], self.download_queue[current_row]
|
|
|
|
# Aktualisiere UI
|
|
self.update_queue_list()
|
|
self.queue_list.setCurrentRow(new_row)
|
|
|
|
# Queue speichern
|
|
self.save_queue()
|
|
|
|
def start_queue(self):
|
|
"""Startet die Download-Queue."""
|
|
if not self.download_queue:
|
|
QMessageBox.information(self, "Queue leer", "Die Download-Queue ist leer.")
|
|
return
|
|
|
|
if self.download_thread and self.download_thread.isRunning():
|
|
QMessageBox.warning(self, "Download läuft", "Es läuft bereits ein Download.")
|
|
return
|
|
|
|
# Starte den ersten Download in der Queue
|
|
self.process_next_queue_item()
|
|
|
|
def process_next_queue_item(self):
|
|
"""Verarbeitet das nächste Element in der Queue."""
|
|
if not self.download_queue:
|
|
self.log_output.append("Download-Queue abgeschlossen.")
|
|
self.current_queue_item = None
|
|
self.update_queue_buttons()
|
|
return
|
|
|
|
# Nehme das erste Element aus der Queue
|
|
self.current_queue_item = self.download_queue[0]
|
|
self.current_queue_item.status = "Wird heruntergeladen"
|
|
self.update_queue_list()
|
|
|
|
# Starte den Download
|
|
self.tabs.setCurrentIndex(0) # Wechsle zum Ausgabe-Tab
|
|
self.log_output.clear()
|
|
self.log_output.append(f"Starte Download von: {self.current_queue_item.url}")
|
|
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,
|
|
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)
|
|
|
|
self.download_thread.start()
|
|
|
|
def queue_download_finished(self, success, message):
|
|
"""Callback wenn ein Download aus der Queue fertig ist."""
|
|
# UI-Elemente wieder aktivieren
|
|
self.disable_ui_during_download(False)
|
|
# Button-Text zurücksetzen
|
|
self.download_btn.setText("Download starten")
|
|
|
|
# Bearbeite das aktuelle Queue-Element
|
|
if self.current_queue_item:
|
|
self.current_queue_item.status = "Fertig" if success else "Fehler"
|
|
|
|
# Entferne das Element aus der Queue (es bleibt in der Liste, aber mit Status)
|
|
if self.download_queue and self.download_queue[0].id == self.current_queue_item.id:
|
|
self.download_queue.pop(0)
|
|
|
|
self.update_queue_list()
|
|
|
|
# Queue speichern
|
|
self.save_queue()
|
|
|
|
if success:
|
|
self.log_output.append(message)
|
|
else:
|
|
self.log_output.append(f"Fehler: {message}")
|
|
|
|
# Wenn die Queue nicht manuell abgebrochen wurde, verarbeite das nächste Element
|
|
if not message.startswith("Download wurde abgebrochen"):
|
|
self.process_next_queue_item()
|
|
|
|
# Aktualisiere Queue-Buttons
|
|
self.update_queue_buttons()
|
|
|
|
def clear_queue(self):
|
|
"""Leert die Download-Queue."""
|
|
if not self.download_queue:
|
|
return
|
|
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Queue leeren",
|
|
"Möchten Sie wirklich die gesamte Download-Queue leeren?",
|
|
QMessageBox.Yes | QMessageBox.No,
|
|
QMessageBox.No
|
|
)
|
|
|
|
if reply == QMessageBox.Yes:
|
|
self.download_queue.clear()
|
|
self.update_queue_list()
|
|
self.update_queue_buttons()
|
|
|
|
# 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_()) |