Implementiere eine Download-Queue mit Funktionen zum Hinzufügen, Starten, Leeren und Verwalten von Queue-Elementen. Speichere und lade die Queue aus einer Datei.

This commit is contained in:
Akamaru
2025-05-04 21:06:29 +02:00
parent 443511c0da
commit cc64e3b77b
2 changed files with 422 additions and 10 deletions

431
main.py
View File

@@ -13,10 +13,12 @@ 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)
QDialogButtonBox, QInputDialog, QGroupBox, QCheckBox, QTabWidget,
QListWidgetItem, QMenu, QAction)
from PyQt5.QtCore import QThread, pyqtSignal, Qt
# Hilfsfunktionen für den Ressourcenpfad
@@ -517,6 +519,62 @@ class OptionenDialog(QDialog):
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__()
@@ -534,9 +592,15 @@ class MainWindow(QMainWindow):
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
@@ -601,11 +665,11 @@ class MainWindow(QMainWindow):
main_layout.addWidget(self.series_group)
# Options-Button statt Felder für Standardpfad und yt-dlp-Quelle
optionen_layout = QHBoxLayout()
options_layout = QHBoxLayout()
self.optionen_btn = QPushButton("Optionen...")
self.optionen_btn.clicked.connect(self.open_optionen_dialog)
optionen_layout.addWidget(self.optionen_btn)
main_layout.addLayout(optionen_layout)
options_layout.addWidget(self.optionen_btn)
main_layout.addLayout(options_layout)
# Command Preview
main_layout.addWidget(QLabel("Befehlsvorschau:"))
@@ -614,21 +678,60 @@ class MainWindow(QMainWindow):
self.cmd_preview.setMaximumHeight(60)
main_layout.addWidget(self.cmd_preview)
# Download Button
# Download Buttons
download_buttons_layout = QHBoxLayout()
self.download_btn = QPushButton("Download starten")
self.download_btn.clicked.connect(self.start_download)
main_layout.addWidget(self.download_btn)
download_buttons_layout.addWidget(self.download_btn)
# Log Output
main_layout.addWidget(QLabel("Ausgabe:"))
# 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)
main_layout.addWidget(self.log_output)
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)
# self.optionen_btn.clicked.connect(self.open_optionen_dialog) # Entfernt, um doppeltes Öffnen zu verhindern
# Serie, Staffel, Folge Signals
self.series_input.textChanged.connect(self.update_cmd_preview)
@@ -642,6 +745,7 @@ class MainWindow(QMainWindow):
# Initial update
self.preset_changed()
self.update_cmd_preview()
self.update_queue_buttons()
def load_config(self):
if os.path.exists(CONFIG_FILE):
@@ -1115,10 +1219,317 @@ class MainWindow(QMainWindow):
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()
self.tabs.setCurrentIndex(1) # Wechsle zum Queue-Tab
# Optional: Leere die URL-Box
self.url_input.clear()
# Aktualisiere Queue-Buttons
self.update_queue_buttons()
# Queue speichern
self.save_queue()
QMessageBox.information(self, "Hinzugefügt", "Download wurde zur Queue hinzugefügt.")
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)