1147 lines
49 KiB
Python
1147 lines
49 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 uuid
|
|
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QLabel, QLineEdit, QPushButton, QComboBox, QTextEdit,
|
|
QMessageBox, QListWidget, QFormLayout,
|
|
QGroupBox, QTabWidget, QListWidgetItem, QMenu, QAction, QInputDialog)
|
|
from PyQt5.QtCore import Qt
|
|
from PyQt5.QtGui import QIcon
|
|
|
|
# Imports für die neuen Module
|
|
from config import (get_base_path, get_user_data_dir, get_user_presets_dir,
|
|
CONFIG_FILE, PRESETS_DIR, DEFAULT_CONFIG, SERIES_TEMPLATE)
|
|
from utils import mask_sensitive_data, set_window_icon
|
|
from download_threads import DownloadThread
|
|
from dialogs import PresetDialog, OptionenDialog
|
|
|
|
class QueueItem:
|
|
"""Repräsentiert einen Eintrag in der Download-Queue."""
|
|
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)
|
|
|
|
# Icon für das Hauptfenster setzen
|
|
set_window_icon(self)
|
|
|
|
# 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_() == PresetDialog.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_() == PresetDialog.Accepted:
|
|
new_preset_data = dialog.get_preset_data()
|
|
if not new_preset_data["name"]:
|
|
QMessageBox.warning(self, "Fehler", "Der Preset-Name darf nicht leer sein.")
|
|
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_() == OptionenDialog.Accepted:
|
|
output_dir, use_local_ytdlp, ytdlp_flags, hide_defaults, mkvmerge_path, enable_adn_tab, ffmpeg_path = dialog.get_values()
|
|
self.config["output_dir"] = output_dir
|
|
self.config["use_local_ytdlp"] = use_local_ytdlp
|
|
self.config["ytdlp_flags"] = ytdlp_flags
|
|
self.config["hide_default_presets"] = hide_defaults
|
|
self.config["mkvmerge_path"] = mkvmerge_path
|
|
self.config["enable_adn_tab"] = enable_adn_tab
|
|
self.config["ffmpeg_path"] = ffmpeg_path
|
|
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)
|
|
|
|
# Setze den custom_path_input immer zurück, unabhängig vom Preset-Typ
|
|
self.custom_path_input.setText(preset.get("custom_path", ""))
|
|
|
|
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.update_cmd_preview()
|
|
|
|
def update_cmd_preview(self):
|
|
preset = self.get_current_preset()
|
|
if not preset:
|
|
self.cmd_preview.setText("Kein Preset ausgewählt.")
|
|
return
|
|
|
|
# Zeige Preset-Informationen in der Ausgabe an
|
|
self.show_preset_info(preset)
|
|
|
|
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"])
|
|
# FFmpeg-Pfad
|
|
if flags.get("use_ffmpeg_location") and self.config.get("ffmpeg_path"):
|
|
cmd.extend(["--ffmpeg-location", self.config.get("ffmpeg_path")])
|
|
# 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"]
|
|
|
|
# Generiere den Dateinamen ohne Debug-Ausgaben
|
|
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("\\\\", "\\")
|
|
output_filename = output_name
|
|
else:
|
|
# Verwende benutzerdefinierte Ausgabevorlage, wenn in den Presets aktiviert
|
|
if preset.get("custom_output_template", False) and preset.get("output_template"):
|
|
output_filename = preset.get("output_template")
|
|
else:
|
|
output_filename = "%(title)s.%(ext)s"
|
|
|
|
# Füge den Ausgabepfad zum Befehl hinzu
|
|
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)
|
|
|
|
# Maskiere sensible Daten in der Befehlsvorschau
|
|
command_string = " ".join(formatted_cmd)
|
|
masked_command = mask_sensitive_data(command_string)
|
|
self.cmd_preview.setText(masked_command)
|
|
|
|
def show_preset_info(self, preset):
|
|
"""Zeigt Informationen zum ausgewählten Preset in der Ausgabe an."""
|
|
self.log_output.clear()
|
|
self.log_output.append(f"Preset: {preset['name']}")
|
|
if preset.get("description"):
|
|
self.log_output.append(f"Beschreibung: {preset['description']}")
|
|
|
|
# Ausgabepfad
|
|
custom_path = self.custom_path_input.text() or preset.get("custom_path", "")
|
|
output_dir = custom_path if custom_path else self.config["output_dir"]
|
|
self.log_output.append(f"Ausgabeverzeichnis: {output_dir or '(Standard)'}")
|
|
|
|
# Serien-Informationen
|
|
if preset.get("has_series_template", False):
|
|
self.log_output.append("\nSerien-Einstellungen:")
|
|
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")
|
|
|
|
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}")
|
|
|
|
# Generiere Beispiel-Dateinamen
|
|
output_name = template
|
|
output_name = output_name.replace("{series}", series)
|
|
output_name = output_name.replace("{season}", season)
|
|
output_name = output_name.replace("{episode}", episode)
|
|
output_name = output_name.replace("{path}", "")
|
|
output_name = output_name.replace("{extension}", ".mp4")
|
|
|
|
self.log_output.append(f" Beispiel-Dateiname: {output_name}")
|
|
|
|
# Preset-Argumente
|
|
self.log_output.append("\nArgumente:")
|
|
self.log_output.append(f" {preset['args']}")
|
|
|
|
# Besondere Eigenschaften
|
|
special_features = []
|
|
if preset.get("is_audio", False):
|
|
special_features.append("Audio-Preset")
|
|
if preset.get("username"):
|
|
special_features.append("Authentifizierung")
|
|
if preset.get("referer"):
|
|
special_features.append("Referer")
|
|
if preset.get("hls_ffmpeg", False):
|
|
special_features.append("HLS-ffmpeg")
|
|
if preset.get("sublang") or preset.get("embed_subs") or preset.get("subformat"):
|
|
special_features.append("Untertitel")
|
|
if preset.get("use_format_selection", False):
|
|
special_features.append("Format-Auswahl")
|
|
if preset.get("use_dual_audio", False):
|
|
special_features.append("Dual-Audio")
|
|
|
|
if special_features:
|
|
self.log_output.append("\nBesondere Eigenschaften:")
|
|
self.log_output.append(f" {', '.join(special_features)}")
|
|
|
|
def get_output_filename(self, preset):
|
|
"""Generiert den Ausgabedateinamen basierend auf dem Preset und den aktuellen Einstellungen."""
|
|
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("\\\\", "\\")
|
|
return output_name
|
|
# Im Nicht-Serienfall einfach den Standardnamen zurückgeben
|
|
return "%(title)s.%(ext)s"
|
|
|
|
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)
|
|
use_dual_audio = preset.get("use_dual_audio", False)
|
|
|
|
# Aktualisiere die Serien-Informationen im Preset für den Download
|
|
# (nur temporär für diesen Download, nicht dauerhaft speichern)
|
|
if preset.get("has_series_template", False):
|
|
updated_preset = preset.copy()
|
|
updated_preset["series"] = self.series_input.text() or preset.get("series", "")
|
|
updated_preset["season"] = self.season_input.text() or preset.get("season", "1")
|
|
updated_preset["episode"] = self.episode_input.text() or preset.get("episode", "1")
|
|
else:
|
|
updated_preset = preset
|
|
|
|
extra_args = []
|
|
if preset.get("username"):
|
|
extra_args.extend(["-u", preset["username"]])
|
|
if preset.get("password"):
|
|
extra_args.extend(["-p", preset["password"]])
|
|
# Referer
|
|
if preset.get("referer"):
|
|
extra_args.append(f"--referer={preset['referer']}")
|
|
# HLS-ffmpeg
|
|
if preset.get("hls_ffmpeg"):
|
|
extra_args.extend(["--downloader", "ffmpeg", "--hls-use-mpegts"])
|
|
# FFmpeg-Pfad
|
|
if flags.get("use_ffmpeg_location") and self.config.get("ffmpeg_path"):
|
|
extra_args.extend(["--ffmpeg-location", self.config.get("ffmpeg_path")])
|
|
# Untertitel
|
|
if preset.get("sublang"):
|
|
extra_args.extend(["--sub-lang", preset["sublang"]])
|
|
if preset.get("embed_subs"):
|
|
extra_args.append("--embed-subs")
|
|
if preset.get("subformat"):
|
|
extra_args.extend(["--convert-subs", preset["subformat"]])
|
|
# Global Flags
|
|
if flags.get("ignore_config"):
|
|
extra_args.append("--ignore-config")
|
|
if flags.get("remux_mkv") and not is_audio and not use_dual_audio:
|
|
extra_args.extend(["--remux-video", "mkv"])
|
|
if flags.get("embed_metadata"):
|
|
extra_args.append("--embed-metadata")
|
|
|
|
# Prüfe auf Dual-Audio-Muxing
|
|
if preset.get("use_dual_audio", False) and not self.config.get("mkvmerge_path"):
|
|
QMessageBox.warning(self, "Fehler", "Für Dual-Audio Muxing muss der MKVMerge-Pfad in den Optionen angegeben werden.")
|
|
return
|
|
|
|
cmd_args = " ".join(extra_args) + (" " + preset["args"] if preset["args"] else "")
|
|
|
|
# Bestimme den Ausgabedateinamen
|
|
output_filename = None
|
|
if preset.get("has_series_template", False):
|
|
output_filename = self.get_output_filename(preset)
|
|
elif preset.get("custom_output_template", False) and preset.get("output_template"):
|
|
output_filename = preset.get("output_template")
|
|
|
|
self.download_thread = DownloadThread(
|
|
url, output_dir, cmd_args, use_local_ytdlp, output_filename,
|
|
updated_preset, self.config
|
|
)
|
|
|
|
# Signal-Verbindungen
|
|
self.download_thread.update_signal.connect(self.log_output.append)
|
|
self.download_thread.finished_signal.connect(self.download_finished)
|
|
self.download_thread.format_selection_signal.connect(self.show_formats)
|
|
self.download_thread.format_id_input_signal.connect(self.get_format_id_from_user)
|
|
|
|
# UI-Zustand ändern
|
|
self.download_btn.setText("Download abbrechen")
|
|
self.download_btn.setStyleSheet("background-color: #ff6b6b; color: white;")
|
|
self.queue_btn.setEnabled(False) # Queue-Button deaktivieren während des Downloads
|
|
|
|
# Switch zu Log-Tab
|
|
self.tabs.setCurrentIndex(0)
|
|
|
|
# Download starten
|
|
self.download_thread.start()
|
|
|
|
# Config speichern
|
|
self.save_config()
|
|
|
|
def abort_download(self):
|
|
"""Bricht den aktuellen Download ab."""
|
|
if self.download_thread and self.download_thread.isRunning():
|
|
self.download_thread.stop()
|
|
self.log_output.append("Download-Abbruch angefordert...")
|
|
|
|
def download_finished(self, success, message):
|
|
# UI-Zustand zurücksetzen
|
|
self.download_btn.setText("Download starten")
|
|
self.download_btn.setStyleSheet("")
|
|
self.queue_btn.setEnabled(True) # Queue-Button wieder aktivieren
|
|
|
|
if success:
|
|
self.log_output.append(message)
|
|
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()
|
|
|
|
# Wenn aus der Queue, dann nächsten Download starten
|
|
if self.current_queue_item and self.download_queue:
|
|
self.process_next_queue_item()
|
|
else:
|
|
self.current_queue_item = None
|
|
|
|
def show_formats(self, formats):
|
|
"""Zeigt die verfügbaren Formate in einem Dialog an."""
|
|
# Hier könnte man einen Dialog mit den Formaten anzeigen
|
|
# Für einfachheit zeigen wir sie erstmal im Log
|
|
self.log_output.append("\n=== Verfügbare Formate ===")
|
|
for format_line in formats:
|
|
self.log_output.append(format_line)
|
|
self.log_output.append("=== Ende der Formate ===\n")
|
|
|
|
def get_format_id_from_user(self):
|
|
"""Fordert den Benutzer zur Eingabe einer Format-ID auf."""
|
|
dialog = QInputDialog(self)
|
|
dialog.setWindowTitle("Format-ID eingeben")
|
|
dialog.setLabelText("Bitte gib die gewünschte Format-ID ein:")
|
|
dialog.setInputMode(QInputDialog.TextInput)
|
|
dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint) # Fragezeichen entfernen
|
|
|
|
# Icon für den Dialog setzen
|
|
set_window_icon(dialog)
|
|
|
|
ok = dialog.exec_()
|
|
format_id = dialog.textValue()
|
|
|
|
if ok and format_id and self.download_thread:
|
|
self.download_thread.set_format_id(format_id)
|
|
elif self.download_thread:
|
|
# Abbrechen
|
|
self.download_thread.set_format_id("abort")
|
|
|
|
def add_to_queue(self):
|
|
"""Fügt den aktuellen Download zur Queue hinzu."""
|
|
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", "")
|
|
output_dir = custom_path if custom_path else self.config["output_dir"]
|
|
|
|
# Serieninformationen sammeln
|
|
series_info = {}
|
|
if preset.get("has_series_template", False):
|
|
series_info = {
|
|
"series": self.series_input.text() or preset.get("series", ""),
|
|
"season": self.season_input.text() or preset.get("season", "1"),
|
|
"episode": self.episode_input.text() or preset.get("episode", "1")
|
|
}
|
|
|
|
# Bestimme den Ausgabedateinamen
|
|
output_filename = None
|
|
if preset.get("has_series_template", False):
|
|
output_filename = self.get_output_filename(preset)
|
|
elif preset.get("custom_output_template", False) and preset.get("output_template"):
|
|
output_filename = preset.get("output_template")
|
|
|
|
# Queue-Item erstellen
|
|
queue_item = QueueItem(
|
|
url=url,
|
|
preset_data=preset.copy(), # Kopie des Presets mit den aktuellen Werten
|
|
output_dir=output_dir,
|
|
output_filename=output_filename,
|
|
series_info=series_info,
|
|
use_local_ytdlp=self.config["use_local_ytdlp"]
|
|
)
|
|
|
|
# Zur Queue hinzufügen
|
|
self.download_queue.append(queue_item)
|
|
self.update_queue_list()
|
|
self.update_queue_buttons()
|
|
self.save_queue()
|
|
|
|
# Tab zur Queue wechseln
|
|
self.tabs.setCurrentIndex(1)
|
|
|
|
QMessageBox.information(self, "Zur Queue hinzugefügt",
|
|
f"Der Download wurde zur Queue hinzugefügt.\nQueue enthält jetzt {len(self.download_queue)} Elemente.")
|
|
|
|
def update_queue_list(self):
|
|
"""Aktualisiert die Queue-Liste in der UI."""
|
|
self.queue_list.clear()
|
|
for item in self.download_queue:
|
|
list_item = QListWidgetItem(f"{item.status}: {item.get_display_name()}")
|
|
list_item.setData(Qt.UserRole, item.id)
|
|
self.queue_list.addItem(list_item)
|
|
|
|
def update_queue_buttons(self):
|
|
"""Aktualisiert den Zustand der Queue-Buttons."""
|
|
has_items = len(self.download_queue) > 0
|
|
is_downloading = self.download_thread and self.download_thread.isRunning()
|
|
|
|
self.start_queue_btn.setEnabled(has_items and not is_downloading)
|
|
self.clear_queue_btn.setEnabled(has_items)
|
|
|
|
# Button-Text anpassen
|
|
if is_downloading and self.current_queue_item:
|
|
self.start_queue_btn.setText("Queue läuft...")
|
|
else:
|
|
self.start_queue_btn.setText("Queue starten")
|
|
|
|
def start_queue(self):
|
|
"""Startet die Download-Queue."""
|
|
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:
|
|
QMessageBox.information(self, "Queue abgeschlossen", "Alle Downloads in der Queue wurden abgeschlossen.")
|
|
self.current_queue_item = None
|
|
self.update_queue_buttons()
|
|
return
|
|
|
|
# Hole das nächste Element aus der Queue
|
|
self.current_queue_item = self.download_queue.pop(0)
|
|
self.current_queue_item.status = "Wird heruntergeladen..."
|
|
|
|
# Aktualisiere die UI
|
|
self.update_queue_list()
|
|
self.update_queue_buttons()
|
|
self.save_queue()
|
|
|
|
# Switch zu Log-Tab
|
|
self.tabs.setCurrentIndex(0)
|
|
|
|
# Logge den Start
|
|
self.log_output.append(f"\n=== Queue-Download gestartet ===")
|
|
self.log_output.append(f"URL: {self.current_queue_item.url}")
|
|
self.log_output.append(f"Preset: {self.current_queue_item.preset_data.get('name', 'Unbekannt')}")
|
|
if self.current_queue_item.series_info:
|
|
series_info = self.current_queue_item.series_info
|
|
self.log_output.append(f"Serie: {series_info.get('series', '')} S{series_info.get('season', '')}E{series_info.get('episode', '')}")
|
|
self.log_output.append("=" * 35)
|
|
|
|
# Erstelle Download-Thread mit Queue-Daten
|
|
preset = self.current_queue_item.preset_data
|
|
|
|
# Erweitere das Preset um die Serieninfos aus der Queue
|
|
if self.current_queue_item.series_info:
|
|
preset.update(self.current_queue_item.series_info)
|
|
|
|
# Erstelle Command-Args wie im normalen Download
|
|
flags = self.config.get("ytdlp_flags", {})
|
|
is_audio = preset.get("is_audio", False)
|
|
use_dual_audio = preset.get("use_dual_audio", False)
|
|
|
|
extra_args = []
|
|
if preset.get("username"):
|
|
extra_args.extend(["-u", preset["username"]])
|
|
if preset.get("password"):
|
|
extra_args.extend(["-p", preset["password"]])
|
|
if preset.get("referer"):
|
|
extra_args.append(f"--referer={preset['referer']}")
|
|
if preset.get("hls_ffmpeg"):
|
|
extra_args.extend(["--downloader", "ffmpeg", "--hls-use-mpegts"])
|
|
if flags.get("use_ffmpeg_location") and self.config.get("ffmpeg_path"):
|
|
extra_args.extend(["--ffmpeg-location", self.config.get("ffmpeg_path")])
|
|
if preset.get("sublang"):
|
|
extra_args.extend(["--sub-lang", preset["sublang"]])
|
|
if preset.get("embed_subs"):
|
|
extra_args.append("--embed-subs")
|
|
if preset.get("subformat"):
|
|
extra_args.extend(["--convert-subs", preset["subformat"]])
|
|
if flags.get("ignore_config"):
|
|
extra_args.append("--ignore-config")
|
|
if flags.get("remux_mkv") and not is_audio and not use_dual_audio:
|
|
extra_args.extend(["--remux-video", "mkv"])
|
|
if flags.get("embed_metadata"):
|
|
extra_args.append("--embed-metadata")
|
|
|
|
cmd_args = " ".join(extra_args) + (" " + preset["args"] if preset["args"] else "")
|
|
|
|
# Erstelle und starte Download-Thread
|
|
self.download_thread = DownloadThread(
|
|
self.current_queue_item.url,
|
|
self.current_queue_item.output_dir,
|
|
cmd_args,
|
|
self.current_queue_item.use_local_ytdlp,
|
|
self.current_queue_item.output_filename,
|
|
preset,
|
|
self.config
|
|
)
|
|
|
|
# Verbinde Signale
|
|
self.download_thread.update_signal.connect(self.log_output.append)
|
|
self.download_thread.finished_signal.connect(self.queue_download_finished)
|
|
self.download_thread.format_selection_signal.connect(self.show_formats)
|
|
self.download_thread.format_id_input_signal.connect(self.get_format_id_from_user)
|
|
|
|
# Starte Download
|
|
self.download_thread.start()
|
|
|
|
def queue_download_finished(self, success, message):
|
|
"""Callback für abgeschlossene Queue-Downloads."""
|
|
if success:
|
|
self.log_output.append(f"Queue-Download erfolgreich: {message}")
|
|
else:
|
|
self.log_output.append(f"Queue-Download fehlgeschlagen: {message}")
|
|
|
|
# Verarbeite das nächste Element in der Queue nach einer kurzen Pause
|
|
# (Damit der Benutzer die Ausgabe sehen kann)
|
|
from PyQt5.QtCore import QTimer
|
|
QTimer.singleShot(1000, self.process_next_queue_item)
|
|
|
|
def show_queue_context_menu(self, position):
|
|
"""Zeigt ein Kontextmenü für die Queue-Liste."""
|
|
item = self.queue_list.itemAt(position)
|
|
if not item:
|
|
return
|
|
|
|
menu = QMenu(self)
|
|
|
|
remove_action = QAction("Aus Queue entfernen", self)
|
|
remove_action.triggered.connect(lambda: self.remove_from_queue(item))
|
|
menu.addAction(remove_action)
|
|
|
|
menu.exec_(self.queue_list.mapToGlobal(position))
|
|
|
|
def remove_from_queue(self, list_item):
|
|
"""Entfernt ein Element aus der Queue."""
|
|
item_id = list_item.data(Qt.UserRole)
|
|
self.download_queue = [item for item in self.download_queue if item.id != item_id]
|
|
self.update_queue_list()
|
|
self.update_queue_buttons()
|
|
self.save_queue()
|
|
|
|
def clear_queue(self):
|
|
"""Leert die Download-Queue nach Bestätigung."""
|
|
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()
|
|
self.save_queue()
|
|
|
|
def save_queue(self):
|
|
"""Speichert die aktuelle Queue in eine Datei."""
|
|
queue_file = os.path.join(get_user_data_dir(), "download_queue.json")
|
|
try:
|
|
queue_data = [item.to_dict() for item in self.download_queue]
|
|
with open(queue_file, 'w', encoding='utf-8') as f:
|
|
json.dump(queue_data, f, indent=4, ensure_ascii=False)
|
|
except Exception as e:
|
|
self.log_output.append(f"Fehler beim Speichern der Queue: {str(e)}")
|
|
|
|
def load_queue(self):
|
|
"""Lädt die Queue aus einer gespeicherten Datei."""
|
|
queue_file = os.path.join(get_user_data_dir(), "download_queue.json")
|
|
if not os.path.exists(queue_file):
|
|
return
|
|
|
|
try:
|
|
with open(queue_file, 'r', encoding='utf-8') as f:
|
|
queue_data = json.load(f)
|
|
|
|
self.download_queue = [QueueItem.from_dict(item_data) for item_data in queue_data]
|
|
self.update_queue_list()
|
|
self.update_queue_buttons()
|
|
except Exception as e:
|
|
self.log_output.append(f"Fehler beim Laden der Queue: {str(e)}")
|
|
|
|
def closeEvent(self, event):
|
|
"""Wird beim Schließen der Anwendung aufgerufen."""
|
|
# Speichere die Konfiguration
|
|
self.save_config()
|
|
|
|
# Speichere die Queue
|
|
self.save_queue()
|
|
|
|
# Beende laufende Downloads
|
|
if self.download_thread and self.download_thread.isRunning():
|
|
self.download_thread.stop()
|
|
self.download_thread.wait(3000) # Warte max 3 Sekunden
|
|
|
|
event.accept()
|
|
|
|
|
|
def main():
|
|
app = QApplication(sys.argv)
|
|
|
|
# Icon für die Anwendung setzen
|
|
set_window_icon(app)
|
|
|
|
window = MainWindow()
|
|
window.show()
|
|
|
|
sys.exit(app.exec_())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |