Skripte/editfinde_GUI.py

823 lines
28 KiB
Python
Raw Permalink Normal View History

2026-09-27 01:33:33 +02:00
import csv
import glob
import json
import os
import subprocess
import sys
from PyQt6.QtCore import QThread, Qt, pyqtSignal
from PyQt6.QtGui import QColor, QFont
from PyQt6.QtWidgets import (
QApplication,
QComboBox,
QFileDialog,
QGridLayout,
QGroupBox,
QHBoxLayout,
QHeaderView,
QLabel,
QLineEdit,
QMainWindow,
QMenu,
QMessageBox,
QProgressBar,
QPushButton,
QRadioButton,
QTableWidget,
QTableWidgetItem,
QVBoxLayout,
QWidget,
)
# ================= STANDARD-WERTE FÜR DIE GUI =================
DEFAULT_VIDEO_DIR = r"\\192.168.180.78\fotos\2026"
DEFAULT_EXCLUDES = "#recycle, @eaDir, Backup_Alt, 00immich_upload, Jahre, komp"
# Unterstützte Video- und Foto-Formate
SUPPORTED_EXTENSIONS = [
# Videos
"mp4",
"mov",
"mkv",
# Fotos & RAWs
"jpg",
"jpeg",
"png",
"dng",
"arw",
"heic",
"tif",
"tiff",
"webp",
]
# =============================================================
# ================= EXIFTOOL-SUCHE =================
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
found_exes = [
f
for f in glob.glob(
os.path.join(SCRIPT_DIR, "**", "exiftool.exe"), recursive=True
)
if "(-k)" not in f
]
EXIFTOOL_PATH = found_exes[0] if found_exes else None
# =================================================
def scan_metadata(folder, exclude_dirs):
cmd = [
EXIFTOOL_PATH,
"-r",
"-json",
"-q",
"-m",
"-charset",
"utf8",
"-charset",
"filename=utf8",
]
for exc in exclude_dirs:
if "\\" not in exc and "/" not in exc:
cmd.extend(["-i", exc])
for ext in SUPPORTED_EXTENSIONS:
cmd.extend(["-ext", ext])
cmd.extend([
"-FileName",
"-Directory",
"-Software",
"-Encoder",
"-CompressorName",
"-HandlerDescription",
"-Make",
"-Model",
folder,
])
try:
res = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
except Exception as e:
return None, f"Subprocess-Fehler: {e}"
stdout_clean = res.stdout.strip() if res.stdout else ""
if stdout_clean.startswith("[") and stdout_clean.endswith("]"):
try:
return json.loads(stdout_clean), None
except json.JSONDecodeError as e:
return None, f"Fehler beim Decodieren des JSON: {e}"
return None, res.stderr or "Keine Daten von ExifTool empfangen."
def classify_media(item):
filename = item.get("FileName", "").lower()
software = str(item.get("Software", "")).lower()
encoder = str(item.get("Encoder", "")).lower()
compressor = str(item.get("CompressorName", "")).lower()
handler = str(item.get("HandlerDescription", "")).lower()
make = str(item.get("Make", "")).lower()
model = str(item.get("Model", "")).lower()
combined_info = (
f"{software} {encoder} {compressor} {handler} {filename}".lower()
)
# 1. Fotos (Lightroom, Photoshop, etc.)
if "lightroom" in combined_info or "lrm_export" in filename:
return "Lightroom", "Adobe Lightroom"
if any(ps in combined_info for ps in ["photoshop", "imageready"]):
return "Edit", "Adobe Photoshop"
if "helicon" in combined_info:
return "Edit", "Helicon Focus"
if any(
pe in combined_info
for pe in ["snapseed", "gimp", "affinity", "capture one"]
):
return "Edit", "Foto-Editor"
# 2. Videoschnitt-Programme
if "quik" in combined_info:
return "Edit", "GoPro Quik"
if "gopro" in software and not ("hero" in model):
return "Edit", "GoPro App/Quik"
if any(m in combined_info for m in ["magix", "movie edit pro", "deluxe"]):
return "Edit", "Magix"
if any(
w in combined_info
for w in ["movie maker", "clipchamp", "microsoft corporation"]
):
return "Edit", "Windows Video Tool"
if any(c in combined_info for c in ["capcut", "bytedance", "lemon"]):
return "Trim", "CapCut"
if "inshot" in combined_info:
return "Trim", "InShot"
# 3. Video-Exporte ohne Make/Model
ext = os.path.splitext(filename)[1].lower()
if not make and not model:
if ext in [".mp4", ".mov", ".mkv"] and any(
g in combined_info for g in ["lavf", "handbrake", "x264", "x265"]
):
return "Edit", "Unbekannter Export (Lavf/x264)"
return None, "Original"
def write_sidecar(file_path, tag):
xmp_path = f"{file_path}.xmp"
cmd = [
EXIFTOOL_PATH,
"-tagsfromfile",
file_path,
f"-XMP-digiKam:TagsList={tag}",
f"-XMP-lr:HierarchicalSubject={tag}",
f"-XMP-dc:Subject={tag}",
f"-IPTC:Keywords={tag}",
"-o",
xmp_path,
file_path,
]
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def write_embedded(file_path, tag):
cmd = [
EXIFTOOL_PATH,
"-P",
"-overwrite_original",
# Für Fotos im Windows Explorer ("Markierungen" in JPG/TIF):
f"-XPKeywords={tag}",
f"-IPTC:Keywords={tag}",
# Für Videos im Windows Explorer ("Markierungen" in MP4):
f"-Microsoft:Category={tag}",
# Für Immich & Adobe Standards:
f"-XMP-digiKam:TagsList={tag}",
f"-XMP-lr:HierarchicalSubject={tag}",
f"-XMP-dc:Subject={tag}",
# Apple/QuickTime:
f"-QuickTime:Keywords={tag}",
f"-ItemList:Keyword={tag}",
file_path,
]
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# ================= THREADS =================
class ScanThread(QThread):
status_signal = pyqtSignal(str)
error_signal = pyqtSignal(str)
finished_signal = pyqtSignal(list)
def __init__(self, video_dir, exclude_dirs):
super().__init__()
self.video_dir = video_dir
self.exclude_dirs = exclude_dirs
def run(self):
self.status_signal.emit(
f"Scanne Metadaten in '{self.video_dir}' (Netzwerk-Scan läuft)..."
)
items, error = scan_metadata(self.video_dir, self.exclude_dirs)
if error or items is None:
self.error_signal.emit(error or "Keine Daten empfangen.")
return
detected_files = []
for item in items:
directory = item.get("Directory", "")
skip_file = False
for exclude_path in self.exclude_dirs:
if (
os.path.normpath(exclude_path).lower()
in os.path.normpath(directory).lower()
):
skip_file = True
break
if skip_file:
continue
tag, detected_app = classify_media(item)
if tag:
detected_files.append({
"File": item.get("SourceFile"),
"Suggested_Tag": tag,
"Program": detected_app,
})
self.finished_signal.emit(detected_files)
class ApplyThread(QThread):
status_signal = pyqtSignal(str)
progress_signal = pyqtSignal(int, int)
finished_signal = pyqtSignal(int)
def __init__(self, tasks, mode):
super().__init__()
self.tasks = tasks
self.mode = mode
def run(self):
total = len(self.tasks)
for i, (path, tag) in enumerate(self.tasks, 1):
self.status_signal.emit(f"Verarbeite Datei {i} von {total}...")
self.progress_signal.emit(i, total)
if self.mode == "SIDECAR":
write_sidecar(path, tag)
else:
write_embedded(path, tag)
self.finished_signal.emit(total)
# ================= GUI KLASSE =================
class EditFinderApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Immich Media Tagger (Videos & Fotos)")
self.resize(1250, 780)
self.setAcceptDrops(True)
self.is_syncing_combos = False
self.setup_ui()
self.statusBar().showMessage("Bereit.")
def setup_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# --- Konfigurationsbereich ---
config_group = QGroupBox("Konfiguration")
config_layout = QGridLayout(config_group)
lbl_dir = QLabel("Zielordner:")
self.input_dir = QLineEdit(DEFAULT_VIDEO_DIR)
self.btn_browse = QPushButton("📁 Durchsuchen...")
self.btn_browse.clicked.connect(self.browse_folder)
config_layout.addWidget(lbl_dir, 0, 0)
config_layout.addWidget(self.input_dir, 0, 1)
config_layout.addWidget(self.btn_browse, 0, 2)
lbl_exclude = QLabel("Ausnahmen (kommagetrennt):")
self.input_exclude = QLineEdit(DEFAULT_EXCLUDES)
config_layout.addWidget(lbl_exclude, 1, 0)
config_layout.addWidget(self.input_exclude, 1, 1, 1, 2)
main_layout.addWidget(config_group)
# --- Scan & Import Buttons ---
action_bar_layout = QHBoxLayout()
self.btn_scan = QPushButton("1. Ordner scannen")
self.btn_scan.setMinimumHeight(35)
self.btn_scan.clicked.connect(self.start_scan)
self.btn_add_manual = QPushButton("➕ Dateien hinzufügen...")
self.btn_add_manual.setMinimumHeight(35)
self.btn_add_manual.clicked.connect(self.browse_manual_files)
# NEUER BUTTON: Ganzen Ordner inkl. Unterordner hinzufügen
self.btn_add_folder = QPushButton("📁 Ordner hinzufügen (rekursiv)...")
self.btn_add_folder.setMinimumHeight(35)
self.btn_add_folder.clicked.connect(self.browse_manual_folder)
self.btn_clear = QPushButton("🗑️ Liste leeren")
self.btn_clear.setMinimumHeight(35)
self.btn_clear.clicked.connect(self.clear_table)
self.progress_bar = QProgressBar()
self.progress_bar.setMinimumHeight(25)
self.progress_bar.setTextVisible(True)
self.progress_bar.setValue(0)
action_bar_layout.addWidget(self.btn_scan, 1)
action_bar_layout.addWidget(self.btn_add_manual, 1)
action_bar_layout.addWidget(self.btn_add_folder, 1)
action_bar_layout.addWidget(self.btn_clear, 1)
action_bar_layout.addWidget(self.progress_bar, 2)
main_layout.addLayout(action_bar_layout)
# --- Massenauswahl-Leiste ---
mass_group = QGroupBox("Massenbearbeitung")
mass_layout = QHBoxLayout(mass_group)
self.btn_select_all = QPushButton("☑ Alle auswählen")
self.btn_select_all.clicked.connect(lambda: self.set_all_checks(True))
self.btn_deselect_all = QPushButton("☐ Keine auswählen")
self.btn_deselect_all.clicked.connect(lambda: self.set_all_checks(False))
lbl_mass = QLabel("Für markierte Zeilen Tag setzen:")
self.combo_mass = QComboBox()
self.combo_mass.addItems(["Ignorieren", "Edit", "Trim", "Lightroom"])
self.combo_mass.setCurrentText("Edit")
self.btn_apply_mass = QPushButton("Auf Markierte anwenden")
self.btn_apply_mass.clicked.connect(self.apply_mass_tag)
mass_layout.addWidget(self.btn_select_all)
mass_layout.addWidget(self.btn_deselect_all)
mass_layout.addSpacing(25)
mass_layout.addWidget(lbl_mass)
mass_layout.addWidget(self.combo_mass)
mass_layout.addWidget(self.btn_apply_mass)
mass_layout.addStretch()
main_layout.addWidget(mass_group)
# --- Tabelle ---
self.table = QTableWidget(0, 5)
self.table.setHorizontalHeaderLabels([
"✓",
"Dateiname (Klick zum Öffnen)",
"Erkanntes Programm",
"Vorgeschlagen",
"Aktion",
])
header = self.table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Interactive)
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Interactive)
header.setSectionResizeMode(4, QHeaderView.ResizeMode.Interactive)
self.table.setColumnWidth(0, 35)
self.table.setColumnWidth(2, 170)
self.table.setColumnWidth(3, 110)
self.table.setColumnWidth(4, 140)
self.table.cellClicked.connect(self.on_cell_clicked)
self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.table.customContextMenuRequested.connect(self.open_context_menu)
main_layout.addWidget(self.table)
# --- Footer ---
bottom_layout = QHBoxLayout()
self.radio_sidecar = QRadioButton("XMP Sidecar erzeugen (Empfohlen)")
self.radio_sidecar.setChecked(True)
self.radio_embedded = QRadioButton(
"Direkt in Datei schreiben (Embedded)"
)
self.btn_apply = QPushButton("2. Tags anwenden")
self.btn_apply.setMinimumHeight(35)
self.btn_apply.setEnabled(False)
self.btn_apply.clicked.connect(self.start_apply)
bottom_layout.addWidget(self.radio_sidecar)
bottom_layout.addWidget(self.radio_embedded)
bottom_layout.addStretch()
bottom_layout.addWidget(self.btn_apply)
main_layout.addLayout(bottom_layout)
# --- Tabelle komplett leeren ---
def clear_table(self):
if self.table.rowCount() == 0:
self.statusBar().showMessage("Die Liste ist bereits leer.")
return
reply = QMessageBox.question(
self,
"Liste leeren",
f"Möchtest du wirklich alle {self.table.rowCount()} Einträge aus der Tabelle entfernen?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
self.table.setRowCount(0)
self.btn_apply.setEnabled(False)
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
self.progress_bar.setFormat("")
self.statusBar().showMessage("Tabelle wurde geleert.")
# --- Zeile in Tabelle einfügen ---
def insert_table_row(
self, file_path, program, suggested, default_action="Ignorieren"
):
for r in range(self.table.rowCount()):
existing_item = self.table.item(r, 1)
if (
existing_item
and existing_item.data(Qt.ItemDataRole.UserRole) == file_path
):
return False
row = self.table.rowCount()
self.table.insertRow(row)
# 0. Checkbox-Zelle
check_item = QTableWidgetItem()
check_item.setFlags(
Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsEnabled
)
check_item.setCheckState(Qt.CheckState.Unchecked)
self.table.setItem(row, 0, check_item)
# 1. Dateiname
short_name = os.path.basename(file_path)
parent_dir = os.path.basename(os.path.dirname(file_path))
display_name = f"{parent_dir}/{short_name}"
link_font = QFont()
link_font.setUnderline(True)
link_color = QColor("#0066cc")
item_name = QTableWidgetItem(f"▶ {display_name}")
item_name.setFont(link_font)
item_name.setForeground(link_color)
item_name.setToolTip(
f"Klicken zum Öffnen:\n{file_path}\n(Rechtsklick für Ordner)"
)
item_name.setData(Qt.ItemDataRole.UserRole, file_path)
item_name.setFlags(item_name.flags() & ~Qt.ItemFlag.ItemIsEditable)
self.table.setItem(row, 1, item_name)
# 2. Programm
item_prog = QTableWidgetItem(program)
item_prog.setFlags(item_prog.flags() & ~Qt.ItemFlag.ItemIsEditable)
self.table.setItem(row, 2, item_prog)
# 3. Vorgeschlagen
item_sugg = QTableWidgetItem(suggested)
item_sugg.setFlags(item_sugg.flags() & ~Qt.ItemFlag.ItemIsEditable)
self.table.setItem(row, 3, item_sugg)
# 4. Aktion Dropdown
combo = QComboBox()
combo.addItems(["Ignorieren", "Edit", "Trim", "Lightroom"])
combo.setCurrentText(default_action)
combo.currentTextChanged.connect(
lambda text, c=combo: self.on_row_combo_changed(c, text)
)
self.table.setCellWidget(row, 4, combo)
return True
def on_row_combo_changed(self, trigger_combo, new_text):
if self.is_syncing_combos:
return
trigger_row = -1
for r in range(self.table.rowCount()):
if self.table.cellWidget(r, 4) == trigger_combo:
trigger_row = r
break
if trigger_row == -1:
return
check_item = self.table.item(trigger_row, 0)
if check_item and check_item.checkState() == Qt.CheckState.Checked:
self.is_syncing_combos = True
for r in range(self.table.rowCount()):
c_item = self.table.item(r, 0)
if (
c_item
and c_item.checkState() == Qt.CheckState.Checked
and r != trigger_row
):
other_combo = self.table.cellWidget(r, 4)
if other_combo:
other_combo.setCurrentText(new_text)
self.is_syncing_combos = False
def set_all_checks(self, state: bool):
target_state = Qt.CheckState.Checked if state else Qt.CheckState.Unchecked
for r in range(self.table.rowCount()):
item = self.table.item(r, 0)
if item:
item.setCheckState(target_state)
def apply_mass_tag(self):
new_tag = self.combo_mass.currentText()
count = 0
self.is_syncing_combos = True
for r in range(self.table.rowCount()):
check_item = self.table.item(r, 0)
if check_item and check_item.checkState() == Qt.CheckState.Checked:
combo = self.table.cellWidget(r, 4)
if combo:
combo.setCurrentText(new_tag)
count += 1
self.is_syncing_combos = False
self.statusBar().showMessage(
f"Tag '{new_tag}' auf {count} markierte Dateien angewendet."
)
# --- Einzelne Dateien manuell auswählen ---
def browse_manual_files(self):
ext_filter = " ".join([f"*.{e}" for e in SUPPORTED_EXTENSIONS])
files, _ = QFileDialog.getOpenFileNames(
self,
"Medien manuell auswählen",
self.input_dir.text(),
f"Unterstützte Medien ({ext_filter});;Alle Dateien (*.*)",
)
if files:
self.table.setUpdatesEnabled(False)
added_count = 0
for f in files:
norm_p = os.path.normpath(f)
if self.insert_table_row(
norm_p, "Manuell hinzugefügt", "Edit", default_action="Edit"
):
added_count += 1
self.table.setUpdatesEnabled(True)
self.btn_apply.setEnabled(self.table.rowCount() > 0)
self.statusBar().showMessage(
f"{added_count} Datei(en) manuell zur Tabelle hinzugefügt."
)
# --- Ganzen Ordner rekursiv (inkl. aller Unterordner) hinzufügen ---
def browse_manual_folder(self):
folder = QFileDialog.getExistingDirectory(
self, "Ordner auswählen (inkl. Unterordner)", self.input_dir.text()
)
if folder:
self.statusBar().showMessage(f"Durchsuche Ordner '{folder}'...")
valid_exts = tuple(f".{e}" for e in SUPPORTED_EXTENSIONS)
matched_files = []
# Rekursiver Scan durch alle Unterverzeichnisse
for root, _, files in os.walk(folder):
for f in files:
if f.lower().endswith(valid_exts):
matched_files.append(os.path.normpath(os.path.join(root, f)))
self.table.setUpdatesEnabled(False)
added_count = 0
for p in matched_files:
if self.insert_table_row(
p, "Ordner-Import", "Edit", default_action="Edit"
):
added_count += 1
self.table.setUpdatesEnabled(True)
self.btn_apply.setEnabled(self.table.rowCount() > 0)
self.statusBar().showMessage(
f"{added_count} Datei(en) aus Ordnerstruktur hinzugefügt."
)
# --- Drag & Drop Unterstützung (Dateien und ganze Ordner) ---
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.acceptProposedAction()
def dropEvent(self, event):
paths = []
valid_exts = tuple(f".{e}" for e in SUPPORTED_EXTENSIONS)
for url in event.mimeData().urls():
p = url.toLocalFile()
if os.path.isfile(p) and p.lower().endswith(valid_exts):
paths.append(os.path.normpath(p))
elif os.path.isdir(p):
# Ganze gedroppte Ordner rekursiv durchsuchen
for root, _, files in os.walk(p):
for f in files:
if f.lower().endswith(valid_exts):
paths.append(os.path.normpath(os.path.join(root, f)))
if paths:
self.table.setUpdatesEnabled(False)
added_count = 0
for p in paths:
if self.insert_table_row(
p, "Drag & Drop", "Edit", default_action="Edit"
):
added_count += 1
self.table.setUpdatesEnabled(True)
self.btn_apply.setEnabled(self.table.rowCount() > 0)
self.statusBar().showMessage(
f"{added_count} Datei(en) per Drag & Drop hinzugefügt."
)
def browse_folder(self):
folder = QFileDialog.getExistingDirectory(
self, "Zielordner auswählen", self.input_dir.text()
)
if folder:
self.input_dir.setText(os.path.normpath(folder))
def start_scan(self):
video_dir = self.input_dir.text().strip()
if not os.path.exists(video_dir):
QMessageBox.warning(
self,
"Fehler",
f"Der Ordner '{video_dir}' existiert nicht oder ist nicht"
" erreichbar.",
)
return
raw_excludes = self.input_exclude.text().split(",")
exclude_dirs = [x.strip() for x in raw_excludes if x.strip()]
self.btn_scan.setEnabled(False)
self.btn_apply.setEnabled(False)
self.table.setRowCount(0)
self.progress_bar.setRange(0, 0)
self.progress_bar.setFormat("Scanne Medien...")
self.scan_thread = ScanThread(video_dir, exclude_dirs)
self.scan_thread.status_signal.connect(self.statusBar().showMessage)
self.scan_thread.error_signal.connect(self.show_error)
self.scan_thread.finished_signal.connect(self.populate_table)
self.scan_thread.start()
def show_error(self, error_msg):
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
self.progress_bar.setFormat("Fehler aufgetreten")
self.btn_scan.setEnabled(True)
self.statusBar().showMessage("Fehler beim Scannen!")
QMessageBox.critical(self, "Scan Fehler", error_msg)
def populate_table(self, files):
self.statusBar().showMessage(
f"Scan abgeschlossen: {len(files)} Treffer geladen."
)
self.table.setUpdatesEnabled(False)
for f in files:
self.insert_table_row(
f["File"],
f["Program"],
f["Suggested_Tag"],
default_action="Ignorieren",
)
self.table.setUpdatesEnabled(True)
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(100)
self.progress_bar.setFormat(f"Scan fertig ({len(files)} Treffer)")
self.btn_scan.setEnabled(True)
if self.table.rowCount() > 0:
self.btn_apply.setEnabled(True)
def on_cell_clicked(self, row, column):
if column == 1:
item = self.table.item(row, 1)
if item:
file_path = item.data(Qt.ItemDataRole.UserRole)
if file_path and os.path.exists(file_path):
self.statusBar().showMessage(
f"Öffne Datei: {os.path.basename(file_path)}..."
)
try:
os.startfile(file_path)
except Exception as e:
QMessageBox.warning(
self, "Fehler", f"Konnte Datei nicht öffnen:\n{e}"
)
else:
QMessageBox.warning(
self, "Nicht gefunden", "Datei ist nicht mehr erreichbar."
)
def open_context_menu(self, position):
row = self.table.rowAt(position.y())
if row >= 0:
item = self.table.item(row, 1)
if item:
file_path = item.data(Qt.ItemDataRole.UserRole)
menu = QMenu()
action_play = menu.addAction("▶ Datei öffnen")
action_explorer = menu.addAction("📁 Im Explorer anzeigen")
action = menu.exec(self.table.viewport().mapToGlobal(position))
if action == action_play:
os.startfile(file_path)
elif action == action_explorer:
subprocess.Popen(f'explorer /select,"{file_path}"')
def start_apply(self):
tasks = []
for row in range(self.table.rowCount()):
file_path = self.table.item(row, 1).data(Qt.ItemDataRole.UserRole)
combo = self.table.cellWidget(row, 4)
tag = combo.currentText()
if tag != "Ignorieren":
tasks.append((file_path, tag))
if not tasks:
QMessageBox.information(
self,
"Hinweis",
"Alle Dateien stehen auf 'Ignorieren'. Nichts zu tun.",
)
return
self.btn_scan.setEnabled(False)
self.btn_apply.setEnabled(False)
self.progress_bar.setRange(0, len(tasks))
self.progress_bar.setValue(0)
self.progress_bar.setFormat("Schreibe Tags: %v / %m (%p%)")
mode = "SIDECAR" if self.radio_sidecar.isChecked() else "EMBEDDED"
self.apply_thread = ApplyThread(tasks, mode)
self.apply_thread.status_signal.connect(self.statusBar().showMessage)
self.apply_thread.progress_signal.connect(self.update_apply_progress)
self.apply_thread.finished_signal.connect(self.finish_apply)
self.apply_thread.start()
def update_apply_progress(self, current, total):
self.progress_bar.setValue(current)
def finish_apply(self, total):
self.statusBar().showMessage(
f"Fertig! {total} Dateien erfolgreich getaggt."
)
self.progress_bar.setValue(total)
self.progress_bar.setFormat("Abgeschlossen")
self.btn_scan.setEnabled(True)
self.btn_apply.setEnabled(True)
QMessageBox.information(
self, "Abgeschlossen", f"Es wurden {total} Dateien verarbeitet."
)
if __name__ == "__main__":
app = QApplication(sys.argv)
if not EXIFTOOL_PATH:
QMessageBox.critical(
None,
"Fehler",
f"'exiftool.exe' wurde im Ordner {SCRIPT_DIR} nicht gefunden!",
)
sys.exit(1)
window = EditFinderApp()
window.show()
sys.exit(app.exec())