Several fixes

This commit is contained in:
Ignacio Serantes
2026-04-06 20:44:49 +02:00
parent ca260d4219
commit a717acef87
8 changed files with 151 additions and 98 deletions

View File

@@ -34,9 +34,9 @@ from itertools import groupby
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QLabel, QVBoxLayout, QHBoxLayout, QLineEdit,
QTextEdit, QPushButton, QFileDialog, QComboBox, QSlider, QMessageBox, QSizePolicy,
QMenu, QInputDialog, QTableWidget, QTableWidgetItem, QHeaderView, QTabWidget, QProgressDialog,
QDockWidget, QAbstractItemView, QRadioButton, QButtonGroup, QListView,
QStyledItemDelegate, QStyle, QDialog, QKeySequenceEdit, QDialogButtonBox
QMenu, QInputDialog, QTableWidget, QTableWidgetItem, QHeaderView, QTabWidget,
QProgressDialog, QDockWidget, QAbstractItemView, QRadioButton, QButtonGroup,
QListView, QStyledItemDelegate, QStyle, QDialog, QKeySequenceEdit, QDialogButtonBox
)
from PySide6.QtGui import (
QDesktopServices, QFont, QFontMetrics, QIcon, QTransform, QImageReader, QPalette,
@@ -1290,12 +1290,6 @@ class MainWindow(QMainWindow):
self.history_tab = HistoryWidget(self)
self.tags_tabs.addTab(self.history_tab, UITexts.HISTORY_TAB)
# Initialize the shortcut controller
self.shortcut_controller = AppShortcutController(self)
self.favorites_tab.favorites_changed.connect(
self.shortcut_controller.refresh_favorite_shortcuts)
self.main_dock.setWidget(self.tags_tabs)
self.addDockWidget(Qt.RightDockWidgetArea, self.main_dock)
@@ -1758,31 +1752,35 @@ class MainWindow(QMainWindow):
menu.addSeparator()
duplicates_menu = menu.addMenu(QIcon.fromTheme("edit-find-replace"), UITexts.MENU_DUPLICATES)
duplicates_menu = menu.addMenu(
QIcon.fromTheme("edit-find-replace"), UITexts.MENU_DUPLICATES)
duplicates_menu.setEnabled(HAVE_IMAGEHASH)
detect_current_action = duplicates_menu.addAction(UITexts.MENU_DETECT_CURRENT_SEARCH)
detect_current_action = duplicates_menu.addAction(
UITexts.MENU_DETECT_CURRENT_SEARCH)
detect_current_action.triggered.connect(self.start_duplicate_detection)
detect_all_action = duplicates_menu.addAction(UITexts.MENU_DETECT_ALL)
detect_all_action.triggered.connect(self.detect_all_duplicates)
force_full_action = duplicates_menu.addAction(UITexts.MENU_FORCE_FULL_ANALYSIS)
force_full_action.triggered.connect(lambda: self.start_duplicate_detection(force_full=True))
force_full_action.triggered.connect(
lambda: self.start_duplicate_detection(force_full=True))
review_ignored_action = duplicates_menu.addAction(UITexts.MENU_REVIEW_IGNORED)
review_ignored_action.triggered.connect(self.review_ignored_duplicates)
duplicates_menu.addSeparator()
clean_hashes_action = duplicates_menu.addAction(QIcon.fromTheme("edit-clear-all"),
UITexts.MENU_CLEAN_UP_HASHES)
clean_hashes_action = duplicates_menu.addAction(
QIcon.fromTheme("edit-clear-all"), UITexts.MENU_CLEAN_UP_HASHES)
clean_hashes_action.triggered.connect(self.clean_duplicate_hashes)
if self.duplicate_cache:
count, size_bytes = self.duplicate_cache.get_hash_stats()
size_mb = size_bytes / (1024 * 1024)
clear_hashes_action = duplicates_menu.addAction(QIcon.fromTheme("user-trash-full"),
clear_hashes_action = duplicates_menu.addAction(
QIcon.fromTheme("user-trash-full"),
UITexts.MENU_CLEAR_HASHES.format(count, size_mb))
clear_hashes_action.triggered.connect(self.clear_duplicate_hashes)
@@ -1831,22 +1829,28 @@ class MainWindow(QMainWindow):
QApplication.restoreOverrideCursor()
if paths is None:
QMessageBox.warning(self, UITexts.WARNING, "Whitelist is empty. Please configure it in Settings.")
QMessageBox.warning(
self, UITexts.WARNING,
"Whitelist is empty. Please configure it in Settings.")
return
if not paths:
QMessageBox.information(self, UITexts.DUPLICATE_DETECTION_TITLE, UITexts.DUPLICATE_NONE_FOUND)
QMessageBox.information(
self, UITexts.DUPLICATE_DETECTION_TITLE, UITexts.DUPLICATE_NONE_FOUND)
return
self.start_duplicate_detection(custom_paths=paths)
# Por defecto usamos el modo optimizado (incremental) para no repetir comparaciones
self.start_duplicate_detection(force_full=False, custom_paths=paths)
def _gather_files_for_duplicates(self):
"""Helper to collect image paths based on whitelist and blacklist settings."""
whitelist_str = APP_CONFIG.get("duplicate_whitelist", "")
blacklist_str = APP_CONFIG.get("duplicate_blacklist", "")
whitelist = [os.path.abspath(os.path.expanduser(p.strip())) for p in whitelist_str.split(',') if p.strip()]
blacklist = [os.path.abspath(os.path.expanduser(p.strip())) for p in blacklist_str.split(',') if p.strip()]
whitelist = [os.path.abspath(os.path.expanduser(p.strip()))
for p in whitelist_str.split(',') if p.strip()]
blacklist = [os.path.abspath(os.path.expanduser(p.strip()))
for p in blacklist_str.split(',') if p.strip()]
if not whitelist:
return None
@@ -1861,7 +1865,8 @@ class MainWindow(QMainWindow):
for root, dirs, files in os.walk(root_path):
abs_root = os.path.abspath(root)
# Prune dirs to stop walking into blacklisted paths
dirs[:] = [d for d in dirs if os.path.join(abs_root, d) not in blacklist_set]
dirs[:] = [d for d in dirs
if os.path.join(abs_root, d) not in blacklist_set]
if abs_root in blacklist_set:
continue
@@ -1900,9 +1905,11 @@ class MainWindow(QMainWindow):
return
ignored = self.duplicate_cache.get_all_exceptions()
if not ignored:
QMessageBox.information(self, UITexts.DUPLICATE_DETECTION_TITLE, UITexts.DUPLICATE_NONE_FOUND)
QMessageBox.information(
self, UITexts.DUPLICATE_DETECTION_TITLE, UITexts.DUPLICATE_NONE_FOUND)
return
dialog = DuplicateManagerDialog(ignored, self.duplicate_cache, self, review_mode=True)
dialog = DuplicateManagerDialog(
ignored, self.duplicate_cache, self, review_mode=True)
dialog.show()
def show_about_dialog(self):
@@ -2428,7 +2435,8 @@ class MainWindow(QMainWindow):
confirm.setInformativeText(
UITexts.CONFIRM_DELETE_INFO.format(os.path.basename(paths[0])))
else:
confirm.setText(f"Are you sure you want to permanently delete {len(paths)} images?")
confirm.setText(
f"Are you sure you want to permanently delete {len(paths)} images?")
confirm.setInformativeText("This action CANNOT be undone.")
confirm.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
confirm.setDefaultButton(QMessageBox.No)
@@ -3401,7 +3409,7 @@ class MainWindow(QMainWindow):
"""Updates the circular progress bar value."""
self.progress_bar.setValue(value)
def on_thumbnail_loaded(self, path, size):
def on_thumbnail_loaded(self, _path, _size):
"""Called when a thumbnail has been loaded asynchronously from DB."""
self.thumbnail_view.viewport().update()
@@ -3856,7 +3864,8 @@ class MainWindow(QMainWindow):
self._setup_viewer_sync(viewer)
self.viewers.append(viewer)
viewer.destroyed.connect(
lambda obj=viewer: self.viewers.remove(obj) if obj in self.viewers else None)
lambda obj=viewer: self.viewers.remove(obj)
if obj in self.viewers else None)
if len(paths) > 1:
viewer.set_comparison_mode(len(paths))
@@ -4440,7 +4449,8 @@ class MainWindow(QMainWindow):
action_open_fullscreen = open_submenu.addAction(
QIcon.fromTheme("view-fullscreen"),
UITexts.CONTEXT_MENU_OPEN_FULLSCREEN_VIEWER)
action_open_fullscreen.triggered.connect(lambda: self.open_in_fullscreen_viewer(selected_indexes[0]))
action_open_fullscreen.triggered.connect(
lambda: self.open_in_fullscreen_viewer(selected_indexes[0]))
path = self.proxy_model.data(selected_indexes[0], PATH_ROLE)
action_open_location = menu.addAction(QIcon.fromTheme("folder-search"),
@@ -5089,7 +5099,8 @@ class MainWindow(QMainWindow):
return
# Get all image paths currently known to the application or provided list
paths_to_scan = custom_paths if custom_paths is not None else self.get_all_image_paths()
paths_to_scan = custom_paths \
if custom_paths is not None else self.get_all_image_paths()
if not paths_to_scan:
QMessageBox.information(self, UITexts.DUPLICATE_DETECTION_TITLE,
UITexts.DUPLICATE_NO_IMAGES)
@@ -5100,11 +5111,15 @@ class MainWindow(QMainWindow):
threshold = APP_CONFIG.get("duplicate_threshold", 90)
self.duplicate_detector = DuplicateDetector(
paths_to_scan, self.duplicate_cache, self.thread_pool_manager, method, threshold, force_full=force_full)
paths_to_scan, self.duplicate_cache,
self.thread_pool_manager, method, threshold, force_full=force_full)
self.duplicate_detector.progress_update.connect(self.on_duplicate_detection_progress)
self.duplicate_detector.duplicates_found.connect(self.on_duplicates_found)
self.duplicate_detector.detection_finished.connect(self.on_duplicate_detection_finished)
self.duplicate_detector.progress_update.connect(
self.on_duplicate_detection_progress)
self.duplicate_detector.duplicates_found.connect(
self.on_duplicates_found)
self.duplicate_detector.detection_finished.connect(
self.on_duplicate_detection_finished)
self.progress_bar.setValue(0)
self.progress_bar.setCustomColor(None)
@@ -5172,11 +5187,6 @@ def main():
thread_pool_manager = ThreadPoolManager()
cache = ThumbnailCache()
args = [a for a in sys.argv[1:] if a != "--x11"]
if args:
path = " ".join(args).strip()
if path.startswith("file:/"):
path = path[6:]
win = MainWindow(cache, args, thread_pool_manager, duplicate_cache)
app.installEventFilter(win.shortcut_controller)

View File

@@ -1,3 +1,7 @@
¿Sería posible añadir una opción para limpiar automáticamente los hashes de archivos que ya no existen sin borrar toda la base de datos?
¿Podrías optimizar el proceso de borrado en lote para que sea más eficiente si hay miles de entradas que limpiar?
Implement a bulk rename feature for the selected pet or face tags.
Add a visual indicator (e.g., an icon in the status bar) for the "Link Panes" status instead of text.

View File

@@ -561,6 +561,7 @@ _UI_TEXTS = {
"VIEWER_MENU_LINK_PANES": "Link Panes",
"DUPLICATE_OPEN_COMPARISON": "Open Comparison",
"DUPLICATE_LIST_HEADER": "Duplicate Pairs",
"IGNORED_DATE": "Ignored Date",
"SETTINGS_GROUP_SCANNER": "Scanner",
"SETTINGS_GROUP_AREAS": "Areas",
"SETTINGS_GROUP_THUMBNAILS": "Thumbnails",
@@ -1080,6 +1081,7 @@ _UI_TEXTS = {
"VIEWER_MENU_LINK_PANES": "Vincular Paneles",
"DUPLICATE_OPEN_COMPARISON": "Abrir Comparación",
"DUPLICATE_LIST_HEADER": "Parejas Duplicadas",
"IGNORED_DATE": "Fecha Ignorado",
"SETTINGS_GROUP_SCANNER": "Escáner",
"SETTINGS_GROUP_AREAS": "Áreas",
"SETTINGS_GROUP_THUMBNAILS": "Miniaturas",
@@ -1608,6 +1610,7 @@ _UI_TEXTS = {
"VIEWER_MENU_LINK_PANES": "Vincular Paneis",
"DUPLICATE_OPEN_COMPARISON": "Abrir Comparación",
"DUPLICATE_LIST_HEADER": "Parellas Duplicadas",
"IGNORED_DATE": "Data Ignorado",
"SETTINGS_GROUP_SCANNER": "Escáner",
"SETTINGS_GROUP_AREAS": "Áreas",
"SETTINGS_GROUP_THUMBNAILS": "Miniaturas",

View File

@@ -24,7 +24,7 @@ from constants import (
logger = logging.getLogger(__name__)
# Result structure for duplicate detection
DuplicateResult = collections.namedtuple('DuplicateResult', ['path1', 'path2', 'hash_value', 'is_exception', 'similarity'])
DuplicateResult = collections.namedtuple('DuplicateResult', ['path1', 'path2', 'hash_value', 'is_exception', 'similarity', 'timestamp'])
class BKTree:
@@ -241,7 +241,15 @@ class DuplicateCache(QObject):
self._hash_cache[(dev_id, inode_key_bytes)] = (hash_value, mtime, path)
return True
def remove_hash_for_path(self, path):
def remove_hash_for_path(self, path, clear_relationships=True):
"""
Removes the hash entry for a path.
Args:
path: File path.
clear_relationships: If True, also wipes all entries in pending and
exceptions DBs involving this file.
"""
dev_id, inode_key_bytes = self._get_inode_info(path)
if not inode_key_bytes or not self._lmdb_env:
return False
@@ -255,6 +263,7 @@ class DuplicateCache(QObject):
self._hash_cache.pop((dev_id, inode_key_bytes), None)
# Also remove any exceptions involving this path
if clear_relationships:
self._remove_pair_entries_for_path(dev_id, inode_key_bytes, self._exceptions_db)
self._remove_pair_entries_for_path(dev_id, inode_key_bytes, self._pending_db)
return True
@@ -271,7 +280,7 @@ class DuplicateCache(QObject):
return None
return self._get_pair_lmdb_key_from_ids(dev1, inode1, dev2, inode2)
def mark_as_exception(self, path1, path2, is_exception=True, similarity=None):
def mark_as_exception(self, path1, path2, is_exception=True, similarity=None, timestamp=None):
if not self._lmdb_env:
return False
@@ -285,9 +294,8 @@ class DuplicateCache(QObject):
return False
# Store paths in value to make exception recovery independent of hash DB
val_str = f"{path1}|{path2}"
if similarity is not None:
val_str += f"|{similarity}"
ts = timestamp if timestamp is not None else int(time.time())
val_str = f"{path1}|{path2}|{similarity if similarity is not None else ''}|{ts}"
value = val_str.encode('utf-8')
with QMutexLocker(self._db_lock):
@@ -315,34 +323,35 @@ class DuplicateCache(QObject):
with self._lmdb_env.begin(write=False) as txn:
return txn.get(exception_key, db=self._exceptions_db) is not None
def _remove_pair_entries_for_path(self, target_dev, target_inode, db_handle):
def _remove_pair_entries_for_path(self, target_dev, target_inode, db_handle, txn=None):
"""Removes all entries involving a specific (dev, inode) pair from a pair-based DB."""
if not self._lmdb_env:
return
target_inode_hex = target_inode.hex()
with QMutexLocker(self._db_lock):
with self._lmdb_env.begin(write=True) as txn:
cursor = txn.cursor(db=db_handle)
def do_remove(t):
cursor = t.cursor(db=db_handle)
keys_to_delete = []
for key_bytes, _ in cursor:
key_str = key_bytes.decode('utf-8')
# Key format: "dev1-inode1_hex-dev2-inode2_hex"
parts = key_str.split('-')
dev1 = int(parts[0])
inode1_hex = parts[1]
dev2 = int(parts[2])
inode2_hex = parts[3]
if len(parts) < 4: continue
dev1, inode1_hex, dev2, inode2_hex = int(parts[0]), parts[1], int(parts[2]), parts[3]
if (dev1 == target_dev and inode1_hex == target_inode_hex) or \
(dev2 == target_dev and inode2_hex == target_inode_hex):
keys_to_delete.append(key_bytes)
for key in keys_to_delete:
txn.delete(key, db=db_handle)
t.delete(key, db=db_handle)
def mark_as_pending(self, path1, path2, is_pending=True, similarity=None):
if txn:
do_remove(txn)
else:
with QMutexLocker(self._db_lock):
with self._lmdb_env.begin(write=True) as t:
do_remove(t)
def mark_as_pending(self, path1, path2, is_pending=True, similarity=None, timestamp=None):
"""Marks a pair as pending review."""
if not self._lmdb_env or self._pending_db is None:
return False
@@ -352,9 +361,8 @@ class DuplicateCache(QObject):
return False
# Store paths in value to allow reconstruction without scanning
val_str = f"{path1}|{path2}"
if similarity is not None:
val_str += f"|{similarity}"
ts = timestamp if timestamp is not None else int(time.time())
val_str = f"{path1}|{path2}|{similarity if similarity is not None else ''}|{ts}"
value = val_str.encode('utf-8')
with QMutexLocker(self._db_lock):
@@ -381,9 +389,10 @@ class DuplicateCache(QObject):
try:
parts = value_bytes.decode('utf-8').split('|')
p1, p2 = parts[0], parts[1]
sim = int(parts[2]) if len(parts) > 2 else None
sim = int(parts[2]) if len(parts) > 2 and parts[2] else None
ts = int(parts[3]) if len(parts) > 3 else 0
if os.path.exists(p1) and os.path.exists(p2):
results.append(DuplicateResult(p1, p2, None, False, sim))
results.append(DuplicateResult(p1, p2, None, False, sim, ts))
else:
keys_to_delete.append(key)
except Exception:
@@ -414,6 +423,7 @@ class DuplicateCache(QObject):
try:
p1, p2 = None, None
sim = None
ts = 0
val_str = value_bytes.decode('utf-8')
if '|' in val_str:
@@ -421,8 +431,12 @@ class DuplicateCache(QObject):
parts = val_str.split('|')
if len(parts) >= 2:
p1, p2 = parts[0], parts[1]
if len(parts) > 2:
if len(parts) > 2 and parts[2]:
sim = int(parts[2])
if len(parts) > 3:
ts = int(parts[3])
else:
ts = int(os.path.getmtime(p1)) if os.path.exists(p1) else 0
if not p1 or not p2:
# Legacy format fallback: lookup paths in hash db
@@ -438,7 +452,7 @@ class DuplicateCache(QObject):
if p1 and p2:
if os.path.exists(p1) and os.path.exists(p2):
results.append(DuplicateResult(p1, p2, None, True, sim))
results.append(DuplicateResult(p1, p2, None, True, sim, ts))
except Exception:
continue
return results
@@ -606,7 +620,7 @@ class DuplicateDetector(QThread):
mtime = stat_info.st_mtime
dev, inode = stat_info.st_dev, struct.pack('Q', stat_info.st_ino)
cached_h = None if self.force_full else \
cached_h = \
self.duplicate_cache.get_hash_for_path(path, mtime, dev, inode)
if cached_h:
@@ -658,13 +672,6 @@ class DuplicateDetector(QThread):
# Signal phase transition to exactly 50%
self.progress_update.emit(total_files, total_files * 2, UITexts.DUPLICATE_MSG_ANALYZING.format(filename="..."))
if not self.force_full and not dirty_paths:
# No files changed and no re-scan forced.
# We can skip Phase 2 as all results were loaded from the pending cache.
self.duplicates_found.emit(found_duplicates)
self.detection_finished.emit()
return
# 3. Phase 2: Comparison (Optimized with BK-Tree)
hash_map = collections.defaultdict(list)
bk_tree = BKTree(lambda a, b: a - b)
@@ -715,10 +722,11 @@ class DuplicateDetector(QThread):
if canonical not in unique_duplicate_pairs:
if not self.duplicate_cache.is_exception(p1, p2):
sim = int((1.0 - (distance / MAX_DHASH_DISTANCE)) * 100)
res = DuplicateResult(p1, p2, str(h1), False, sim)
ts = int(time.time())
res = DuplicateResult(p1, p2, str(h1), False, sim, ts)
found_duplicates.append(res)
unique_duplicate_pairs.add(canonical)
self.duplicate_cache.mark_as_pending(p1, p2, True, similarity=sim)
self.duplicate_cache.mark_as_pending(p1, p2, True, similarity=sim, timestamp=ts)
self.duplicates_found.emit(found_duplicates)
self.detection_finished.emit()

View File

@@ -1,4 +1,5 @@
import os
from datetime import datetime
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame,
QSplitter, QWidget, QMessageBox, QApplication, QMenu,
@@ -56,10 +57,18 @@ class DuplicateManagerDialog(QDialog):
left_layout.addLayout(header_layout)
self.table_widget = QTableWidget()
if self.review_mode:
self.table_widget.setColumnCount(3)
self.table_widget.setHorizontalHeaderLabels([UITexts.IGNORED_DATE, "%", UITexts.CONTEXT_MENU_OPEN])
self.table_widget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
self.table_widget.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
self.table_widget.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
else:
self.table_widget.setColumnCount(2)
self.table_widget.setHorizontalHeaderLabels(["%", UITexts.CONTEXT_MENU_OPEN]) # Usamos una cadena existente o genérica
self.table_widget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
self.table_widget.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
self.table_widget.verticalHeader().setVisible(False)
self.table_widget.setSelectionBehavior(QAbstractItemView.SelectRows)
self.table_widget.setSelectionMode(QAbstractItemView.SingleSelection)
@@ -311,17 +320,30 @@ class DuplicateManagerDialog(QDialog):
row = self.table_widget.rowCount()
self.table_widget.insertRow(row)
# Columna 0: Porcentaje (usamos DisplayRole con int para que ordene numéricamente)
if self.review_mode:
# Column 0: Ignored Date
ts = dup.timestamp if hasattr(dup, 'timestamp') and dup.timestamp else 0
date_str = datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M") if ts else "-"
date_item = QTableWidgetItem(date_str)
date_item.setData(Qt.UserRole, i) # Store original index here for _load_pair
date_item.setTextAlignment(Qt.AlignCenter)
self.table_widget.setItem(row, 0, date_item)
col_offset = 1
else:
col_offset = 0
# Columna similarity (usamos DisplayRole con int para que ordene numéricamente)
sim_item = QTableWidgetItem()
sim_item.setData(Qt.DisplayRole, dup.similarity if dup.similarity is not None else 0)
sim_item.setTextAlignment(Qt.AlignCenter)
if not self.review_mode:
sim_item.setData(Qt.UserRole, i) # Guardamos el índice original en la lista duplicates
# Columna 1: Nombres de ficheros
names_item = QTableWidgetItem(f"{name1}{name2}")
self.table_widget.setItem(row, 0, sim_item)
self.table_widget.setItem(row, 1, names_item)
self.table_widget.setItem(row, col_offset, sim_item)
self.table_widget.setItem(row, col_offset + 1, names_item)
self.counter_lbl.setText(str(len(self.duplicates)))
self.table_widget.blockSignals(False)
@@ -661,6 +683,11 @@ class DuplicateManagerDialog(QDialog):
def _skip(self):
if self.review_mode and self.current_dup_pair:
self.cache.mark_as_exception(self.current_dup_pair.path1, self.current_dup_pair.path2, False)
# Borramos los hashes para que el detector las trate como imágenes nuevas
# y fuerce una nueva comparación en el siguiente escaneo.
# Usamos clear_relationships=False para no perder otras posibles coincidencias ya marcadas.
self.cache.remove_hash_for_path(self.current_dup_pair.path1, clear_relationships=False)
self.cache.remove_hash_for_path(self.current_dup_pair.path2, clear_relationships=False)
self._handle_action(skip=False, permanent=False)
else:
self._handle_action(skip=True)

View File

@@ -758,7 +758,8 @@ class ThumbnailCache(QObject):
self._broken_cache[key] = (mtime, error_msg)
def get_broken_info(self, path, size, mtime, inode, dev_id):
"""Returns the error message if a thumbnail is known to have failed, else None."""
"""Returns the error message if a thumbnail is known to have failed, else
None."""
key = (dev_id, struct.pack('Q', inode) if inode is not None else None, size)
with self._read_lock():
info = self._broken_cache.get(key)
@@ -889,7 +890,8 @@ class ThumbnailCache(QObject):
# Check if known to be broken
broken_msg = self.get_broken_info(path, target_tier, mtime, inode, device_id)
if broken_msg:
return ThumbnailResult(self._broken_images.get(target_tier), mtime, target_tier)
return ThumbnailResult(
self._broken_images.get(target_tier), mtime, target_tier)
best_img, best_mtime, best_tier = None, 0, 0
@@ -1488,7 +1490,6 @@ class ImageScanner(QThread):
def __init__(self, cache, paths, is_file_list=False, viewers=None,
thread_pool_manager=None, target_sizes=None):
# is_file_list is not used
if not paths or not isinstance(paths, (list, tuple)):
logger.warning("ImageScanner initialized with empty or invalid paths")
paths = []
@@ -1851,7 +1852,8 @@ class ImageScanner(QThread):
return
for f_path, _ in tasks:
r = ScannerWorker(self.cache, f_path, semaphore=sem, target_sizes=self.target_sizes)
r = ScannerWorker(
self.cache, f_path, semaphore=sem, target_sizes=self.target_sizes)
r.setAutoDelete(False)
runnables.append(r)
self._current_workers.append(r)

View File

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

View File

@@ -649,7 +649,6 @@ class LayoutsWidget(QWidget):
item_name = QTableWidgetItem(name)
item_name.setData(Qt.UserRole, f_path)
item_name.setData(Qt.UserRole, f_path) # Store full path in item
item_date = QTableWidgetItem(dt)
self.table.setItem(i, 0, item_name)