Skripte/metaddateGUItest.py
2026-09-27 01:33:33 +02:00

1035 lines
No EOL
54 KiB
Python

import os
import sys
import re
import subprocess
import exiftool # High-Speed Bibliothek
from datetime import datetime, timedelta, timezone
try:
from zoneinfo import ZoneInfo
except ImportError:
ZoneInfo = None
# PyQt6 für die moderne Benutzeroberfläche
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QGroupBox, QLineEdit, QPushButton,
QCheckBox, QProgressBar, QFileDialog,
QMessageBox, QLabel, QGridLayout, QRadioButton,
QDateEdit, QTableWidget, QTableWidgetItem, QHeaderView,
QSplitter)
from PyQt6.QtCore import QThread, pyqtSignal, QDate, Qt
from PyQt6.QtGui import QColor, QFont
# ==========================================
# HILFSFUNKTIONEN
# ==========================================
def sicheres_datum(j, m, t, std=12, min_td=0, sek=0):
"""Fängt fehlerhafte Zeiten ab."""
try:
s_std = min(23, int(std))
s_min = min(59, int(min_td))
s_sek = min(59, int(sek))
return datetime(int(j), int(m), int(t), s_std, s_min, s_sek)
except ValueError:
# Gibt None zurück, damit fehlerhafte Daten (z. B. 61. Tag) verworfen werden
return None
def utc_zu_lokalzeit_deutschland(dt_utc):
"""Konvertiert UTC-Zeit (Google Pixel) in deutsche Lokalzeit (MEZ / MESZ) ohne externe Libs."""
if dt_utc is None:
return None
year = dt_utc.year
# Letzter Sonntag im März (Sommerzeit Start)
mar_last_sun = 31 - (datetime(year, 3, 31).weekday() + 1) % 7
# Letzter Sonntag im Oktober (Sommerzeit Ende)
oct_last_sun = 31 - (datetime(year, 10, 31).weekday() + 1) % 7
sommerzeit_start = datetime(year, 3, mar_last_sun, 1, 0, 0) # 01:00 UTC
sommerzeit_ende = datetime(year, 10, oct_last_sun, 1, 0, 0) # 01:00 UTC
if sommerzeit_start <= dt_utc < sommerzeit_ende:
return dt_utc + timedelta(hours=2) # MESZ (UTC+2)
else:
return dt_utc + timedelta(hours=1) # MEZ (UTC+1)
# ==========================================
# BACKGROUND WORKER (QThread)
# ==========================================
class WorkerThread(QThread):
# Signale für die Live-Aktualisierung der GUI-Tabelle
table_signal = pyqtSignal(str, str, str, str, str) # Dateiname, Status, Alt, Neu, Bemerkung
progress_signal = pyqtSignal(int)
finished_signal = pyqtSignal(str)
error_signal = pyqtSignal(str)
# Signal zur Übertragung der Zähler an die GUI-Statistik
stats_signal = pyqtSignal(int, int, int, int, int, int, int) # gesamt, exif, win, skip, ign, unmoeglich, fehler
def __init__(self, ordner, exiftool, log_dir, unterordner, modus, festes_datum, dry_run, rename_files, fallback_folder, excl_prefixes, excl_exts, dateien_liste=None):
super().__init__()
self.ordner_pfad = ordner
self.exiftool_pfad = exiftool
self.log_ordner_pfad = log_dir
self.unterordner_einbeziehen = unterordner
self.modus = modus
self.festes_datum = festes_datum
self.dry_run = dry_run
self.rename_files = rename_files
self.fallback_folder = fallback_folder
self.excl_prefixes = excl_prefixes
self.excl_exts = excl_exts
self.log_datei_pfad = ""
self.dateien_liste = dateien_liste # NEU: Liste der reingezogenen Dateien
def schreibe_log(self, text):
"""Schreibt eine Zeile sauber in die Log-Datei (ohne Steuerzeichen wie \\r)."""
# In der GUI feuern wir das Signal an die Konsole (falls vorhanden) und schreiben parallel in die Datei
if self.log_datei_pfad:
try:
clean_text = text.replace("\r", "")
with open(self.log_datei_pfad, "a", encoding="utf-8") as lf:
lf.write(clean_text + "\n")
except Exception: pass
def do_rename(self, filepath, dateiname, prefix, neues_datum):
"""Übernimmt das Präfix und benennt die Datei nach dem Schema YYYYMMDD_HHMMSS um (Kollisionsgeschützt)."""
if not self.rename_files:
return filepath, dateiname, ""
ext = os.path.splitext(dateiname)[1].lower()
neu_name = f"{prefix}_{neues_datum.strftime('%Y%m%d_%H%M%S')}"
neu_file = neu_name + ext
# Sofortiger Abbruch, wenn der Name ohnehin schon perfekt ist
if dateiname == neu_file:
return filepath, dateiname, ""
dir_name = os.path.dirname(filepath)
neu_filepath = os.path.join(dir_name, neu_file)
# Slashes normalisieren, um falsche "Umbenennungen" bei Netzlaufwerken zu verhindern
safe_filepath = os.path.normcase(os.path.normpath(filepath))
# Kollisionsschutz: Wenn Datei exakt denselben Namen hätte (selbe Sekunde)
c = 1
while os.path.exists(neu_filepath) and safe_filepath != os.path.normcase(os.path.normpath(neu_filepath)):
neu_file = f"{neu_name}_{c}{ext}"
neu_filepath = os.path.join(dir_name, neu_file)
c += 1
if safe_filepath != os.path.normcase(os.path.normpath(neu_filepath)):
if not self.dry_run:
try:
os.rename(filepath, neu_filepath)
except Exception as e:
return filepath, dateiname, f" | ❌ Rename-Fehler: {e}"
return neu_filepath, neu_file, f" | 🔄 Umbenannt: {neu_file}"
return filepath, dateiname, ""
def run(self):
try:
# ==========================================
# COUNTER (ZÄHLER) INITIALISIEREN
# ==========================================
stat_exif_neu = 0 # Echt neu geschriebene EXIF-Daten
stat_windows_fix = 0 # Nur Windows-Datum korrigiert (EXIF war schon ok)
stat_blitz_skip = 0 # Per Blitz-Skip komplett übersprungen
stat_ignoriert = 0 # Kein Muster im Namen gefunden
stat_jahr_unmoeglich = 0# Jahr vor 1970 oder nach 2038
stat_fehler = 0 # Technische Fehler (Datei gesperrt etc.)
# Log-Dateiname automatisch aus dem Zielordner generieren
ordner_name_aktuell = os.path.basename(self.ordner_pfad.strip("\\/")) or "Root"
if not os.path.exists(self.log_ordner_pfad):
try:
os.makedirs(self.log_ordner_pfad, exist_ok=True)
except:
pass # Ignore if it fails
if os.path.exists(self.log_ordner_pfad):
self.log_datei_pfad = os.path.join(self.log_ordner_pfad, f"log_{ordner_name_aktuell}_{datetime.now().strftime('%H%M%S')}.txt")
with open(self.log_datei_pfad, "w", encoding="utf-8") as f:
f.write(f"=== REPARATUR-LOG FÜR ORDNER: {ordner_name_aktuell} ===\n")
f.write(f"Ausgeführt am: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}\n")
f.write(f"Speicherort dieses Logs: {self.log_datei_pfad}\n")
f.write(f"Unterordner einbeziehen: {'JA' if self.unterordner_einbeziehen else 'NEIN (Nur Hauptordner)'}\n")
f.write(f"Modus: {'FESTES DATUM' if self.modus == 'fixed' else 'AUTOMATISCH'}\n")
f.write(f"Toleranz-Einstellung: Abweichungen <= 240 Sekunden werden ignoriert.\n")
f.write(f"PXL-Behandlung: Google Pixel UTC-Zeitstempel werden automatisch in MEZ/MESZ umgerechnet.\n")
f.write(f"DRY RUN AKTIV: {'JA (Vorschau)' if self.dry_run else 'NEIN (Scharfer Durchlauf)'}\n")
f.write(f"UMBENENNUNG AKTIV: {'JA' if self.rename_files else 'NEIN'}\n")
f.write(f"ORDNER-FALLBACK AKTIV: {'JA' if self.fallback_folder else 'NEIN'}\n")
if self.fallback_folder:
f.write(f" -> FALLBACK-AUSSCHLUSS PRÄFIXE: {', '.join(self.excl_prefixes) if self.excl_prefixes else 'Keine'}\n")
f.write(f" -> FALLBACK-AUSSCHLUSS TYPEN: {', '.join(self.excl_exts) if self.excl_exts else 'Keine'}\n")
f.write("-" * 100 + "\n\n")
if self.dry_run:
self.schreibe_log("⚠️ ACHTUNG: VORSCHAU (DRY RUN) IST AKTIV! Es werden KEINE Dateien verändert.\n")
# ==========================================
# PHASE 1: Alle Dateien finden und zählen
# ==========================================
self.schreibe_log("Schritt 1: Scanne nach Dateien...")
alle_dateien = []
# NEU: Wenn Dateien per Drag & Drop reingezogen wurden, nimm diese direkt!
if self.dateien_liste:
alle_dateien = self.dateien_liste
self.schreibe_log("-> Drag & Drop Modus aktiv: Verarbeite manuell hinzugefügte Dateien.")
else:
# ALT: Bisherige Ordner-Suche
if os.path.exists(self.ordner_pfad):
for root_dir, dirs, files in os.walk(self.ordner_pfad):
if not self.unterordner_einbeziehen:
dirs.clear()
for dateiname in files:
if dateiname.lower().endswith((".mp4", ".jpg", ".jpeg", ".png",".gif",".m2ts",".wmv")):
alle_dateien.append(os.path.join(root_dir, dateiname))
gesamt_anzahl = len(alle_dateien)
if gesamt_anzahl == 0:
self.schreibe_log("Keine passenden Bilder oder Videos gefunden!")
self.finished_signal.emit("Keine passenden Dateien gefunden.")
return
self.schreibe_log(f"-> {gesamt_anzahl} Dateien gefunden.")
self.schreibe_log(f"-> Modus: {'Inklusive Unterordner' if self.unterordner_einbeziehen else 'NUR Hauptordner'}\n")
self.schreibe_log("Schritt 2: Starte Metadaten-Reparatur (High-Speed Modus)...\n")
# Statistik vor Beginn initialisieren
self.stats_signal.emit(gesamt_anzahl, 0, 0, 0, 0, 0, 0)
letzter_ordner = None
# ==========================================
# HIGHSPEED: ExifTool im Batch-Modus starten!
# Bleibt für die gesamte Schleife im Hintergrund offen.
# ==========================================
if not os.path.exists(self.exiftool_pfad):
self.error_signal.emit("ExifTool nicht gefunden! Bitte Pfad prüfen.")
return
with exiftool.ExifTool(executable=self.exiftool_pfad) as et:
for index, filepath in enumerate(alle_dateien, start=1):
dateiname = os.path.basename(filepath)
ordner_name = os.path.basename(os.path.dirname(filepath))
neues_datum = None
folder_fallback_used = False
prozent = int((index / gesamt_anzahl) * 100)
self.progress_signal.emit(prozent)
fortschritt = f"[{index:3}/{gesamt_anzahl} | {prozent:3}%]"
if ordner_name != letzter_ordner:
trennlinie = f"\n{'#'*100}\n# ORDNER: {ordner_name}\n{'#'*100}"
self.schreibe_log(trennlinie)
# NEU: Wir senden einen speziellen Platzhalter für die Tabelle
self.table_signal.emit(f"📁 {ordner_name}", "ORDNER", "", "", "")
letzter_ordner = ordner_name
try:
mtime = os.path.getmtime(filepath)
aktuelle_mtime = datetime.fromtimestamp(mtime).replace(microsecond=0)
mtime_str = aktuelle_mtime.strftime('%d.%m.%Y %H:%M:%S')
except Exception as e:
self.table_signal.emit(dateiname, "FEHLER", "-", "-", f"Lese-Fehler: {e}")
self.schreibe_log(f"{fortschritt} FEHLER beim Lesen der Datei-Zeit: {e} | {dateiname}")
stat_fehler += 1
self.stats_signal.emit(gesamt_anzahl, stat_exif_neu, stat_windows_fix, stat_blitz_skip, stat_ignoriert, stat_jahr_unmoeglich, stat_fehler)
continue
# ==========================================
# PRÄFIX EXTRAKTION (Smarte Geräte-Erkennung für Renamer)
# ==========================================
prefix = ""
ext_check = os.path.splitext(dateiname)[1].lower()
orig_up = dateiname.upper()
if "PXL" in orig_up: prefix = "PXL"
elif "DSC" in orig_up: prefix = "DSC"
elif "DJI" in orig_up: prefix = "DJI"
elif "XIV" in orig_up: prefix = "XIV"
elif "WHATSAPP" in orig_up or "WA" in orig_up: prefix = "WA"
elif "SIGNAL" in orig_up: prefix = "Signal"
elif "SCREENSHOT" in orig_up: prefix = "Screenshot"
elif "GOPR" in orig_up or "GH" in orig_up: prefix = "GoPro"
elif "WP" in orig_up: prefix = "WP"
elif orig_up.startswith("IMG"): prefix = "IMG"
elif orig_up.startswith("VID"): prefix = "VID"
else:
prefix = "VID" if ext_check in ['.mp4', '.mov', '.wmv', '.m2ts'] else "IMG"
# ==========================================
# SOFORTIGER ABBRUCH: GLOBALE BLACKLIST
# ==========================================
if prefix.upper() in self.excl_prefixes or ext_check in self.excl_exts:
self.table_signal.emit(dateiname, "IGNORIERT", mtime_str, "-", "Blacklist (Präfix/Typ gesperrt)")
self.schreibe_log(f"{fortschritt} IGNORIERT (Blacklist): {dateiname}")
stat_ignoriert += 1
self.stats_signal.emit(gesamt_anzahl, stat_exif_neu, stat_windows_fix, stat_blitz_skip, stat_ignoriert, stat_jahr_unmoeglich, stat_fehler)
continue # Bricht ab und springt direkt zur nächsten Datei!
# ==========================================
# DATUM ERMITTELN
# ==========================================
if self.modus == "fixed":
neues_datum = datetime(
year=self.festes_datum.year, month=self.festes_datum.month, day=self.festes_datum.day,
hour=aktuelle_mtime.hour, minute=aktuelle_mtime.minute, second=aktuelle_mtime.second
)
else:
# REGULÄRE AUSDRÜCKE (MUSTER-ERKENNUNG ANHAND DES DATEINAMENS)
match_pxl = re.search(r'PXL_(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})', dateiname)
# Muster 1: Standard-Zeitstempel (Vollständig mit Unterstrich)
match_exakt = re.search(r'(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})', dateiname)
# Muster 2: Reines Datum mit Trennpunkten
match_datum_nur = re.search(r'(\d{4})\.(\d{2})\.(\d{2})', dateiname)
# Muster 3: DJI Alt (App-Export mit fehlerhafter Uhrzeit)
match_dji_alt = re.search(r'_0_(\d{8})(\d{3,5})_photo', dateiname)
# Muster 4: DSC Sony/Kamera-Format
match_dsc = re.search(r'^(\d{8})-DSC', dateiname)
# Muster 5: DJI Neu (Sauberer, 14-stelliger Zeitstempel)
match_dji_neu = re.search(r'_(\d{14})_(photo|video)', dateiname)
# Muster 6: Reines 8-stelliges Datum ohne Uhrzeit irgendwo im Namen
match_acht_ziffern = re.search(r'(?<!\d)(\d{4})(\d{2})(\d{2})(?!\d)', dateiname)
# Muster 7: Datum mit Bindestrichen ohne Uhrzeit
match_bindestrich = re.search(r'(\d{4})-(\d{2})-(\d{2})', dateiname)
# Muster 8: HwVideoEditor Format
match_hw = re.search(r'HwVideoEditor_(\d{4})_(\d{2})_(\d{2})_(\d{2})(\d{2})(\d{2})', dateiname)
# Muster 8b: LightCut Format
match_lv = re.search(r'lv_0_(\d{14})', dateiname)
# Muster 9: Reines Datum mit Unterstrichen
match_unterstrich = re.search(r'(\d{4})_(\d{2})_(\d{2})', dateiname)
# Muster 11: WhatsApp Format
match_whatsapp = re.search(r'(\d{4})-(\d{2})-(\d{2}) at (\d{2})\.(\d{2})\.(\d{2})', dateiname)
# Muster 12: Samsung / Android Screenshots
match_samsung = re.search(r'(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})', dateiname)
# Muster 13: Signal Messenger
match_signal = re.search(r'signal-(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})', dateiname)
# Muster 14: Deutsches Datumsformat (Tag zuerst!)
match_german = re.search(r'(?<!\d)(\d{2})\.(\d{2})\.(\d{4})(?!\d)', dateiname)
# Muster 16: Durchgehende Bindestriche
match_bindestrich_voll = re.search(r'(\d{4})-(\d{2})-(\d{2})-(\d{2})-(\d{2})-(\d{2})', dateiname)
# Muster 18: Windows Phone
match_wp = re.search(r'WP_(\d{4})(\d{2})(\d{2})_(\d{2})_(\d{2})_(\d{2})', dateiname)
# PRIORIERTES FORMAT-MATCHING
if match_pxl:
j, m, t, std, min_td, sek = match_pxl.groups()
neues_datum = utc_zu_lokalzeit_deutschland(sicheres_datum(j, m, t, std, min_td, sek))
elif match_hw:
j, m, t, std, min_td, sek = match_hw.groups()
neues_datum = sicheres_datum(j, m, t, std, min_td, sek)
elif match_lv:
ts = match_lv.group(1)
neues_datum = sicheres_datum(ts[0:4], ts[4:6], ts[6:8], ts[8:10], ts[10:12], ts[12:14])
elif match_dji_neu:
ts = match_dji_neu.group(1)
neues_datum = sicheres_datum(ts[0:4], ts[4:6], ts[6:8], ts[8:10], ts[10:12], ts[12:14])
elif match_bindestrich_voll:
j, m, t, std, min_td, sek = match_bindestrich_voll.groups()
neues_datum = sicheres_datum(j, m, t, std, min_td, sek)
elif match_whatsapp:
j, m, t, std, min_td, sek = match_whatsapp.groups()
neues_datum = sicheres_datum(j, m, t, std, min_td, sek)
elif match_samsung:
j, m, t, std, min_td, sek = match_samsung.groups()
neues_datum = sicheres_datum(j, m, t, std, min_td, sek)
elif match_signal:
j, m, t, std, min_td, sek = match_signal.groups()
neues_datum = sicheres_datum(j, m, t, std, min_td, sek)
elif match_exakt:
j, m, t, std, min_td, sek = match_exakt.groups()
neues_datum = sicheres_datum(j, m, t, std, min_td, sek)
elif match_dji_alt:
datum_str = match_dji_alt.group(1)
zeit_str = match_dji_alt.group(2)
j, m, t = datum_str[0:4], datum_str[4:6], datum_str[6:8]
try:
sek = int(zeit_str[-2:])
rest = zeit_str[:-2]
if len(rest) == 4: std, min_td = int(rest[0:2]), int(rest[2:4])
elif len(rest) == 3: std, min_td = (int(rest[0:2]), int(rest[2])) if int(rest[0:2]) <= 23 else (int(rest[0]), int(rest[1:3]))
else: std, min_td = 12, 0
neues_datum = sicheres_datum(j, m, t, std, min_td, sek)
except:
neues_datum = sicheres_datum(j, m, t)
# --- Formate ohne Uhrzeit (Mit Uhrzeit-Rettung) ---
elif match_dsc:
j, m, t = match_dsc.group(1)[0:4], match_dsc.group(1)[4:6], match_dsc.group(1)[6:8]
if aktuelle_mtime.year == int(j) and aktuelle_mtime.month == int(m) and aktuelle_mtime.day == int(t):
neues_datum = sicheres_datum(j, m, t, aktuelle_mtime.hour, aktuelle_mtime.minute, aktuelle_mtime.second)
else: neues_datum = sicheres_datum(j, m, t)
elif match_datum_nur:
j, m, t = match_datum_nur.groups()
if aktuelle_mtime.year == int(j) and aktuelle_mtime.month == int(m) and aktuelle_mtime.day == int(t):
neues_datum = sicheres_datum(j, m, t, aktuelle_mtime.hour, aktuelle_mtime.minute, aktuelle_mtime.second)
else: neues_datum = sicheres_datum(j, m, t)
elif match_german:
t, m, j = match_german.groups()
if aktuelle_mtime.year == int(j) and aktuelle_mtime.month == int(m) and aktuelle_mtime.day == int(t):
neues_datum = sicheres_datum(j, m, t, aktuelle_mtime.hour, aktuelle_mtime.minute, aktuelle_mtime.second)
else: neues_datum = sicheres_datum(j, m, t)
elif match_acht_ziffern:
j, m, t = match_acht_ziffern.groups()
if aktuelle_mtime.year == int(j) and aktuelle_mtime.month == int(m) and aktuelle_mtime.day == int(t):
neues_datum = sicheres_datum(j, m, t, aktuelle_mtime.hour, aktuelle_mtime.minute, aktuelle_mtime.second)
else: neues_datum = sicheres_datum(j, m, t)
elif match_bindestrich:
j, m, t = match_bindestrich.groups()
if aktuelle_mtime.year == int(j) and aktuelle_mtime.month == int(m) and aktuelle_mtime.day == int(t):
neues_datum = sicheres_datum(j, m, t, aktuelle_mtime.hour, aktuelle_mtime.minute, aktuelle_mtime.second)
else: neues_datum = sicheres_datum(j, m, t)
elif match_unterstrich:
j, m, t = match_unterstrich.groups()
if aktuelle_mtime.year == int(j) and aktuelle_mtime.month == int(m) and aktuelle_mtime.day == int(t):
neues_datum = sicheres_datum(j, m, t, aktuelle_mtime.hour, aktuelle_mtime.minute, aktuelle_mtime.second)
else: neues_datum = sicheres_datum(j, m, t)
elif match_wp:
j, m, t, std, min_td, sek = match_wp.groups()
neues_datum = sicheres_datum(j, m, t, std, min_td, sek)
# ==========================================
# ORDNER-FALLBACK (Tiefensuche + Filter)
# ==========================================
if neues_datum is None and self.fallback_folder:
if prefix.upper() in self.excl_prefixes or ext_check in self.excl_exts:
pass # Auf Schwarzer Liste -> kein Fallback
else:
safe_filepath = filepath.replace('\\', '/')
path_parts = [p for p in safe_filepath.split('/') if p]
for part in reversed(path_parts[:-1]):
match_folder = re.search(r'(\d{4})[-_.](\d{2})[-_.](\d{2})', part)
if not match_folder:
match_folder = re.search(r'(?<!\d)(\d{4})(\d{2})(\d{2})(?!\d)', part)
if match_folder:
j, m, t = match_folder.groups()
nd = sicheres_datum(j, m, t, aktuelle_mtime.hour, aktuelle_mtime.minute, aktuelle_mtime.second)
if nd:
neues_datum = nd
folder_fallback_used = True
break
# ==========================================
# Datumsänderung prüfen & anwenden
# ==========================================
if neues_datum:
if neues_datum.year < 1970 or neues_datum.year > 2038:
self.table_signal.emit(dateiname, "FEHLER", mtime_str, "-", f"Jahr {neues_datum.year} unmöglich")
self.schreibe_log(f"{fortschritt} ÜBERSPRÜNGEN (Jahr {neues_datum.year} unmöglich): {dateiname}")
stat_jahr_unmoeglich += 1
else:
try:
TOLERANZ_SEKUNDEN = 240
kann_exif_schreiben = not dateiname.lower().endswith((".wmv", ".m2ts", ".gif"))
exif_passt = False
vorher_datum_exif = "[LEER / Keine]"
# -----------------------------------------------------------------
# HIGHSPEED-LESEN MIT PYEXIFTOOL + EXIF-TRUST-CHECK
# -----------------------------------------------------------------
if kann_exif_schreiben:
# NEU: Wir fragen nach DateTimeOriginal UND CreateDate (für Videos!)
raw_exif_output = et.execute("-m", "-DateTimeOriginal", "-CreateDate", "-s3", filepath).strip()
if raw_exif_output:
# Hat eine Datei beide Tags, liefert ExifTool zwei Zeilen. Wir nehmen die erste.
vorher_datum_exif = raw_exif_output.split('\n')[0].strip()
try:
# Robuster Regex-Filter für das reine Datum
match_exif = re.search(r'(\d{4}:\d{2}:\d{2} \d{2}:\d{2}:\d{2})', vorher_datum_exif)
if match_exif:
clean_exif_str = match_exif.group(1)
# Überschreibt den unsauberen String, damit "QuickTime" aus der Tabelle verschwindet
vorher_datum_exif = clean_exif_str
exif_dt = datetime.strptime(clean_exif_str, '%Y:%m:%d %H:%M:%S')
# WICHTIGER EXIF-TRUST-CHECK
if exif_dt.date() == neues_datum.date():
exif_passt = True
neues_datum = exif_dt
folder_fallback_used = False # Versteckt den Fallback-Hinweis
elif abs((exif_dt - neues_datum).total_seconds()) <= TOLERANZ_SEKUNDEN:
exif_passt = True
folder_fallback_used = False # Versteckt den Fallback-Hinweis
except ValueError:
pass
# Jetzt erst formatieren, nachdem der Trust-Check das Datum evtl. korrigiert hat
exif_datum = neues_datum.strftime('%Y:%m:%d %H:%M:%S')
win_time_diff = abs((aktuelle_mtime - neues_datum).total_seconds())
display_alt = vorher_datum_exif if kann_exif_schreiben else mtime_str
fallback_msg = " [Ordner-Fallback]" if folder_fallback_used else ""
# -----------------------------------------------------------------
# FALL A: SKIP
# -----------------------------------------------------------------
if exif_passt and (win_time_diff <= TOLERANZ_SEKUNDEN):
filepath, dateiname, ren_msg = self.do_rename(filepath, dateiname, prefix, neues_datum)
self.table_signal.emit(dateiname, "SKIP", display_alt, exif_datum, f"Bereits exakt{fallback_msg}{ren_msg}")
self.schreibe_log(f"{fortschritt} BLITZ-SKIP -> {exif_datum} | {dateiname}{fallback_msg}{ren_msg}")
stat_blitz_skip += 1
# -----------------------------------------------------------------
# FALL B: FIX WIN-TIME ONLY
# -----------------------------------------------------------------
elif exif_passt or not kann_exif_schreiben:
filepath, dateiname, ren_msg = self.do_rename(filepath, dateiname, prefix, neues_datum)
if not self.dry_run:
timestamp = neues_datum.timestamp()
os.utime(filepath, (timestamp, timestamp))
status_text = "DRY RUN (FIX)" if self.dry_run else "FIX (WIN)"
self.table_signal.emit(dateiname, status_text, display_alt, exif_datum, f"Nur Windows-Zeit{fallback_msg}{ren_msg}")
aktion = "[VORSCHAU] WÜRDE WIN-ZEIT FIXEN" if self.dry_run else "WINDOWS-TIME FIX"
self.schreibe_log(f"{fortschritt} {aktion} -> {exif_datum} | Vorher Win-MTime: {mtime_str} | {dateiname}{fallback_msg}{ren_msg}")
stat_windows_fix += 1
# -----------------------------------------------------------------
# FALL C: HIGHSPEED-SCHREIBEN MIT PYEXIFTOOL
# -----------------------------------------------------------------
else:
filepath, dateiname, ren_msg = self.do_rename(filepath, dateiname, prefix, neues_datum)
if not self.dry_run:
# execute feuert den Befehl direkt in die bereits offene Instanz!
output = et.execute("-m", f"-AllDates={exif_datum}", "-overwrite_original_in_place", filepath)
timestamp = neues_datum.timestamp()
os.utime(filepath, (timestamp, timestamp))
if "unchanged" in output:
self.table_signal.emit(dateiname, "FIX (WIN)", display_alt, exif_datum, f"EXIF war bereits OK{fallback_msg}{ren_msg}")
self.schreibe_log(f"{fortschritt} WINDOWS-TIME FIX -> {exif_datum} | Vorher EXIF: {vorher_datum_exif} | {dateiname}{fallback_msg}{ren_msg}")
stat_windows_fix += 1
else:
self.table_signal.emit(dateiname, "ERFOLG", display_alt, exif_datum, f"EXIF neu{fallback_msg}{ren_msg}")
self.schreibe_log(f"{fortschritt} ERFOLG (EXIF neu) -> {exif_datum} | Vorher EXIF: {vorher_datum_exif} | {dateiname}{fallback_msg}{ren_msg}")
stat_exif_neu += 1
else:
self.table_signal.emit(dateiname, "VORSCHAU", display_alt, exif_datum, f"Würde EXIF schreiben{fallback_msg}{ren_msg}")
self.schreibe_log(f"{fortschritt} [VORSCHAU] WÜRDE EXIF SCHREIBEN -> {exif_datum} | Vorher EXIF: {vorher_datum_exif} | {dateiname}{fallback_msg}{ren_msg}")
stat_exif_neu += 1
except Exception as e:
self.table_signal.emit(dateiname, "FEHLER", "-", "-", f"Fehler: {str(e)[:30]}")
self.schreibe_log(f"{fortschritt} FEHLER bei {dateiname}: {e}")
stat_fehler += 1
else:
self.table_signal.emit(dateiname, "IGNORIERT", mtime_str, "-", "Kein Datum im Dateinamen/Ordner gefunden")
self.schreibe_log(f"{fortschritt} IGNORIERT (Kein gültiges Datum im Namen): {dateiname}")
stat_ignoriert += 1
# Live-Statistik nach jeder Datei updaten
self.stats_signal.emit(gesamt_anzahl, stat_exif_neu, stat_windows_fix, stat_blitz_skip, stat_ignoriert, stat_jahr_unmoeglich, stat_fehler)
# ==========================================
# ZUSAMMENFASSUNG (DASHBOARD)
# ==========================================
ZUSAMMENFASSUNG = f"""
{"="*100}
ZUSAMMENFASSUNG FÜR ORDNER: {ordner_name_aktuell} {'(VORSCHAU - KEINE DATEIEN VERÄNDERT)' if self.dry_run else ''}
{"-"*100}
Modus: {'FESTES DATUM ERZWUNGEN' if self.modus == 'fixed' else 'Automatisch (Regex)'}
Unterordner einbeziehen: {'JA' if self.unterordner_einbeziehen else 'NEIN (Nur Hauptordner)'}
Gesamt gescannte Dateien: {gesamt_anzahl}
[+] EXIF {'würde ' if self.dry_run else ''}neu geschrieben: {stat_exif_neu}
[*] Nur Windows-Zeit {'würde ' if self.dry_run else ''}fixiert: {stat_windows_fix}
[>] BLITZ-SKIP (Bereits korrekt): {stat_blitz_skip}
[-] Ignoriert (Kein Datum im Name):{stat_ignoriert}
[!] Übersprungen (Jahr unmöglich): {stat_jahr_unmoeglich}
[x] Technische Fehler: {stat_fehler}
{"="*100}"""
self.schreibe_log(ZUSAMMENFASSUNG)
self.finished_signal.emit("Vorschau erfolgreich beendet!" if self.dry_run else "Änderungen erfolgreich angewendet!")
except Exception as e:
self.error_signal.emit(str(e))
# ==========================================
# HAUPTFENSTER (GUI)
# ==========================================
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Metadaten Reparatur Tool")
self.resize(1400, 750)
self.worker = None
# NEU: Drag & Drop fürs ganze Fenster aktivieren & Speicherliste anlegen
self.setAcceptDrops(True)
self.dropped_files = []
self.setup_ui()
def setup_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(5, 5, 5, 5)
self.splitter = QSplitter(Qt.Orientation.Horizontal)
main_layout.addWidget(self.splitter)
# --- LINKE SEITE (Einstellungen) ---
left_panel = QWidget()
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(5, 5, 10, 5)
# 1. PFADE
path_box = QGroupBox("Pfade & Zielordner")
grid = QGridLayout()
self.input_ordner = QLineEdit(r"\\192.168.180.78\fotos\Still to komp\2025")
self.input_ordner.textChanged.connect(self.check_ordner_reset)
script_dir = os.path.dirname(os.path.abspath(__file__))
exe_path = os.path.join(script_dir, "exiftool-13.59_64", "exiftool-13.59_64", "exiftool.exe")
self.input_exif = QLineEdit(exe_path)
self.input_log = QLineEdit(r"C:\Users\janni\Documents\GitLab\metadaten-aus-namen\Logs")
btn_ordner = QPushButton("...")
btn_ordner.clicked.connect(self.browse_ordner)
btn_exif = QPushButton("...")
btn_exif.clicked.connect(self.browse_exif)
btn_log = QPushButton("...")
btn_log.clicked.connect(self.browse_log)
grid.addWidget(QLabel("Zielordner:"), 0, 0)
grid.addWidget(self.input_ordner, 0, 1)
grid.addWidget(btn_ordner, 0, 2)
grid.addWidget(QLabel("ExifTool:"), 1, 0)
grid.addWidget(self.input_exif, 1, 1)
grid.addWidget(btn_exif, 1, 2)
grid.addWidget(QLabel("Log-Ordner:"), 2, 0)
grid.addWidget(self.input_log, 2, 1)
grid.addWidget(btn_log, 2, 2)
path_box.setLayout(grid)
left_layout.addWidget(path_box)
# 2. MODUS
mode_box = QGroupBox("Verarbeitungs-Modus")
mode_layout = QVBoxLayout()
self.radio_auto = QRadioButton("Datum automatisch aus Dateinamen (Regex)")
self.radio_auto.setChecked(True)
self.radio_fixed = QRadioButton("Festes Datum erzwingen:")
self.date_input = QDateEdit(QDate.currentDate())
self.date_input.setDisplayFormat("dd.MM.yyyy")
self.date_input.setCalendarPopup(True)
self.date_input.setEnabled(False)
self.radio_fixed.toggled.connect(self.date_input.setEnabled)
mode_layout.addWidget(self.radio_auto)
fixed_layout = QHBoxLayout()
fixed_layout.addWidget(self.radio_fixed)
fixed_layout.addWidget(self.date_input)
fixed_layout.addStretch()
mode_layout.addLayout(fixed_layout)
mode_box.setLayout(mode_layout)
left_layout.addWidget(mode_box)
# 3. OPTIONEN
options_box = QGroupBox("Optionen")
opt_layout = QVBoxLayout()
self.check_unterordner = QCheckBox("Unterordner einbeziehen")
self.check_unterordner.setChecked(True)
self.check_rename = QCheckBox("Dateien einheitlich umbenennen (Präfix_YYYYMMDD_HHMMSS)")
self.check_rename.setChecked(False)
self.check_folder_fallback = QCheckBox("Fallback: Datum aus Ordnernamen lesen (z.B. YYYY-MM-DD)")
self.check_folder_fallback.setChecked(True)
fallback_filter_layout = QHBoxLayout()
fallback_filter_layout.setContentsMargins(25, 0, 0, 10)
fallback_filter_layout.addWidget(QLabel("Generell ignorieren - Präfixe:"))
self.input_excl_prefixes = QLineEdit("DSC,XIV")
self.input_excl_prefixes.setPlaceholderText("z.B. DSC, IMG")
fallback_filter_layout.addWidget(self.input_excl_prefixes)
fallback_filter_layout.addWidget(QLabel("Dateitypen:"))
self.input_excl_exts = QLineEdit(".m2ts")
self.input_excl_exts.setPlaceholderText("z.B. .m2ts, .gif")
fallback_filter_layout.addWidget(self.input_excl_exts)
self.check_folder_fallback.toggled.connect(self.input_excl_prefixes.setEnabled)
self.check_folder_fallback.toggled.connect(self.input_excl_exts.setEnabled)
opt_layout.addWidget(self.check_unterordner)
opt_layout.addWidget(self.check_folder_fallback)
opt_layout.addLayout(fallback_filter_layout)
opt_layout.addWidget(self.check_rename)
options_box.setLayout(opt_layout)
left_layout.addWidget(options_box)
# ==========================================
# 4. LIVE-STATISTIK
# ==========================================
stats_box = QGroupBox("Live-Statistik")
stats_layout = QGridLayout()
self.lbl_stat_gesamt = QLabel("0")
self.lbl_stat_exif = QLabel("0")
self.lbl_stat_win = QLabel("0")
self.lbl_stat_skip = QLabel("0")
self.lbl_stat_ign = QLabel("0")
self.lbl_stat_err = QLabel("0")
font_bold = QFont()
font_bold.setBold(True)
for lbl in [self.lbl_stat_gesamt, self.lbl_stat_exif, self.lbl_stat_win, self.lbl_stat_skip, self.lbl_stat_ign, self.lbl_stat_err]:
lbl.setFont(font_bold)
self.lbl_stat_exif.setStyleSheet("color: #d28e00;")
self.lbl_stat_win.setStyleSheet("color: #d28e00;")
self.lbl_stat_skip.setStyleSheet("color: gray;")
self.lbl_stat_ign.setStyleSheet("color: gray;")
self.lbl_stat_err.setStyleSheet("color: red;")
stats_layout.addWidget(QLabel("Gesamt gescannte Dateien:"), 0, 0)
stats_layout.addWidget(self.lbl_stat_gesamt, 0, 1)
stats_layout.addWidget(QLabel("[+] EXIF geschrieben / Vorschau:"), 1, 0)
stats_layout.addWidget(self.lbl_stat_exif, 1, 1)
stats_layout.addWidget(QLabel("[*] Nur Windows-Zeit fixiert:"), 2, 0)
stats_layout.addWidget(self.lbl_stat_win, 2, 1)
stats_layout.addWidget(QLabel("[>] BLITZ-SKIP (Bereits exakt):"), 3, 0)
stats_layout.addWidget(self.lbl_stat_skip, 3, 1)
stats_layout.addWidget(QLabel("[-] Ignoriert (Kein Datum gefunden):"), 4, 0)
stats_layout.addWidget(self.lbl_stat_ign, 4, 1)
stats_layout.addWidget(QLabel("[x] Fehler / Unmögliches Jahr:"), 5, 0)
stats_layout.addWidget(self.lbl_stat_err, 5, 1)
stats_box.setLayout(stats_layout)
left_layout.addWidget(stats_box)
# 5. START BUTTONS & PROGRESS
left_layout.addStretch()
button_layout = QHBoxLayout()
self.btn_preview = QPushButton("🔍 Vorschau (Simulation)")
self.btn_preview.setMinimumHeight(45)
font_prev = self.btn_preview.font()
font_prev.setBold(True)
self.btn_preview.setFont(font_prev)
self.btn_preview.clicked.connect(lambda: self.start_processing(is_dry_run=True))
self.btn_apply = QPushButton("⚠️ Änderungen Anwenden")
self.btn_apply.setMinimumHeight(45)
font_app = self.btn_apply.font()
font_app.setBold(True)
self.btn_apply.setFont(font_app)
self.btn_apply.setStyleSheet("background-color: #b91d47; color: white;")
self.btn_apply.clicked.connect(lambda: self.start_processing(is_dry_run=False))
button_layout.addWidget(self.btn_preview)
button_layout.addWidget(self.btn_apply)
left_layout.addLayout(button_layout)
self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)
self.progress_bar.setTextVisible(True)
left_layout.addWidget(self.progress_bar)
self.splitter.addWidget(left_panel)
# --- RECHTE SEITE (Tabelle) ---
right_panel = QWidget()
right_layout = QVBoxLayout(right_panel)
right_layout.setContentsMargins(0, 5, 5, 5)
# NEU: Kopfzeile über der Tabelle mit Titel und "Liste leeren"-Button
table_top_layout = QHBoxLayout()
lbl_table_title = QLabel("Dateiansicht / Drag & Drop Bereich")
font_title = lbl_table_title.font()
font_title.setBold(True)
lbl_table_title.setFont(font_title)
self.btn_clear_list = QPushButton("🗑️ Liste leeren")
self.btn_clear_list.setStyleSheet("background-color: #f0f0f0; padding: 5px;")
self.btn_clear_list.clicked.connect(self.clear_drag_drop_list)
self.btn_clear_list.setEnabled(False) # Erst aktiv, wenn Dateien da sind
table_top_layout.addWidget(lbl_table_title)
table_top_layout.addStretch()
table_top_layout.addWidget(self.btn_clear_list)
right_layout.addLayout(table_top_layout)
self.table = QTableWidget(0, 5)
self.table.setHorizontalHeaderLabels(["Dateiname", "Status", "Altes Datum", "Neues Datum", "Bemerkung"])
header = self.table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Interactive)
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Interactive)
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Interactive)
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Interactive)
header.setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch)
self.table.setColumnWidth(0, 320)
self.table.setColumnWidth(1, 120)
self.table.setColumnWidth(2, 160)
self.table.setColumnWidth(3, 160)
self.table.setSortingEnabled(True)
self.table.setAlternatingRowColors(True)
self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
right_layout.addWidget(self.table)
self.splitter.addWidget(right_panel)
self.splitter.setSizes([450, 950])
# --- FUNKTIONEN ---
def check_ordner_reset(self):
"""Leert die Drag&Drop-Liste, wenn der User wieder einen normalen Ordnerpfad eintippt."""
if self.input_ordner.text() != "DRAG & DROP MODUS (Manuelle Auswahl)":
self.dropped_files = []
self.btn_clear_list.setEnabled(False)
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.accept()
else:
event.ignore()
def dropEvent(self, event):
urls = event.mimeData().urls()
if not urls: return
gueltige_endungen = (".mp4", ".jpg", ".jpeg", ".png", ".gif", ".m2ts", ".wmv")
neue_dateien = []
for url in urls:
pfad = url.toLocalFile()
# Nur echte Dateien mit den richtigen Endungen akzeptieren
if os.path.isfile(pfad) and pfad.lower().endswith(gueltige_endungen):
neue_dateien.append(os.path.normpath(pfad))
if neue_dateien:
# Damit check_ordner_reset nicht sofort unsere Liste löscht,
# blockieren wir kurz das Signal des Textfeldes
self.input_ordner.blockSignals(True)
self.input_ordner.setText("DRAG & DROP MODUS (Manuelle Auswahl)")
self.input_ordner.blockSignals(False)
self.dropped_files.extend(neue_dateien)
# Duplikate entfernen (falls man aus Versehen Dateien doppelt reinzieht)
self.dropped_files = list(dict.fromkeys(self.dropped_files))
# Anzeige in der Tabelle leeren und vorbereiten
self.table.setRowCount(0)
for datei in self.dropped_files:
self.add_table_row(os.path.basename(datei), "BEREIT", "-", "-", os.path.dirname(datei))
self.btn_clear_list.setEnabled(True)
event.accept()
def clear_drag_drop_list(self):
"""Leert die manuell per Drag & Drop hinzugefügten Dateien und setzt die GUI zurück."""
self.dropped_files = []
self.table.setRowCount(0)
self.btn_clear_list.setEnabled(False)
# Signal blockieren, damit check_ordner_reset nicht im Kreis läuft
self.input_ordner.blockSignals(True)
if self.input_ordner.text() == "DRAG & DROP MODUS (Manuelle Auswahl)":
self.input_ordner.clear()
self.input_ordner.blockSignals(False)
# Setzt die Statistik zurück
self.update_stats(0, 0, 0, 0, 0, 0, 0)
def browse_ordner(self):
folder = QFileDialog.getExistingDirectory(self, "Zielordner auswählen")
if folder: self.input_ordner.setText(folder)
def browse_exif(self):
file, _ = QFileDialog.getOpenFileName(self, "exiftool.exe auswählen", "", "Executable (*.exe)")
if file: self.input_exif.setText(file)
def browse_log(self):
folder = QFileDialog.getExistingDirectory(self, "Log-Ordner auswählen")
if folder: self.input_log.setText(folder)
def update_stats(self, gesamt, exif, win, skip, ign, unmoeglich, fehler):
self.lbl_stat_gesamt.setText(str(gesamt))
self.lbl_stat_exif.setText(str(exif))
self.lbl_stat_win.setText(str(win))
self.lbl_stat_skip.setText(str(skip))
self.lbl_stat_ign.setText(str(ign))
self.lbl_stat_err.setText(str(unmoeglich + fehler))
def add_table_row(self, dateiname, status, alt, neu, bemerkung):
row = self.table.rowCount()
self.table.insertRow(row)
# ==========================================
# NEU: Ordner-Platzhalter formatieren
# ==========================================
if status == "ORDNER":
for col in range(5):
text = dateiname if col == 0 else ""
item = QTableWidgetItem(text)
font = item.font()
font.setBold(True)
item.setFont(font)
item.setForeground(QColor("#5aa5ff")) # Hellblaues Highlight
item.setBackground(QColor(100, 100, 100, 40)) # Dezenter Hintergrund für die ganze Zeile
self.table.setItem(row, col, item)
self.table.scrollToBottom()
return
self.table.setItem(row, 0, QTableWidgetItem(dateiname))
status_item = QTableWidgetItem(status)
font = status_item.font()
font.setBold(True)
status_item.setFont(font)
if "ERFOLG" in status or "FIX" in status:
status_item.setForeground(QColor("green"))
elif "FEHLER" in status or "WARNUNG" in status:
status_item.setForeground(QColor("red"))
elif "SKIP" in status or "IGNORIERT" in status:
status_item.setForeground(QColor("gray"))
elif "VORSCHAU" in status or "DRY RUN" in status:
status_item.setForeground(QColor("#d28e00"))
elif status == "BEREIT":
status_item.setForeground(QColor("#5aa5ff")) # Hellblau für bereit
self.table.setItem(row, 1, status_item)
self.table.setItem(row, 2, QTableWidgetItem(alt))
self.table.setItem(row, 3, QTableWidgetItem(neu))
self.table.setItem(row, 4, QTableWidgetItem(bemerkung))
self.table.scrollToBottom()
def start_processing(self, is_dry_run=True):
gewaehlter_modus = "fixed" if self.radio_fixed.isChecked() else "auto"
festes_date = self.date_input.date().toPyDate()
is_rename_active = self.check_rename.isChecked()
is_folder_fallback_active = self.check_folder_fallback.isChecked()
excl_pref = [p.strip().upper() for p in self.input_excl_prefixes.text().split(",") if p.strip()]
excl_ext = [e.strip().lower() for e in self.input_excl_exts.text().split(",") if e.strip()]
if not is_dry_run:
antwort = QMessageBox.question(
self, "Scharfer Modus",
"Achtung: Dateien werden nun physisch überschrieben und umbenannt.\n\nHast du die Vorschau geprüft und bist dir sicher?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if antwort == QMessageBox.StandardButton.No:
return
# Bei Neustart die Live-Farben für die Statistik an den Modus anpassen
if not is_dry_run:
self.lbl_stat_exif.setStyleSheet("color: green;")
self.lbl_stat_win.setStyleSheet("color: green;")
else:
self.lbl_stat_exif.setStyleSheet("color: #d28e00;")
self.lbl_stat_win.setStyleSheet("color: #d28e00;")
self.btn_preview.setEnabled(False)
self.btn_apply.setEnabled(False)
self.progress_bar.setValue(0)
self.table.setSortingEnabled(False)
self.table.setRowCount(0)
# Statistik auf 0 setzen
self.update_stats(0, 0, 0, 0, 0, 0, 0)
# Prüfen, ob wir die manuellen Dateien oder den Ordner nutzen
files_to_process = self.dropped_files if self.dropped_files else None
self.worker = WorkerThread(
ordner=self.input_ordner.text(),
exiftool=self.input_exif.text(),
log_dir=self.input_log.text(),
unterordner=self.check_unterordner.isChecked(),
modus=gewaehlter_modus,
festes_datum=festes_date,
dry_run=is_dry_run,
rename_files=is_rename_active,
fallback_folder=is_folder_fallback_active,
excl_prefixes=excl_pref,
excl_exts=excl_ext,
dateien_liste=files_to_process # NEU: Liste übergeben
)
self.worker.table_signal.connect(self.add_table_row)
self.worker.progress_signal.connect(self.progress_bar.setValue)
self.worker.stats_signal.connect(self.update_stats)
self.worker.finished_signal.connect(self.process_finished)
self.worker.error_signal.connect(self.process_error)
self.worker.start()
def process_finished(self, msg):
self.btn_preview.setEnabled(True)
self.btn_apply.setEnabled(True)
self.table.setSortingEnabled(True)
QMessageBox.information(self, "Fertig", msg)
def process_error(self, err_msg):
self.btn_preview.setEnabled(True)
self.btn_apply.setEnabled(True)
self.table.setSortingEnabled(True)
QMessageBox.critical(self, "Kritischer Fehler", err_msg)
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setStyle("Fusion")
window = MainWindow()
window.show()
sys.exit(app.exec())