- Raise the process soft file-descriptor limit to the hard limit at startup
(GLib aborts the process with 'Creating pipes for GWakeup: Too many open
files' when cover/metadata loading exhausts a 1024-soft-limit environment).
- Reduce per-loader network concurrency 8 -> 6 threads to lower socket
pressure against the store server.
- Library/metadata workers: delete the worker on its own thread via
worker.finished -> worker.deleteLater (bound) instead of letting Python GC
delete it on the GUI thread after untracking.
- Cover templates: compute the transparent-window scan outside the lock so a
slow first scan never holds the lock and stalls other cover threads.
- Store: larger cover/date batches (500/400/300) so the store loads 3449
archives with far fewer QThread + ThreadPoolExecutor creations.
Connecting a signal to a Python lambda has no QObject receiver, so PySide6
treats it as a direct connection and runs the lambda on the emitting (worker)
thread. The scrape dialog connected its results signals to lambdas, so
_on_results/_on_scraped executed on the worker thread and called
accept()/done() there -> QThread::wait on itself and GUI widget mutation off
the GUI thread -> 'QThread: Destroyed while thread is still running' abort.
Connect to bound methods (queued to the GUI thread) and carry the generation
on the worker (read via sender().gen). Also replaced the thread.finished
lambdas in load_library/metadata with bound-method cleanup keyed by a
thread._worker attribute.
- Cache the UMD/game cover templates AND their transparent-window rects
(computed once, under a lock). The old code re-ran an O(w*h) Python pixel
scan on every composited cover, stalling video cover loading and scraping
for ~1s per cover with GIL contention.
- ScrapeDialog: ignore stale search/scrape results via a generation counter
and cancel in-flight workers when a new search/scrape starts; clear threads
on close.
- MetadataWorker (library details): cancel stale metadata fetches and delete
finished workers/threads instead of leaking one per item selection.
Library covers used the same QThread+worker architecture as the store but
never deleted finished threads/loaders, so every reload leaked one QThread
and one CoverLoader. Library item loading leaked its QThread too.
CoverShowcase now removes + deleteLater's loaders on their finished signal
and deletes their threads on thread.finished; MainWindow.load_library deletes
the finished worker and thread instead of only untracking them.
StorePage cover/date loaders were never removed from tracking once their
thread finished, and _populate/_begin_scan dropped the whole list while
threads were still running. That garbage-collected QObjects whose QThread
had already been deleted, leaving a dangling thread affinity that crashed
Qt (SIGSEGV) when searching the store after it fully loaded.
Loaders now remove themselves and deleteLater on their finished signal, and
the tracking list is kept until each thread winds down.
- config: point SSL at certifi bundle when frozen (fixes store/NPS/TMDB SSL errors)
- gui: set app name + window icon for the taskbar
- spec: contents_directory=psp-preserve; bundle a .desktop file for Wayland launchers
- runner/Dockerfile: Debian + Wine + Windows CPython 3.12 + PyInstaller + act_runner
- runner/entrypoint.sh: self-register with the windows:host label
- workflow windows job now builds via winpython under Wine
- Rework Settings tab into grouped sections
- Add preferred-region matching and full-hash-by-default setting
- Replace combo-box square arrow with a proper chevron
- Fix UMD Refresh button (clicked bool was clearing the view)
- Remove pkg2zip.exe fallback (pure-Python decryptor is the primary path)
- Move LAN IP/share paths to .env; use example defaults in config