863 lines
34 KiB
Python
863 lines
34 KiB
Python
import sys
|
|
import json
|
|
import re
|
|
import os
|
|
import requests
|
|
import time
|
|
import hashlib
|
|
from urllib.parse import urlparse
|
|
from datetime import datetime
|
|
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
|
QPushButton, QLabel, QLineEdit, QMessageBox, QTableWidget,
|
|
QTableWidgetItem, QHeaderView, QDialog, QFormLayout,
|
|
QFileDialog, QHBoxLayout, QComboBox, QProgressBar,
|
|
QTabWidget, QGroupBox, QCheckBox, QSizePolicy, QStyle,
|
|
QApplication)
|
|
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
|
from PyQt5.QtGui import QIntValidator, QIcon
|
|
from PyQt5.QtCore import QSize
|
|
from github import Github
|
|
from github.GithubException import GithubException
|
|
|
|
from threading import Event
|
|
|
|
class DownloadWorker(QThread):
|
|
progress = pyqtSignal(str, int, int, float) # status, current, total, speed (KB/s)
|
|
finished = pyqtSignal()
|
|
error = pyqtSignal(str)
|
|
skipped = pyqtSignal() # Neues Signal für übersprungene Downloads
|
|
check_file_exists = pyqtSignal(str, str) # filename, path
|
|
|
|
def __init__(self, url, save_path, md5_hash=None):
|
|
super().__init__()
|
|
self.url = url
|
|
self.save_path = save_path
|
|
self.md5_hash = md5_hash
|
|
self.is_paused = False
|
|
self.is_cancelled = False
|
|
self.overwrite_event = Event()
|
|
self.can_overwrite = False
|
|
|
|
def calculate_md5(self, file_path):
|
|
hash_md5 = hashlib.md5()
|
|
with open(file_path, "rb") as f:
|
|
for chunk in iter(lambda: f.read(4096), b""):
|
|
hash_md5.update(chunk)
|
|
return hash_md5.hexdigest()
|
|
|
|
def pause(self):
|
|
self.is_paused = True
|
|
|
|
def resume(self):
|
|
self.is_paused = False
|
|
|
|
def cancel(self):
|
|
self.is_cancelled = True
|
|
|
|
def run(self):
|
|
save_path = None
|
|
file = None
|
|
try:
|
|
filename = os.path.basename(urlparse(self.url).path)
|
|
if not filename:
|
|
filename = 'download'
|
|
|
|
save_path = os.path.join(self.save_path, filename)
|
|
|
|
# Prüfe ob die Datei bereits existiert
|
|
if os.path.exists(save_path):
|
|
if self.md5_hash:
|
|
# Wenn MD5 Hash verfügbar ist, prüfe diesen
|
|
current_md5 = self.calculate_md5(save_path)
|
|
if current_md5.lower() == self.md5_hash.lower():
|
|
self.skipped.emit()
|
|
return
|
|
else:
|
|
# Wenn kein MD5 Hash verfügbar ist, frage den Hauptthread
|
|
self.check_file_exists.emit(filename, save_path)
|
|
if not self.overwrite_event.wait(timeout=30.0): # 30 Sekunden Timeout
|
|
self.error.emit("Timeout beim Warten auf Benutzerantwort")
|
|
return
|
|
if not self.can_overwrite:
|
|
self.skipped.emit()
|
|
return
|
|
|
|
response = requests.get(self.url, stream=True)
|
|
response.raise_for_status()
|
|
|
|
total_size = int(response.headers.get('content-length', 0))
|
|
block_size = 8192
|
|
current_size = 0
|
|
start_time = time.time()
|
|
last_time = start_time
|
|
last_size = 0
|
|
speed = 0
|
|
|
|
file = open(save_path, 'wb')
|
|
for data in response.iter_content(block_size):
|
|
if self.is_cancelled:
|
|
break
|
|
|
|
while self.is_paused:
|
|
time.sleep(0.1)
|
|
last_time = time.time() # Reset time for speed calculation
|
|
continue
|
|
|
|
current_size += len(data)
|
|
file.write(data)
|
|
|
|
# Calculate speed
|
|
current_time = time.time()
|
|
time_diff = current_time - last_time
|
|
|
|
if time_diff >= 0.5: # Update speed every half second
|
|
speed = (current_size - last_size) / 1024 / time_diff # KB/s
|
|
last_time = current_time
|
|
last_size = current_size
|
|
|
|
if total_size:
|
|
self.progress.emit(filename, current_size, total_size, speed)
|
|
|
|
file.close()
|
|
file = None
|
|
|
|
if self.is_cancelled and save_path and os.path.exists(save_path):
|
|
try:
|
|
os.remove(save_path)
|
|
except:
|
|
pass # Ignore errors while deleting
|
|
elif not self.is_cancelled:
|
|
# Überprüfe MD5 nach dem Download
|
|
if self.md5_hash:
|
|
current_md5 = self.calculate_md5(save_path)
|
|
if current_md5.lower() != self.md5_hash.lower():
|
|
raise Exception("MD5 Prüfsumme stimmt nicht überein")
|
|
self.finished.emit()
|
|
|
|
except Exception as e:
|
|
self.error.emit(str(e))
|
|
finally:
|
|
if file:
|
|
file.close()
|
|
if self.is_cancelled and save_path and os.path.exists(save_path):
|
|
try:
|
|
os.remove(save_path)
|
|
except:
|
|
pass # Ignore errors while deleting
|
|
|
|
class DownloadWidget(QWidget):
|
|
def __init__(self, filename, worker, download_manager):
|
|
super().__init__()
|
|
self.worker = worker
|
|
self.download_manager = download_manager
|
|
self.setup_ui(filename)
|
|
|
|
# Verbinde die Signale für die Dateiexistenz-Prüfung
|
|
self.worker.check_file_exists.connect(self.on_check_file_exists)
|
|
self.can_overwrite = False
|
|
|
|
def on_check_file_exists(self, filename, path):
|
|
msg = QMessageBox()
|
|
msg.setIcon(QMessageBox.Question)
|
|
msg.setText(f"Die Datei {filename} existiert bereits.")
|
|
msg.setInformativeText("Möchten Sie die Datei überschreiben?")
|
|
msg.setWindowTitle("Datei existiert")
|
|
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
|
|
msg.setDefaultButton(QMessageBox.No)
|
|
|
|
self.worker.can_overwrite = msg.exec_() == QMessageBox.Yes
|
|
self.worker.overwrite_event.set() # Signal dass die Antwort bereit ist
|
|
|
|
def setup_ui(self, filename):
|
|
layout = QHBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(10) # Etwas Abstand zwischen den Elementen
|
|
|
|
# Progress Bar mit integrierter Geschwindigkeitsanzeige
|
|
self.progress_bar = QProgressBar()
|
|
self.progress_bar.setFixedWidth(250) # Feste Breite für den Balken
|
|
self.progress_bar.setFormat("%p%") # Nur Prozent im Balken
|
|
self.progress_bar.setStyleSheet("""
|
|
QProgressBar {
|
|
text-align: center;
|
|
padding: 1px;
|
|
border: 1px solid #999;
|
|
border-radius: 3px;
|
|
background: white;
|
|
}
|
|
QProgressBar::chunk {
|
|
background-color: #0A0;
|
|
width: 1px;
|
|
}
|
|
""")
|
|
|
|
# Filename Label
|
|
self.name_label = QLabel(filename)
|
|
self.name_label.setFixedWidth(400) # Feste Breite für Dateinamen
|
|
self.name_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
|
# Kürze lange Dateinamen mit Ellipsis
|
|
self.name_label.setTextFormat(Qt.PlainText)
|
|
self.name_label.setTextInteractionFlags(Qt.NoTextInteraction)
|
|
self.name_label.setToolTip(filename) # Zeige vollen Namen als Tooltip
|
|
self.name_label.setStyleSheet("text-align: left; white-space: nowrap;")
|
|
self.name_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
|
|
# Buttons mit Icons
|
|
style = QApplication.style()
|
|
|
|
self.pause_btn = QPushButton()
|
|
self.pause_btn.setIcon(style.standardIcon(QStyle.SP_MediaPause))
|
|
self.pause_btn.setIconSize(QSize(16, 16))
|
|
self.pause_btn.setFixedSize(32, 32)
|
|
self.pause_btn.setToolTip("Pause")
|
|
self.pause_btn.clicked.connect(self.toggle_pause)
|
|
|
|
self.cancel_btn = QPushButton()
|
|
self.cancel_btn.setIcon(style.standardIcon(QStyle.SP_DialogCloseButton))
|
|
self.cancel_btn.setIconSize(QSize(16, 16))
|
|
self.cancel_btn.setFixedSize(32, 32)
|
|
self.cancel_btn.setToolTip("Abbrechen")
|
|
self.cancel_btn.clicked.connect(self.cancel_download)
|
|
|
|
# Layout in gewünschter Reihenfolge
|
|
layout.addWidget(self.progress_bar)
|
|
layout.addWidget(self.name_label)
|
|
layout.addWidget(self.pause_btn)
|
|
layout.addWidget(self.cancel_btn)
|
|
|
|
def toggle_pause(self):
|
|
style = QApplication.style()
|
|
if self.worker.is_paused:
|
|
self.worker.resume()
|
|
self.pause_btn.setIcon(style.standardIcon(QStyle.SP_MediaPause))
|
|
self.pause_btn.setToolTip("Pause")
|
|
else:
|
|
self.worker.pause()
|
|
self.pause_btn.setIcon(style.standardIcon(QStyle.SP_MediaPlay))
|
|
self.pause_btn.setToolTip("Fortsetzen")
|
|
|
|
def cancel_download(self):
|
|
self.worker.cancel()
|
|
self.cleanup()
|
|
self.progress_bar.setFormat("Abgebrochen")
|
|
# Signal an DownloadManager senden, dass ein Download beendet wurde
|
|
self.download_manager.start_waiting_downloads()
|
|
|
|
def update_progress(self, current, total, speed):
|
|
progress = int(current * 100 / total)
|
|
speed_mb = speed / 1024 # Convert KB/s to MB/s
|
|
self.progress_bar.setValue(progress) # Setze den Prozentwert
|
|
self.progress_bar.setMaximum(100) # Maximalwert ist 100%
|
|
self.progress_bar.setFormat(f"{progress}% - {speed_mb:.2f} MB/s")
|
|
|
|
def cleanup(self):
|
|
self.pause_btn.setEnabled(False)
|
|
self.cancel_btn.setEnabled(False)
|
|
|
|
class DownloadManager(QWidget):
|
|
def __init__(self, config, parent=None):
|
|
super().__init__(parent)
|
|
self.config = config
|
|
self.downloads = {} # Keep track of active downloads
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Header layout with title and buttons
|
|
header_layout = QHBoxLayout()
|
|
header_layout.addWidget(QLabel("Downloads"))
|
|
|
|
# Buttons rechts ausrichten
|
|
header_layout.addStretch()
|
|
|
|
cancel_all_btn = QPushButton("Alle Abbrechen")
|
|
cancel_all_btn.clicked.connect(self.cancel_all_downloads)
|
|
header_layout.addWidget(cancel_all_btn)
|
|
|
|
clean_btn = QPushButton("Säubern")
|
|
clean_btn.clicked.connect(self.clean_completed)
|
|
header_layout.addWidget(clean_btn)
|
|
|
|
layout.addLayout(header_layout)
|
|
|
|
# Group box for downloads
|
|
group_box = QGroupBox()
|
|
self.download_layout = QVBoxLayout()
|
|
group_box.setLayout(self.download_layout)
|
|
layout.addWidget(group_box)
|
|
|
|
def cancel_all_downloads(self):
|
|
for worker, widget in self.downloads.items():
|
|
if widget.progress_bar.format() not in ["Fertig", "Abgebrochen", "Fehler"]:
|
|
widget.cancel_download()
|
|
|
|
def clean_completed(self):
|
|
# Sammle alle Widgets die entfernt werden sollen
|
|
to_remove = []
|
|
for worker, widget in self.downloads.items():
|
|
if (widget.progress_bar.format() == "Fertig" or
|
|
widget.progress_bar.format() == "Abgebrochen" or
|
|
widget.progress_bar.format() == "Fehler"):
|
|
to_remove.append((worker, widget))
|
|
|
|
# Entferne die Widgets und Worker
|
|
for worker, widget in to_remove:
|
|
widget.setParent(None)
|
|
widget.deleteLater()
|
|
del self.downloads[worker]
|
|
|
|
def add_download(self, url, save_path, md5_hash=None, repo_name=None, release_version=None):
|
|
# Zähle aktive Downloads
|
|
active_downloads = sum(1 for w in self.downloads.values()
|
|
if w.progress_bar.format() not in ["Fertig", "Abgebrochen", "Fehler", "Übersprungen"])
|
|
|
|
max_downloads = self.config.get("max_concurrent_downloads", 3)
|
|
|
|
# Erstelle den finalen Download-Pfad
|
|
if self.config.get("use_subfolders", True) and repo_name and release_version:
|
|
# Erstelle den Unterordner basierend auf dem Format
|
|
subfolder = self.config.get("subfolder_format", "{repo} - {version}")
|
|
subfolder = subfolder.format(
|
|
repo=repo_name.split("/")[-1], # Nur den Repo-Namen ohne Owner
|
|
version=release_version,
|
|
date=datetime.now().strftime("%Y-%m-%d")
|
|
)
|
|
save_path = os.path.join(save_path, subfolder)
|
|
# Erstelle den Ordner wenn er nicht existiert
|
|
os.makedirs(save_path, exist_ok=True)
|
|
|
|
worker = DownloadWorker(url, save_path, md5_hash)
|
|
filename = os.path.basename(urlparse(url).path)
|
|
|
|
download_widget = DownloadWidget(filename, worker, self)
|
|
self.download_layout.addWidget(download_widget)
|
|
|
|
worker.progress.connect(
|
|
lambda fn, cur, tot, speed, w=download_widget: w.update_progress(cur, tot, speed))
|
|
worker.finished.connect(
|
|
lambda w=download_widget: self.on_download_finished(w))
|
|
worker.error.connect(
|
|
lambda e, w=download_widget: self.on_download_error(e, w))
|
|
worker.skipped.connect(
|
|
lambda w=download_widget: self.on_download_skipped(w))
|
|
|
|
self.downloads[worker] = download_widget
|
|
|
|
# Starte Download nur wenn unter dem Limit
|
|
if active_downloads < max_downloads:
|
|
worker.start()
|
|
else:
|
|
download_widget.progress_bar.setFormat("Wartend...")
|
|
|
|
def on_download_finished(self, widget):
|
|
widget.cleanup()
|
|
widget.progress_bar.setFormat("Fertig")
|
|
widget.name_label.setText(f"{widget.name_label.text()} - Fertig")
|
|
self.start_waiting_downloads()
|
|
|
|
def on_download_error(self, error, widget):
|
|
widget.cleanup()
|
|
widget.progress_bar.setFormat("Fehler")
|
|
widget.name_label.setText(f"{widget.name_label.text()} - Fehler")
|
|
QMessageBox.warning(self, "Fehler", f"Download-Fehler: {error}")
|
|
self.start_waiting_downloads()
|
|
|
|
def on_download_skipped(self, widget):
|
|
widget.cleanup()
|
|
widget.progress_bar.setFormat("Übersprungen")
|
|
widget.name_label.setText(f"{widget.name_label.text()} - Übersprungen")
|
|
self.start_waiting_downloads()
|
|
|
|
def start_waiting_downloads(self):
|
|
# Zähle aktive Downloads
|
|
active_downloads = sum(1 for w in self.downloads.values()
|
|
if w.progress_bar.format() not in ["Fertig", "Abgebrochen", "Fehler", "Wartend...", "Übersprungen"])
|
|
|
|
max_downloads = self.config.get("max_concurrent_downloads", 3)
|
|
|
|
# Starte wartende Downloads wenn möglich
|
|
if active_downloads < max_downloads:
|
|
# Sortiere die Downloads nach Hinzufügereihenfolge
|
|
sorted_downloads = sorted(self.downloads.items(), key=lambda x: id(x[0]))
|
|
for worker, widget in sorted_downloads:
|
|
if widget.progress_bar.format() == "Wartend...":
|
|
if active_downloads < max_downloads:
|
|
worker.start()
|
|
active_downloads += 1
|
|
else:
|
|
break
|
|
|
|
class OptionsDialog(QDialog):
|
|
def __init__(self, config, parent=None):
|
|
super().__init__(parent)
|
|
self.config = config
|
|
self.setWindowTitle("Optionen")
|
|
self.setModal(True)
|
|
self.setMinimumWidth(500)
|
|
# Entferne das Fragezeichen
|
|
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Tab Widget erstellen
|
|
tab_widget = QTabWidget()
|
|
|
|
# Allgemeine Einstellungen Tab
|
|
general_tab = QWidget()
|
|
general_layout = QFormLayout(general_tab)
|
|
|
|
# GitHub Token Input
|
|
self.token_input = QLineEdit()
|
|
self.token_input.setEchoMode(QLineEdit.Password)
|
|
self.token_input.setText(self.config.get("github_token", ""))
|
|
general_layout.addRow("GitHub Token:", self.token_input)
|
|
|
|
# Download Path Input
|
|
self.path_input = QLineEdit()
|
|
self.path_input.setText(self.config.get("download_path", ""))
|
|
general_layout.addRow("Download Pfad:", self.path_input)
|
|
|
|
# Browse Button
|
|
browse_btn = QPushButton("Durchsuchen...")
|
|
browse_btn.clicked.connect(self.browse_path)
|
|
general_layout.addRow("", browse_btn)
|
|
|
|
# Max concurrent downloads
|
|
self.max_downloads = QLineEdit()
|
|
self.max_downloads.setText(str(self.config.get("max_concurrent_downloads", 3)))
|
|
self.max_downloads.setValidator(QIntValidator(1, 10)) # Limit between 1 and 10
|
|
general_layout.addRow("Max. gleichzeitige Downloads:", self.max_downloads)
|
|
|
|
# Download Format Tab
|
|
format_tab = QWidget()
|
|
format_layout = QFormLayout(format_tab)
|
|
|
|
# Checkbox für Unterordner
|
|
self.use_subfolders = QCheckBox("Unterordner für jedes Release erstellen")
|
|
self.use_subfolders.setChecked(self.config.get("use_subfolders", True))
|
|
format_layout.addRow(self.use_subfolders)
|
|
|
|
# Format für Unterordner
|
|
self.subfolder_format = QLineEdit()
|
|
self.subfolder_format.setText(self.config.get("subfolder_format", "{repo} - {version}"))
|
|
self.subfolder_format.setPlaceholderText("z.B. {repo} - {version}")
|
|
format_layout.addRow("Unterordner Format:", self.subfolder_format)
|
|
|
|
# Hilfetext für Variablen
|
|
help_label = QLabel("Verfügbare Variablen:\n{repo} - Repository Name\n{version} - Release Version\n{date} - Release Datum")
|
|
help_label.setStyleSheet("color: gray;")
|
|
format_layout.addRow(help_label)
|
|
|
|
# Info Tab
|
|
info_tab = QWidget()
|
|
info_layout = QVBoxLayout(info_tab)
|
|
|
|
# Info Text
|
|
info_text = QLabel("""
|
|
©2025 Akamaru<br>
|
|
Version 1.0<br>
|
|
<br>
|
|
Sourcecode auf PonyGit:<br>
|
|
<a href="https://git.ponywave.de/Akamaru/DownloadDoggo">https://git.ponywave.de/Akamaru/DownloadDoggo</a>
|
|
""")
|
|
info_text.setTextFormat(Qt.RichText)
|
|
info_text.setOpenExternalLinks(True)
|
|
info_text.setAlignment(Qt.AlignCenter)
|
|
info_layout.addWidget(info_text)
|
|
info_layout.addStretch()
|
|
|
|
# Tabs hinzufügen
|
|
tab_widget.addTab(general_tab, "Allgemein")
|
|
tab_widget.addTab(format_tab, "Download Format")
|
|
tab_widget.addTab(info_tab, "Info")
|
|
|
|
layout.addWidget(tab_widget)
|
|
|
|
# Save Button
|
|
save_btn = QPushButton("Speichern")
|
|
save_btn.clicked.connect(self.accept)
|
|
layout.addWidget(save_btn)
|
|
|
|
def browse_path(self):
|
|
path = QFileDialog.getExistingDirectory(self, "Download Pfad auswählen")
|
|
if path:
|
|
self.path_input.setText(path)
|
|
|
|
def get_values(self):
|
|
return {
|
|
"github_token": self.token_input.text(),
|
|
"download_path": self.path_input.text(),
|
|
"max_concurrent_downloads": int(self.max_downloads.text()),
|
|
"use_subfolders": self.use_subfolders.isChecked(),
|
|
"subfolder_format": self.subfolder_format.text()
|
|
}
|
|
|
|
class DownloadDoggo(QMainWindow):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setWindowTitle("Download Doggo - GitHub Release Manager")
|
|
self.setGeometry(100, 100, 1000, 800)
|
|
|
|
# Icon setzen
|
|
icon_path = self.get_resource_path("icon.ico")
|
|
if os.path.exists(icon_path):
|
|
icon = QIcon(icon_path)
|
|
self.setWindowIcon(icon)
|
|
# Icon auch für die Taskbar setzen
|
|
import ctypes
|
|
myappid = 'akamaru.downloaddoggo.1.0'
|
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
|
|
|
|
# Initialisierung
|
|
self.setup()
|
|
|
|
def get_resource_path(self, relative_path):
|
|
"""Gibt den absoluten Pfad zu einer Ressource zurück, funktioniert auch in der EXE"""
|
|
try:
|
|
# PyInstaller erstellt einen temporären Ordner und speichert den Pfad in _MEIPASS
|
|
base_path = sys._MEIPASS
|
|
except Exception:
|
|
base_path = os.path.abspath(".")
|
|
|
|
return os.path.join(base_path, relative_path)
|
|
|
|
def setup(self):
|
|
"""Initialisiert die Anwendung"""
|
|
# Load config
|
|
self.load_config()
|
|
|
|
# Initialize GitHub API
|
|
token = self.config.get("github_token", "")
|
|
if not token:
|
|
self.github = None
|
|
else:
|
|
self.github = Github(token)
|
|
|
|
# Setup UI
|
|
self.setup_ui()
|
|
|
|
# Fenster zentrieren
|
|
screen = QApplication.primaryScreen().geometry()
|
|
x = (screen.width() - self.width()) // 2
|
|
y = (screen.height() - self.height()) // 2
|
|
self.move(x, y)
|
|
|
|
def load_config(self):
|
|
try:
|
|
with open("config.json", "r") as f:
|
|
self.config = json.load(f)
|
|
# Ensure download_path exists in config
|
|
if "download_path" not in self.config:
|
|
self.config["download_path"] = ""
|
|
except FileNotFoundError:
|
|
self.config = {
|
|
"github_token": "",
|
|
"download_path": "",
|
|
"repos": []
|
|
}
|
|
self.save_config()
|
|
|
|
def save_config(self):
|
|
with open("config.json", "w") as f:
|
|
json.dump(self.config, f, indent=4)
|
|
|
|
def setup_ui(self):
|
|
central_widget = QWidget()
|
|
self.setCentralWidget(central_widget)
|
|
main_layout = QVBoxLayout(central_widget)
|
|
|
|
# Create tab widget
|
|
tab_widget = QTabWidget()
|
|
|
|
# Repositories tab
|
|
repo_tab = QWidget()
|
|
repo_layout = QVBoxLayout(repo_tab)
|
|
|
|
# Options Button
|
|
options_btn = QPushButton("Optionen")
|
|
options_btn.clicked.connect(self.show_options)
|
|
repo_layout.addWidget(options_btn)
|
|
|
|
# Repository input
|
|
input_layout = QVBoxLayout()
|
|
self.repo_input = QLineEdit()
|
|
self.repo_input.setPlaceholderText("Repository (Format: owner/repo oder GitHub URL)")
|
|
add_repo_btn = QPushButton("Repository hinzufügen")
|
|
add_repo_btn.clicked.connect(self.add_repository)
|
|
input_layout.addWidget(self.repo_input)
|
|
input_layout.addWidget(add_repo_btn)
|
|
repo_layout.addLayout(input_layout)
|
|
|
|
# Repository table
|
|
self.repo_table = QTableWidget()
|
|
self.repo_table.setColumnCount(4)
|
|
self.repo_table.setHorizontalHeaderLabels(["Repository", "Letztes Release", "Release Datum", ""])
|
|
header = self.repo_table.horizontalHeader()
|
|
header.setSectionResizeMode(0, QHeaderView.Stretch)
|
|
header.setSectionResizeMode(1, QHeaderView.Stretch)
|
|
header.setSectionResizeMode(2, QHeaderView.Stretch)
|
|
header.setSectionResizeMode(3, QHeaderView.Fixed)
|
|
self.repo_table.setColumnWidth(3, 100)
|
|
repo_layout.addWidget(self.repo_table)
|
|
|
|
# Update button
|
|
update_btn = QPushButton("Nach Updates suchen")
|
|
update_btn.clicked.connect(self.check_updates)
|
|
repo_layout.addWidget(update_btn)
|
|
|
|
# Download manager
|
|
self.download_manager = DownloadManager(self.config)
|
|
repo_layout.addWidget(self.download_manager)
|
|
|
|
# Add tabs
|
|
tab_widget.addTab(repo_tab, "Repositories")
|
|
main_layout.addWidget(tab_widget)
|
|
|
|
# Load existing repositories
|
|
self.update_repo_table()
|
|
|
|
def show_options(self):
|
|
dialog = OptionsDialog(self.config, self)
|
|
if dialog.exec_() == QDialog.Accepted:
|
|
values = dialog.get_values()
|
|
self.config["github_token"] = values["github_token"]
|
|
self.config["download_path"] = values["download_path"]
|
|
self.config["max_concurrent_downloads"] = values["max_concurrent_downloads"]
|
|
self.config["use_subfolders"] = values["use_subfolders"]
|
|
self.config["subfolder_format"] = values["subfolder_format"]
|
|
self.save_config()
|
|
|
|
# Reinitialize GitHub API with new token
|
|
token = values["github_token"]
|
|
if not token:
|
|
self.github = None
|
|
else:
|
|
try:
|
|
self.github = Github(token)
|
|
# Test the connection
|
|
self.github.get_user().login
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Fehler",
|
|
f"Der GitHub Token ist ungültig: {str(e)}")
|
|
self.github = None
|
|
|
|
def extract_repo_info(self, input_text):
|
|
# Check if input is a URL
|
|
if "github.com" in input_text:
|
|
try:
|
|
# Remove .git extension if present
|
|
input_text = input_text.replace(".git", "")
|
|
# Parse URL
|
|
parsed = urlparse(input_text)
|
|
# Split path and remove empty elements
|
|
path_parts = [p for p in parsed.path.split("/") if p]
|
|
if len(path_parts) >= 2:
|
|
return f"{path_parts[0]}/{path_parts[1]}"
|
|
except Exception:
|
|
return None
|
|
|
|
# Check if input is in owner/repo format
|
|
elif "/" in input_text:
|
|
parts = input_text.strip().split("/")
|
|
if len(parts) == 2:
|
|
return f"{parts[0]}/{parts[1]}"
|
|
|
|
return None
|
|
|
|
def start_download(self, repo_name, release):
|
|
if not self.config["download_path"]:
|
|
QMessageBox.warning(self, "Fehler",
|
|
"Bitte legen Sie zuerst einen Download-Pfad in den Optionen fest.")
|
|
return
|
|
|
|
# Versuche MD5 aus der Release-Beschreibung zu extrahieren
|
|
md5_hashes = {}
|
|
if release.body:
|
|
# Suche nach MD5-Hashes im Format: filename: md5hash oder md5:filename:hash
|
|
md5_matches = re.finditer(r"(?:md5:)?([^:\n]+):\s*([a-fA-F0-9]{32})", release.body, re.IGNORECASE)
|
|
for match in md5_matches:
|
|
filename = match.group(1).strip()
|
|
md5_hash = match.group(2).lower()
|
|
md5_hashes[filename] = md5_hash
|
|
|
|
for asset in release.get_assets():
|
|
# Versuche den MD5-Hash für dieses Asset zu finden
|
|
md5_hash = None
|
|
asset_name = asset.name
|
|
if asset_name in md5_hashes:
|
|
md5_hash = md5_hashes[asset_name]
|
|
|
|
self.download_manager.add_download(
|
|
asset.browser_download_url,
|
|
self.config["download_path"],
|
|
md5_hash,
|
|
repo_name,
|
|
release.tag_name
|
|
)
|
|
|
|
def show_release_selection(self, repo_name):
|
|
try:
|
|
repo = self.github.get_repo(repo_name)
|
|
releases = list(repo.get_releases())
|
|
|
|
if not releases:
|
|
QMessageBox.warning(self, "Fehler", "Keine Releases gefunden.")
|
|
return
|
|
|
|
dialog = QDialog(self)
|
|
dialog.setWindowTitle(f"Release auswählen - {repo_name}")
|
|
dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint) # Entfernt das Fragezeichen
|
|
dialog.setFixedWidth(400) # Setzt eine feste Breite
|
|
layout = QVBoxLayout(dialog)
|
|
|
|
# Download type selection
|
|
download_type = QComboBox()
|
|
download_type.addItems(["Letztes Release", "Release wählen", "Alle Releases"])
|
|
layout.addWidget(download_type)
|
|
|
|
# Release selection
|
|
release_combo = QComboBox()
|
|
release_combo.setVisible(False)
|
|
for release in releases:
|
|
release_combo.addItem(f"{release.title} ({release.tag_name})")
|
|
layout.addWidget(release_combo)
|
|
|
|
# Download button
|
|
download_btn = QPushButton("Download starten")
|
|
layout.addWidget(download_btn)
|
|
|
|
def on_type_changed(text):
|
|
release_combo.setVisible(text == "Release wählen")
|
|
|
|
def start_selected_download():
|
|
selected_type = download_type.currentText()
|
|
|
|
if selected_type == "Letztes Release":
|
|
self.start_download(repo_name, releases[0])
|
|
elif selected_type == "Release wählen":
|
|
selected_idx = release_combo.currentIndex()
|
|
self.start_download(repo_name, releases[selected_idx])
|
|
else: # Alle Releases
|
|
for release in releases:
|
|
self.start_download(repo_name, release)
|
|
|
|
dialog.accept()
|
|
|
|
download_type.currentTextChanged.connect(on_type_changed)
|
|
download_btn.clicked.connect(start_selected_download)
|
|
|
|
dialog.exec_()
|
|
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Fehler", f"Fehler beim Laden der Releases: {str(e)}")
|
|
|
|
def add_repository(self):
|
|
if not self.github:
|
|
QMessageBox.warning(self, "Fehler",
|
|
"Bitte geben Sie zuerst einen GitHub Token in den Optionen ein.")
|
|
self.show_options()
|
|
return
|
|
|
|
repo_input = self.repo_input.text().strip()
|
|
repo_name = self.extract_repo_info(repo_input)
|
|
|
|
if not repo_name:
|
|
QMessageBox.warning(self, "Fehler",
|
|
"Bitte geben Sie ein Repository im Format 'owner/repo' oder als GitHub URL ein.")
|
|
return
|
|
|
|
try:
|
|
# Check if repository exists
|
|
repo = self.github.get_repo(repo_name)
|
|
releases = list(repo.get_releases())
|
|
if releases:
|
|
latest_release = releases[0]
|
|
release_date = latest_release.published_at.strftime("%d.%m.%Y") if latest_release.published_at else ""
|
|
release_tag = latest_release.tag_name
|
|
else:
|
|
release_date = ""
|
|
release_tag = None
|
|
|
|
# Add to config
|
|
repo_data = {
|
|
"name": repo_name,
|
|
"last_release": release_tag,
|
|
"release_date": release_date,
|
|
"added_date": datetime.now().isoformat()
|
|
}
|
|
|
|
if repo_name not in [r["name"] for r in self.config["repos"]]:
|
|
self.config["repos"].append(repo_data)
|
|
self.save_config()
|
|
self.update_repo_table()
|
|
self.repo_input.clear()
|
|
else:
|
|
QMessageBox.warning(self, "Fehler", "Dieses Repository existiert bereits in der Liste.")
|
|
|
|
except GithubException as e:
|
|
QMessageBox.warning(self, "Fehler", f"Repository nicht gefunden oder API-Fehler: {str(e)}")
|
|
|
|
def update_repo_table(self):
|
|
self.repo_table.setRowCount(len(self.config["repos"]))
|
|
for i, repo in enumerate(self.config["repos"]):
|
|
# Repository Name
|
|
name_item = QTableWidgetItem(repo["name"])
|
|
self.repo_table.setItem(i, 0, name_item)
|
|
|
|
# Release Version
|
|
self.repo_table.setItem(i, 1, QTableWidgetItem(repo["last_release"] or "Kein Release"))
|
|
|
|
# Release Datum
|
|
self.repo_table.setItem(i, 2, QTableWidgetItem(repo.get("release_date", "")))
|
|
|
|
# Add download button
|
|
download_btn = QPushButton("Download")
|
|
download_btn.clicked.connect(lambda checked, name=repo["name"]:
|
|
self.show_release_selection(name))
|
|
self.repo_table.setCellWidget(i, 3, download_btn)
|
|
|
|
# Setze Hintergrundfarbe wenn neues Release verfügbar
|
|
try:
|
|
repo_obj = self.github.get_repo(repo["name"])
|
|
releases = list(repo_obj.get_releases())
|
|
if releases and releases[0].tag_name != repo["last_release"]:
|
|
for col in range(4):
|
|
if col != 3: # Nicht den Button einfärben
|
|
item = self.repo_table.item(i, col)
|
|
item.setBackground(Qt.green)
|
|
except Exception as e:
|
|
print(f"Fehler beim Prüfen von Updates für {repo['name']}: {str(e)}")
|
|
|
|
def check_updates(self):
|
|
if not self.github:
|
|
QMessageBox.warning(self, "Fehler",
|
|
"Bitte geben Sie zuerst einen GitHub Token in den Optionen ein.")
|
|
self.show_options()
|
|
return
|
|
|
|
for i, repo_data in enumerate(self.config["repos"]):
|
|
try:
|
|
repo = self.github.get_repo(repo_data["name"])
|
|
releases = list(repo.get_releases())
|
|
if releases:
|
|
latest_release = releases[0]
|
|
# Aktualisiere immer das Datum, auch wenn kein neues Release
|
|
repo_data["release_date"] = latest_release.published_at.strftime("%d.%m.%Y")
|
|
if latest_release.tag_name != repo_data["last_release"]:
|
|
repo_data["last_release"] = latest_release.tag_name
|
|
else:
|
|
repo_data["release_date"] = ""
|
|
repo_data["last_release"] = None
|
|
|
|
except GithubException as e:
|
|
print(f"Fehler beim Prüfen von {repo_data['name']}: {str(e)}")
|
|
|
|
self.save_config()
|
|
self.update_repo_table()
|
|
|
|
if __name__ == "__main__":
|
|
app = QApplication(sys.argv)
|
|
window = DownloadDoggo()
|
|
window.show()
|
|
sys.exit(app.exec_()) |