store: add NoPayStation source (PSP + PS1 Classics .pkg)

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.
This commit is contained in:
2026-09-07 21:18:55 -04:00
parent 4b6e407b3f
commit 53f671d09a
5 changed files with 444 additions and 42 deletions
+46
View File
@@ -463,3 +463,49 @@ class LocalShareSource(Source):
return dest
finally:
shutil.rmtree(tmp, ignore_errors=True)
class NoPayStationSource(Source):
"""Browse NoPayStation .pkg lists and install PSP/PSX PSN content.
``content_type`` is ``"psp"`` (PSP_GAMES.tsv) or ``"psx"`` (PSX_GAMES.tsv).
Downloading decrypts the ``.pkg`` with the ``pkg2zip`` binary and writes a
``<TITLE_ID>/EBOOT.PBP`` game folder into ``dest_dir``.
"""
label = "NoPayStation"
def __init__(self, content_type, tsv_url="", pkg2zip_path=""):
from . import nopaystation
self.content_type = content_type
self.tsv_url = tsv_url or nopaystation.tsv_url(content_type)
self.pkg2zip = pkg2zip_path or ""
def scan(self, root=None):
from . import nopaystation
return nopaystation.fetch_items(self.content_type, self.tsv_url)
def download(self, item, dest_dir, progress_cb=None, cancel_event=None):
from . import nopaystation
dest_dir = Path(dest_dir)
dest_dir.mkdir(parents=True, exist_ok=True)
pkg2zip = nopaystation.find_pkg2zip(self.pkg2zip)
if pkg2zip is None:
raise RuntimeError(
"pkg2zip not found — install it (https://github.com/mmozeiko/pkg2zip) "
"or set its path in Settings → Store."
)
title_id = item.get("title_id") or item.get("content_id") or ""
pkg_name = re.sub(r"[^A-Za-z0-9_-]", "", title_id) or "pkg"
tmp = Path(tempfile.mkdtemp(prefix="nps-download-"))
pkg = tmp / f"{pkg_name}.pkg"
try:
if progress_cb is not None:
progress_cb("Downloading", 0.0, "")
download_url(item["pkg_url"], pkg, None, progress_cb, cancel_event)
folder = nopaystation.extract_pkg(pkg, title_id, dest_dir, pkg2zip,
progress_cb, cancel_event)
return folder
finally:
shutil.rmtree(tmp, ignore_errors=True)