From 46c675ce79106cb17b41a715fae10a7a5a4d41c3 Mon Sep 17 00:00:00 2001 From: Mathieu Moran Date: Tue, 8 Sep 2026 22:45:36 -0400 Subject: [PATCH] PSP Sync: list themes and DLC in the device inventory --- README.md | 4 ++-- app/gui.py | 43 ++++++++++++++++++++++++++++++++++--------- app/psp.py | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 475509a..3291e05 100644 --- a/README.md +++ b/README.md @@ -379,8 +379,8 @@ token**, then pick **Server** in the Store tab's source selector. Use Connect a PSP over USB (or mount a UMD) and the **PSP Sync** tab detects it: -- **Games / UMD Videos / Homebrew-PSN / PKG** — tables of what's installed, with - delete-from-PSP (checkboxes and right-click). +- **Games / UMD Videos / Homebrew-PSN / Themes / DLC / PKG** — tables of what's + installed, with delete-from-PSP (checkboxes and right-click). - **UMD** — reads `PARAM.SFO` from a mounted UMD, shows the title/serial, and can dump the disc image into your library. - **Sync from Library** — copy discs from your library onto the PSP, optionally diff --git a/app/gui.py b/app/gui.py index 7bc1a7a..98388bb 100644 --- a/app/gui.py +++ b/app/gui.py @@ -2599,7 +2599,7 @@ class PspSyncPage(QWidget): def __init__(self, parent=None): super().__init__(parent) self._root = "" - self._inventory = {"games": [], "videos": [], "homebrew": [], "pkg": []} + self._inventory = {"games": [], "videos": [], "homebrew": [], "pkg": [], "themes": [], "dlc": []} self._library_items = [] self._lib_raw = {} self._lib_norm = {} @@ -2638,16 +2638,21 @@ class PspSyncPage(QWidget): self.games_table = self._make_table(["", "Name", "Category", "Added", "Release", "Region", "Size"], stretch_col=1) self.videos_table = self._make_table(["", "Name", "Added", "Size"], stretch_col=1) self.homebrew_table = self._make_table(["", "ID", "Name", "Added", "Size"], stretch_col=2) + self.themes_table = self._make_table(["", "Name", "Added", "Size"], stretch_col=1) + self.dlc_table = self._make_table(["", "Game", "Name", "Added", "Size"], stretch_col=2) self.pkg_table = self._make_table(["", "Name", "Added", "Size"], stretch_col=1) self.tabs.addTab(self.games_table, "Games") self.tabs.addTab(self.videos_table, "UMD Videos") self.tabs.addTab(self.homebrew_table, "Homebrew / PSN") + self.tabs.addTab(self.themes_table, "Themes") + self.tabs.addTab(self.dlc_table, "DLC") self.tabs.addTab(self.pkg_table, "PKG") self.tabs.addTab(self._build_umd_tab(), "UMD") self.tabs.addTab(self._build_sync_tab(), "Sync from Library") layout.addWidget(self.tabs, 1) - for tbl in (self.games_table, self.videos_table, self.homebrew_table, self.pkg_table): + for tbl in (self.games_table, self.videos_table, self.homebrew_table, + self.themes_table, self.dlc_table, self.pkg_table): tbl.itemSelectionChanged.connect(self._update_delete_enabled) tbl.itemChanged.connect(self._update_delete_enabled) tbl.setContextMenuPolicy(Qt.CustomContextMenu) @@ -2711,7 +2716,7 @@ class PspSyncPage(QWidget): self.sync_search.textChanged.connect(self._filter_sync) bar.addWidget(self.sync_search, 1) self.sync_type = QComboBox() - self.sync_type.addItems(["All", "Games", "Videos"]) + self.sync_type.addItems(["All", "Games", "Videos", "PS1", "Themes", "DLC"]) self.sync_type.currentTextChanged.connect(self._filter_sync) bar.addWidget(self.sync_type) self.sync_installed = QComboBox() @@ -2883,6 +2888,12 @@ class PspSyncPage(QWidget): continue if typ == "Videos" and it.get("system") != "videos": continue + if typ == "PS1" and it.get("system") != "ps1": + continue + if typ == "Themes" and it.get("system") != "theme": + continue + if typ == "DLC" and it.get("system") != "dlc": + continue on_psp = bool(it.get("on_psp")) if inst == "Not installed" and on_psp: continue @@ -2906,7 +2917,8 @@ class PspSyncPage(QWidget): title = QTableWidgetItem(label) title.setData(Qt.UserRole, it) self.sync_table.setItem(row, 1, title) - kind_label = {"games": "Game", "videos": "Video", "ps1": "PS1"}.get(it.get("system"), "Game") + kind_label = {"games": "Game", "videos": "Video", "ps1": "PS1", + "theme": "Theme", "dlc": "DLC"}.get(it.get("system"), "Game") self.sync_table.setItem(row, 2, QTableWidgetItem(kind_label)) size = it.get("size") self.sync_table.setItem(row, 3, NumericItem(f"{size / (1024 * 1024):.1f} MB" if size else "", size or 0)) @@ -2959,7 +2971,7 @@ class PspSyncPage(QWidget): self._gen += 1 gen = self._gen if not self._root: - self._inventory = {"games": [], "videos": [], "homebrew": [], "pkg": []} + self._inventory = {"games": [], "videos": [], "homebrew": [], "pkg": [], "themes": [], "dlc": []} self.status.setText("No PSP connected") self._populate(self._inventory) return @@ -2979,21 +2991,26 @@ class PspSyncPage(QWidget): if gen != self._gen: return if "error" in res: - self._inventory = {"games": [], "videos": [], "homebrew": [], "pkg": []} + self._inventory = {"games": [], "videos": [], "homebrew": [], "pkg": [], "themes": [], "dlc": []} self.status.setText(f"Error reading PSP: {res['error']}") log.warning("psp inventory error: %s", res["error"]) else: inventory = res["inventory"] self._inventory = inventory - log.info("psp inventory: %d game(s), %d video(s), %d homebrew, %d pkg", + log.info("psp inventory: %d game(s), %d video(s), %d homebrew, %d theme(s), %d dlc, %d pkg", len(inventory["games"]), len(inventory["videos"]), - len(inventory["homebrew"]), len(inventory.get("pkg", []))) + len(inventory["homebrew"]), len(inventory.get("themes", [])), + len(inventory.get("dlc", [])), len(inventory.get("pkg", []))) usage = res.get("usage") parts = [ f"{len(inventory['games'])} game(s)", f"{len(inventory['videos'])} video(s)", f"{len(inventory['homebrew'])} homebrew", ] + if inventory.get("themes"): + parts.append(f"{len(inventory['themes'])} theme(s)") + if inventory.get("dlc"): + parts.append(f"{len(inventory['dlc'])} dlc") if usage is not None: free_mb = usage["free"] / (1024 * 1024) total_mb = usage["total"] / (1024 * 1024) @@ -3007,6 +3024,8 @@ class PspSyncPage(QWidget): self._fill_table(self.games_table, inventory.get("games", []), "games") self._fill_table(self.videos_table, inventory.get("videos", []), "videos") self._fill_table(self.homebrew_table, inventory.get("homebrew", []), "homebrew") + self._fill_table(self.themes_table, inventory.get("themes", []), "theme") + self._fill_table(self.dlc_table, inventory.get("dlc", []), "dlc") self._fill_table(self.pkg_table, inventory.get("pkg", []), "pkg") @staticmethod @@ -3055,6 +3074,11 @@ class PspSyncPage(QWidget): table.setItem(row, 2, name) table.setItem(row, 3, QTableWidgetItem(added)) table.setItem(row, 4, NumericItem(f"{size / (1024 * 1024):.1f} MB" if size else "", size)) + elif kind == "dlc": + table.setItem(row, 1, QTableWidgetItem(it.get("id") or "")) + table.setItem(row, 2, name) + table.setItem(row, 3, QTableWidgetItem(added)) + table.setItem(row, 4, NumericItem(f"{size / (1024 * 1024):.1f} MB" if size else "", size)) else: table.setItem(row, 1, name) table.setItem(row, 2, QTableWidgetItem(added)) @@ -3064,7 +3088,8 @@ class PspSyncPage(QWidget): def _current_inventory_table(self): w = self.tabs.currentWidget() - if w in (self.games_table, self.videos_table, self.homebrew_table, self.pkg_table): + if w in (self.games_table, self.videos_table, self.homebrew_table, + self.themes_table, self.dlc_table, self.pkg_table): return w return None diff --git a/app/psp.py b/app/psp.py index 47b025f..64b731d 100644 --- a/app/psp.py +++ b/app/psp.py @@ -221,8 +221,39 @@ def _pkg_items(root): return items +def _theme_items(root): + """Installed themes (``PSP/THEME/*.ptf``).""" + items = [] + theme_dir = root / "PSP" / "THEME" + if theme_dir.is_dir(): + try: + entries = sorted(theme_dir.iterdir()) + except OSError: + return items + for path in entries: + if path.is_file() and path.suffix.lower() == ".ptf": + items.append(_file_item(path, "theme")) + return items + + +def _dlc_items(root): + """Installed DLC (``PSP/GAME//*.edat``).""" + items = [] + game_dir = root / "PSP" / "GAME" + if game_dir.is_dir(): + for path in sorted(game_dir.rglob("*")): + if not path.is_file() or path.suffix.lower() != ".edat": + continue + if path.name.upper() == "ISO.BIN.EDAT": + continue # package wrapper, not DLC content + item = _file_item(path, "dlc") + item["id"] = path.parent.name + items.append(item) + return items + + def list_installed(root): - """Return ``{games, videos, homebrew, pkg}`` inventories for a PSP root.""" + """Return ``{games, videos, homebrew, pkg, themes, dlc}`` for a PSP root.""" root = Path(root) games, videos, homebrew_isos = _iso_items(root) return { @@ -230,6 +261,8 @@ def list_installed(root): "videos": videos, "homebrew": _homebrew_items(root) + homebrew_isos, "pkg": _pkg_items(root), + "themes": _theme_items(root), + "dlc": _dlc_items(root), }