diff --git a/.gitignore b/.gitignore
index ca4443c..099f196 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,5 @@ config.json
main.spec
*.7z
queue.json
+/__pycache__
+download_queue.json
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..16172c1
--- /dev/null
+++ b/config.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env -S uv run --script
+"""
+Konfiguration und Konstanten für den Video Download Helper
+"""
+import os
+import sys
+
+# 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
+
+# Konfigurationspfade
+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
+
+# Standardkonfiguration
+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}"
\ No newline at end of file
diff --git a/dialogs.py b/dialogs.py
new file mode 100644
index 0000000..ed2a34c
--- /dev/null
+++ b/dialogs.py
@@ -0,0 +1,572 @@
+#!/usr/bin/env -S uv run --script
+"""
+Dialog-Klassen für den Video Download Helper
+"""
+import os
+import subprocess
+from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QFormLayout, QLineEdit, QTextEdit,
+ QPushButton, QComboBox, QCheckBox, QTabWidget, QWidget,
+ QHBoxLayout, QGroupBox, QLabel, QDialogButtonBox,
+ QFileDialog, QMessageBox)
+from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QIcon
+from config import get_base_path, SERIES_TEMPLATE
+from utils import set_window_icon
+from download_threads import YtDlpDownloadThread
+
+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
+ set_window_icon(self)
+
+ 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("Beispiele:
" +
+ "%(title)s-%(id)s.%(ext)s
" +
+ "%(uploader)s/%(title)s.%(ext)s
" +
+ "%(playlist)s/%(playlist_index)s-%(title)s.%(ext)s
" +
+ "Mehr Beispiele in der yt-dlp Dokumentation")
+ 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):
+ dialog = QFileDialog(self)
+ dialog.setFileMode(QFileDialog.Directory)
+ dialog.setWindowTitle("Eigenen Zielordner auswählen")
+ if self.custom_path_edit.text():
+ dialog.setDirectory(self.custom_path_edit.text())
+
+ # Icon setzen
+ set_window_icon(dialog)
+
+ if dialog.exec_():
+ directory = dialog.selectedFiles()[0]
+ self.custom_path_edit.setText(directory)
+
+
+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
+ set_window_icon(self)
+
+ 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 = (
+ "Version: 1.4
"
+ "© 2025 Akamaru
"
+ "Sourcecode: https://git.ponywave.de/Akamaru/video-download-helper
"
+ "Erstellt mit Hilfe von Claude, GPT & Gemini"
+ )
+ info_label = QLabel(info_text)
+ info_label.setOpenExternalLinks(True)
+ info_label.setTextFormat(Qt.RichText)
+ info_layout.addWidget(info_label)
+ info_layout.addStretch(1)
+ tab_info.setLayout(info_layout)
+ tabs.addTab(tab_info, "Info")
+
+ main_layout.addWidget(tabs)
+ # Stelle sicher, dass QDialogButtonBox nur einmal erstellt und verbunden wird
+ if hasattr(self, 'button_box'):
+ try:
+ main_layout.removeWidget(self.button_box)
+ self.button_box.deleteLater()
+ except Exception:
+ pass
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ self.button_box.accepted.connect(self.accept)
+ self.button_box.rejected.connect(self.reject)
+ main_layout.addWidget(self.button_box)
+ self.setLayout(main_layout)
+
+ def browse_output_dir(self):
+ dialog = QFileDialog(self)
+ dialog.setFileMode(QFileDialog.Directory)
+ dialog.setWindowTitle("Standardpfad auswählen")
+ dialog.setDirectory(self.output_dir_input.text() or os.path.expanduser("~"))
+
+ # Icon setzen
+ set_window_icon(dialog)
+
+ if dialog.exec_():
+ directory = dialog.selectedFiles()[0]
+ self.output_dir_input.setText(directory)
+
+ def browse_mkvmerge_path(self):
+ dialog = QFileDialog(self)
+ dialog.setFileMode(QFileDialog.ExistingFile)
+ dialog.setWindowTitle("MKVMerge-Executable auswählen")
+ dialog.setDirectory(self.mkvmerge_path_input.text() or "C:\\Program Files\\MKVToolNix")
+ dialog.setNameFilter("Executable (*.exe)")
+
+ # Icon setzen
+ set_window_icon(dialog)
+
+ if dialog.exec_():
+ file_path = dialog.selectedFiles()[0]
+ self.mkvmerge_path_input.setText(file_path)
+
+ def browse_ffmpeg_path(self):
+ dialog = QFileDialog(self)
+ dialog.setFileMode(QFileDialog.ExistingFile)
+ dialog.setWindowTitle("FFmpeg-Executable auswählen")
+ dialog.setDirectory(self.ffmpeg_path_input.text() or "C:\\ffmpeg\\bin")
+ dialog.setNameFilter("Executable (*.exe)")
+
+ # Icon setzen
+ set_window_icon(dialog)
+
+ if dialog.exec_():
+ file_path = dialog.selectedFiles()[0]
+ 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")
\ No newline at end of file
diff --git a/download_threads.py b/download_threads.py
new file mode 100644
index 0000000..e50e340
--- /dev/null
+++ b/download_threads.py
@@ -0,0 +1,644 @@
+#!/usr/bin/env -S uv run --script
+"""
+Download-Thread-Klassen für den Video Download Helper
+"""
+import os
+import re
+import subprocess
+import urllib.request
+from PyQt5.QtCore import QThread, pyqtSignal
+from config import get_base_path, get_temp_dir
+from utils import mask_sensitive_data
+
+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 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)}")
\ No newline at end of file
diff --git a/main.py b/main.py
index 578f04a..8882700 100644
--- a/main.py
+++ b/main.py
@@ -10,1244 +10,20 @@
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
+ QMessageBox, QListWidget, QFormLayout,
+ QGroupBox, QTabWidget, QListWidgetItem, QMenu, QAction, QInputDialog)
+from PyQt5.QtCore import 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("Beispiele:
" +
- "%(title)s-%(id)s.%(ext)s
" +
- "%(uploader)s/%(title)s.%(ext)s
" +
- "%(playlist)s/%(playlist_index)s-%(title)s.%(ext)s
" +
- "Mehr Beispiele in der yt-dlp Dokumentation")
- 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 = (
- "Version: 1.4
"
- "© 2025 Akamaru
"
- "Sourcecode: https://git.ponywave.de/Akamaru/video-download-helper
"
- "Erstellt mit Hilfe von Claude, GPT & Gemini"
- )
- info_label = QLabel(info_text)
- info_label.setOpenExternalLinks(True)
- info_label.setTextFormat(Qt.RichText)
- info_layout.addWidget(info_label)
- info_layout.addStretch(1)
- tab_info.setLayout(info_layout)
- tabs.addTab(tab_info, "Info")
-
- main_layout.addWidget(tabs)
- # Stelle sicher, dass QDialogButtonBox nur einmal erstellt und verbunden wird
- if hasattr(self, 'button_box'):
- try:
- main_layout.removeWidget(self.button_box)
- self.button_box.deleteLater()
- except Exception:
- pass
- self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
- self.button_box.accepted.connect(self.accept)
- self.button_box.rejected.connect(self.reject)
- main_layout.addWidget(self.button_box)
- self.setLayout(main_layout)
-
- def browse_output_dir(self):
- directory = QFileDialog.getExistingDirectory(self, "Standardpfad auswählen", self.output_dir_input.text() or os.path.expanduser("~"))
- if directory:
- self.output_dir_input.setText(directory)
-
- def 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")
-
+# Imports für die neuen Module
+from config import (get_base_path, get_user_data_dir, get_user_presets_dir,
+ CONFIG_FILE, PRESETS_DIR, DEFAULT_CONFIG, SERIES_TEMPLATE)
+from utils import mask_sensitive_data, set_window_icon
+from download_threads import DownloadThread
+from dialogs import PresetDialog, OptionenDialog
class QueueItem:
"""Repräsentiert einen Eintrag in der Download-Queue."""
@@ -1312,9 +88,7 @@ class MainWindow(QMainWindow):
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))
+ set_window_icon(self)
# Stelle sicher, dass Verzeichnisse existieren
self.ensure_directories()
@@ -1589,7 +363,7 @@ class MainWindow(QMainWindow):
def add_preset(self):
dialog = PresetDialog(self)
- if dialog.exec_() == QDialog.Accepted:
+ if dialog.exec_() == PresetDialog.Accepted:
preset_data = dialog.get_preset_data()
if not preset_data["name"]:
QMessageBox.warning(self, "Fehler", "Der Preset-Name darf nicht leer sein.")
@@ -1618,7 +392,7 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, "Warnung", "Kein Preset ausgewählt.")
return
dialog = PresetDialog(self, current_preset)
- if dialog.exec_() == QDialog.Accepted:
+ if dialog.exec_() == PresetDialog.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.")
@@ -1683,7 +457,7 @@ class MainWindow(QMainWindow):
self,
self.config.get("ytdlp_flags", DEFAULT_CONFIG["ytdlp_flags"])
)
- if dialog.exec_() == QDialog.Accepted:
+ if dialog.exec_() == OptionenDialog.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
@@ -1967,122 +741,80 @@ class MainWindow(QMainWindow):
extra_args.extend(["-u", preset["username"]])
if preset.get("password"):
extra_args.extend(["-p", preset["password"]])
+ # Referer
if preset.get("referer"):
extra_args.append(f"--referer={preset['referer']}")
+ # HLS-ffmpeg
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")])
+ # Untertitel
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"]])
+ # Global Flags
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"]
+ # Prüfe auf Dual-Audio-Muxing
+ 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.save_config()
- self.log_output.clear()
- self.log_output.append(f"Starte Download von: {url}")
+ cmd_args = " ".join(extra_args) + (" " + preset["args"] if preset["args"] else "")
+
+ # Bestimme den Ausgabedateinamen
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
+ output_filename = self.get_output_filename(preset)
+ elif preset.get("custom_output_template", False) and preset.get("output_template"):
+ output_filename = preset.get("output_template")
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
+ url, output_dir, cmd_args, use_local_ytdlp, output_filename,
+ updated_preset, self.config
)
- self.download_thread.update_signal.connect(self.update_log)
+
+ # Signal-Verbindungen
+ self.download_thread.update_signal.connect(self.log_output.append)
self.download_thread.finished_signal.connect(self.download_finished)
+ self.download_thread.format_selection_signal.connect(self.show_formats)
+ self.download_thread.format_id_input_signal.connect(self.get_format_id_from_user)
- # 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
+ # UI-Zustand ändern
self.download_btn.setText("Download abbrechen")
- self.disable_ui_during_download(True)
+ self.download_btn.setStyleSheet("background-color: #ff6b6b; color: white;")
+ self.queue_btn.setEnabled(False) # Queue-Button deaktivieren während des Downloads
+ # Switch zu Log-Tab
+ self.tabs.setCurrentIndex(0)
+
+ # Download starten
self.download_thread.start()
+
+ # Config speichern
+ self.save_config()
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())
+ self.log_output.append("Download-Abbruch angefordert...")
def download_finished(self, success, message):
- # UI-Elemente wieder aktivieren
- self.disable_ui_during_download(False)
- # Button-Text zurücksetzen
+ # UI-Zustand zurücksetzen
self.download_btn.setText("Download starten")
+ self.download_btn.setStyleSheet("")
+ self.queue_btn.setEnabled(True) # Queue-Button wieder aktivieren
if success:
self.log_output.append(message)
@@ -2093,47 +825,41 @@ class MainWindow(QMainWindow):
# Queue-Buttons nach Download aktualisieren
self.update_queue_buttons()
+
+ # Wenn aus der Queue, dann nächsten Download starten
+ if self.current_queue_item and self.download_queue:
+ self.process_next_queue_item()
+ else:
+ self.current_queue_item = None
+
+ def show_formats(self, formats):
+ """Zeigt die verfügbaren Formate in einem Dialog an."""
+ # Hier könnte man einen Dialog mit den Formaten anzeigen
+ # Für einfachheit zeigen wir sie erstmal im Log
+ self.log_output.append("\n=== Verfügbare Formate ===")
+ for format_line in formats:
+ self.log_output.append(format_line)
+ self.log_output.append("=== Ende der Formate ===\n")
- 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 get_format_id_from_user(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
+ set_window_icon(dialog)
+
+ ok = dialog.exec_()
+ format_id = dialog.textValue()
+
+ if ok and format_id and self.download_thread:
+ self.download_thread.set_format_id(format_id)
+ elif self.download_thread:
+ # Abbrechen
+ self.download_thread.set_format_id("abort")
def add_to_queue(self):
"""Fügt den aktuellen Download zur Queue hinzu."""
@@ -2148,159 +874,67 @@ class MainWindow(QMainWindow):
# 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
+ # Serieninformationen sammeln
+ series_info = {}
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)
+ "episode": self.episode_input.text() or preset.get("episode", "1")
}
+
+ # Bestimme den Ausgabedateinamen
+ output_filename = None
+ if preset.get("has_series_template", False):
output_filename = self.get_output_filename(preset)
+ elif preset.get("custom_output_template", False) and preset.get("output_template"):
+ output_filename = preset.get("output_template")
- # 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 erstellen
queue_item = QueueItem(
url=url,
- preset_data=preset,
+ preset_data=preset.copy(), # Kopie des Presets mit den aktuellen Werten
output_dir=output_dir,
output_filename=output_filename,
series_info=series_info,
- use_local_ytdlp=self.config["use_local_ytdlp"],
- extra_args=full_args
+ use_local_ytdlp=self.config["use_local_ytdlp"]
)
- # Füge das Element zur Queue hinzu
+ # Zur Queue hinzufügen
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()
+
+ # Tab zur Queue wechseln
+ self.tabs.setCurrentIndex(1)
+
+ QMessageBox.information(self, "Zur Queue hinzugefügt",
+ f"Der Download wurde zur Queue hinzugefügt.\nQueue enthält jetzt {len(self.download_queue)} Elemente.")
def update_queue_list(self):
- """Aktualisiert die Anzeige der Queue-Liste."""
+ """Aktualisiert die Queue-Liste in der UI."""
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
+ list_item.setData(Qt.UserRole, item.id)
self.queue_list.addItem(list_item)
def update_queue_buttons(self):
- """Aktualisiert den Status der Queue-Buttons basierend auf dem Zustand der Queue."""
+ """Aktualisiert den Zustand der Queue-Buttons."""
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()
+ self.clear_queue_btn.setEnabled(has_items)
- # 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()
+ # Button-Text anpassen
+ if is_downloading and self.current_queue_item:
+ self.start_queue_btn.setText("Queue läuft...")
+ else:
+ self.start_queue_btn.setText("Queue starten")
def start_queue(self):
"""Startet die Download-Queue."""
@@ -2318,118 +952,126 @@ class MainWindow(QMainWindow):
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.")
+ QMessageBox.information(self, "Queue abgeschlossen", "Alle Downloads in der Queue wurden 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"
+
+ # Hole das nächste Element aus der Queue
+ self.current_queue_item = self.download_queue.pop(0)
+ self.current_queue_item.status = "Wird heruntergeladen..."
+
+ # Aktualisiere die UI
self.update_queue_list()
+ self.update_queue_buttons()
+ self.save_queue()
- # 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}")
+ # Switch zu Log-Tab
+ self.tabs.setCurrentIndex(0)
+
+ # Logge den Start
+ self.log_output.append(f"\n=== Queue-Download gestartet ===")
+ self.log_output.append(f"URL: {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}")
+ if self.current_queue_item.series_info:
+ series_info = self.current_queue_item.series_info
+ self.log_output.append(f"Serie: {series_info.get('series', '')} S{series_info.get('season', '')}E{series_info.get('episode', '')}")
+ self.log_output.append("=" * 35)
- # 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)
+ # Erstelle Download-Thread mit Queue-Daten
+ preset = self.current_queue_item.preset_data
- # Ü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
+ # Erweitere das Preset um die Serieninfos aus der Queue
+ if self.current_queue_item.series_info:
+ preset.update(self.current_queue_item.series_info)
- 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.")
+ # Erstelle Command-Args wie im normalen Download
+ flags = self.config.get("ytdlp_flags", {})
+ is_audio = preset.get("is_audio", False)
+ use_dual_audio = preset.get("use_dual_audio", False)
- # 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
+ extra_args = []
+ if preset.get("username"):
+ extra_args.extend(["-u", preset["username"]])
+ if preset.get("password"):
+ extra_args.extend(["-p", preset["password"]])
+ if preset.get("referer"):
+ extra_args.append(f"--referer={preset['referer']}")
+ if preset.get("hls_ffmpeg"):
+ extra_args.extend(["--downloader", "ffmpeg", "--hls-use-mpegts"])
+ if 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 and not use_dual_audio:
+ extra_args.extend(["--remux-video", "mkv"])
+ if flags.get("embed_metadata"):
+ extra_args.append("--embed-metadata")
- # Download-Thread erstellen und starten
+ cmd_args = " ".join(extra_args) + (" " + preset["args"] if preset["args"] else "")
+
+ # Erstelle und starte Download-Thread
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.current_queue_item.url,
+ self.current_queue_item.output_dir,
+ cmd_args,
+ self.current_queue_item.use_local_ytdlp,
+ self.current_queue_item.output_filename,
+ preset,
+ self.config
)
- self.download_thread.update_signal.connect(self.update_log)
+ # Verbinde Signale
+ self.download_thread.update_signal.connect(self.log_output.append)
self.download_thread.finished_signal.connect(self.queue_download_finished)
+ self.download_thread.format_selection_signal.connect(self.show_formats)
+ self.download_thread.format_id_input_signal.connect(self.get_format_id_from_user)
- # 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)
-
+ # Starte Download
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()
-
+ """Callback für abgeschlossene Queue-Downloads."""
if success:
- self.log_output.append(message)
+ self.log_output.append(f"Queue-Download erfolgreich: {message}")
else:
- self.log_output.append(f"Fehler: {message}")
+ self.log_output.append(f"Queue-Download fehlgeschlagen: {message}")
+
+ # Verarbeite das nächste Element in der Queue nach einer kurzen Pause
+ # (Damit der Benutzer die Ausgabe sehen kann)
+ from PyQt5.QtCore import QTimer
+ QTimer.singleShot(1000, self.process_next_queue_item)
+
+ def show_queue_context_menu(self, position):
+ """Zeigt ein Kontextmenü für die Queue-Liste."""
+ item = self.queue_list.itemAt(position)
+ if not item:
+ return
- # Wenn die Queue nicht manuell abgebrochen wurde, verarbeite das nächste Element
- if not message.startswith("Download wurde abgebrochen"):
- self.process_next_queue_item()
+ menu = QMenu(self)
- # Aktualisiere Queue-Buttons
+ remove_action = QAction("Aus Queue entfernen", self)
+ remove_action.triggered.connect(lambda: self.remove_from_queue(item))
+ menu.addAction(remove_action)
+
+ menu.exec_(self.queue_list.mapToGlobal(position))
+
+ def remove_from_queue(self, list_item):
+ """Entfernt ein Element aus der Queue."""
+ item_id = list_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()
+ self.save_queue()
def clear_queue(self):
- """Leert die Download-Queue."""
+ """Leert die Download-Queue nach Bestätigung."""
if not self.download_queue:
return
@@ -2445,53 +1087,61 @@ class MainWindow(QMainWindow):
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
+
+ def save_queue(self):
+ """Speichert die aktuelle Queue in eine Datei."""
+ queue_file = os.path.join(get_user_data_dir(), "download_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:
+ self.log_output.append(f"Fehler beim Speichern der Queue: {str(e)}")
+
+ def load_queue(self):
+ """Lädt die Queue aus einer gespeicherten Datei."""
+ queue_file = os.path.join(get_user_data_dir(), "download_queue.json")
+ if not os.path.exists(queue_file):
+ return
- # 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))
+ try:
+ with open(queue_file, 'r', encoding='utf-8') as f:
+ queue_data = json.load(f)
+
+ self.download_queue = [QueueItem.from_dict(item_data) for item_data in queue_data]
+ self.update_queue_list()
+ self.update_queue_buttons()
+ except Exception as e:
+ self.log_output.append(f"Fehler beim Laden der Queue: {str(e)}")
+
+ def closeEvent(self, event):
+ """Wird beim Schließen der Anwendung aufgerufen."""
+ # Speichere die Konfiguration
+ self.save_config()
- ok = dialog.exec_()
- format_id = dialog.textValue()
+ # Speichere die Queue
+ self.save_queue()
- 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
+ # Beende laufende Downloads
+ if self.download_thread and self.download_thread.isRunning():
+ self.download_thread.stop()
+ self.download_thread.wait(3000) # Warte max 3 Sekunden
+
+ event.accept()
-if __name__ == "__main__":
+def 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))
+ # Icon für die Anwendung setzen
+ set_window_icon(app)
window = MainWindow()
window.show()
- sys.exit(app.exec_())
\ No newline at end of file
+
+ sys.exit(app.exec_())
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..c7191a5
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env -S uv run --script
+"""
+Utility-Funktionen für den Video Download Helper
+"""
+import re
+import os
+from PyQt5.QtGui import QIcon
+from config import get_base_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
+
+def set_window_icon(window):
+ """Setzt das Icon für ein Fenster falls verfügbar."""
+ icon_path = os.path.join(get_base_path(), "icon.ico")
+ if os.path.exists(icon_path):
+ window.setWindowIcon(QIcon(icon_path))
\ No newline at end of file