Fix scrape crash + cover compose responsiveness
build / Windows x64 (Wine) (push) Successful in 6m16s
build / Linux x86_64 (push) Successful in 6m22s

- 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.
This commit is contained in:
2026-09-14 21:26:57 -04:00
parent 8b24d391ac
commit 7381f2c39d
2 changed files with 122 additions and 53 deletions
+33 -22
View File
@@ -773,11 +773,16 @@ def save_cover(release_id, system, data):
# --------------------------------------------------------------------------- #
# Game cover compositing (IGDB key art -> full PSP cover)
# --------------------------------------------------------------------------- #
_game_template_image = None
_game_template_cache = None # (template QImage or None, window (x, y, w, h))
_game_template_lock = threading.Lock()
def _transparent_rect(img):
"""Bounding box of the fully-transparent pixels in `img`."""
"""Bounding box of the fully-transparent pixels in `img`.
Run once per template (the result is cached) — an O(w*h) scan is far too
slow to repeat for every composed cover.
"""
minx, miny, maxx, maxy = img.width(), img.height(), -1, -1
for y in range(img.height()):
for x in range(img.width()):
@@ -790,24 +795,31 @@ def _transparent_rect(img):
def _game_template():
"""Load the PSP game cover template (top header + transparent window)."""
global _game_template_image
if _game_template_image is None:
try:
from PySide6.QtGui import QImage
except Exception: # noqa: BLE001 - headless API may lack PySide6
return None
img = QImage()
for path in (
config.COVERS_DIR / "UMD_GAME_COVER_TEMPLATE.png",
config.DATA_DIR / "covers" / "UMD_GAME_COVER_TEMPLATE.png",
):
if path.exists():
img.load(str(path))
if not img.isNull():
break
_game_template_image = img.convertToFormat(QImage.Format_ARGB32) if not img.isNull() else None
return _game_template_image
"""Load the PSP game cover template; return ``(template, window)``."""
global _game_template_cache
if _game_template_cache is None:
with _game_template_lock:
if _game_template_cache is None:
try:
from PySide6.QtGui import QImage
except Exception: # noqa: BLE001 - headless API may lack PySide6
_game_template_cache = (None, (0, 0, 0, 0))
return _game_template_cache
img = QImage()
for path in (
config.COVERS_DIR / "UMD_GAME_COVER_TEMPLATE.png",
config.DATA_DIR / "covers" / "UMD_GAME_COVER_TEMPLATE.png",
):
if path.exists():
img.load(str(path))
if not img.isNull():
break
img = img.convertToFormat(QImage.Format_ARGB32) if not img.isNull() else None
_game_template_cache = (
img,
_transparent_rect(img) if img is not None and not img.isNull() else (0, 0, 0, 0),
)
return _game_template_cache
def compose_game_cover(key_art_bytes):
@@ -822,14 +834,13 @@ def compose_game_cover(key_art_bytes):
except Exception: # noqa: BLE001
return key_art_bytes
template = _game_template()
template, (x, y, w, h) = _game_template()
art = QImage.fromData(key_art_bytes)
if template is None or template.isNull() or art.isNull():
return key_art_bytes
art = art.convertToFormat(QImage.Format_ARGB32)
out = QImage(template.size(), QImage.Format_ARGB32)
out.fill(Qt.transparent)
x, y, w, h = _transparent_rect(template)
painter = QPainter(out)
if w > 0 and h > 0: