Files
DownloadDoggo/main.py
2025-08-03 20:14:04 +02:00

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_())