v0.9.16
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user