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:
431
main.py
431
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)
|
||||
|
Reference in New Issue
Block a user