From cc64e3b77b36bb820eceb324ae1a800fe24f4130 Mon Sep 17 00:00:00 2001 From: Akamaru Date: Sun, 4 May 2025 21:06:29 +0200 Subject: [PATCH] =?UTF-8?q?Implementiere=20eine=20Download-Queue=20mit=20F?= =?UTF-8?q?unktionen=20zum=20Hinzuf=C3=BCgen,=20Starten,=20Leeren=20und=20?= =?UTF-8?q?Verwalten=20von=20Queue-Elementen.=20Speichere=20und=20lade=20d?= =?UTF-8?q?ie=20Queue=20aus=20einer=20Datei.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + main.py | 431 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 422 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 34fe2ec..ca4443c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ bin/yt-dlp.exe config.json main.spec *.7z +queue.json diff --git a/main.py b/main.py index ead9613..c60a7c7 100644 --- a/main.py +++ b/main.py @@ -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)