This commit is contained in:
Ignacio Serantes
2026-03-31 23:35:57 +02:00
parent ff7c1aa373
commit cb751b2970
14 changed files with 2431 additions and 119 deletions

View File

@@ -33,10 +33,12 @@ from PySide6.QtCore import (
QByteArray, QBuffer, QIODevice, Qt, QTimer, QRunnable, QThreadPool, QFile
)
from PySide6.QtGui import QImage, QImageReader, QImageIOHandler
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,
MIN_FREE_RAM_PERCENT, DISK_CACHE_MAX_BYTES, HAVE_BAGHEERASEARCH_LIB,
IMAGE_EXTENSIONS,
SCANNER_SETTINGS_DEFAULTS, SEARCH_CMD, THUMBNAIL_SIZES,
UITexts
)
@@ -334,7 +336,6 @@ class CacheWriter(QThread):
self._condition_new_data.wakeAll()
self._condition_space_available.wakeAll()
self._mutex.unlock()
self.wait()
def run(self):
self.setPriority(QThread.IdlePriority)
@@ -442,7 +443,6 @@ class CacheLoader(QThread):
self._mutex.lock()
self._condition.wakeAll()
self._mutex.unlock()
self.wait()
def run(self):
self.setPriority(QThread.IdlePriority)
@@ -558,12 +558,22 @@ class ThumbnailCache(QObject):
self._lmdb_env = None
def lmdb_close(self):
# Stop and wait for worker threads to ensure they are not accessing
# the LMDB environment while it's being closed.
if hasattr(self, '_cache_writer') and self._cache_writer:
self._cache_writer.stop()
while self._cache_writer.isRunning():
if QApplication.instance(): # Check if QApplication is still valid
QApplication.processEvents() # Keep UI responsive
QThread.msleep(50)
self._cache_writer = None
if hasattr(self, '_cache_loader') and self._cache_loader:
self._cache_loader.stop()
while self._cache_loader.isRunning():
if QApplication.instance(): # Check if QApplication is still valid
QApplication.processEvents() # Keep UI responsive
QThread.msleep(50)
self._cache_loader = None
self._loading_set.clear()
self._futures.clear()
@@ -658,8 +668,9 @@ class ThumbnailCache(QObject):
import psutil
mem = psutil.virtual_memory()
if (mem.available / mem.total) * 100 < MIN_FREE_RAM_PERCENT:
logger.warning(f"Low system memory detected (< {MIN_FREE_RAM_PERCENT}%). "
"Applying aggressive tiered pruning.")
logger.warning(f"Low system memory detected "
f"(< {MIN_FREE_RAM_PERCENT}%). "
f"Applying aggressive tiered pruning.")
# Strategy: first clear ALL cached high-res tiers to free space quickly
# while keeping the 128px grid thumbnails intact.
@@ -1189,8 +1200,14 @@ class ThumbnailCache(QObject):
return None
if not img.save(buf, "PNG"):
logger.error("Failed to save image to buffer")
return None
# libpng errors (like "Incorrect data in iCCP") can cause save() topi
# fail.
# Converting to a standard format strips problematic metadata/profiles.
ba.clear()
buf.seek(0)
if not img.convertToFormat(QImage.Format_ARGB32).save(buf, "PNG"):
logger.error("Failed to save image to buffer")
return None
return ba.data()
except Exception as e:
logger.error(f"Error converting image to bytes: {e}")
@@ -1382,27 +1399,38 @@ class ThumbnailGenerator(QThread):
# The signal/slot mechanism handles thread safety automatically.
emitter.progress_tick.connect(on_tick, Qt.QueuedConnection)
started_count = 0
for path in self.paths:
# Process in batches to avoid saturating the global thread pool queue.
# This allows the application to respond to stop() signals almost immediately.
batch_size = max(4, pool.maxThreadCount() * 2)
for i in range(0, len(self.paths), batch_size):
if self._abort:
break
runnable = ScannerWorker(self.cache, path, target_sizes=[self.size],
load_metadata=False, signal_emitter=emitter,
semaphore=sem)
runnable.setAutoDelete(False)
self._workers_mutex.lock()
if self._abort:
batch_slice = self.paths[i : i + batch_size]
started_in_batch = 0
for path in batch_slice:
if self._abort:
break
runnable = ScannerWorker(self.cache, path, target_sizes=[self.size],
load_metadata=False, signal_emitter=emitter,
semaphore=sem)
runnable.setAutoDelete(False)
self._workers_mutex.lock()
self._workers.append(runnable)
self._workers_mutex.unlock()
break
self._workers.append(runnable)
self._workers_mutex.unlock()
pool.start(runnable)
started_count += 1
pool.start(runnable)
started_in_batch += 1
if started_count > 0:
sem.acquire(started_count)
if started_in_batch > 0:
# Wait for the current batch to finish before queuing more
sem.acquire(started_in_batch)
self._workers_mutex.lock()
self._workers.clear()
self._workers_mutex.unlock()
self._workers_mutex.lock()
self._workers.clear()
@@ -1886,4 +1914,3 @@ class ImageScanner(QThread):
self.mutex.lock()
self.condition.wakeAll()
self.mutex.unlock()
self.wait()