- 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
- 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
pkg2zip's prebuilt releases are Windows-only, so ship pkg2zip.exe in
data/bin/ (MIT, mmozeiko) and invoke it through Wine on Linux. find_pkg2zip
prefers the bundled binary, then a configured path, then a native pkg2zip on
PATH. Adds data/bin/ATTRIBUTION.md and README credit.
- Save icon bumped to 56x40.
- Added a checkbox column for multi-select and a 'Delete selected' button.
- Right-click context menu on a row offers Export (copy the save folder to a
chosen dir) and Delete (remove from all locations, with confirmation).
The Save column now displays a small (40x32, letterboxed) icon from the
save folder's ICON0.PNG when present, using the first location (PSP,
PPSSPP, cloud) that has one.
Cancelling the store scrape now closes the dialog and drops its reference
immediately, so queued progress signals can't re-show it (QProgressDialog
re-shows on setValue while a value is pending). _scraping stays true until
the worker finishes to prevent a second concurrent scrape.
- PSP_GAMES.tsv and PSX_GAMES.tsv use different headers (PSP adds Type/RAP
columns, PSX adds Original Name). Parse the header row and map columns by
name instead of hardcoding positions — PSP names were showing as 'Minis'/
'PC Engine' (the Type column) with broken download URLs.
- GameDetailsPanel.set_item referenced is_ps1 before it was assigned, raising
UnboundLocalError and leaving the detail panel stale.
New 'NoPayStation' option in the Store tab lists PSP_GAMES.tsv and
PSX_GAMES.tsv, downloads each item's encrypted .pkg and decrypts/extracts
it with pkg2zip into a configurable output dir (<dir>/PSP/<TITLE_ID>/ and
<dir>/PSX/<TITLE_ID>/). Downloaded PSP games and PS1 Classics appear in the
Library (games/eboot and ps1/eboot respectively) and copy to the PSP as
PSP/GAME folders.
- app/nopaystation.py: TSV fetch/parse (cached) + pkg2zip discovery/extract.
- app/transfers.py: NoPayStationSource (scan + download).
- Settings: NPS output dir + pkg2zip path (QSettings nps_dir/pkg2zip_path).
- StorePage: NPS source + content-type combo (PSP Games / PS1 Classics),
per-item cover system, TSV release dates, nps-download job kind.
- Library: _nps_library_items ingest, on-PSP detection, folder copy/delete,
PPSSPP play disabled for encrypted PSP EBOOTs.
- README: document the source, pkg2zip + npdrm_free requirements.
Add a muted, selectable filename line under the title in both details
panels, with the full path as its tooltip (archive name in the Store,
disc/ISO filename in the Library).