From a824a01579e173e173520ca2e7f6c84aca1372ca Mon Sep 17 00:00:00 2001 From: Ignacio Serantes Date: Sun, 19 Apr 2026 17:39:01 +0200 Subject: [PATCH] v0.9.24 --- bagheeraview.py | 2 +- constants.py | 31 +++++++++++++++++++------------ imagecontroller.py | 6 +++--- imagescanner.py | 8 ++++---- imageviewer.py | 27 +++++++++++++++++++++------ metadatamanager.py | 36 +++++++++++++++++++++++++++++++++++- propertiesdialog.py | 34 ++++++++++++++++++++++------------ pyproject.toml | 2 +- setup.py | 2 +- xmpmanager.py | 13 +++++++++++-- 10 files changed, 118 insertions(+), 43 deletions(-) diff --git a/bagheeraview.py b/bagheeraview.py index b81ee72..6b4e33d 100755 --- a/bagheeraview.py +++ b/bagheeraview.py @@ -14,7 +14,7 @@ Classes: MainWindow: The main application window containing the thumbnail grid and docks. """ __appname__ = "BagheeraView" -__version__ = "0.9.23" +__version__ = "0.9.24" __author__ = "Ignacio Serantes" __email__ = "kde@aynoa.net" __license__ = "LGPL" diff --git a/constants.py b/constants.py index d3a1f14..51b4364 100644 --- a/constants.py +++ b/constants.py @@ -29,7 +29,7 @@ if FORCE_X11: # --- CONFIGURATION --- PROG_NAME = "Bagheera Image Viewer" PROG_ID = "bagheeraview" -PROG_VERSION = "0.9.23" +PROG_VERSION = "0.9.24" PROG_AUTHOR = "Ignacio Serantes" # --- CACHE SETTINGS --- @@ -57,17 +57,21 @@ DISK_CACHE_MAX_BYTES = 10 * 1024 * 1024 * 1024 # --- PATHS --- CONFIG_FILE = f"{PROG_ID}rc" -CONFIG_LOCATION = '.config/iserantes' -CONFIG_DIR = os.path.join(os.path.expanduser("~"), CONFIG_LOCATION, PROG_ID) +CONFIG_LOCATION = os.environ.get('XDG_CONFIG_HOME') +CONFIG_DIR = os.path.join(CONFIG_LOCATION, 'iserantes', PROG_ID) CONFIG_PATH = os.path.join(CONFIG_DIR, CONFIG_FILE) -CACHE_PATH = os.path.join(CONFIG_DIR, "thumbnails") + +APP_DATA_LOCATION = os.path.expanduser('~/.local/share') +APP_DATA_DIR = os.path.join(APP_DATA_LOCATION, 'iserantes', PROG_ID) + +CACHE_PATH = os.path.join(APP_DATA_DIR, "thumbnails") HISTORY_FILE = "history.json" -HISTORY_PATH = os.path.join(CONFIG_DIR, HISTORY_FILE) -LAYOUTS_DIR = os.path.join(CONFIG_DIR, "layouts") # Layouts saving directory +HISTORY_PATH = os.path.join(APP_DATA_DIR, HISTORY_FILE) +LAYOUTS_DIR = os.path.join(APP_DATA_DIR, "layouts") # Layouts saving directory FAVORITES_FILE = "favorites.json" -FAVORITES_PATH = os.path.join(CONFIG_DIR, FAVORITES_FILE) -DUPLICATE_CACHE_PATH = os.path.join(CONFIG_DIR, "duplicates") +FAVORITES_PATH = os.path.join(APP_DATA_DIR, FAVORITES_FILE) +DUPLICATE_CACHE_PATH = os.path.join(APP_DATA_DIR, "duplicates") DUPLICATE_HASH_DB_NAME = b"hashes" DUPLICATE_EXCEPTIONS_DB_NAME = b"exceptions" DUPLICATE_PENDING_DB_NAME = b"pending" @@ -204,15 +208,15 @@ if importlib.util.find_spec("mediapipe") is not None: pass HAVE_FACE_RECOGNITION = importlib.util.find_spec("face_recognition") is not None -MEDIAPIPE_FACE_MODEL_PATH = os.path.join(CONFIG_DIR, - "blaze_face_short_range.tflite") +MEDIAPIPE_FACE_MODEL_PATH = os.path.join( + APP_DATA_DIR, "blaze_face_short_range.tflite") MEDIAPIPE_FACE_MODEL_URL = ( "https://storage.googleapis.com/mediapipe-models/face_detector/" "blaze_face_short_range/float16/1/blaze_face_short_range.tflite" ) -MEDIAPIPE_OBJECT_MODEL_PATH = os.path.join(CONFIG_DIR, - "efficientdet_lite0.tflite") +MEDIAPIPE_OBJECT_MODEL_PATH = os.path.join( + APP_DATA_DIR, "efficientdet_lite0.tflite") MEDIAPIPE_OBJECT_MODEL_URL = ( "https://storage.googleapis.com/mediapipe-models/object_detector/" "efficientdet_lite0/float16/1/efficientdet_lite0.tflite" @@ -782,6 +786,7 @@ _UI_TEXTS = { "RENAME_ERROR_EXISTS": "File '{}' already exists.", "FILE_RENAMED": "File renamed to {}", "ERROR_RENAME": "Could not rename file: {}", + "ERROR_JPEG_METADATA_LIMIT": "Metadata size limit exceeded for '{}'. This JPEG file has too much existing metadata (XMP) to save more.", "MAIN_DOCK_TITLE": "", "LAYOUTS_TAB": "Layouts", "LAYOUTS_TABLE_HEADER": ["Name", "Last Modified"], @@ -1333,6 +1338,7 @@ _UI_TEXTS = { "RENAME_ERROR_EXISTS": "El archivo '{}' ya existe.", "FILE_RENAMED": "Archivo renombrado a {}", "ERROR_RENAME": "No se pudo renombrar el archivo: {}", + "ERROR_JPEG_METADATA_LIMIT": "Límite de metadatos excedido para '{}'. Este archivo JPEG ya tiene demasiados metadatos (XMP) para guardar más.", "MAIN_DOCK_TITLE": "Panel principal", "LAYOUTS_TAB": "Diseños", "LAYOUTS_TABLE_HEADER": ["Nombre", "Última Modificación"], @@ -1885,6 +1891,7 @@ _UI_TEXTS = { "RENAME_ERROR_EXISTS": "O ficheiro '{}' xa existe.", "FILE_RENAMED": "Ficheiro renomeado a {}", "ERROR_RENAME": "Non se puido renomear o ficheiro: {}", + "ERROR_JPEG_METADATA_LIMIT": "Límite de metadatos excedido para '{}'. Este ficheiro JPEG xa ten demasiados metadatos (XMP) para gardar máis.", "MAIN_DOCK_TITLE": "Panel principal", "LAYOUTS_TAB": "Deseños", "LAYOUTS_TABLE_HEADER": ["Nome", "Última Modificación"], diff --git a/imagecontroller.py b/imagecontroller.py index 034b225..3268321 100644 --- a/imagecontroller.py +++ b/imagecontroller.py @@ -345,7 +345,7 @@ class ImageController(QObject): faces_to_save.append(face_copy) - XmpManager.save_faces(path, faces_to_save) + return XmpManager.save_faces(path, faces_to_save) def add_face(self, name, x, y, w, h, region_type="Face"): """Adds a new face. The full tag path should be passed as 'name'.""" @@ -390,8 +390,8 @@ class ImageController(QObject): self.metadata_changed.emit(current_path, {'tags': new_tags_list, 'rating': self._current_rating}) - except IOError as e: - print(f"Error setting tags for {current_path}: {e}") + except Exception: + raise def set_rating(self, new_rating): current_path = self.get_current_path() diff --git a/imagescanner.py b/imagescanner.py index 192f210..e76b393 100644 --- a/imagescanner.py +++ b/imagescanner.py @@ -36,9 +36,9 @@ from PySide6.QtGui import QImage, QImageReader, QImageIOHandler, QIcon from PySide6.QtWidgets import QApplication from constants import ( - APP_CONFIG, CACHE_PATH, CACHE_MAX_SIZE, CACHE_MAX_RAM_BYTES, CONFIG_DIR, - MIN_FREE_RAM_PERCENT, DISK_CACHE_MAX_BYTES, HAVE_BAGHEERASEARCH_LIB, - IMAGE_EXTENSIONS, + APP_CONFIG, CACHE_PATH, CACHE_MAX_SIZE, CACHE_MAX_RAM_BYTES, + APP_DATA_DIR, MIN_FREE_RAM_PERCENT, DISK_CACHE_MAX_BYTES, + HAVE_BAGHEERASEARCH_LIB, IMAGE_EXTENSIONS, SCANNER_SETTINGS_DEFAULTS, SEARCH_CMD, THUMBNAIL_SIZES, UITexts ) @@ -543,7 +543,7 @@ class ThumbnailCache(QObject): def lmdb_open(self): # Initialize LMDB environment - cache_dir = Path(CONFIG_DIR) + cache_dir = Path(APP_DATA_DIR) cache_dir.mkdir(parents=True, exist_ok=True) try: diff --git a/imageviewer.py b/imageviewer.py index 409bcc6..4015d30 100644 --- a/imageviewer.py +++ b/imageviewer.py @@ -239,7 +239,10 @@ class FastTagManager: current_path = controller.get_current_path() if controller else None if not current_path: return - controller.toggle_tag(tag_name, is_checked) + try: + controller.toggle_tag(tag_name, is_checked) + except Exception as e: + QMessageBox.critical(self.viewer, UITexts.ERROR, str(e)) self.viewer.update_status_bar() if self.main_win: if is_checked: @@ -2836,8 +2839,11 @@ class ImageViewer(QWidget): self.main_win.face_names_history = updated_history # Save changes and add new tag - self.controller.save_faces() - self.controller.toggle_tag(new_full_tag, True) + try: + self.controller.save_faces() + self.controller.toggle_tag(new_full_tag, True) + except Exception as e: + QMessageBox.critical(self, UITexts.ERROR, str(e)) if self.canvas: self.canvas.update() @@ -3137,7 +3143,10 @@ class ImageViewer(QWidget): self.canvas.update() if added_count > 0: - self.controller.save_faces() + try: + self.controller.save_faces() + except Exception as e: + QMessageBox.critical(self, UITexts.ERROR, str(e)) def run_pet_detection(self): """Runs pet detection on the current image.""" @@ -3196,7 +3205,10 @@ class ImageViewer(QWidget): self.canvas.update() if added_count > 0: - self.controller.save_faces() + try: + self.controller.save_faces() + except Exception as e: + QMessageBox.critical(self, UITexts.ERROR, str(e)) def run_body_detection(self): """Runs body detection on the current image.""" @@ -3257,7 +3269,10 @@ class ImageViewer(QWidget): self.canvas.update() if added_count > 0: - self.controller.save_faces() + try: + self.controller.save_faces() + except Exception as e: + QMessageBox.critical(self, UITexts.ERROR, str(e)) def toggle_filmstrip(self): """Shows or hides the filmstrip widget.""" diff --git a/metadatamanager.py b/metadatamanager.py index 19eda02..adf2277 100644 --- a/metadatamanager.py +++ b/metadatamanager.py @@ -9,6 +9,7 @@ Classes: """ import os import collections +import logging from PySide6.QtDBus import QDBusConnection, QDBusMessage, QDBus try: import exiv2 @@ -21,6 +22,8 @@ except ImportError: from utils import preserve_mtime from constants import RATING_XATTR_NAME, XATTR_NAME +logger = logging.getLogger(__name__) + _app_modified_callback = None MetadataResult = collections.namedtuple('MetadataResult', ['tags', 'rating']) @@ -141,6 +144,31 @@ class MetadataManager: iptc = image.iptcData() xmp = image.xmpData() + # Remove keys that are no longer in the dictionary + containers = [ + (exif, exiv2.ExifKey, "Exif."), + (iptc, exiv2.IptcKey, "Iptc."), + (xmp, exiv2.XmpKey, "Xmp.") + ] + + for container, key_class, prefix in containers: + keys_to_remove = [] + for datum in container: + k = datum.key() + # Only consider keys belonging to this specific container + if k.startswith(prefix) and k not in metadata_dict: + keys_to_remove.append(k) + + for key in keys_to_remove: + try: + x_key = key_class(key) + it = container.findKey(x_key) + if it != container.end(): + container.erase(it) + except Exception as e: + print(f"Error removing metadata key {key}: {e}") + + # Set or update values from the dictionary for key, value in metadata_dict.items(): try: if key.startswith("Exif."): @@ -156,7 +184,13 @@ class MetadataManager: notify_baloo(path) mark_app_modified(path) except Exception as e: - print(f"Error writing metadata for {path}: {e}") + error_msg = str(e) + if "kerTooLargeJpegSegment" in error_msg or "38" in error_msg: + msg = UITexts.ERROR_JPEG_METADATA_LIMIT.format(os.path.basename(path)) + logger.error(msg) + raise IOError(msg) from e + logger.error(f"Error writing metadata for {path}: {e}") + raise class XattrManager: diff --git a/propertiesdialog.py b/propertiesdialog.py index efe80c9..0f4d834 100644 --- a/propertiesdialog.py +++ b/propertiesdialog.py @@ -139,8 +139,9 @@ class PropertiesDialog(QDialog): meta_layout = QVBoxLayout(meta_widget) self.meta_toolbar = QToolBar() - self._setup_table_toolbar(self.meta_toolbar, self.on_add_meta, self.on_delete_meta, - self.on_delete_all_meta, self.on_save_meta, self.on_cancel_meta) + self._setup_table_toolbar( + self.meta_toolbar, self.on_add_meta, self.on_delete_meta, + self.on_delete_all_meta, self.on_save_meta, self.on_cancel_meta) meta_layout.addWidget(self.meta_toolbar) self.table = QTableWidget() @@ -172,8 +173,9 @@ class PropertiesDialog(QDialog): exif_layout = QVBoxLayout(exif_widget) self.exif_toolbar = QToolBar() - self._setup_table_toolbar(self.exif_toolbar, self.on_add_exif, self.on_delete_exif, - self.on_delete_all_exif, self.on_save_exif, self.on_cancel_exif) + self._setup_table_toolbar( + self.exif_toolbar, self.on_add_exif, self.on_delete_exif, + self.on_delete_all_exif, self.on_save_exif, self.on_cancel_exif) exif_layout.addWidget(self.exif_toolbar) self.exif_table = QTableWidget() @@ -219,17 +221,21 @@ class PropertiesDialog(QDialog): # Start background loading self.reload_metadata() - def _setup_table_toolbar(self, toolbar, add_slot, del_slot, del_all_slot, save_slot, cancel_slot): + def _setup_table_toolbar(self, toolbar, add_slot, del_slot, del_all_slot, save_slot, + cancel_slot): """Helper to populate toolbars with buttons.""" toolbar.addAction(QIcon.fromTheme("list-add"), UITexts.CREATE, add_slot) toolbar.addAction(QIcon.fromTheme("list-remove"), UITexts.DELETE, del_slot) - toolbar.addAction(QIcon.fromTheme("edit-clear-all"), UITexts.PROPERTIES_DELETE_ALL, del_all_slot) + toolbar.addAction( + QIcon.fromTheme("edit-clear-all"), UITexts.PROPERTIES_DELETE_ALL, + del_all_slot) toolbar.addSeparator() toolbar.addAction(QIcon.fromTheme("document-save"), UITexts.SAVE, save_slot) toolbar.addAction(QIcon.fromTheme("edit-undo"), UITexts.CANCEL, cancel_slot) def on_add_meta(self): - key, ok = QInputDialog.getText(self, UITexts.PROPERTIES_ADD_ATTR, UITexts.PROPERTIES_ADD_ATTR_NAME) + key, ok = QInputDialog.getText( + self, UITexts.PROPERTIES_ADD_ATTR, UITexts.PROPERTIES_ADD_ATTR_NAME) if ok and key: row = self.table.rowCount() self.table.insertRow(row) @@ -240,7 +246,8 @@ class PropertiesDialog(QDialog): self.table.editItem(v_item) def on_delete_meta(self): - rows = sorted(set(index.row() for index in self.table.selectedIndexes()), reverse=True) + rows = sorted(set(index.row() for index in self.table.selectedIndexes()), + reverse=True) for row in rows: self.table.removeRow(row) @@ -261,13 +268,14 @@ class PropertiesDialog(QDialog): XattrManager.set_attribute(self.path, k, v) self.reload_metadata() except Exception as e: - QMessageBox.warning(self, UITexts.ERROR, f"Error: {e}") + QMessageBox.critical(self, UITexts.ERROR, str(e)) def on_cancel_meta(self): self.update_metadata_table(self.original_xattrs) def on_add_exif(self): - key, ok = QInputDialog.getText(self, UITexts.PROPERTIES_ADD_ATTR, UITexts.PROPERTIES_ADD_ATTR_NAME) + key, ok = QInputDialog.getText( + self, UITexts.PROPERTIES_ADD_ATTR, UITexts.PROPERTIES_ADD_ATTR_NAME) if ok and key: row = self.exif_table.rowCount() self.exif_table.insertRow(row) @@ -278,7 +286,9 @@ class PropertiesDialog(QDialog): self.exif_table.editItem(v_item) def on_delete_exif(self): - rows = sorted(set(index.row() for index in self.exif_table.selectedIndexes()), reverse=True) + rows = sorted( + set(index.row() for index in self.exif_table.selectedIndexes()), + reverse=True) for row in rows: self.exif_table.removeRow(row) @@ -295,7 +305,7 @@ class PropertiesDialog(QDialog): MetadataManager.write_metadata(self.path, new_exif) self.reload_metadata() except Exception as e: - QMessageBox.warning(self, UITexts.ERROR, f"Error: {e}") + QMessageBox.critical(self, UITexts.ERROR, str(e)) def on_cancel_exif(self): self.update_exif_table(self.original_exif) diff --git a/pyproject.toml b/pyproject.toml index d34b2c0..26aec4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bagheeraview" -version = "0.9.23" +version = "0.9.24" authors = [ { name = "Ignacio Serantes" } ] diff --git a/setup.py b/setup.py index 2e70f4b..0e1768b 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="bagheeraview", - version="0.9.23", + version="0.9.24", author="Ignacio Serantes", description="Bagheera Image Viewer - An image viewer for KDE with Baloo in mind", long_description="A fast image viewer built with PySide6, featuring search and " diff --git a/xmpmanager.py b/xmpmanager.py index 2cbcc5f..e3725b5 100644 --- a/xmpmanager.py +++ b/xmpmanager.py @@ -17,13 +17,17 @@ Dependencies: """ import os import re +import logging from utils import preserve_mtime from metadatamanager import notify_baloo, mark_app_modified +from constants import UITexts try: import exiv2 except ImportError: exiv2 = None +logger = logging.getLogger(__name__) + class XmpManager: """ @@ -167,5 +171,10 @@ class XmpManager: mark_app_modified(path) return True except Exception as e: - print(f"Error saving faces to XMP: {e}") - return False + error_msg = str(e) + if "kerTooLargeJpegSegment" in error_msg or "38" in error_msg: + msg = UITexts.ERROR_JPEG_METADATA_LIMIT.format(os.path.basename(path)) + logger.error(msg) + raise IOError(msg) from e + logger.error(f"Error saving faces to XMP: {e}") + raise