This commit is contained in:
Ignacio Serantes
2026-03-26 09:01:41 +01:00
parent dfddfd17b3
commit a7ce2ceb75
10 changed files with 178 additions and 113 deletions

View File

@@ -14,7 +14,7 @@ Classes:
MainWindow: The main application window containing the thumbnail grid and docks. MainWindow: The main application window containing the thumbnail grid and docks.
""" """
__appname__ = "BagheeraView" __appname__ = "BagheeraView"
__version__ = "0.9.13" __version__ = "0.9.14"
__author__ = "Ignacio Serantes" __author__ = "Ignacio Serantes"
__email__ = "kde@aynoa.net" __email__ = "kde@aynoa.net"
__license__ = "LGPL" __license__ = "LGPL"
@@ -77,7 +77,7 @@ from widgets import (
TagEditWidget, LayoutsWidget, HistoryWidget, RatingWidget, CommentWidget, TagEditWidget, LayoutsWidget, HistoryWidget, RatingWidget, CommentWidget,
FavoritesWidget FavoritesWidget
) )
from metadatamanager import XattrManager from metadatamanager import load_common_metadata
class ShortcutHelpDialog(QDialog): class ShortcutHelpDialog(QDialog):
@@ -626,19 +626,20 @@ class ThumbnailDelegate(QStyledItemDelegate):
# Optimization: Use QPixmapCache to avoid expensive QImage->QPixmap # Optimization: Use QPixmapCache to avoid expensive QImage->QPixmap
# conversion on every paint event. # conversion on every paint event.
cache_key = f"thumb_{path}_{mtime}_{thumb_size}" actual_tier = self.main_win.cache.get_available_tier(path, thumb_size, mtime)
cache_key = f"thumb_{path}_{mtime}_{thumb_size}_{actual_tier}"
source_pixmap = QPixmapCache.find(cache_key) source_pixmap = QPixmapCache.find(cache_key)
if not source_pixmap or source_pixmap.isNull(): if not source_pixmap or source_pixmap.isNull():
# Not in UI cache, try to get from main thumbnail cache (Memory/LMDB) # Not in UI cache, try to get from main thumbnail cache (Memory/LMDB)
inode = index.data(INODE_ROLE) inode = index.data(INODE_ROLE)
device_id = index.data(DEVICE_ROLE) device_id = index.data(DEVICE_ROLE)
img, _ = self.main_win.cache.get_thumbnail( res = self.main_win.cache.get_thumbnail(
path, requested_size=thumb_size, curr_mtime=mtime, path, requested_size=thumb_size, curr_mtime=mtime,
inode=inode, device_id=device_id, async_load=True) inode=inode, device_id=device_id, async_load=True)
if img and not img.isNull(): if res.image and not res.image.isNull():
source_pixmap = QPixmap.fromImage(img) source_pixmap = QPixmap.fromImage(res.image)
QPixmapCache.insert(cache_key, source_pixmap) QPixmapCache.insert(cache_key, source_pixmap)
else: else:
# Fallback: Check a separate cache key for the placeholder to avoid # Fallback: Check a separate cache key for the placeholder to avoid
@@ -3458,26 +3459,27 @@ class MainWindow(QMainWindow):
if not path: if not path:
return return
# Determine tags and rating based on provided metadata or disk read
if metadata:
tags = metadata.get('tags', [])
rating = metadata.get('rating', 0)
else:
res = load_common_metadata(path)
tags, rating = res.tags, res.rating
# Find the item in the source model and update its data # Find the item in the source model and update its data
for row in range(self.thumbnail_model.rowCount()): for row in range(self.thumbnail_model.rowCount()):
item = self.thumbnail_model.item(row) item = self.thumbnail_model.item(row)
if item and item.data(PATH_ROLE) == path: if item and item.data(PATH_ROLE) == path:
# Reload metadata for this item from xattr
try:
if metadata and 'tags' in metadata:
tags = metadata['tags']
else: # Fallback to reading from disk if not provided
raw = XattrManager.get_attribute(path, XATTR_NAME)
tags = sorted(list(set(t.strip() for t in raw.split(',')
if t.strip()))) if raw else []
item.setData(tags, TAGS_ROLE) item.setData(tags, TAGS_ROLE)
except Exception: item.setData(rating, RATING_ROLE)
item.setData([], TAGS_ROLE)
try: tooltip_text = f"{os.path.basename(path)}\n{path}"
item.setData(metadata.get('rating', 0) if tags:
if metadata else 0, RATING_ROLE) display_tags = [t.split('/')[-1] for t in tags]
except Exception: tooltip_text += f"\n{UITexts.TAGS_TAB}: {', '.join(
item.setData(0, RATING_ROLE) # Default to 0 if error display_tags)}"
item.setToolTip(tooltip_text)
# Notify the view that the data has changed # Notify the view that the data has changed
source_idx = self.thumbnail_model.indexFromItem(item) source_idx = self.thumbnail_model.indexFromItem(item)
@@ -3485,13 +3487,10 @@ class MainWindow(QMainWindow):
source_idx, source_idx, [TAGS_ROLE, RATING_ROLE]) source_idx, source_idx, [TAGS_ROLE, RATING_ROLE])
# Update internal data structure to prevent stale data on rebuild # Update internal data structure to prevent stale data on rebuild
current_tags = item.data(TAGS_ROLE) self._update_internal_data(path, tags=tags, rating=rating)
current_rating = item.data(RATING_ROLE)
self._update_internal_data(path, tags=current_tags,
rating=current_rating)
# Update proxy filter cache to prevent stale filtering # Update proxy filter cache to prevent stale filtering
self.proxy_model.add_to_cache(path, current_tags) self.proxy_model.add_to_cache(path, tags)
break break
if self.main_dock.isVisible(): if self.main_dock.isVisible():
@@ -3594,8 +3593,13 @@ class MainWindow(QMainWindow):
if not paths: if not paths:
return return
# Prioritize visible paths
visible = self.get_visible_image_paths()
visible_set = set(visible)
ordered_paths = visible + [p for p in paths if p not in visible_set]
self.thumbnail_generator = ThumbnailGenerator( self.thumbnail_generator = ThumbnailGenerator(
self.cache, paths, size, self.thread_pool_manager) self.cache, ordered_paths, size, self.thread_pool_manager)
self.thumbnail_generator.generation_complete.connect( self.thumbnail_generator.generation_complete.connect(
self.on_high_res_generation_finished) self.on_high_res_generation_finished)
self.thumbnail_generator.progress.connect( self.thumbnail_generator.progress.connect(

View File

@@ -62,6 +62,8 @@ Ahora que la carga es rápida, ¿cómo puedo implementar una precarga inteligent
¿Cómo puedo hacer que la selección de archivos sea persistente incluso después de recargar o filtrar la vista? ¿Cómo puedo hacer que la selección de archivos sea persistente incluso después de recargar o filtrar la vista?
v0.9.14 -
· Corregido el problema de resolución de los thumbnails
v0.9.13 - v0.9.13 -
· Añadida la opción de favoritos. · Añadida la opción de favoritos.

View File

@@ -29,7 +29,7 @@ if FORCE_X11:
# --- CONFIGURATION --- # --- CONFIGURATION ---
PROG_NAME = "Bagheera Image Viewer" PROG_NAME = "Bagheera Image Viewer"
PROG_ID = "bagheeraview" PROG_ID = "bagheeraview"
PROG_VERSION = "0.9.13" PROG_VERSION = "0.9.14"
PROG_AUTHOR = "Ignacio Serantes" PROG_AUTHOR = "Ignacio Serantes"
# --- CACHE SETTINGS --- # --- CACHE SETTINGS ---

View File

@@ -112,8 +112,8 @@ class ImagePreloader(QThread):
img = reader.read() img = reader.read()
if not img.isNull(): if not img.isNull():
# Load tags and rating here to avoid re-reading in main thread # Load tags and rating here to avoid re-reading in main thread
tags, rating = load_common_metadata(path) res = load_common_metadata(path)
self.image_ready.emit(idx, path, img, tags, rating) self.image_ready.emit(idx, path, img, res.tags, res.rating)
except Exception as e: except Exception as e:
logger.warning(f"ImagePreloader failed to load {path}: {e}") logger.warning(f"ImagePreloader failed to load {path}: {e}")
@@ -220,7 +220,8 @@ class ImageController(QObject):
if path and self._loaded_path == path and not self.pixmap_original.isNull(): if path and self._loaded_path == path and not self.pixmap_original.isNull():
# Ensure metadata is consistent with current path # Ensure metadata is consistent with current path
if self._current_metadata_path != path: if self._current_metadata_path != path:
self._current_tags, self._current_rating = load_common_metadata(path) res = load_common_metadata(path)
self._current_tags, self._current_rating = res.tags, res.rating
self._current_metadata_path = path self._current_metadata_path = path
self._trigger_preload() self._trigger_preload()
@@ -258,7 +259,8 @@ class ImageController(QObject):
# Load tags and rating if not already set for this path # Load tags and rating if not already set for this path
if self._current_metadata_path != path: if self._current_metadata_path != path:
self._current_tags, self._current_rating = load_common_metadata(path) res = load_common_metadata(path)
self._current_tags, self._current_rating = res.tags, res.rating
self._current_metadata_path = path self._current_metadata_path = path
self._loaded_path = path self._loaded_path = path
@@ -778,7 +780,8 @@ class ImageController(QObject):
# Reload from disk if not provided to ensure consistency # Reload from disk if not provided to ensure consistency
path = self.get_current_path() path = self.get_current_path()
if path: if path:
self._current_tags, self._current_rating = load_common_metadata(path) res = load_common_metadata(path)
self._current_tags, self._current_rating = res.tags, res.rating
self._current_metadata_path = path self._current_metadata_path = path
else: else:
self._current_tags = [] self._current_tags = []
@@ -810,7 +813,8 @@ class ImageController(QObject):
# Reload metadata for the current image to avoid stale/empty state # Reload metadata for the current image to avoid stale/empty state
path = self.get_current_path() path = self.get_current_path()
if path: if path:
self._current_tags, self._current_rating = load_common_metadata(path) res = load_common_metadata(path)
self._current_tags, self._current_rating = res.tags, res.rating
self._current_metadata_path = path self._current_metadata_path = path
else: else:
self._current_tags = [] self._current_tags = []

View File

@@ -36,13 +36,13 @@ from PySide6.QtGui import QImage, QImageReader, QImageIOHandler
from constants import ( from constants import (
APP_CONFIG, CACHE_PATH, CACHE_MAX_SIZE, CONFIG_DIR, DISK_CACHE_MAX_BYTES, APP_CONFIG, CACHE_PATH, CACHE_MAX_SIZE, CONFIG_DIR, DISK_CACHE_MAX_BYTES,
HAVE_BAGHEERASEARCH_LIB, IMAGE_EXTENSIONS, RATING_XATTR_NAME, HAVE_BAGHEERASEARCH_LIB, IMAGE_EXTENSIONS,
SCANNER_SETTINGS_DEFAULTS, SEARCH_CMD, THUMBNAIL_SIZES, XATTR_NAME, SCANNER_SETTINGS_DEFAULTS, SEARCH_CMD, THUMBNAIL_SIZES,
UITexts UITexts
) )
from imageviewer import ImageViewer from imageviewer import ImageViewer
from metadatamanager import XattrManager from metadatamanager import load_common_metadata
if HAVE_BAGHEERASEARCH_LIB: if HAVE_BAGHEERASEARCH_LIB:
from bagheera_search_lib import BagheeraSearcher from bagheera_search_lib import BagheeraSearcher
@@ -50,6 +50,10 @@ if HAVE_BAGHEERASEARCH_LIB:
# Set up logging for better debugging # Set up logging for better debugging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Result structure for thumbnail retrieval to ensure API stability
ThumbnailResult = collections.namedtuple('ThumbnailResult', ['image', 'mtime', 'tier'])
EMPTY_THUMBNAIL = ThumbnailResult(None, 0, 0)
class ThreadPoolManager: class ThreadPoolManager:
"""Manages a global QThreadPool to dynamically adjust thread count.""" """Manages a global QThreadPool to dynamically adjust thread count."""
@@ -151,11 +155,10 @@ class ScannerWorker(QRunnable):
return return
# Check if a valid thumbnail for this size exists # Check if a valid thumbnail for this size exists
thumb, mtime = self.cache.get_thumbnail(self.path, size, res = self.cache.get_thumbnail(
curr_mtime=curr_mtime, self.path, size, curr_mtime=curr_mtime,
inode=curr_inode, inode=curr_inode, device_id=curr_dev)
device_id=curr_dev) if not res.image or res.mtime != curr_mtime or res.tier != size:
if not thumb or mtime != curr_mtime:
# Use generation lock to prevent multiple threads generating # Use generation lock to prevent multiple threads generating
with self.cache.generation_lock( with self.cache.generation_lock(
self.path, size, curr_mtime, self.path, size, curr_mtime,
@@ -177,19 +180,20 @@ class ScannerWorker(QRunnable):
else: else:
# Another thread generated it, re-fetch # Another thread generated it, re-fetch
if size == min_size: if size == min_size:
re_thumb, _ = self.cache.get_thumbnail( re_res = self.cache.get_thumbnail(
self.path, size, curr_mtime=curr_mtime, self.path, size, curr_mtime=curr_mtime,
inode=curr_inode, device_id=curr_dev, inode=curr_inode, device_id=curr_dev,
async_load=False) async_load=False)
smallest_thumb_for_signal = re_thumb smallest_thumb_for_signal = re_res.image
elif size == min_size: elif size == min_size:
# valid thumb exists, use it for signal # valid thumb exists, use it for signal
smallest_thumb_for_signal = thumb smallest_thumb_for_signal = res.image
tags = [] tags = []
rating = 0 rating = 0
if self.load_metadata_flag: if self.load_metadata_flag:
tags, rating = self._load_metadata(fd) res_meta = load_common_metadata(fd)
tags, rating = res_meta.tags, res_meta.rating
self.result = (self.path, smallest_thumb_for_signal, self.result = (self.path, smallest_thumb_for_signal,
curr_mtime, tags, rating, curr_inode, curr_dev) curr_mtime, tags, rating, curr_inode, curr_dev)
except Exception as e: except Exception as e:
@@ -206,21 +210,6 @@ class ScannerWorker(QRunnable):
if self.semaphore: if self.semaphore:
self.semaphore.release() self.semaphore.release()
def _load_metadata(self, path_or_fd):
"""Loads tag and rating data for a path or file descriptor."""
tags = []
raw_tags = XattrManager.get_attribute(path_or_fd, XATTR_NAME)
if raw_tags:
tags = sorted(list(set(t.strip()
for t in raw_tags.split(',') if t.strip())))
raw_rating = XattrManager.get_attribute(path_or_fd, RATING_XATTR_NAME, "0")
try:
rating = int(raw_rating)
except ValueError:
rating = 0
return tags, rating
def generate_thumbnail(path, size, fd=None): def generate_thumbnail(path, size, fd=None):
"""Generates a QImage thumbnail for a given path and size.""" """Generates a QImage thumbnail for a given path and size."""
@@ -475,12 +464,12 @@ class CacheLoader(QThread):
# Call synchronous get_thumbnail to fetch and decode # Call synchronous get_thumbnail to fetch and decode
# This puts the result into the RAM cache # This puts the result into the RAM cache
img, _ = self.cache.get_thumbnail( res = self.cache.get_thumbnail(
path, size, curr_mtime=mtime, inode=inode, path, size, curr_mtime=mtime, inode=inode,
device_id=dev, async_load=False device_id=dev, async_load=False
) )
if img: if res.image:
self.cache.thumbnail_loaded.emit(path, size) self.cache.thumbnail_loaded.emit(path, size)
self.cache._mark_load_complete(path, size) self.cache._mark_load_complete(path, size)
@@ -722,7 +711,7 @@ class ThumbnailCache(QObject):
def _check_disk_cache(self, path, search_order, mtime, dev_id, inode_key): def _check_disk_cache(self, path, search_order, mtime, dev_id, inode_key):
"""Helper to check LMDB synchronously.""" """Helper to check LMDB synchronously."""
if not self._lmdb_env or not inode_key or dev_id == 0: if not self._lmdb_env or not inode_key or dev_id == 0:
return None, 0 return EMPTY_THUMBNAIL
txn = None txn = None
try: try:
@@ -754,35 +743,52 @@ class ThumbnailCache(QObject):
self._thumbnail_cache[path][size] = (img, mtime) self._thumbnail_cache[path][size] = (img, mtime)
self._cache_bytes_size += img.sizeInBytes() self._cache_bytes_size += img.sizeInBytes()
self._path_to_inode[path] = (dev_id, inode_key) self._path_to_inode[path] = (dev_id, inode_key)
return img, mtime return ThumbnailResult(img, mtime, size)
except Exception as e: except Exception as e:
logger.debug(f"Cache lookup error for {path}: {e}") logger.debug(f"Cache lookup error for {path}: {e}")
finally: finally:
if txn: if txn:
txn.abort() txn.abort()
return None, 0 return EMPTY_THUMBNAIL
def get_available_tier(self, path, requested_size, mtime):
"""Returns the best available tier in memory for the given path."""
target_tier = self._get_tier_for_size(requested_size)
with self._read_lock():
cached_sizes = self._thumbnail_cache.get(path)
if not cached_sizes:
return 0
if target_tier in cached_sizes:
img, cached_mtime = cached_sizes[target_tier]
if img and not img.isNull() and cached_mtime == mtime:
return target_tier
for size in THUMBNAIL_SIZES:
if size in cached_sizes:
img, cached_mtime = cached_sizes[size]
if img and not img.isNull() and cached_mtime == mtime:
return size
return 0
def get_thumbnail(self, path, requested_size, curr_mtime=None, def get_thumbnail(self, path, requested_size, curr_mtime=None,
inode=None, device_id=None, async_load=False): inode=None, device_id=None, async_load=False):
""" """
Safely retrieve a thumbnail from cache, finding the best available size. Safely retrieve a thumbnail from cache, finding the best available size.
Returns: tuple (QImage or None, mtime) or (None, 0) if not found. Returns: ThumbnailResult object.
""" """
# 1. Determine the ideal tier and create a prioritized search order.
target_tier = self._get_tier_for_size(requested_size) target_tier = self._get_tier_for_size(requested_size)
search_order = [target_tier] + \ search_order = [target_tier] + \
sorted([s for s in THUMBNAIL_SIZES if s > target_tier]) + \ sorted([s for s in THUMBNAIL_SIZES if s > target_tier]) + \
sorted([s for s in THUMBNAIL_SIZES if s < target_tier], reverse=True) sorted([s for s in THUMBNAIL_SIZES if s < target_tier], reverse=True)
# 2. Resolve file identity (mtime, dev, inode)
mtime, dev_id, inode_key = self._resolve_file_identity( mtime, dev_id, inode_key = self._resolve_file_identity(
path, curr_mtime, inode, device_id) path, curr_mtime, inode, device_id)
if mtime is None: if mtime is None:
return None, 0 return EMPTY_THUMBNAIL
best_img, best_mtime, best_tier = None, 0, 0
# 3. Check memory cache (fastest)
with self._read_lock(): with self._read_lock():
cached_sizes = self._thumbnail_cache.get(path) cached_sizes = self._thumbnail_cache.get(path)
if cached_sizes: if cached_sizes:
@@ -790,15 +796,22 @@ class ThumbnailCache(QObject):
if size in cached_sizes: if size in cached_sizes:
img, cached_mtime = cached_sizes[size] img, cached_mtime = cached_sizes[size]
if img and not img.isNull() and cached_mtime == mtime: if img and not img.isNull() and cached_mtime == mtime:
return img, mtime if size == target_tier:
return ThumbnailResult(img, mtime, size)
if not best_img:
best_img, best_mtime, best_tier = img, mtime, size
# 4. Handle Async Request
if async_load: if async_load:
self._queue_async_load(path, target_tier, mtime, dev_id, inode_key) self._queue_async_load(path, target_tier, mtime, dev_id, inode_key)
return None, 0 return ThumbnailResult(best_img, best_mtime, best_tier)
# 5. Check Disk Cache (Sync fallback) res = self._check_disk_cache(path, [target_tier], mtime, dev_id, inode_key)
return self._check_disk_cache(path, search_order, mtime, dev_id, inode_key) if res.image:
return res
if best_img:
return ThumbnailResult(best_img, best_mtime, best_tier)
other_tiers = [s for s in search_order if s != target_tier]
return self._check_disk_cache(path, other_tiers, mtime, dev_id, inode_key)
def _mark_load_complete(self, path, size): def _mark_load_complete(self, path, size):
"""Remove item from pending loading set.""" """Remove item from pending loading set."""
@@ -849,6 +862,7 @@ class ThumbnailCache(QObject):
if self._cache_writer: if self._cache_writer:
self._cache_writer.enqueue( self._cache_writer.enqueue(
(dev_id, inode_key, img, mtime, size, path), block=block) (dev_id, inode_key, img, mtime, size, path), block=block)
self.thumbnail_loaded.emit(path, size)
return True return True
def invalidate_path(self, path): def invalidate_path(self, path):
@@ -1074,12 +1088,32 @@ class ThumbnailCache(QObject):
for size in THUMBNAIL_SIZES: for size in THUMBNAIL_SIZES:
try: try:
db = self._get_device_db(device_id, size, write=True) with env.begin(write=True) as txn:
# Get DB handle using the current transaction and don't force create
db = self._get_device_db(device_id, size, write=False, txn=txn)
if not db: if not db:
continue continue
with env.begin(write=True) as txn: try:
txn.delete(inode_key, db=db) txn.delete(inode_key, db=db)
except lmdb.Error as e:
if "Invalid argument" in str(e):
# Handle potential stale DB handles (EINVAL)
db_name = f"dev_{device_id}_{size}".encode('utf-8')
self._db_lock.lock()
self._db_handles.pop(db_name, None)
self._db_lock.unlock()
# Retry with a fresh handle within the same transaction
db = self._get_device_db(
device_id, size, write=False, txn=txn)
if db:
txn.delete(inode_key, db=db)
elif "not found" in str(e).lower():
pass
else:
raise
except lmdb.NotFoundError:
pass
except Exception as e: except Exception as e:
logger.error(f"Error deleting from LMDB for size {size}: {e}") logger.error(f"Error deleting from LMDB for size {size}: {e}")

View File

@@ -34,6 +34,7 @@ from constants import (
ICON_THEME_VIEWER_FALLBACK, KSCREEN_DOCTOR_MARGIN, KWINOUTPUTCONFIG_PATH, ICON_THEME_VIEWER_FALLBACK, KSCREEN_DOCTOR_MARGIN, KWINOUTPUTCONFIG_PATH,
VIEWER_AUTO_RESIZE_WINDOW_DEFAULT, VIEWER_FORM_MARGIN, VIEWER_LABEL, VIEWER_AUTO_RESIZE_WINDOW_DEFAULT, VIEWER_FORM_MARGIN, VIEWER_LABEL,
VIEWER_WHEEL_SPEED_DEFAULT, XATTR_NAME, ZOOM_DESKTOP_RATIO, UITexts, VIEWER_WHEEL_SPEED_DEFAULT, XATTR_NAME, ZOOM_DESKTOP_RATIO, UITexts,
AVAILABLE_FACE_ENGINES, AVAILABLE_PET_ENGINES
) )
from imagecontroller import ImageController from imagecontroller import ImageController
from widgets import FaceNameInputWidget from widgets import FaceNameInputWidget
@@ -156,9 +157,9 @@ class FilmstripLoader(QThread):
# Small sleep to prevent UI freezing during heavy IO bursts # Small sleep to prevent UI freezing during heavy IO bursts
self.msleep(1) self.msleep(1)
try: try:
img, _ = self.cache.get_thumbnail(path, self.icon_size) res = self.cache.get_thumbnail(path, self.icon_size)
if img and not img.isNull(): if res.image and not res.image.isNull():
self.thumbnail_loaded.emit(index, img) self.thumbnail_loaded.emit(index, res.image)
except Exception: except Exception:
pass pass
@@ -2535,6 +2536,13 @@ class ImageViewer(QWidget):
action.setCheckable(True) action.setCheckable(True)
action.setChecked(item.get("checked", False)) action.setChecked(item.get("checked", False))
if "enabled" in item:
action.setEnabled(item["enabled"])
if "tooltip" in item:
action.setToolTip(item["tooltip"])
action.setStatusTip(item["tooltip"])
def restore_scroll_for_pane(self, pane, config): def restore_scroll_for_pane(self, pane, config):
""" """
Applies the saved scrollbar positions from a layout configuration. Applies the saved scrollbar positions from a layout configuration.
@@ -2802,8 +2810,13 @@ class ImageViewer(QWidget):
{"text": UITexts.VIEWER_MENU_DETECT_AREAS, {"text": UITexts.VIEWER_MENU_DETECT_AREAS,
"icon": "edit-image-face-recognize", "submenu": [ "icon": "edit-image-face-recognize", "submenu": [
{"text": UITexts.VIEWER_MENU_DETECT_FACES, {"text": UITexts.VIEWER_MENU_DETECT_FACES,
"action": "detect_faces"}, "action": "detect_faces",
{"text": UITexts.VIEWER_MENU_DETECT_PETS, "action": "detect_pets"}, "enabled": bool(AVAILABLE_FACE_ENGINES),
"tooltip": "" if AVAILABLE_FACE_ENGINES else UITexts.NO_FACE_LIBS},
{"text": UITexts.VIEWER_MENU_DETECT_PETS,
"action": "detect_pets",
"enabled": bool(AVAILABLE_PET_ENGINES),
"tooltip": "" if AVAILABLE_PET_ENGINES else UITexts.NO_FACE_LIBS},
]}, ]},
"separator", "separator",
{"text": UITexts.VIEWER_MENU_MANIPULATE, {"text": UITexts.VIEWER_MENU_MANIPULATE,

View File

@@ -8,6 +8,7 @@ Classes:
MetadataManager: A class with static methods to read metadata from files. MetadataManager: A class with static methods to read metadata from files.
""" """
import os import os
import collections
from PySide6.QtDBus import QDBusConnection, QDBusMessage, QDBus from PySide6.QtDBus import QDBusConnection, QDBusMessage, QDBus
try: try:
import exiv2 import exiv2
@@ -18,6 +19,9 @@ except ImportError:
from utils import preserve_mtime from utils import preserve_mtime
from constants import RATING_XATTR_NAME, XATTR_NAME from constants import RATING_XATTR_NAME, XATTR_NAME
MetadataResult = collections.namedtuple('MetadataResult', ['tags', 'rating'])
EMPTY_METADATA = MetadataResult([], 0)
def notify_baloo(path): def notify_baloo(path):
""" """
@@ -54,9 +58,9 @@ def load_common_metadata(path):
raw_rating = XattrManager.get_attribute(path, RATING_XATTR_NAME, "0") raw_rating = XattrManager.get_attribute(path, RATING_XATTR_NAME, "0")
try: try:
rating = int(raw_rating) rating = int(raw_rating)
except ValueError: except (ValueError, TypeError):
rating = 0 rating = 0
return tags, rating return MetadataResult(tags, rating)
class MetadataManager: class MetadataManager:

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "bagheeraview" name = "bagheeraview"
version = "0.9.13" version = "0.9.14"
authors = [ authors = [
{ name = "Ignacio Serantes" } { name = "Ignacio Serantes" }
] ]

View File

@@ -958,11 +958,8 @@ class SettingsDialog(QDialog):
def update_mediapipe_status(self): def update_mediapipe_status(self):
"""Checks for MediaPipe model file and updates UI accordingly.""" """Checks for MediaPipe model file and updates UI accordingly."""
if "mediapipe" not in AVAILABLE_FACE_ENGINES or not self.face_engine_combo: # --- Rostros ---
if self.download_model_btn: if self.face_engine_combo and "mediapipe" in AVAILABLE_FACE_ENGINES:
self.download_model_btn.hide()
return
model_exists = os.path.exists(MEDIAPIPE_FACE_MODEL_PATH) model_exists = os.path.exists(MEDIAPIPE_FACE_MODEL_PATH)
mediapipe_index = self.face_engine_combo.findText("mediapipe") mediapipe_index = self.face_engine_combo.findText("mediapipe")
@@ -975,20 +972,27 @@ class SettingsDialog(QDialog):
self.download_model_btn.setVisible(not model_exists) self.download_model_btn.setVisible(not model_exists)
if self.face_engine_combo.currentText() == "mediapipe" and not model_exists: if self.face_engine_combo.currentText() == "mediapipe" and not model_exists:
if self.face_engine_combo.count() > 1:
for i in range(self.face_engine_combo.count()): for i in range(self.face_engine_combo.count()):
if self.face_engine_combo.model().item(i).isEnabled(): if self.face_engine_combo.model().item(i).isEnabled():
self.face_engine_combo.setCurrentIndex(i) self.face_engine_combo.setCurrentIndex(i)
break break
elif self.download_model_btn:
self.download_model_btn.hide()
if "mediapipe" not in AVAILABLE_PET_ENGINES or not self.pet_engine_combo: # --- Mascotas (Pets) ---
if not AVAILABLE_PET_ENGINES:
self.pet_engine_combo.setEnabled(False)
self.pet_tags_edit.setEnabled(False)
self.pet_history_spin.setEnabled(False)
self.pet_color_btn.setEnabled(False)
if self.download_pet_model_btn: if self.download_pet_model_btn:
self.download_pet_model_btn.hide() self.download_pet_model_btn.hide()
return elif "mediapipe" in AVAILABLE_PET_ENGINES:
pet_model_exists = os.path.exists(MEDIAPIPE_OBJECT_MODEL_PATH) pet_model_exists = os.path.exists(MEDIAPIPE_OBJECT_MODEL_PATH)
if self.download_pet_model_btn: if self.download_pet_model_btn:
self.download_pet_model_btn.setVisible(not pet_model_exists) self.download_pet_model_btn.setVisible(not pet_model_exists)
elif self.download_pet_model_btn:
self.download_pet_model_btn.hide()
def start_model_download(self): def start_model_download(self):
"""Starts the background thread to download the MediaPipe model.""" """Starts the background thread to download the MediaPipe model."""

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="bagheeraview", name="bagheeraview",
version="0.9.13", version="0.9.14",
author="Ignacio Serantes", author="Ignacio Serantes",
description="Bagheera Image Viewer - An image viewer for KDE with Baloo in mind", 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 " long_description="A fast image viewer built with PySide6, featuring search and "