Spiltte main.py
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
63
config.py
Normal 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
572
dialogs.py
Normal 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>© 2025 Akamaru</b><br>"
|
||||||
|
"<b>Sourcecode:</b> <a href='https://git.ponywave.de/Akamaru/video-download-helper'>https://git.ponywave.de/Akamaru/video-download-helper</a><br>"
|
||||||
|
"<b>Erstellt mit Hilfe von Claude, GPT & Gemini</b>"
|
||||||
|
)
|
||||||
|
info_label = QLabel(info_text)
|
||||||
|
info_label.setOpenExternalLinks(True)
|
||||||
|
info_label.setTextFormat(Qt.RichText)
|
||||||
|
info_layout.addWidget(info_label)
|
||||||
|
info_layout.addStretch(1)
|
||||||
|
tab_info.setLayout(info_layout)
|
||||||
|
tabs.addTab(tab_info, "Info")
|
||||||
|
|
||||||
|
main_layout.addWidget(tabs)
|
||||||
|
# Stelle sicher, dass QDialogButtonBox nur einmal erstellt und verbunden wird
|
||||||
|
if hasattr(self, 'button_box'):
|
||||||
|
try:
|
||||||
|
main_layout.removeWidget(self.button_box)
|
||||||
|
self.button_box.deleteLater()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||||
|
self.button_box.accepted.connect(self.accept)
|
||||||
|
self.button_box.rejected.connect(self.reject)
|
||||||
|
main_layout.addWidget(self.button_box)
|
||||||
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
|
def browse_output_dir(self):
|
||||||
|
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
644
download_threads.py
Normal 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)}")
|
24
utils.py
Normal file
24
utils.py
Normal 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))
|
Reference in New Issue
Block a user