Files
video-download-helper/main.py
2025-05-04 21:15:41 +02:00

1539 lines
66 KiB
Python

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "PyQt5==5.15.9",
# "PyInstaller==5.13.2",
# "requests==2.31.0",
# ]
# ///
import sys
import os
import json
import subprocess
import re
import urllib.request
import uuid
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QComboBox, QTextEdit,
QFileDialog, QMessageBox, QListWidget, QDialog, QFormLayout,
QDialogButtonBox, QInputDialog, QGroupBox, QCheckBox, QTabWidget,
QListWidgetItem, QMenu, QAction)
from PyQt5.QtCore import QThread, pyqtSignal, Qt
# 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_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
CONFIG_FILE = os.path.join(get_user_data_dir(), "config.json")
PRESETS_DIR = os.path.join(get_base_path(), "presets") # Nur für Lesezugriff auf mitgelieferte Presets
DEFAULT_CONFIG = {
"output_dir": "",
"use_local_ytdlp": True,
"last_preset": "",
"presets": [],
"ytdlp_flags": {
"ignore_config": False,
"remux_mkv": False,
"embed_metadata": False
},
"hide_default_presets": False
}
# Template-Variablen für Serien
SERIES_TEMPLATE = "{series} S{season}E{episode}{extension}"
class DownloadThread(QThread):
update_signal = pyqtSignal(str)
finished_signal = pyqtSignal(bool, str)
def __init__(self, url, output_dir, cmd_args, use_local_ytdlp, output_filename=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
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"
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)
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:
default_path = os.path.join(self.output_dir, "%(title)s.%(ext)s")
self.update_signal.emit(f"Debug - Standard-Ausgabepfad: {default_path}")
cmd.extend(["-o", default_path])
elif self.output_filename:
self.update_signal.emit(f"Debug - Nur Ausgabedateiname: {self.output_filename}")
cmd.extend(["-o", self.output_filename])
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
class PresetDialog(QDialog):
def __init__(self, parent=None, preset_data=None):
super().__init__(parent)
self.setWindowTitle("Preset erstellen/bearbeiten")
self.resize(500, 450)
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
}
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout()
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)
# 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)
# Untertitel-Optionen
self.sublang_edit = QLineEdit(self.preset_data.get("sublang", ""))
self.sublang_edit.setPlaceholderText("z.B. de, en, de,en ...")
form_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))
form_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)
form_layout.addRow("Untertitel-Format:", self.subformat_combo)
# Eigener Pfad (immer sichtbar, mit Durchsuchen)
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)
# Login-Felder
self.username_edit = QLineEdit(self.preset_data.get("username", ""))
self.username_edit.setPlaceholderText("Optional: Benutzername für Login (-u)")
form_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)
form_layout.addRow("Passwort:", pw_hbox)
pw_hint = QLabel("Hinweis: Passwörter werden im Klartext lokal gespeichert!")
pw_hint.setStyleSheet("color: red;")
form_layout.addRow(pw_hint)
# 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)
layout.addLayout(form_layout)
layout.addWidget(self.series_box)
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 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()
}
def browse_custom_path(self):
directory = QFileDialog.getExistingDirectory(self, "Eigenen Zielordner auswählen", self.custom_path_edit.text() or "")
if directory:
self.custom_path_edit.setText(directory)
class YtDlpDownloadThread(QThread):
finished_signal = pyqtSignal(bool, str)
def run(self):
try:
url = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe"
bin_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin")
os.makedirs(bin_dir, exist_ok=True)
dest_path = os.path.join(bin_dir, "yt-dlp.exe")
urllib.request.urlretrieve(url, dest_path)
self.finished_signal.emit(True, f"yt-dlp.exe wurde erfolgreich nach {dest_path} heruntergeladen.")
except Exception as e:
self.finished_signal.emit(False, f"Fehler beim Herunterladen von yt-dlp.exe: {str(e)}")
class OptionenDialog(QDialog):
def __init__(self, output_dir, use_local_ytdlp, parent=None, ytdlp_flags=None):
super().__init__(parent)
self.setWindowTitle("Optionen")
self.resize(420, 250)
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
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
}
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)
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)
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."))
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.1<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):
directory = QFileDialog.getExistingDirectory(self, "Standardpfad auswählen", self.output_dir_input.text() or os.path.expanduser("~"))
if directory:
self.output_dir_input.setText(directory)
def 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()
},
self.hide_defaults_cb.isChecked()
)
def download_ytdlp(self):
self.download_btn.setEnabled(False)
self.download_btn.setText("Lade herunter...")
self.thread = YtDlpDownloadThread()
self.thread.finished_signal.connect(self.download_finished)
self.thread.start()
def download_finished(self, success, message):
bin_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin")
ytdlp_path = os.path.join(bin_dir, "yt-dlp.exe")
self.download_btn.setEnabled(not os.path.exists(ytdlp_path))
self.download_btn.setText("yt-dlp.exe herunterladen")
if success:
QMessageBox.information(self, "Erfolg", message)
else:
QMessageBox.critical(self, "Fehler", message)
def update_ytdlp(self):
self.update_btn.setEnabled(False)
self.update_btn.setText("Aktualisiere...")
# Bestimme, ob lokal oder systemweit
use_local = self.ytdlp_source_combo.currentIndex() == 0
if use_local:
bin_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin")
ytdlp_path = os.path.join(bin_dir, "yt-dlp.exe")
cmd = [ytdlp_path, "-U"]
else:
cmd = ["yt-dlp", "-U"]
try:
creationflags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
result = subprocess.run(cmd, capture_output=True, text=True, creationflags=creationflags)
if result.returncode == 0:
QMessageBox.information(self, "Erfolg", f"yt-dlp wurde aktualisiert.\n\n{result.stdout}")
else:
QMessageBox.warning(self, "Fehler", f"Fehler beim Updaten von yt-dlp:\n{result.stderr or result.stdout}")
except Exception as e:
QMessageBox.critical(self, "Fehler", f"Fehler beim Ausführen von yt-dlp -U: {str(e)}")
self.update_btn.setEnabled(True)
self.update_btn.setText("yt-dlp.exe updaten")
class QueueItem:
"""Repräsentiert einen Eintrag in der Download-Queue."""
def __init__(self, url, preset_data, output_dir=None, output_filename=None,
series_info=None, use_local_ytdlp=True, extra_args=None):
self.id = str(uuid.uuid4()) # Eindeutige ID für diesen Queue-Eintrag
self.url = url
self.preset_data = preset_data.copy() if preset_data else {}
self.output_dir = output_dir
self.output_filename = output_filename
self.series_info = series_info.copy() if series_info else {}
self.use_local_ytdlp = use_local_ytdlp
self.extra_args = extra_args or ""
self.status = "Wartend"
def get_display_name(self):
"""Gibt einen lesbaren Namen für die Queue-Anzeige zurück."""
preset_name = self.preset_data.get("name", "Unbekannt")
if self.series_info:
series = self.series_info.get("series", "")
season = self.series_info.get("season", "")
episode = self.series_info.get("episode", "")
if series and season and episode:
return f"{self.url} - {series} S{season}E{episode} ({preset_name})"
return f"{self.url} ({preset_name})"
def to_dict(self):
"""Konvertiert das QueueItem in ein JSON-serialisierbares Dictionary."""
return {
"id": self.id,
"url": self.url,
"preset_data": self.preset_data,
"output_dir": self.output_dir,
"output_filename": self.output_filename,
"series_info": self.series_info,
"use_local_ytdlp": self.use_local_ytdlp,
"extra_args": self.extra_args,
"status": self.status
}
@classmethod
def from_dict(cls, data):
"""Erstellt ein QueueItem aus einem Dictionary."""
item = cls(
url=data["url"],
preset_data=data["preset_data"],
output_dir=data["output_dir"],
output_filename=data["output_filename"],
series_info=data["series_info"],
use_local_ytdlp=data["use_local_ytdlp"],
extra_args=data["extra_args"]
)
item.id = data["id"]
item.status = data["status"]
return item
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Video Download Helper")
self.resize(800, 600)
# Stelle sicher, dass Verzeichnisse existieren
self.ensure_directories()
# Default-Presets automatisch anlegen, falls keine vorhanden
if not any(f.endswith('.json') for f in os.listdir(PRESETS_DIR)):
self.create_default_presets()
self.config = self.load_config()
self.presets = self.load_presets()
self.download_thread = None
self.download_queue = [] # Liste für die Download-Queue
self.current_queue_item = None # Aktuell laufender Download aus der Queue
# UI initialisieren
self.setup_ui()
# Queue aus gespeicherter Datei laden (nach UI-Setup)
self.load_queue()
def ensure_directories(self):
"""Stellt sicher, dass alle benötigten Verzeichnisse existieren."""
# Stelle sicher, dass der Presets-Ordner existiert
os.makedirs(PRESETS_DIR, exist_ok=True)
# Stelle sicher, dass der bin-Ordner existiert
bin_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin")
os.makedirs(bin_dir, exist_ok=True)
def setup_ui(self):
central_widget = QWidget()
main_layout = QVBoxLayout()
# URL Input
url_layout = QHBoxLayout()
url_layout.addWidget(QLabel("Video-URL:"))
self.url_input = QLineEdit()
url_layout.addWidget(self.url_input)
main_layout.addLayout(url_layout)
# Preset Selection
preset_layout = QHBoxLayout()
preset_layout.addWidget(QLabel("Preset:"))
self.preset_combo = QComboBox()
self.update_preset_combo()
preset_layout.addWidget(self.preset_combo)
# Preset Buttons
self.add_preset_btn = QPushButton("Neu")
self.add_preset_btn.clicked.connect(self.add_preset)
preset_layout.addWidget(self.add_preset_btn)
self.edit_preset_btn = QPushButton("Bearbeiten")
self.edit_preset_btn.clicked.connect(self.edit_preset)
preset_layout.addWidget(self.edit_preset_btn)
self.delete_preset_btn = QPushButton("Löschen")
self.delete_preset_btn.clicked.connect(self.delete_preset)
preset_layout.addWidget(self.delete_preset_btn)
main_layout.addLayout(preset_layout)
# Serien-Einstellungen
self.series_group = QGroupBox("Serien-Einstellungen")
self.series_group.setVisible(False)
series_form = QFormLayout()
self.series_input = QLineEdit()
self.season_input = QLineEdit()
self.episode_input = QLineEdit()
series_form.addRow("Serie:", self.series_input)
series_form.addRow("Staffel:", self.season_input)
series_form.addRow("Folge:", self.episode_input)
self.custom_path_input = QLineEdit()
series_form.addRow("Eigener Pfad:", self.custom_path_input)
self.series_group.setLayout(series_form)
main_layout.addWidget(self.series_group)
# Options-Button statt Felder für Standardpfad und yt-dlp-Quelle
options_layout = QHBoxLayout()
self.optionen_btn = QPushButton("Optionen...")
self.optionen_btn.clicked.connect(self.open_optionen_dialog)
options_layout.addWidget(self.optionen_btn)
main_layout.addLayout(options_layout)
# Command Preview
main_layout.addWidget(QLabel("Befehlsvorschau:"))
self.cmd_preview = QTextEdit()
self.cmd_preview.setReadOnly(True)
self.cmd_preview.setMaximumHeight(60)
main_layout.addWidget(self.cmd_preview)
# Download Buttons
download_buttons_layout = QHBoxLayout()
self.download_btn = QPushButton("Download starten")
self.download_btn.clicked.connect(self.start_download)
download_buttons_layout.addWidget(self.download_btn)
# Neu: Queue-Button
self.queue_btn = QPushButton("Zur Queue hinzufügen")
self.queue_btn.clicked.connect(self.add_to_queue)
download_buttons_layout.addWidget(self.queue_btn)
main_layout.addLayout(download_buttons_layout)
# Neu: Tabbed Layout für Ausgabe und Queue
self.tabs = QTabWidget()
# Log Output Tab
log_tab = QWidget()
log_layout = QVBoxLayout()
log_layout.addWidget(QLabel("Ausgabe:"))
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
log_layout.addWidget(self.log_output)
log_tab.setLayout(log_layout)
self.tabs.addTab(log_tab, "Ausgabe")
# Queue Tab
queue_tab = QWidget()
queue_layout = QVBoxLayout()
queue_layout.addWidget(QLabel("Download-Queue:"))
self.queue_list = QListWidget()
self.queue_list.setContextMenuPolicy(Qt.CustomContextMenu)
self.queue_list.customContextMenuRequested.connect(self.show_queue_context_menu)
queue_layout.addWidget(self.queue_list)
queue_buttons = QHBoxLayout()
self.start_queue_btn = QPushButton("Queue starten")
self.start_queue_btn.clicked.connect(self.start_queue)
queue_buttons.addWidget(self.start_queue_btn)
self.clear_queue_btn = QPushButton("Queue leeren")
self.clear_queue_btn.clicked.connect(self.clear_queue)
queue_buttons.addWidget(self.clear_queue_btn)
queue_layout.addLayout(queue_buttons)
queue_tab.setLayout(queue_layout)
self.tabs.addTab(queue_tab, "Queue")
main_layout.addWidget(self.tabs)
# Connect signals
self.url_input.textChanged.connect(self.update_cmd_preview)
self.preset_combo.currentIndexChanged.connect(self.preset_changed)
# Serie, Staffel, Folge Signals
self.series_input.textChanged.connect(self.update_cmd_preview)
self.season_input.textChanged.connect(self.update_cmd_preview)
self.episode_input.textChanged.connect(self.update_cmd_preview)
self.custom_path_input.textChanged.connect(self.update_cmd_preview)
central_widget.setLayout(main_layout)
self.setCentralWidget(central_widget)
# Initial update
self.preset_changed()
self.update_cmd_preview()
self.update_queue_buttons()
def load_config(self):
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, 'r') as f:
return json.load(f)
except Exception:
return DEFAULT_CONFIG.copy()
return DEFAULT_CONFIG.copy()
def save_config(self):
self.config["output_dir"] = self.output_dir_input.text() if hasattr(self, 'output_dir_input') else self.config.get("output_dir", "")
self.config["use_local_ytdlp"] = self.ytdlp_source_combo.currentIndex() == 0 if hasattr(self, 'ytdlp_source_combo') else self.config.get("use_local_ytdlp", True)
if self.preset_combo.currentIndex() >= 0:
self.config["last_preset"] = self.preset_combo.currentText()
try:
with open(CONFIG_FILE, 'w') as f:
json.dump(self.config, f, indent=4)
except Exception as e:
self.log_output.append(f"Fehler beim Speichern der Konfiguration: {str(e)}")
def load_presets(self):
# Default-Presets immer aus dem Code laden
defaults = [
{
"name": "Audio (MP3)",
"description": "Nur Audio als MP3",
"args": "-x --audio-format mp3",
"has_series_template": False,
"is_audio": True,
"source": "default"
},
{
"name": "Video (Best)",
"description": "Beste Videoqualität",
"args": "-f bestvideo+bestaudio",
"has_series_template": False,
"is_audio": False,
"source": "default"
}
]
presets = defaults.copy()
# User-Presets (können mitgelieferte überschreiben)
user_presets_dir = get_user_presets_dir()
for filename in os.listdir(user_presets_dir):
if filename.endswith('.json'):
try:
with open(os.path.join(user_presets_dir, filename), 'r', encoding='utf-8') as f:
preset = json.load(f)
preset["source"] = "user"
# Überschreibe ggf. gleichnamiges Preset
presets = [p for p in presets if p["name"] != preset["name"]]
presets.append(preset)
except Exception as e:
self.log_output.append(f"Fehler beim Laden von User-Preset {filename}: {str(e)}")
return presets
def create_default_presets(self):
defaults = [
{
"name": "Audio (MP3)",
"description": "Nur Audio als MP3",
"args": "-x --audio-format mp3",
"has_series_template": False,
"is_audio": True,
"source": "default"
},
{
"name": "Video (Best)",
"description": "Beste Videoqualität",
"args": "-f bestvideo+bestaudio",
"has_series_template": False,
"is_audio": False,
"source": "default"
}
]
for preset in defaults:
filename = f"{preset['name'].lower().replace(' ', '_')}.json"
try:
with open(os.path.join(PRESETS_DIR, filename), 'w') as f:
json.dump(preset, f, indent=4)
except Exception as e:
self.log_output.append(f"Fehler beim Erstellen von Standard-Preset {preset['name']}: {str(e)}")
def update_preset_combo(self):
self.preset_combo.clear()
hide_defaults = self.config.get("hide_default_presets", False)
for preset in self.presets:
if hide_defaults and preset.get("source") == "default":
continue
self.preset_combo.addItem(preset["name"])
# Letztes verwendetes Preset auswählen
if self.config["last_preset"]:
index = self.preset_combo.findText(self.config["last_preset"])
if index >= 0:
self.preset_combo.setCurrentIndex(index)
def get_current_preset(self):
if self.preset_combo.currentIndex() >= 0:
preset_name = self.preset_combo.currentText()
for preset in self.presets:
if preset["name"] == preset_name:
return preset
return None
def add_preset(self):
dialog = PresetDialog(self)
if dialog.exec_() == QDialog.Accepted:
preset_data = dialog.get_preset_data()
if not preset_data["name"]:
QMessageBox.warning(self, "Fehler", "Der Preset-Name darf nicht leer sein.")
return
for preset in self.presets:
if preset["name"] == preset_data["name"]:
QMessageBox.warning(self, "Fehler", f"Ein Preset mit dem Namen '{preset_data['name']}' existiert bereits.")
return
preset_data["source"] = "user"
self.presets.append(preset_data)
# Speichern des Presets im User-Presets-Ordner
user_presets_dir = get_user_presets_dir()
filename = f"{preset_data['name'].lower().replace(' ', '_')}.json"
try:
with open(os.path.join(user_presets_dir, filename), 'w', encoding='utf-8') as f:
json.dump(preset_data, f, indent=4, ensure_ascii=False)
self.update_preset_combo()
self.preset_combo.setCurrentText(preset_data["name"])
self.update_cmd_preview()
except Exception as e:
self.log_output.append(f"Fehler beim Speichern des Presets: {str(e)}")
def edit_preset(self):
current_preset = self.get_current_preset()
if not current_preset:
QMessageBox.warning(self, "Warnung", "Kein Preset ausgewählt.")
return
dialog = PresetDialog(self, current_preset)
if dialog.exec_() == QDialog.Accepted:
new_preset_data = dialog.get_preset_data()
if not new_preset_data["name"]:
QMessageBox.warning(self, "Fehler", "Der Preset-Name darf nicht leer sein.")
return
if new_preset_data["name"] != current_preset["name"]:
for preset in self.presets:
if preset["name"] == new_preset_data["name"] and preset != current_preset:
QMessageBox.warning(self, "Fehler", f"Ein Preset mit dem Namen '{new_preset_data['name']}' existiert bereits.")
return
if new_preset_data["name"] != current_preset["name"]:
old_filename = f"{current_preset['name'].lower().replace(' ', '_')}.json"
try:
os.remove(os.path.join(get_user_presets_dir(), old_filename))
except Exception as e:
self.log_output.append(f"Warnung: Konnte alte Preset-Datei nicht löschen: {str(e)}")
for i, preset in enumerate(self.presets):
if preset["name"] == current_preset["name"]:
new_preset_data["source"] = "user"
self.presets[i] = new_preset_data
break
filename = f"{new_preset_data['name'].lower().replace(' ', '_')}.json"
try:
with open(os.path.join(get_user_presets_dir(), filename), 'w', encoding='utf-8') as f:
json.dump(new_preset_data, f, indent=4, ensure_ascii=False)
old_current_index = self.preset_combo.currentIndex()
self.update_preset_combo()
new_index = self.preset_combo.findText(new_preset_data["name"])
if new_index >= 0:
self.preset_combo.setCurrentIndex(new_index)
else:
self.preset_combo.setCurrentIndex(old_current_index)
self.update_cmd_preview()
except Exception as e:
self.log_output.append(f"Fehler beim Speichern des Presets: {str(e)}")
def delete_preset(self):
current_preset = self.get_current_preset()
if not current_preset:
QMessageBox.warning(self, "Warnung", "Kein Preset ausgewählt.")
return
reply = QMessageBox.question(
self,
"Preset löschen",
f"Möchten Sie das Preset '{current_preset['name']}' wirklich löschen?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
self.presets = [p for p in self.presets if p["name"] != current_preset["name"]]
filename = f"{current_preset['name'].lower().replace(' ', '_')}.json"
try:
os.remove(os.path.join(get_user_presets_dir(), filename))
self.update_preset_combo()
self.update_cmd_preview()
except Exception as e:
self.log_output.append(f"Fehler beim Löschen des Presets: {str(e)}")
def open_optionen_dialog(self):
dialog = OptionenDialog(
self.config["output_dir"],
self.config["use_local_ytdlp"],
self,
self.config.get("ytdlp_flags", DEFAULT_CONFIG["ytdlp_flags"])
)
if dialog.exec_() == QDialog.Accepted:
output_dir, use_local_ytdlp, ytdlp_flags, hide_defaults = dialog.get_values()
self.config["output_dir"] = output_dir
self.config["use_local_ytdlp"] = use_local_ytdlp
self.config["ytdlp_flags"] = ytdlp_flags
self.config["hide_default_presets"] = hide_defaults
self.update_preset_combo()
self.update_cmd_preview()
def preset_changed(self):
preset = self.get_current_preset()
if not preset:
self.series_group.setVisible(False)
return
# Aktualisiere Serien-UI basierend auf dem Preset
has_series = preset.get("has_series_template", False)
self.series_group.setVisible(has_series)
if has_series:
self.series_input.setText(preset.get("series", ""))
self.season_input.setText(preset.get("season", "1"))
self.episode_input.setText(preset.get("episode", "1"))
self.custom_path_input.setText(preset.get("custom_path", ""))
self.update_cmd_preview()
def get_output_filename(self, preset):
if preset.get("has_series_template", False):
template = preset.get("series_template", SERIES_TEMPLATE)
series = self.series_input.text() or preset.get("series", "")
season = self.season_input.text() or preset.get("season", "1")
episode = self.episode_input.text() or preset.get("episode", "1")
custom_path = self.custom_path_input.text() or preset.get("custom_path", "")
# Stelle sicher, dass der Pfad mit einem Trennzeichen endet, wenn er nicht leer ist
if custom_path and not custom_path.endswith("/") and not custom_path.endswith("\\"):
custom_path += "/" # Verwende / als universellen Pfadtrenner
output_name = template
output_name = output_name.replace("{series}", series)
output_name = output_name.replace("{season}", season)
output_name = output_name.replace("{episode}", episode)
# Entferne {path} Platzhalter, da custom_path jetzt als output_dir verwendet wird
output_name = output_name.replace("{path}", "")
# {extension} durch .%(ext)s ersetzen (auch rückwärtskompatibel)
output_name = output_name.replace("{extension}", ".%(ext)s")
# Falls jemand %(ext)s nicht im Template hat, ergänzen wir es am Ende
if "%(ext)s" not in output_name:
output_name += ".%(ext)s"
# Entferne doppelte Schrägstriche
while "//" in output_name:
output_name = output_name.replace("//", "/")
while "\\\\" in output_name:
output_name = output_name.replace("\\\\", "\\")
self.log_output.append("Ausgabedatei-Komponenten:")
self.log_output.append(f" Template: {template}")
self.log_output.append(f" Serie: {series}")
self.log_output.append(f" Staffel: {season}")
self.log_output.append(f" Folge: {episode}")
self.log_output.append(f" Eigener Pfad: {custom_path}")
self.log_output.append(f"Generierter Dateiname: {output_name}")
return output_name
# Debug-Ausgabe auch im Nicht-Serienfall
output_name = "%(title)s.%(ext)s"
# Kein custom_path mehr in output_name, da dies bereits als output_dir verwendet wird
self.log_output.append("Ausgabedatei-Komponenten:")
self.log_output.append(f" Template: {output_name}")
self.log_output.append(f" Eigener Pfad: {self.custom_path_input.text() or preset.get('custom_path', '')}")
self.log_output.append(f"Generierter Dateiname: {output_name}")
return output_name
def update_cmd_preview(self):
preset = self.get_current_preset()
if not preset:
self.cmd_preview.setText("Kein Preset ausgewählt.")
return
if self.config["use_local_ytdlp"]:
ytdlp_path = os.path.join(get_base_path(), "bin", "yt-dlp.exe")
if not os.path.exists(ytdlp_path):
ytdlp_path = "yt-dlp" # Fallback auf PATH
else:
ytdlp_path = "yt-dlp"
cmd = [ytdlp_path]
flags = self.config.get("ytdlp_flags", {})
is_audio = preset.get("is_audio", False)
# Login-Optionen
if preset.get("username"):
cmd.extend(["-u", preset["username"]])
if preset.get("password"):
cmd.extend(["-p", preset["password"]])
# Referer
if preset.get("referer"):
cmd.append(f"--referer={preset['referer']}")
# HLS-ffmpeg
if preset.get("hls_ffmpeg"):
cmd.extend(["--downloader", "ffmpeg", "--hls-use-mpegts"])
# Untertitel
if preset.get("sublang"):
cmd.extend(["--sub-lang", preset["sublang"]])
if preset.get("embed_subs"):
cmd.append("--embed-subs")
if preset.get("subformat"):
cmd.extend(["--convert-subs", preset["subformat"]])
if flags.get("ignore_config"):
cmd.append("--ignore-config")
if flags.get("remux_mkv") and not is_audio:
cmd.extend(["--remux-video", "mkv"])
if flags.get("embed_metadata"):
cmd.append("--embed-metadata")
if preset["args"]:
args = []
in_quotes = False
current_arg = ""
for char in preset["args"]:
if char == '"' or char == "'":
in_quotes = not in_quotes
current_arg += char
elif char.isspace() and not in_quotes:
if current_arg:
args.append(current_arg)
current_arg = ""
else:
current_arg += char
if current_arg:
args.append(current_arg)
cmd.extend(args)
# Wenn ein eigener Pfad im Preset definiert ist, diesen verwenden
custom_path = self.custom_path_input.text() or preset.get("custom_path", "")
output_dir = custom_path if custom_path else self.config["output_dir"]
output_filename = self.get_output_filename(preset)
if output_dir:
output_path = os.path.join(output_dir, output_filename)
cmd.extend(["-o", output_path])
else:
cmd.extend(["-o", output_filename])
url = self.url_input.text() or "[URL]"
cmd.append(url)
formatted_cmd = []
for arg in cmd:
if " " in arg and not (arg.startswith('"') and arg.endswith('"')) and not (arg.startswith("'") and arg.endswith("'")):
formatted_cmd.append(f'"{arg}"')
else:
formatted_cmd.append(arg)
self.cmd_preview.setText(" ".join(formatted_cmd))
def start_download(self):
# Wenn bereits ein Download läuft und der Button als "Abbrechen" angezeigt wird
if self.download_thread and self.download_thread.isRunning():
self.abort_download()
return
url = self.url_input.text()
if not url:
QMessageBox.warning(self, "Fehler", "Bitte geben Sie eine URL ein.")
return
preset = self.get_current_preset()
if not preset:
QMessageBox.warning(self, "Fehler", "Bitte wählen Sie ein Preset aus.")
return
# Wenn im Preset ein eigener Pfad definiert ist, diesen bevorzugen
custom_path = self.custom_path_input.text() or preset.get("custom_path", "")
# Wenn custom_path gesetzt ist, verwenden wir diesen anstelle des standard output_dir
output_dir = custom_path if custom_path else self.config["output_dir"]
use_local_ytdlp = self.config["use_local_ytdlp"]
flags = self.config.get("ytdlp_flags", {})
is_audio = preset.get("is_audio", False)
extra_args = []
if preset.get("username"):
extra_args.extend(["-u", preset["username"]])
if preset.get("password"):
extra_args.extend(["-p", preset["password"]])
if preset.get("referer"):
extra_args.append(f"--referer={preset['referer']}")
if preset.get("hls_ffmpeg"):
extra_args.extend(["--downloader", "ffmpeg", "--hls-use-mpegts"])
if preset.get("sublang"):
extra_args.extend(["--sub-lang", preset["sublang"]])
if preset.get("embed_subs"):
extra_args.append("--embed-subs")
if preset.get("subformat"):
extra_args.extend(["--convert-subs", preset["subformat"]])
if flags.get("ignore_config"):
extra_args.append("--ignore-config")
if flags.get("remux_mkv") and not is_audio:
extra_args.extend(["--remux-video", "mkv"])
if flags.get("embed_metadata"):
extra_args.append("--embed-metadata")
full_args = (" ".join(extra_args) + " " + preset["args"]).strip() if extra_args else preset["args"]
self.save_config()
self.log_output.clear()
self.log_output.append(f"Starte Download von: {url}")
output_filename = None
if preset.get("has_series_template", False):
output_filename = self.get_output_filename(preset)
self.log_output.append(f"Verwende Ausgabedateiname: {output_filename}")
self.log_output.append("Download-Parameter:")
self.log_output.append(f"URL: {url}")
self.log_output.append(f"Ausgabeverzeichnis: {output_dir}")
self.log_output.append(f"Argumente: {preset['args']}")
self.log_output.append(f"Lokales yt-dlp: {use_local_ytdlp}")
self.log_output.append(f"yt-dlp-Flags: {flags}")
self.log_output.append(f"Audio-Preset: {is_audio}")
self.log_output.append(f"Benutzername: {preset.get('username','')}")
self.log_output.append(f"Referer: {preset.get('referer','')}")
self.log_output.append(f"HLS-ffmpeg: {preset.get('hls_ffmpeg',False)}")
self.log_output.append(f"Untertitelsprache: {preset.get('sublang','')}")
self.log_output.append(f"Embed-Subs: {preset.get('embed_subs',False)}")
self.log_output.append(f"Untertitel-Format: {preset.get('subformat','')}")
self.log_output.append(f"Ausgabedateiname: {output_filename}")
self.download_thread = DownloadThread(
url=url,
output_dir=output_dir,
cmd_args=full_args,
use_local_ytdlp=use_local_ytdlp,
output_filename=output_filename
)
self.download_thread.update_signal.connect(self.update_log)
self.download_thread.finished_signal.connect(self.download_finished)
# Ändere den Button-Text und deaktiviere UI-Elemente während des Downloads
self.download_btn.setText("Download abbrechen")
self.disable_ui_during_download(True)
self.download_thread.start()
def abort_download(self):
"""Bricht den aktuellen Download ab."""
if self.download_thread and self.download_thread.isRunning():
self.log_output.append("Abbruch angefordert...")
self.download_thread.stop()
# Button wird in download_finished wieder auf "Download starten" gesetzt
def disable_ui_during_download(self, disable=True):
"""Deaktiviert/aktiviert UI-Elemente während des Downloads."""
self.url_input.setReadOnly(disable)
self.preset_combo.setEnabled(not disable)
self.add_preset_btn.setEnabled(not disable)
self.edit_preset_btn.setEnabled(not disable)
self.delete_preset_btn.setEnabled(not disable)
self.optionen_btn.setEnabled(not disable)
if hasattr(self, 'series_input'):
self.series_input.setReadOnly(disable)
if hasattr(self, 'season_input'):
self.season_input.setReadOnly(disable)
if hasattr(self, 'episode_input'):
self.episode_input.setReadOnly(disable)
if hasattr(self, 'custom_path_input'):
self.custom_path_input.setReadOnly(disable)
def update_log(self, text):
self.log_output.append(text)
# Scroll zum Ende
scrollbar = self.log_output.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
def download_finished(self, success, message):
# UI-Elemente wieder aktivieren
self.disable_ui_during_download(False)
# Button-Text zurücksetzen
self.download_btn.setText("Download starten")
if success:
self.log_output.append(message)
QMessageBox.information(self, "Erfolg", message)
else:
self.log_output.append(f"Fehler: {message}")
QMessageBox.warning(self, "Fehler", message)
# Queue-Buttons nach Download aktualisieren
self.update_queue_buttons()
def closeEvent(self, event):
self.save_config()
self.save_queue() # Queue beim Beenden speichern
event.accept()
def load_queue(self):
"""Lädt die gespeicherte Download-Queue aus einer Datei."""
queue_file = os.path.join(get_user_data_dir(), "queue.json")
if os.path.exists(queue_file):
try:
with open(queue_file, 'r', encoding='utf-8') as f:
queue_data = json.load(f)
self.download_queue = [QueueItem.from_dict(item) for item in queue_data]
# Sicherer Log-Aufruf
if hasattr(self, 'log_output'):
self.log_output.append(f"Download-Queue mit {len(self.download_queue)} Elementen geladen.")
# Queue-Liste aktualisieren
if hasattr(self, 'queue_list'):
self.update_queue_list()
except Exception as e:
if hasattr(self, 'log_output'):
self.log_output.append(f"Fehler beim Laden der Download-Queue: {str(e)}")
print(f"Fehler beim Laden der Download-Queue: {str(e)}")
self.download_queue = []
# Aktualisiere Queue-Buttons nach dem Laden
if hasattr(self, 'start_queue_btn') and hasattr(self, 'clear_queue_btn'):
self.update_queue_buttons()
def save_queue(self):
"""Speichert die Download-Queue in eine Datei."""
queue_file = os.path.join(get_user_data_dir(), "queue.json")
try:
queue_data = [item.to_dict() for item in self.download_queue]
with open(queue_file, 'w', encoding='utf-8') as f:
json.dump(queue_data, f, indent=4, ensure_ascii=False)
except Exception as e:
if hasattr(self, 'log_output'):
self.log_output.append(f"Fehler beim Speichern der Download-Queue: {str(e)}")
print(f"Fehler beim Speichern der Download-Queue: {str(e)}")
def add_to_queue(self):
"""Fügt den aktuellen Download zur Queue hinzu."""
url = self.url_input.text()
if not url:
QMessageBox.warning(self, "Fehler", "Bitte geben Sie eine URL ein.")
return
preset = self.get_current_preset()
if not preset:
QMessageBox.warning(self, "Fehler", "Bitte wählen Sie ein Preset aus.")
return
# Wenn im Preset ein eigener Pfad definiert ist, diesen bevorzugen
custom_path = self.custom_path_input.text() or preset.get("custom_path", "")
# Wenn custom_path gesetzt ist, verwenden wir diesen anstelle des standard output_dir
output_dir = custom_path if custom_path else self.config["output_dir"]
series_info = None
output_filename = None
# Wenn es ein Serien-Preset ist, die Serien-Infos separat speichern
if preset.get("has_series_template", False):
series_info = {
"series": self.series_input.text() or preset.get("series", ""),
"season": self.season_input.text() or preset.get("season", "1"),
"episode": self.episode_input.text() or preset.get("episode", "1"),
"template": preset.get("series_template", SERIES_TEMPLATE)
}
output_filename = self.get_output_filename(preset)
# Flags und Extra-Argumente vorbereiten
flags = self.config.get("ytdlp_flags", {})
is_audio = preset.get("is_audio", False)
extra_args = []
if preset.get("username"):
extra_args.extend(["-u", preset["username"]])
if preset.get("password"):
extra_args.extend(["-p", preset["password"]])
if preset.get("referer"):
extra_args.append(f"--referer={preset['referer']}")
if preset.get("hls_ffmpeg"):
extra_args.extend(["--downloader", "ffmpeg", "--hls-use-mpegts"])
if preset.get("sublang"):
extra_args.extend(["--sub-lang", preset["sublang"]])
if preset.get("embed_subs"):
extra_args.append("--embed-subs")
if preset.get("subformat"):
extra_args.extend(["--convert-subs", preset["subformat"]])
if flags.get("ignore_config"):
extra_args.append("--ignore-config")
if flags.get("remux_mkv") and not is_audio:
extra_args.extend(["--remux-video", "mkv"])
if flags.get("embed_metadata"):
extra_args.append("--embed-metadata")
full_args = (" ".join(extra_args) + " " + preset["args"]).strip() if extra_args else preset["args"]
# Erstelle ein Queue-Element
queue_item = QueueItem(
url=url,
preset_data=preset,
output_dir=output_dir,
output_filename=output_filename,
series_info=series_info,
use_local_ytdlp=self.config["use_local_ytdlp"],
extra_args=full_args
)
# Füge das Element zur Queue hinzu
self.download_queue.append(queue_item)
# Aktualisiere die Queue-Liste
self.update_queue_list()
# Zur Ausgabe wechseln und Hinzufügen bestätigen
self.tabs.setCurrentIndex(0) # Wechsle zum Ausgabe-Tab
self.log_output.append(f"Download wurde zur Queue hinzugefügt: {queue_item.get_display_name()}")
# Optional: Leere die URL-Box
self.url_input.clear()
# Aktualisiere Queue-Buttons
self.update_queue_buttons()
# Queue speichern
self.save_queue()
def update_queue_list(self):
"""Aktualisiert die Anzeige der Queue-Liste."""
self.queue_list.clear()
for item in self.download_queue:
list_item = QListWidgetItem(f"{item.status}: {item.get_display_name()}")
list_item.setData(Qt.UserRole, item.id) # Speichere die ID als Daten
self.queue_list.addItem(list_item)
def update_queue_buttons(self):
"""Aktualisiert den Status der Queue-Buttons basierend auf dem Zustand der Queue."""
has_items = len(self.download_queue) > 0
is_downloading = self.download_thread and self.download_thread.isRunning()
self.start_queue_btn.setEnabled(has_items and not is_downloading)
self.clear_queue_btn.setEnabled(has_items and not is_downloading)
def show_queue_context_menu(self, position):
"""Zeigt das Kontextmenü für die Queue-Liste an."""
if not self.queue_list.count():
return
menu = QMenu()
# Aktionen erstellen
remove_action = QAction("Entfernen", self)
remove_action.triggered.connect(self.remove_selected_queue_item)
move_up_action = QAction("Nach oben", self)
move_up_action.triggered.connect(lambda: self.move_queue_item(-1))
move_down_action = QAction("Nach unten", self)
move_down_action.triggered.connect(lambda: self.move_queue_item(1))
# Prüfe ob ein Element ausgewählt ist
if self.queue_list.currentItem():
menu.addAction(remove_action)
menu.addAction(move_up_action)
menu.addAction(move_down_action)
# Zeige Menü
menu.exec_(self.queue_list.mapToGlobal(position))
def remove_selected_queue_item(self):
"""Entfernt das ausgewählte Element aus der Queue."""
current_item = self.queue_list.currentItem()
if not current_item:
return
item_id = current_item.data(Qt.UserRole)
self.download_queue = [item for item in self.download_queue if item.id != item_id]
self.update_queue_list()
self.update_queue_buttons()
# Queue speichern
self.save_queue()
def move_queue_item(self, direction):
"""Verschiebt ein Queue-Element nach oben oder unten."""
current_row = self.queue_list.currentRow()
if current_row < 0:
return
new_row = current_row + direction
if new_row < 0 or new_row >= self.queue_list.count():
return
# Tausche Elemente in der Queue
self.download_queue[current_row], self.download_queue[new_row] = \
self.download_queue[new_row], self.download_queue[current_row]
# Aktualisiere UI
self.update_queue_list()
self.queue_list.setCurrentRow(new_row)
# Queue speichern
self.save_queue()
def start_queue(self):
"""Startet die Download-Queue."""
if not self.download_queue:
QMessageBox.information(self, "Queue leer", "Die Download-Queue ist leer.")
return
if self.download_thread and self.download_thread.isRunning():
QMessageBox.warning(self, "Download läuft", "Es läuft bereits ein Download.")
return
# Starte den ersten Download in der Queue
self.process_next_queue_item()
def process_next_queue_item(self):
"""Verarbeitet das nächste Element in der Queue."""
if not self.download_queue:
self.log_output.append("Download-Queue abgeschlossen.")
self.current_queue_item = None
self.update_queue_buttons()
return
# Nehme das erste Element aus der Queue
self.current_queue_item = self.download_queue[0]
self.current_queue_item.status = "Wird heruntergeladen"
self.update_queue_list()
# Starte den Download
self.tabs.setCurrentIndex(0) # Wechsle zum Ausgabe-Tab
self.log_output.clear()
self.log_output.append(f"Starte Download von: {self.current_queue_item.url}")
self.log_output.append(f"Preset: {self.current_queue_item.preset_data.get('name', 'Unbekannt')}")
self.log_output.append(f"Ausgabeverzeichnis: {self.current_queue_item.output_dir}")
# Download-Thread erstellen und starten
self.download_thread = DownloadThread(
url=self.current_queue_item.url,
output_dir=self.current_queue_item.output_dir,
cmd_args=self.current_queue_item.extra_args,
use_local_ytdlp=self.current_queue_item.use_local_ytdlp,
output_filename=self.current_queue_item.output_filename
)
self.download_thread.update_signal.connect(self.update_log)
self.download_thread.finished_signal.connect(self.queue_download_finished)
# Ändere den Button-Text und deaktiviere UI-Elemente während des Downloads
self.download_btn.setText("Download abbrechen")
self.disable_ui_during_download(True)
self.download_thread.start()
def queue_download_finished(self, success, message):
"""Callback wenn ein Download aus der Queue fertig ist."""
# UI-Elemente wieder aktivieren
self.disable_ui_during_download(False)
# Button-Text zurücksetzen
self.download_btn.setText("Download starten")
# Bearbeite das aktuelle Queue-Element
if self.current_queue_item:
self.current_queue_item.status = "Fertig" if success else "Fehler"
# Entferne das Element aus der Queue (es bleibt in der Liste, aber mit Status)
if self.download_queue and self.download_queue[0].id == self.current_queue_item.id:
self.download_queue.pop(0)
self.update_queue_list()
# Queue speichern
self.save_queue()
if success:
self.log_output.append(message)
else:
self.log_output.append(f"Fehler: {message}")
# Wenn die Queue nicht manuell abgebrochen wurde, verarbeite das nächste Element
if not message.startswith("Download wurde abgebrochen"):
self.process_next_queue_item()
# Aktualisiere Queue-Buttons
self.update_queue_buttons()
def clear_queue(self):
"""Leert die Download-Queue."""
if not self.download_queue:
return
reply = QMessageBox.question(
self,
"Queue leeren",
"Möchten Sie wirklich die gesamte Download-Queue leeren?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
self.download_queue.clear()
self.update_queue_list()
self.update_queue_buttons()
# Queue speichern (leere Liste)
self.save_queue()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())