v0.9.24
This commit is contained in:
@@ -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"
|
||||
|
||||
31
constants.py
31
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"],
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -239,7 +239,10 @@ class FastTagManager:
|
||||
current_path = controller.get_current_path() if controller else None
|
||||
if not current_path:
|
||||
return
|
||||
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
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -139,7 +139,8 @@ 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._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)
|
||||
|
||||
@@ -172,7 +173,8 @@ 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._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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "bagheeraview"
|
||||
version = "0.9.23"
|
||||
version = "0.9.24"
|
||||
authors = [
|
||||
{ name = "Ignacio Serantes" }
|
||||
]
|
||||
|
||||
2
setup.py
2
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 "
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user