Spiltte main.py

This commit is contained in:
Akamaru
2025-09-15 16:18:23 +02:00
parent f445cd4989
commit 12d2a0af73
6 changed files with 1556 additions and 1601 deletions

2
.gitignore vendored
View File

@@ -6,3 +6,5 @@ config.json
main.spec main.spec
*.7z *.7z
queue.json queue.json
/__pycache__
download_queue.json

63
config.py Normal file
View File

@@ -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}"

572
dialogs.py Normal file
View File

@@ -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("<b>Beispiele:</b><br>" +
"%(title)s-%(id)s.%(ext)s<br>" +
"%(uploader)s/%(title)s.%(ext)s<br>" +
"%(playlist)s/%(playlist_index)s-%(title)s.%(ext)s<br><br>" +
"<a href='https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#output-template'>Mehr Beispiele in der yt-dlp Dokumentation</a>")
examples_label.setOpenExternalLinks(True)
examples_label.setTextFormat(Qt.RichText)
output_layout.addRow(examples_label)
output_tab.setLayout(output_layout)
tabs.addTab(output_tab, "Name")
# Tab 4: Authentifizierung
auth_tab = QWidget()
auth_layout = QFormLayout()
# Login-Felder
self.username_edit = QLineEdit(self.preset_data.get("username", ""))
self.username_edit.setPlaceholderText("Optional: Benutzername für Login (-u)")
auth_layout.addRow("Benutzername:", self.username_edit)
pw_hbox = QHBoxLayout()
self.password_edit = QLineEdit(self.preset_data.get("password", ""))
self.password_edit.setEchoMode(QLineEdit.Password)
self.password_edit.setPlaceholderText("Optional: Passwort für Login (-p)")
self.show_pw_cb = QCheckBox("Passwort anzeigen")
self.show_pw_cb.toggled.connect(self.toggle_password_visible)
pw_hbox.addWidget(self.password_edit)
pw_hbox.addWidget(self.show_pw_cb)
auth_layout.addRow("Passwort:", pw_hbox)
pw_hint = QLabel("Hinweis: Passwörter werden im Klartext lokal gespeichert!")
pw_hint.setStyleSheet("color: red;")
auth_layout.addRow(pw_hint)
auth_tab.setLayout(auth_layout)
tabs.addTab(auth_tab, "Authentifizierung")
# Tab 4: Pfade & Serien
path_tab = QWidget()
path_layout = QFormLayout()
# Serien-Template
self.series_box = QGroupBox("Serien-Template aktivieren")
self.series_box.setCheckable(True)
self.series_box.setChecked(self.preset_data.get("has_series_template", False))
series_layout = QFormLayout()
self.template_edit = QLineEdit(self.preset_data.get("series_template", SERIES_TEMPLATE))
self.series_edit = QLineEdit(self.preset_data.get("series", ""))
self.season_edit = QLineEdit(self.preset_data.get("season", "1"))
self.episode_edit = QLineEdit(self.preset_data.get("episode", "1"))
series_layout.addRow("Template:", self.template_edit)
series_layout.addRow("Serie:", self.series_edit)
series_layout.addRow("Staffel:", self.season_edit)
series_layout.addRow("Folge:", self.episode_edit)
help_text = QLabel("Verwende {series}, {season}, {episode}, %(ext)s und {path} im Template.")
series_layout.addRow(help_text)
self.series_box.setLayout(series_layout)
path_layout.addWidget(self.series_box)
path_tab.setLayout(path_layout)
tabs.addTab(path_tab, "Serien")
# Tab 5: ADN (vorher Experte)
adn_tab = QWidget()
adn_layout = QVBoxLayout()
# F-Option und Format-ID Checkbox
self.use_format_selection_cb = QCheckBox("Format-Auswahl aktivieren")
self.use_format_selection_cb.setChecked(self.preset_data.get("use_format_selection", False))
self.use_format_selection_cb.toggled.connect(self.toggle_format_selection)
adn_layout.addWidget(self.use_format_selection_cb)
# Beschreibung der Format-Auswahl
format_desc = QLabel(
"Diese Option führt beim Start des Downloads zuerst 'yt-dlp -F' aus,\n"
"um verfügbare Formate anzuzeigen. Anschließend wird nach einer Format-ID gefragt."
)
adn_layout.addWidget(format_desc)
# Dual-Audio (Jap+Ger) und Untertitel Muxing aktivieren
self.use_dual_audio_cb = QCheckBox("Dual-Audio Muxing aktivieren")
self.use_dual_audio_cb.setChecked(self.preset_data.get("use_dual_audio", False))
self.use_dual_audio_cb.toggled.connect(self.toggle_dual_audio)
adn_layout.addWidget(self.use_dual_audio_cb)
# Dual-Audio Gruppe
self.dual_audio_group = QGroupBox("Dual-Audio Einstellungen")
self.dual_audio_group.setEnabled(self.preset_data.get("use_dual_audio", False))
dual_form = QFormLayout()
# Format ID Präfixe
self.jap_prefix_edit = QLineEdit(self.preset_data.get("jap_prefix", "vostde"))
dual_form.addRow("Prefix für japanische Audio:", self.jap_prefix_edit)
self.ger_prefix_edit = QLineEdit(self.preset_data.get("ger_prefix", "vde"))
dual_form.addRow("Prefix für deutsche Audio:", self.ger_prefix_edit)
# Suffix (Standard: -1)
self.format_suffix_edit = QLineEdit(self.preset_data.get("format_suffix", "-1"))
dual_form.addRow("Format-Suffix:", self.format_suffix_edit)
# Dateinamen für temporäre Dateien
self.temp_jp_filename_edit = QLineEdit(self.preset_data.get("temp_jp_filename", "video_jp.mp4"))
dual_form.addRow("Temp. Dateiname JP:", self.temp_jp_filename_edit)
self.temp_de_filename_edit = QLineEdit(self.preset_data.get("temp_de_filename", "video_de.mp4"))
dual_form.addRow("Temp. Dateiname DE:", self.temp_de_filename_edit)
# Untertitel-Dateien
self.de_sub_filename_edit = QLineEdit(self.preset_data.get("de_sub_filename", "subs.de.ass"))
dual_form.addRow("DE Untertitel-Datei:", self.de_sub_filename_edit)
self.de_forced_sub_filename_edit = QLineEdit(self.preset_data.get("de_forced_sub_filename", "subs.de.forced.ass"))
dual_form.addRow("DE forced Untertitel:", self.de_forced_sub_filename_edit)
# Cleanup-Option
self.cleanup_temp_cb = QCheckBox("Temporäre Dateien nach dem Muxing löschen")
self.cleanup_temp_cb.setChecked(self.preset_data.get("cleanup_temp", True))
dual_form.addRow(self.cleanup_temp_cb)
self.dual_audio_group.setLayout(dual_form)
adn_layout.addWidget(self.dual_audio_group)
adn_tab.setLayout(adn_layout)
# ADN Tab nur hinzufügen, wenn aktiviert
self.adn_tab_index = None
if self.parent() and self.parent().config.get("enable_adn_tab", False):
self.adn_tab_index = tabs.addTab(adn_tab, "ADN")
layout.addWidget(tabs)
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
self.setLayout(layout)
def toggle_password_visible(self, checked):
self.password_edit.setEchoMode(QLineEdit.Normal if checked else QLineEdit.Password)
def toggle_format_selection(self, checked):
# Diese Methode setzt Flags, wenn die Format-Auswahl aktiviert wird
pass
def toggle_dual_audio(self, checked):
# Aktiviere/Deaktiviere die Dual-Audio-Einstellungen
self.dual_audio_group.setEnabled(checked)
def get_preset_data(self):
return {
"name": self.name_edit.text(),
"description": self.description_edit.text(),
"args": self.args_edit.toPlainText(),
"has_series_template": self.series_box.isChecked(),
"series_template": self.template_edit.text(),
"series": self.series_edit.text(),
"season": self.season_edit.text(),
"episode": self.episode_edit.text(),
"custom_path": self.custom_path_edit.text(),
"is_audio": self.is_audio_cb.isChecked(),
"username": self.username_edit.text(),
"password": self.password_edit.text(),
"referer": self.referer_edit.text(),
"hls_ffmpeg": self.hls_ffmpeg_cb.isChecked(),
"sublang": self.sublang_edit.text(),
"embed_subs": self.embed_subs_cb.isChecked(),
"subformat": self.subformat_combo.currentData(),
"use_format_selection": self.use_format_selection_cb.isChecked(),
"use_dual_audio": self.use_dual_audio_cb.isChecked(),
"jap_prefix": self.jap_prefix_edit.text(),
"ger_prefix": self.ger_prefix_edit.text(),
"format_suffix": self.format_suffix_edit.text(),
"temp_jp_filename": self.temp_jp_filename_edit.text(),
"temp_de_filename": self.temp_de_filename_edit.text(),
"de_sub_filename": self.de_sub_filename_edit.text(),
"de_forced_sub_filename": self.de_forced_sub_filename_edit.text(),
"cleanup_temp": self.cleanup_temp_cb.isChecked(),
"custom_output_template": self.custom_output_template_cb.isChecked(),
"output_template": self.output_template_edit.text()
}
def browse_custom_path(self):
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 = (
"<b>Version:</b> 1.4<br>"
"<b>&copy; 2025 Akamaru</b><br>"
"<b>Sourcecode:</b> <a href='https://git.ponywave.de/Akamaru/video-download-helper'>https://git.ponywave.de/Akamaru/video-download-helper</a><br>"
"<b>Erstellt mit Hilfe von Claude, GPT &amp; Gemini</b>"
)
info_label = QLabel(info_text)
info_label.setOpenExternalLinks(True)
info_label.setTextFormat(Qt.RichText)
info_layout.addWidget(info_label)
info_layout.addStretch(1)
tab_info.setLayout(info_layout)
tabs.addTab(tab_info, "Info")
main_layout.addWidget(tabs)
# Stelle sicher, dass QDialogButtonBox nur einmal erstellt und verbunden wird
if hasattr(self, 'button_box'):
try:
main_layout.removeWidget(self.button_box)
self.button_box.deleteLater()
except Exception:
pass
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
main_layout.addWidget(self.button_box)
self.setLayout(main_layout)
def browse_output_dir(self):
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")

644
download_threads.py Normal file
View File

@@ -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)}")

1852
main.py

File diff suppressed because it is too large Load Diff

24
utils.py Normal file
View File

@@ -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))