Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99e9cdc49a | ||
|
|
a4d132f134 | ||
|
|
3a69f3f6a1 |
+24
-22
@@ -797,28 +797,30 @@ def _transparent_rect(img):
|
|||||||
def _game_template():
|
def _game_template():
|
||||||
"""Load the PSP game cover template; return ``(template, window)``."""
|
"""Load the PSP game cover template; return ``(template, window)``."""
|
||||||
global _game_template_cache
|
global _game_template_cache
|
||||||
if _game_template_cache is None:
|
if _game_template_cache is not None:
|
||||||
with _game_template_lock:
|
return _game_template_cache
|
||||||
if _game_template_cache is None:
|
try:
|
||||||
try:
|
from PySide6.QtGui import QImage
|
||||||
from PySide6.QtGui import QImage
|
except Exception: # noqa: BLE001 - headless API may lack PySide6
|
||||||
except Exception: # noqa: BLE001 - headless API may lack PySide6
|
_game_template_cache = (None, (0, 0, 0, 0))
|
||||||
_game_template_cache = (None, (0, 0, 0, 0))
|
return _game_template_cache
|
||||||
return _game_template_cache
|
img = QImage()
|
||||||
img = QImage()
|
for path in (
|
||||||
for path in (
|
config.COVERS_DIR / "UMD_GAME_COVER_TEMPLATE.png",
|
||||||
config.COVERS_DIR / "UMD_GAME_COVER_TEMPLATE.png",
|
config.DATA_DIR / "covers" / "UMD_GAME_COVER_TEMPLATE.png",
|
||||||
config.DATA_DIR / "covers" / "UMD_GAME_COVER_TEMPLATE.png",
|
):
|
||||||
):
|
if path.exists():
|
||||||
if path.exists():
|
img.load(str(path))
|
||||||
img.load(str(path))
|
if not img.isNull():
|
||||||
if not img.isNull():
|
break
|
||||||
break
|
img = img.convertToFormat(QImage.Format_ARGB32) if not img.isNull() else None
|
||||||
img = img.convertToFormat(QImage.Format_ARGB32) if not img.isNull() else None
|
entry = (
|
||||||
_game_template_cache = (
|
img,
|
||||||
img,
|
_transparent_rect(img) if img is not None and not img.isNull() else (0, 0, 0, 0),
|
||||||
_transparent_rect(img) if img is not None and not img.isNull() else (0, 0, 0, 0),
|
)
|
||||||
)
|
with _game_template_lock:
|
||||||
|
if _game_template_cache is None:
|
||||||
|
_game_template_cache = entry
|
||||||
return _game_template_cache
|
return _game_template_cache
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+79
-38
@@ -136,7 +136,7 @@ class CoverLoader(QObject):
|
|||||||
def run(self):
|
def run(self):
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
pool = ThreadPoolExecutor(max_workers=8)
|
pool = ThreadPoolExecutor(max_workers=6)
|
||||||
futures = {
|
futures = {
|
||||||
pool.submit(load_cover, it): i
|
pool.submit(load_cover, it): i
|
||||||
for i, it in enumerate(self.items)
|
for i, it in enumerate(self.items)
|
||||||
@@ -172,20 +172,22 @@ _template_lock = threading.Lock()
|
|||||||
def _umd_template():
|
def _umd_template():
|
||||||
"""Return ``(template QImage, transparent window rect)`` — loaded once."""
|
"""Return ``(template QImage, transparent window rect)`` — loaded once."""
|
||||||
global _template_cache
|
global _template_cache
|
||||||
if _template_cache is None:
|
if _template_cache is not None:
|
||||||
with _template_lock:
|
return _template_cache
|
||||||
if _template_cache is None:
|
img = QImage()
|
||||||
img = QImage()
|
for path in (
|
||||||
for path in (
|
config.COVERS_DIR / "UMD_COVER_TEMPLATE.png",
|
||||||
config.COVERS_DIR / "UMD_COVER_TEMPLATE.png",
|
config.DATA_DIR / "covers" / "UMD_COVER_TEMPLATE.png",
|
||||||
config.DATA_DIR / "covers" / "UMD_COVER_TEMPLATE.png",
|
):
|
||||||
):
|
if path.exists():
|
||||||
if path.exists():
|
img.load(str(path))
|
||||||
img.load(str(path))
|
if not img.isNull():
|
||||||
if not img.isNull():
|
break
|
||||||
break
|
img = img.convertToFormat(QImage.Format_ARGB32)
|
||||||
img = img.convertToFormat(QImage.Format_ARGB32)
|
entry = (img, _transparent_window(img))
|
||||||
_template_cache = (img, _transparent_window(img))
|
with _template_lock:
|
||||||
|
if _template_cache is None:
|
||||||
|
_template_cache = entry
|
||||||
return _template_cache
|
return _template_cache
|
||||||
|
|
||||||
|
|
||||||
@@ -969,10 +971,11 @@ class ScrapeSearchWorker(QObject):
|
|||||||
results = Signal(list) # candidates with thumb_bytes added
|
results = Signal(list) # candidates with thumb_bytes added
|
||||||
finished = Signal()
|
finished = Signal()
|
||||||
|
|
||||||
def __init__(self, system, query):
|
def __init__(self, system, query, gen):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.system = system
|
self.system = system
|
||||||
self.query = query
|
self.query = query
|
||||||
|
self.gen = gen
|
||||||
self._cancel = threading.Event()
|
self._cancel = threading.Event()
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
@@ -1003,11 +1006,12 @@ class ScrapeFetchWorker(QObject):
|
|||||||
result = Signal(dict) # {"meta": {...}, "cover": bytes|None}
|
result = Signal(dict) # {"meta": {...}, "cover": bytes|None}
|
||||||
finished = Signal()
|
finished = Signal()
|
||||||
|
|
||||||
def __init__(self, system, rid, candidate):
|
def __init__(self, system, rid, candidate, gen):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.system = system
|
self.system = system
|
||||||
self.rid = rid
|
self.rid = rid
|
||||||
self.candidate = candidate
|
self.candidate = candidate
|
||||||
|
self.gen = gen
|
||||||
self._cancel = threading.Event()
|
self._cancel = threading.Event()
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
@@ -1122,17 +1126,17 @@ class ScrapeDialog(QDialog):
|
|||||||
self.list.clear()
|
self.list.clear()
|
||||||
self.candidates = []
|
self.candidates = []
|
||||||
thread = QThread(self)
|
thread = QThread(self)
|
||||||
worker = ScrapeSearchWorker(self.system, query)
|
worker = ScrapeSearchWorker(self.system, query, gen)
|
||||||
worker.moveToThread(thread)
|
worker.moveToThread(thread)
|
||||||
thread.started.connect(worker.run)
|
thread.started.connect(worker.run)
|
||||||
worker.results.connect(lambda r, g=gen: self._on_results(r, g))
|
worker.results.connect(self._on_results)
|
||||||
worker.finished.connect(thread.quit)
|
worker.finished.connect(thread.quit)
|
||||||
self._threads.append(thread)
|
self._threads.append(thread)
|
||||||
self._workers.append(worker)
|
self._workers.append(worker)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
def _on_results(self, results, gen):
|
def _on_results(self, results):
|
||||||
if gen != self._search_gen:
|
if getattr(self.sender(), "gen", None) != self._search_gen:
|
||||||
return
|
return
|
||||||
self.search_btn.setEnabled(True)
|
self.search_btn.setEnabled(True)
|
||||||
self.candidates = results
|
self.candidates = results
|
||||||
@@ -1172,17 +1176,17 @@ class ScrapeDialog(QDialog):
|
|||||||
self.search_btn.setEnabled(False)
|
self.search_btn.setEnabled(False)
|
||||||
self.status.setText("Scraping…")
|
self.status.setText("Scraping…")
|
||||||
thread = QThread(self)
|
thread = QThread(self)
|
||||||
worker = ScrapeFetchWorker(self.system, self.rid, candidate)
|
worker = ScrapeFetchWorker(self.system, self.rid, candidate, gen)
|
||||||
worker.moveToThread(thread)
|
worker.moveToThread(thread)
|
||||||
thread.started.connect(worker.run)
|
thread.started.connect(worker.run)
|
||||||
worker.result.connect(lambda p, g=gen: self._on_scraped(p, g))
|
worker.result.connect(self._on_scraped)
|
||||||
worker.finished.connect(thread.quit)
|
worker.finished.connect(thread.quit)
|
||||||
self._threads.append(thread)
|
self._threads.append(thread)
|
||||||
self._workers.append(worker)
|
self._workers.append(worker)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
def _on_scraped(self, payload, gen):
|
def _on_scraped(self, payload):
|
||||||
if gen != self._search_gen:
|
if getattr(self.sender(), "gen", None) != self._search_gen:
|
||||||
return
|
return
|
||||||
meta = payload.get("meta") or {}
|
meta = payload.get("meta") or {}
|
||||||
cover = payload.get("cover")
|
cover = payload.get("cover")
|
||||||
@@ -1198,7 +1202,15 @@ class ScrapeDialog(QDialog):
|
|||||||
self.search_btn.setEnabled(True)
|
self.search_btn.setEnabled(True)
|
||||||
self.status.setText("Could not fetch anything for that selection.")
|
self.status.setText("Could not fetch anything for that selection.")
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def _shutdown_threads(self):
|
||||||
|
"""Cancel workers and wait for their threads to fully finish.
|
||||||
|
|
||||||
|
Must run before the dialog is destroyed — QThread children abort the
|
||||||
|
process with "QThread: Destroyed while thread is still running" if they
|
||||||
|
are deleted while their worker thread is alive. ``done()`` covers
|
||||||
|
accept()/reject() (including the accept() in ``_on_scraped``); the
|
||||||
|
close event covers the window-manager close path.
|
||||||
|
"""
|
||||||
for w in self._workers:
|
for w in self._workers:
|
||||||
w.cancel()
|
w.cancel()
|
||||||
for t in self._threads:
|
for t in self._threads:
|
||||||
@@ -1207,8 +1219,15 @@ class ScrapeDialog(QDialog):
|
|||||||
t.wait()
|
t.wait()
|
||||||
self._workers = []
|
self._workers = []
|
||||||
self._threads = []
|
self._threads = []
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
self._shutdown_threads()
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
|
def done(self, result):
|
||||||
|
self._shutdown_threads()
|
||||||
|
super().done(result)
|
||||||
|
|
||||||
|
|
||||||
class SettingsPage(QWidget):
|
class SettingsPage(QWidget):
|
||||||
saved = Signal()
|
saved = Signal()
|
||||||
@@ -1615,7 +1634,7 @@ class StoreCoverLoader(QObject):
|
|||||||
def run(self):
|
def run(self):
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
pool = ThreadPoolExecutor(max_workers=8)
|
pool = ThreadPoolExecutor(max_workers=6)
|
||||||
futures = {
|
futures = {
|
||||||
pool.submit(load_cover, it, 8, self.quick): row
|
pool.submit(load_cover, it, 8, self.quick): row
|
||||||
for row, it in self.tasks
|
for row, it in self.tasks
|
||||||
@@ -2268,11 +2287,11 @@ class StorePage(QWidget):
|
|||||||
return (0, -date_int, -mtime)
|
return (0, -date_int, -mtime)
|
||||||
return (1, 0, -mtime)
|
return (1, 0, -mtime)
|
||||||
|
|
||||||
CHUNK = 150 # items filled per timer tick
|
CHUNK = 200 # items filled per timer tick
|
||||||
COVER_BATCH = 200 # covers fetched for the initially-visible area
|
COVER_BATCH = 500 # covers fetched for the initially-visible area
|
||||||
COVER_SCROLL_BATCH = 100
|
COVER_SCROLL_BATCH = 400
|
||||||
RECENT_LIMIT = 300 # default: show only the newest N store items
|
RECENT_LIMIT = 300 # default: show only the newest N store items
|
||||||
DATE_BATCH = 150 # release-date metadata fetched per batch
|
DATE_BATCH = 300 # release-date metadata fetched per batch
|
||||||
|
|
||||||
def _populate(self, items, show_count):
|
def _populate(self, items, show_count):
|
||||||
self._generation += 1
|
self._generation += 1
|
||||||
@@ -4856,12 +4875,13 @@ class MainWindow(QMainWindow):
|
|||||||
thread = QThread(self)
|
thread = QThread(self)
|
||||||
worker = LibraryWorker(self._ps1_dir, self._ps1_output_dir, self._nps_dir)
|
worker = LibraryWorker(self._ps1_dir, self._ps1_output_dir, self._nps_dir)
|
||||||
worker.moveToThread(thread)
|
worker.moveToThread(thread)
|
||||||
|
thread._worker = worker
|
||||||
thread.started.connect(worker.run)
|
thread.started.connect(worker.run)
|
||||||
worker.result.connect(self._on_library_loaded)
|
worker.result.connect(self._on_library_loaded)
|
||||||
worker.finished.connect(thread.quit)
|
|
||||||
worker.finished.connect(worker.deleteLater)
|
worker.finished.connect(worker.deleteLater)
|
||||||
|
worker.finished.connect(thread.quit)
|
||||||
thread.finished.connect(thread.deleteLater)
|
thread.finished.connect(thread.deleteLater)
|
||||||
thread.finished.connect(lambda t=thread, w=worker: self._forget_library_thread(t, w))
|
thread.finished.connect(self._forget_library_thread)
|
||||||
self._library_threads.append(thread)
|
self._library_threads.append(thread)
|
||||||
self._library_workers.append(worker)
|
self._library_workers.append(worker)
|
||||||
thread.start()
|
thread.start()
|
||||||
@@ -4875,10 +4895,12 @@ class MainWindow(QMainWindow):
|
|||||||
self.store_page.set_library_items(self._library_cache)
|
self.store_page.set_library_items(self._library_cache)
|
||||||
self.statusBar().showMessage("")
|
self.statusBar().showMessage("")
|
||||||
|
|
||||||
def _forget_library_thread(self, thread, worker):
|
def _forget_library_thread(self):
|
||||||
|
thread = self.sender()
|
||||||
|
worker = getattr(thread, "_worker", None)
|
||||||
if thread in self._library_threads:
|
if thread in self._library_threads:
|
||||||
self._library_threads.remove(thread)
|
self._library_threads.remove(thread)
|
||||||
if worker in self._library_workers:
|
if worker is not None and worker in self._library_workers:
|
||||||
self._library_workers.remove(worker)
|
self._library_workers.remove(worker)
|
||||||
|
|
||||||
def _apply_library_filter(self):
|
def _apply_library_filter(self):
|
||||||
@@ -5012,20 +5034,23 @@ class MainWindow(QMainWindow):
|
|||||||
thread = QThread(self)
|
thread = QThread(self)
|
||||||
worker = MetadataWorker(title, gen, system, rid)
|
worker = MetadataWorker(title, gen, system, rid)
|
||||||
worker.moveToThread(thread)
|
worker.moveToThread(thread)
|
||||||
|
thread._worker = worker
|
||||||
thread.started.connect(worker.run)
|
thread.started.connect(worker.run)
|
||||||
worker.result.connect(self._on_metadata)
|
worker.result.connect(self._on_metadata)
|
||||||
worker.finished.connect(worker.deleteLater)
|
worker.finished.connect(worker.deleteLater)
|
||||||
worker.finished.connect(thread.quit)
|
worker.finished.connect(thread.quit)
|
||||||
thread.finished.connect(thread.deleteLater)
|
thread.finished.connect(thread.deleteLater)
|
||||||
thread.finished.connect(lambda t=thread, w=worker: self._forget_meta_thread(t, w))
|
thread.finished.connect(self._forget_meta_thread)
|
||||||
self._meta_threads.append(thread)
|
self._meta_threads.append(thread)
|
||||||
self._meta_workers.append(worker)
|
self._meta_workers.append(worker)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
def _forget_meta_thread(self, thread, worker):
|
def _forget_meta_thread(self):
|
||||||
|
thread = self.sender()
|
||||||
|
worker = getattr(thread, "_worker", None)
|
||||||
if thread in self._meta_threads:
|
if thread in self._meta_threads:
|
||||||
self._meta_threads.remove(thread)
|
self._meta_threads.remove(thread)
|
||||||
if worker in self._meta_workers:
|
if worker is not None and worker in self._meta_workers:
|
||||||
self._meta_workers.remove(worker)
|
self._meta_workers.remove(worker)
|
||||||
|
|
||||||
def _on_metadata(self, gen, meta):
|
def _on_metadata(self, gen, meta):
|
||||||
@@ -5929,8 +5954,24 @@ class MainWindow(QMainWindow):
|
|||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
|
def _raise_nofile_limit():
|
||||||
|
"""Raise the soft file-descriptor limit to the hard limit.
|
||||||
|
|
||||||
|
Cover/metadata loading opens many short-lived sockets, and each worker
|
||||||
|
QThread's GLib event dispatcher creates a wakeup pipe. On systems with a
|
||||||
|
1024 soft limit this aborts the process with GLib's "Too many open files".
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import resource
|
||||||
|
_soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
|
||||||
|
resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard))
|
||||||
|
except Exception: # noqa: BLE001 - best effort, never block startup
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-7s %(name)-14s %(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-7s %(name)-14s %(message)s")
|
||||||
|
_raise_nofile_limit()
|
||||||
init_db() # migrate legacy on-share DB/covers to local storage, then schema
|
init_db() # migrate legacy on-share DB/covers to local storage, then schema
|
||||||
# Auto-ingest any missing DATs so scanning matches against the catalog
|
# Auto-ingest any missing DATs so scanning matches against the catalog
|
||||||
# even on a fresh/reset database (no manual `python -m app.ingest` step).
|
# even on a fresh/reset database (no manual `python -m app.ingest` step).
|
||||||
|
|||||||
Reference in New Issue
Block a user