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