Add PS1 games to the Library with covers, metadata, and EBOOT actions

This commit is contained in:
2026-09-07 16:20:39 -04:00
parent 777d316c0f
commit 4d5c48cb94
4 changed files with 201 additions and 78 deletions
+103 -9
View File
@@ -559,6 +559,7 @@ class MetadataWorker(QObject):
class GameDetailsPanel(QWidget):
edit_requested = Signal() # emitted when the user wants to hand-tag the item
link_requested = Signal() # emitted when the user wants to pick a movie/game to link
convert_requested = Signal() # emitted when the user wants to convert a PS1 disc to EBOOT
copy_requested = Signal() # emitted when the user wants to copy the current item to the PSP
delete_requested = Signal() # emitted when the user wants to delete the current item
play_requested = Signal() # emitted when the user wants to launch the item in PPSSPP
@@ -569,6 +570,7 @@ class GameDetailsPanel(QWidget):
self.setFixedWidth(300)
self._youtube_url = ""
self._meta = None
self._copy_available = False
outer = QVBoxLayout(self)
outer.setContentsMargins(12, 12, 0, 12)
@@ -627,6 +629,12 @@ class GameDetailsPanel(QWidget):
self.play_btn.setToolTip("Launch this game in PPSSPP")
self.layout.addWidget(self.play_btn)
self.convert_btn = QPushButton("\u2699 Convert to EBOOT")
self.convert_btn.clicked.connect(self.convert_requested.emit)
self.convert_btn.setVisible(False)
self.convert_btn.setToolTip("Convert this PS1 disc to an EBOOT.PBP")
self.layout.addWidget(self.convert_btn)
btn_grid = QGridLayout()
btn_grid.setHorizontalSpacing(6)
btn_grid.setVerticalSpacing(6)
@@ -658,14 +666,28 @@ class GameDetailsPanel(QWidget):
def set_item(self, item):
self.system = item.get("system")
self._ps1_kind = item.get("ps1_kind")
path = item.get("path") or ""
self.play_btn.setEnabled(item.get("system") == "games" and bool(path) and Path(path).suffix.lower() == ".iso")
is_ps1 = item.get("system") == "ps1"
is_ps1_disc = is_ps1 and item.get("ps1_kind") == "disc"
self.play_btn.setEnabled(
(item.get("system") == "games" and bool(path) and Path(path).suffix.lower() == ".iso")
or (is_ps1 and item.get("ps1_kind") == "eboot" and bool(path))
)
self.play_btn.setVisible(not is_ps1_disc)
self.convert_btn.setVisible(is_ps1_disc)
self.copy_btn.setEnabled(self._copy_available and not is_ps1_disc)
self.copy_btn.setToolTip(
"Copy this EBOOT to /PSP/GAME" if is_ps1 else "Copy this disc to the connected PSP"
)
self.del_psp_btn.setEnabled(bool(item.get("on_psp")))
self.title.setText(item.get("title_override") or item.get("name") or "Unknown")
parts = []
if item.get("serial"):
parts.append(item["serial"])
if item.get("region"):
if is_ps1 and item.get("ps1_status"):
parts.append(item["ps1_status"])
elif item.get("region"):
parts.append(item["region"])
self.subtitle.setText(" · ".join(parts))
self._clear_meta()
@@ -676,7 +698,8 @@ class GameDetailsPanel(QWidget):
self._set_cover(item.get("release_id"), item.get("system"))
def set_copy_available(self, available):
self.copy_btn.setEnabled(bool(available))
self._copy_available = bool(available)
self.copy_btn.setEnabled(self._copy_available and getattr(self, "_ps1_kind", None) != "disc")
def set_psp_installed(self, on_psp):
self.del_psp_btn.setEnabled(bool(on_psp))
@@ -875,7 +898,7 @@ def _scrape_cover(system, candidate, rid, timeout=20):
if data:
if system == "videos":
return compose_umd_cover(data)
if candidate.get("source") == "igdb":
if candidate.get("source") == "igdb" and system == "games":
return covers.compose_game_cover(data)
return data
if title:
@@ -3024,7 +3047,7 @@ class PspSyncPage(QWidget):
self._workers = []
def build_library_items():
def build_library_items(ps1_dir="", output_dir=""):
"""Build the library item list from the DB with a single joined query."""
items = []
seen = set()
@@ -3093,16 +3116,59 @@ def build_library_items():
conn.commit()
finally:
conn.close()
items.extend(_ps1_library_items(ps1_dir, output_dir, overrides, dates))
return items
def _ps1_library_items(ps1_dir, output_dir, overrides, dates):
"""PS1 discs (pending) + converted EBOOTs, merged into the library list."""
if not ps1_dir:
return []
try:
entries = ps1.scan_library(ps1_dir, output_dir)
except Exception: # noqa: BLE001
return []
out = []
for e in entries:
# A converted disc is represented by its EBOOT entry (one row per game).
if e["kind"] == "disc" and e.get("status") == "converted":
continue
name = e["name"]
rid = covers.stable_key(name)
path = Path(e["path"])
try:
size = path.stat().st_size if path.exists() else 0
except OSError:
size = 0
out.append({
"name": name,
"release_id": rid,
"system": "ps1",
"canonical": name,
"serial": e.get("serial") or "",
"region": None,
"title_override": overrides.get(("ps1", str(rid))),
"release_date": dates.get((str(rid), "ps1")) or "",
"path": str(path),
"size": size,
"ps1_kind": e["kind"],
"ps1_status": e.get("status") or "pending",
})
return out
class LibraryWorker(QObject):
result = Signal(list)
finished = Signal()
def __init__(self, ps1_dir="", output_dir=""):
super().__init__()
self.ps1_dir = ps1_dir
self.output_dir = output_dir
def run(self):
try:
items = build_library_items()
items = build_library_items(self.ps1_dir, self.output_dir)
except Exception: # noqa: BLE001
items = []
self.result.emit(items)
@@ -3470,7 +3536,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"])
self.library_type.addItems(["All types", "Games", "Videos", "PS1"])
self.library_type.currentTextChanged.connect(self._apply_library_filter)
bar.addWidget(self.library_type)
self.library_psp = QComboBox()
@@ -3520,6 +3586,7 @@ class MainWindow(QMainWindow):
self.cover_grid.scrape_requested.connect(self._scrape_grid_item)
self.details_panel.edit_requested.connect(self._edit_details)
self.details_panel.link_requested.connect(self._scrape_item)
self.details_panel.convert_requested.connect(self._convert_current_item)
self.details_panel.copy_requested.connect(self._copy_current_to_psp)
self.details_panel.delete_requested.connect(self._delete_current)
self.details_panel.delete_psp_requested.connect(self._delete_current_from_psp)
@@ -3688,7 +3755,7 @@ class MainWindow(QMainWindow):
self.statusBar().showMessage("Loading library…")
log.info("loading library…")
thread = QThread(self)
worker = LibraryWorker()
worker = LibraryWorker(self._ps1_dir, self._ps1_output_dir)
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.result.connect(self._on_library_loaded)
@@ -3726,6 +3793,8 @@ class MainWindow(QMainWindow):
continue
if typ == "Videos" and it["system"] != "videos":
continue
if typ == "PS1" and it["system"] != "ps1":
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"):
@@ -3751,7 +3820,8 @@ class MainWindow(QMainWindow):
title.setIcon(self._psp_dot_icon())
title.setToolTip("Installed on PSP")
self.library_table.setItem(row, 0, title)
self.library_table.setItem(row, 1, QTableWidgetItem("Game" if it["system"] == "games" else "Video"))
kind_label = {"games": "Game", "videos": "Video", "ps1": "PS1"}.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 ""))
size = it.get("size")
@@ -3886,6 +3956,18 @@ class MainWindow(QMainWindow):
self.details_panel.set_item(it)
self._scrape_item()
def _convert_current_item(self):
item = getattr(self, "_current_item", None)
if item is None or item.get("system") != "ps1" or item.get("ps1_kind") != "disc":
return
game_id = item.get("serial") or ""
if not game_id:
text, ok = QInputDialog.getText(self, "PS1", "Game ID (e.g. SLUS00594):")
if not ok or not text.strip():
return
game_id = text.strip()
self._on_ps1_convert({"source": item.get("path"), "title": item.get("name"), "game_id": game_id})
def _on_store_download(self, items):
if not items:
return
@@ -3967,6 +4049,8 @@ class MainWindow(QMainWindow):
return
if item.get("system") == "videos":
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
path = item.get("path")
if not path or not Path(path).exists():
QMessageBox.warning(self, "Missing file", f"File not found:\n{path}")
@@ -4039,6 +4123,15 @@ class MainWindow(QMainWindow):
psp_root = Path(self._psp_root)
if system == "videos":
dest_dir = psp_root / "ISO" / "VIDEO"
elif system == "ps1":
# PS1 EBOOTs install to /PSP/GAME/<folder>/EBOOT.PBP.
folder = item.get("serial") or item.get("name") or "PS1"
folder = "".join(c for c in folder if c not in '/\\:*?"<>|').strip() or "PS1"
dest_dir = psp_root / "PSP" / "GAME" / folder
log.info("copying ps1 eboot to psp: %s -> %s", item["path"], dest_dir / "EBOOT.PBP")
self.queue.enqueue(transfers.FileCopySource(item["path"]), dest_dir,
{"dest_name": "EBOOT.PBP"}, item.get("name") or folder, kind="psp-install")
return True
else:
cat = "".join(c for c in (self._iso_category or "").strip() if c not in "/\\")
dest_dir = (psp_root / "ISO" / cat) if cat else (psp_root / "ISO")
@@ -4179,6 +4272,7 @@ class MainWindow(QMainWindow):
QTimer.singleShot(0, self._refresh_saves)
if kind == "ps1-convert":
QTimer.singleShot(0, self.ps1_page.refresh)
QTimer.singleShot(0, self.load_library)
# -- Common ------------------------------------------------------------ #
@staticmethod