Add NoPayStation themes, DLC and minis support

This commit is contained in:
2026-09-07 23:34:56 -04:00
parent 398c9f5dad
commit c94bcdc993
5 changed files with 282 additions and 62 deletions
+95 -11
View File
@@ -1681,7 +1681,7 @@ class StorePage(QWidget):
self.source_combo.currentIndexChanged.connect(self._on_source_changed)
bar.addWidget(self.source_combo)
self.nps_type = QComboBox()
self.nps_type.addItems(["PSP Games", "PS1 Classics"])
self.nps_type.addItems(["PSP Games", "PS1 Classics", "PSP Themes", "PSP DLC"])
self.nps_type.setVisible(False)
self.nps_type.currentIndexChanged.connect(self._on_nps_type_changed)
bar.addWidget(self.nps_type)
@@ -1788,7 +1788,7 @@ class StorePage(QWidget):
if self.source_combo.currentIndex() == 1:
return transfers.ArchiveOrgSource(self._archive_id, self._archive_sig, self._archive_user)
if self.source_combo.currentIndex() == 3:
ct = "psp" if self.nps_type.currentIndex() == 0 else "psx"
ct = ("psp", "psx", "theme", "dlc")[self.nps_type.currentIndex()]
return transfers.NoPayStationSource(ct, pkg2zip_path=self._pkg2zip)
return transfers.LocalShareSource(self._store_dir)
@@ -3266,11 +3266,13 @@ def _ps1_library_items(ps1_dir, output_dir, overrides, dates):
def _nps_library_items(nps_dir, overrides, dates):
"""NoPayStation-downloaded PSP games and PS1 Classics, merged into the library.
"""NoPayStation-downloaded content, merged into the library.
Scans ``<nps>/PSP/*/EBOOT.PBP`` (PSP PSN games) and ``<nps>/PSX/*/EBOOT.PBP``
(PS1 Classics). PSP games are ``system="games"`` with ``psp_kind="eboot"``;
PSX games are ``system="ps1"`` with ``ps1_kind="eboot"``.
Scans ``<nps>/PSP/*/EBOOT.PBP`` (PSP PSN games), ``<nps>/PSX/*/EBOOT.PBP``
(PS1 Classics), ``<nps>/THEME/*/*.ptf`` (themes) and ``<nps>/DLC/*/*.edat``
(DLC). PSP games are ``system="games"`` with ``psp_kind="eboot"``; PSX games
are ``system="ps1"`` with ``ps1_kind="eboot"``; themes/DLC use
``psp_kind="theme"/"dlc"``.
"""
if not nps_dir:
return []
@@ -3308,9 +3310,43 @@ def _nps_library_items(nps_dir, overrides, dates):
else:
item["psp_kind"] = "eboot"
out.append(item)
# Themes: <nps>/THEME/<TITLE_ID>/<name>.ptf (extension case varies)
theme_dir = root / "THEME"
if theme_dir.is_dir():
for ptf in sorted(theme_dir.rglob("*")):
if ptf.is_file() and ptf.suffix.lower() == ".ptf":
out.append(_nps_extra_item(ptf, "theme", ptf.stem))
# DLC: <nps>/DLC/<GAME_ID>/<name>.edat
dlc_dir = root / "DLC"
if dlc_dir.is_dir():
for edat in sorted(dlc_dir.rglob("*")):
if edat.is_file() and edat.suffix.lower() == ".edat":
out.append(_nps_extra_item(edat, "dlc", edat.stem))
return out
def _nps_extra_item(path, psp_kind, name):
try:
size = path.stat().st_size
except OSError:
size = 0
rid = covers.stable_key(name)
return {
"name": name,
"release_id": rid,
"system": psp_kind,
"canonical": name,
"serial": "",
"region": None,
"release_date": "",
"path": str(path),
"size": size,
"psp_kind": psp_kind,
}
class LibraryWorker(QObject):
result = Signal(list)
finished = Signal()
@@ -3957,7 +3993,7 @@ class MainWindow(QMainWindow):
self.library_search.textChanged.connect(self._apply_library_filter)
bar.addWidget(self.library_search, 1)
self.library_type = QComboBox()
self.library_type.addItems(["All types", "Games", "Videos", "PS1"])
self.library_type.addItems(["All types", "Games", "Videos", "PS1", "Themes", "DLC"])
self.library_type.currentTextChanged.connect(self._apply_library_filter)
bar.addWidget(self.library_type)
self.library_psp = QComboBox()
@@ -4227,6 +4263,10 @@ class MainWindow(QMainWindow):
continue
if typ == "PS1" and it["system"] != "ps1":
continue
if typ == "Themes" and it["system"] != "theme":
continue
if typ == "DLC" and it["system"] != "dlc":
continue
if q and q not in (it["canonical"] or it["name"]).lower():
continue
if psp_f == "On PSP" and not it.get("on_psp"):
@@ -4252,7 +4292,8 @@ class MainWindow(QMainWindow):
title.setIcon(self._psp_dot_icon())
title.setToolTip("Installed on PSP")
self.library_table.setItem(row, 0, title)
kind_label = {"games": "Game", "videos": "Video", "ps1": "PS1"}.get(it["system"], "Game")
kind_label = {"games": "Game", "videos": "Video", "ps1": "PS1",
"theme": "Theme", "dlc": "DLC"}.get(it["system"], "Game")
self.library_table.setItem(row, 1, QTableWidgetItem(kind_label))
self.library_table.setItem(row, 2, QTableWidgetItem(it.get("region") or ""))
self.library_table.setItem(row, 3, QTableWidgetItem(it.get("serial") or ""))
@@ -4410,7 +4451,7 @@ class MainWindow(QMainWindow):
if not self._nps_dir:
QMessageBox.warning(self, "No NPS output dir", "Set the NoPayStation output dir in Settings → Store.")
return
sub = "PSP" if src.content_type == "psp" else "PSX"
sub = {"psp": "PSP", "psx": "PSX", "theme": "THEME", "dlc": "DLC"}.get(src.content_type, "PSP")
dest_dir = Path(self._nps_dir) / sub
kind = "nps-download"
else:
@@ -4457,6 +4498,13 @@ class MainWindow(QMainWindow):
self._maybe_auto_sync()
def _on_psp(self, item):
psp_kind = item.get("psp_kind")
if psp_kind in ("theme", "dlc") and self._psp_root:
name = Path(item.get("path") or "").name
if psp_kind == "theme":
return (Path(self._psp_root) / "PSP" / "THEME" / name).exists()
game_id = Path(item.get("path") or "").parent.name
return (Path(self._psp_root) / "PSP" / "GAME" / game_id / name).exists()
if not self._psp_index:
return False
stem = Path(item.get("canonical") or item.get("name") or "").stem
@@ -4508,8 +4556,8 @@ class MainWindow(QMainWindow):
return # UMD Video discs can't be played in PPSSPP
if item.get("system") == "ps1" and item.get("ps1_kind") != "eboot":
return # raw PS1 discs are converted, not played
if item.get("psp_kind") == "eboot":
return # encrypted PSP PSN EBOOTs can't run in PPSSPP
if item.get("psp_kind") in ("eboot", "theme", "dlc"):
return # encrypted PSN EBOOTs/themes/DLC can't run in PPSSPP
path = item.get("path")
if not path or not Path(path).exists():
QMessageBox.warning(self, "Missing file", f"File not found:\n{path}")
@@ -4582,6 +4630,23 @@ class MainWindow(QMainWindow):
psp_root = Path(self._psp_root)
if system == "videos":
dest_dir = psp_root / "ISO" / "VIDEO"
elif item.get("psp_kind") == "theme":
# Themes install as a single .ptf under /PSP/THEME.
dest_name = Path(item["path"]).name
dest_dir = psp_root / "PSP" / "THEME"
src = transfers.FileCopySource(item["path"])
log.info("copying theme to psp: %s -> %s", item["path"], dest_dir / dest_name)
self.queue.enqueue(src, dest_dir, {"dest_name": dest_name}, dest_name, kind="psp-install")
return True
elif item.get("psp_kind") == "dlc":
# DLC installs as a single .edat into /PSP/GAME/<GAME_ID>.
game_id = Path(item["path"]).parent.name
dest_name = Path(item["path"]).name
dest_dir = psp_root / "PSP" / "GAME" / game_id
src = transfers.FileCopySource(item["path"])
log.info("copying dlc to psp: %s -> %s", item["path"], dest_dir / dest_name)
self.queue.enqueue(src, dest_dir, {"dest_name": dest_name}, dest_name, kind="psp-install")
return True
elif system == "ps1" or item.get("psp_kind") == "eboot":
# PS1 EBOOTs and NoPayStation PSP games install as a whole game
# folder under /PSP/GAME (optionally nested in a Category Lite
@@ -4674,6 +4739,25 @@ class MainWindow(QMainWindow):
QMessageBox.information(self, "Not on PSP", "That disc is not installed on the PSP.")
return
stem = Path(item.get("canonical") or item.get("name") or "").stem
psp_kind = item.get("psp_kind")
if psp_kind in ("theme", "dlc"):
name = Path(item.get("path") or "").name
target = (Path(self._psp_root) / "PSP" / "THEME" / name) if psp_kind == "theme" \
else (Path(self._psp_root) / "PSP" / "GAME" / Path(item.get("path") or "").parent.name / name)
if not target.exists():
QMessageBox.warning(self, "Not found", f"Could not find the installed copy on the PSP:\n{name}")
return
res = QMessageBox.question(
self, "Delete from PSP",
f"Delete this from the PSP?\n\n{name}\n\n{target}",
QMessageBox.Yes | QMessageBox.Cancel, QMessageBox.Cancel,
)
if res != QMessageBox.Yes:
return
if not transfers.remove_path(target):
QMessageBox.warning(self, "Delete incomplete", f"Could not delete:\n{target}")
self._refresh_psp()
return
if item.get("system") == "ps1" or item.get("psp_kind") == "eboot":
# PS1 EBOOTs / NoPayStation PSP games: delete the whole game folder,
# matched by the source folder id (or title).