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

823 lines
No EOL
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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