diff --git a/README.md b/README.md index f8f57f3..608f26f 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,8 @@ python3 -m app.openvgdb # OpenVGDB/GameFAQs -> openvgdb-covers.zip The **PS1** tab scans a folder of PlayStation 1 rips and converts them into PSP `EBOOT.PBP` files with a pure-Python popstation converter (no external tools). +PS1 discs (pending) and converted EBOOTs also show up in the **Library** tab +(filter by **PS1**), where you can convert a disc or copy an EBOOT to the PSP. - **Source formats** — `.iso`, `.img`, `.bin` (with a matching `.cue` for multi-track discs). diff --git a/app/covers.py b/app/covers.py index a1bedd4..24970ac 100644 --- a/app/covers.py +++ b/app/covers.py @@ -24,16 +24,31 @@ from .db import get_connection log = logging.getLogger("covers") -_LIBRETRO_BASE = "https://raw.githubusercontent.com/libretro-thumbnails/Sony_-_PlayStation_Portable/master" _LIBRETRO_KINDS = ("Named_Boxarts",) -_TREE_URL = "https://api.github.com/repos/libretro-thumbnails/Sony_-_PlayStation_Portable/git/trees/master?recursive=1" +_PLATFORMS = { + "games": {"libretro_repo": "Sony_-_PlayStation_Portable", "igdb_id": 38, "ss_id": "61"}, + "ps1": {"libretro_repo": "Sony_-_PlayStation", "igdb_id": 7, "ss_id": "57"}, +} + + +def _platform(system): + """Platform parameters (libretro repo, IGDB id, ScreenScraper id).""" + return _PLATFORMS.get(system) or _PLATFORMS["games"] + + +def _libretro_base(system="games"): + return ("https://raw.githubusercontent.com/libretro-thumbnails/" + f"{_platform(system)['libretro_repo']}/master") + + +def _libretro_tree_url(system="games"): + return ("https://api.github.com/repos/libretro-thumbnails/" + f"{_platform(system)['libretro_repo']}/git/trees/master?recursive=1") _SS_API = "https://api.screenscraper.fr/api2/jeuInfos.php" -_SS_PSP_ID = "61" # Sony PSP _IGDB_TOKEN_URL = "https://id.twitch.tv/oauth2/token" _IGDB_API = "https://api.igdb.com/v4/games" _IGDB_IMAGE = "https://images.igdb.com/igdb/image/upload/" -_IGDB_PSP_ID = 38 # Sony PSP _TMDB_SEARCH = "https://api.themoviedb.org/3/search/movie" _TMDB_IMAGE = "https://image.tmdb.org/t/p/" _GOOGLE_CSE_API = "https://www.googleapis.com/customsearch/v1" @@ -52,7 +67,7 @@ _igdb_token_state = {"token": "", "expires": 0.0} # IGDB OAuth2 token cache _igdb_lock = threading.Lock() _igdb_last = 0.0 # monotonic clock of the last IGDB API call -_index_state = {"ts": 0.0, "names": {}, "norm": {}} +_index_state = {} # system -> {"ts", "names", "norm"} _INDEX_TTL = 86400.0 # refresh the libretro file index once a day _neg_cache = {} # cache key -> time.time() of the last failed fetch @@ -187,9 +202,9 @@ def ensure_cover_bundles(server_url=""): return downloaded -def cover_url(name, kind="Named_Boxarts"): +def cover_url(name, kind="Named_Boxarts", system="games"): """Public libretro URL of an image for a catalog name.""" - return f"{_LIBRETRO_BASE}/{kind}/{urllib.parse.quote(name)}.png" + return f"{_libretro_base(system)}/{kind}/{urllib.parse.quote(name)}.png" def cover_path(release_id, system): @@ -261,7 +276,7 @@ def _candidate_names(name): yield s -def _fetch_png(name, kind, timeout): +def _fetch_png(name, kind, timeout, system="games"): """Fetch a real image from libretro, following symlink text bodies. GitHub serves a repo symlink as a tiny text body containing the target @@ -269,7 +284,7 @@ def _fetch_png(name, kind, timeout): """ target = name for _ in range(_MAX_HOPS + 1): - data = _http_get(cover_url(target, kind), timeout) + data = _http_get(cover_url(target, kind, system), timeout) if data is None: return None if _is_image(data): @@ -284,26 +299,29 @@ def _fetch_png(name, kind, timeout): # --------------------------------------------------------------------------- # # libretro filename index + fuzzy matching # --------------------------------------------------------------------------- # -def _index_path(): - return config.COVERS_DIR / "libretro-index.json" +def _index_path(system="games"): + if system == "games": + return config.COVERS_DIR / "libretro-index.json" + return config.COVERS_DIR / f"libretro-index-{system}.json" -def _load_index(force=False): - st = _index_state +def _load_index(force=False, system="games"): + st = _index_state.get(system) or {"ts": 0.0, "names": {}, "norm": {}} + _index_state[system] = st if not force and st["names"] and time.time() - st["ts"] < _INDEX_TTL: return st["names"] - path = _index_path() + path = _index_path(system) if not force and path.exists(): try: # Load the on-disk index so it doubles as a fallback if the network # is down, but only return it immediately while it's still fresh. names = json.loads(path.read_text(encoding="utf-8")) - _set_index(names) + _set_index(names, system) if time.time() - path.stat().st_mtime < _INDEX_TTL: return names except Exception: pass - data = _http_get(_TREE_URL, timeout=30) + data = _http_get(_libretro_tree_url(system), timeout=30) if data: try: tree = json.loads(data).get("tree", []) @@ -314,7 +332,7 @@ def _load_index(force=False): prefix = kind + "/" if p.startswith(prefix) and p.endswith(".png"): names[kind].append(p[len(prefix):-4]) - _set_index(names) + _set_index(names, system) try: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(names), encoding="utf-8") @@ -326,9 +344,10 @@ def _load_index(force=False): return st["names"] or {kind: [] for kind in _LIBRETRO_KINDS} -def _set_index(names): +def _set_index(names, system="games"): """Store the raw filename index plus a pre-normalized copy for fast lookup.""" - st = _index_state + st = _index_state.get(system) or {"ts": 0.0, "names": {}, "norm": {}} + _index_state[system] = st st["names"] = names st["norm"] = {k: [(_norm(n), n) for n in v] for k, v in names.items()} st["ts"] = time.time() @@ -344,11 +363,12 @@ def _plain(s): return re.sub(r"\s*\([^()]*\)", "", s).strip() -def _index_best(name, kind): +def _index_best(name, kind, system="games"): """Return the best-matching repo filename for `name`, or None.""" - st = _index_state + st = _index_state.get(system) or {"ts": 0.0, "names": {}, "norm": {}} if not st["norm"]: - _load_index() + _load_index(system=system) + st = _index_state.get(system) or st pairs = st["norm"].get(kind, []) db = _norm(name) if not pairs or len(db) < 5: @@ -385,24 +405,25 @@ def _index_best(name, kind): return best -def _fetch_libretro(name, timeout, indexed_only=False): - # 0. Shipped offline bundle first (fast, no network). - data = fetch_libretro_local(name) - if data is not None: - return data +def _fetch_libretro(name, timeout, system="games", indexed_only=False): + # 0. Shipped offline bundle first (fast, no network) — PSP only for now. + if system == "games": + data = fetch_libretro_local(name) + if data is not None: + return data if not indexed_only: # 1. Cheap: exact + progressively stripped names (no index needed). for kind in _LIBRETRO_KINDS: for cand in _candidate_names(name): - data = _fetch_png(cand, kind, timeout) + data = _fetch_png(cand, kind, timeout, system) if data is not None: return data # 2. Fuzzy/exact via the precomputed index (covers exact names too, so the # bulk loader skips the wasteful exact pass entirely). for kind in _LIBRETRO_KINDS: - best = _index_best(name, kind) + best = _index_best(name, kind, system) if best: - data = _fetch_png(best, kind, timeout) + data = _fetch_png(best, kind, timeout, system) if data is not None: return data return None @@ -577,7 +598,7 @@ def _screenscraper_box_urls(jeu): return urls -def _fetch_screenscraper(name, timeout): +def _fetch_screenscraper(name, timeout, system="games"): creds = sources.load() devid = creds.get("screenscraper_devid") devpassword = creds.get("screenscraper_devpassword") @@ -588,7 +609,7 @@ def _fetch_screenscraper(name, timeout): "devpassword": devpassword, "softname": creds.get("screenscraper_softname") or "psp-preserve", "output": "json", - "systemeid": _SS_PSP_ID, + "systemeid": _platform(system)["ss_id"], "romtype": "rom", "romnom": name, } @@ -834,19 +855,20 @@ def fetch_cover(name, release_id, system, timeout=10, quick=False): if data is not None: log.info("cover %r <- manual", name) elif system != "videos": - # 2. Games: libretro (full box art) first, then the shipped OpenVGDB - # bundle (offline), then ScreenScraper (full box art, non-quick), - # then Wikipedia (keyless, non-quick), then IGDB last — IGDB only - # stores cropped 3:4 key art. - data = _fetch_libretro(name, t, indexed_only=quick) + # 2. Games/PS1: libretro (full box art) first, then the shipped + # OpenVGDB bundle (PSP only, offline), then ScreenScraper (non-quick), + # then Wikipedia (keyless, non-quick), then IGDB last. + data = _fetch_libretro(name, t, system=system, indexed_only=quick) if data is not None: log.info("cover %r <- libretro", name) else: - data = fetch_openvgdb_local(name) - if data is not None: - log.info("cover %r <- openvgdb", name) - elif not quick and time.time() < deadline: - data = _fetch_screenscraper(name, t) + data = None + if system == "games": + data = fetch_openvgdb_local(name) + if data is not None: + log.info("cover %r <- openvgdb", name) + if data is None and not quick and time.time() < deadline: + data = _fetch_screenscraper(name, t, system=system) if data is not None: log.info("cover %r <- screenscraper", name) if data is None and not quick and time.time() < deadline: @@ -854,11 +876,12 @@ def fetch_cover(name, release_id, system, timeout=10, quick=False): if data is not None: log.info("cover %r <- wikipedia", name) if data is None and time.time() < deadline: - data = _fetch_igdb_cover(name, t) + data = _fetch_igdb_cover(name, t, system=system) if data is not None: - # IGDB only has cropped 3:4 key art; composite it into the - # full PSP cover template (header bar + artwork). - data = compose_game_cover(data) + if system == "games": + # IGDB only has cropped 3:4 PSP key art; composite it + # into the full PSP cover template (header + artwork). + data = compose_game_cover(data) log.info("cover %r <- igdb", name) if data is None: @@ -1063,8 +1086,8 @@ def _wiki_file_search(query, limit, timeout): return out -def _search_libretro(query, limit): - names = _load_index().get("Named_Boxarts", []) +def _search_libretro(query, limit, system="games"): + names = _load_index(system=system).get("Named_Boxarts", []) db = _norm(query) results = [] if not db: @@ -1090,8 +1113,8 @@ def _search_libretro(query, limit): results.append({ "title": n, "source": "libretro", - "thumb_url": cover_url(n), - "image_url": cover_url(n), + "thumb_url": cover_url(n, system=system), + "image_url": cover_url(n, system=system), }) return results @@ -1141,9 +1164,9 @@ def _search_openvgdb(query, limit): _META_TTL = 86400.0 -def get_game_metadata(title, timeout=10): - """Return game metadata from IGDB.""" - return get_game_metadata_igdb(title, timeout) +def get_game_metadata(title, timeout=10, system="games"): + """Return game metadata from IGDB (system picks the platform).""" + return get_game_metadata_igdb(title, timeout, system) # --------------------------------------------------------------------------- # @@ -1268,28 +1291,30 @@ def _igdb_game_meta(g): } -def _igdb_search(query, limit=20, timeout=10): +def _igdb_search(query, limit=20, timeout=10, system="games"): cid, _ = _igdb_creds() if not cid: return [] term = plain_title(query) or query + platform_id = _platform(system)["igdb_id"] q = ( f'search "{term}"; ' "fields name, cover.image_id, first_release_date, summary, genres.name, " "involved_companies.company.name, involved_companies.developer, involved_companies.publisher, " "rating, videos.video_id; " - f"where platforms = ({_IGDB_PSP_ID}); " + f"where platforms = ({platform_id}); " f"limit {max(limit, 1)};" ) return _igdb_post(q, timeout) or [] -def _fetch_igdb_cover(name, timeout): +def _fetch_igdb_cover(name, timeout, system="games"): cid, _ = _igdb_creds() if not cid: return None term = plain_title(name) or name - q = f'search "{term}"; fields cover.image_id; where platforms = ({_IGDB_PSP_ID}); limit 10;' + platform_id = _platform(system)["igdb_id"] + q = f'search "{term}"; fields cover.image_id; where platforms = ({platform_id}); limit 10;' games = _igdb_post(q, timeout) or [] tried = set() for g in games: @@ -1303,9 +1328,9 @@ def _fetch_igdb_cover(name, timeout): return None -def search_games_igdb(query, limit=20, timeout=10): +def search_games_igdb(query, limit=20, timeout=10, system="games"): out = [] - for g in _igdb_search(query, limit, timeout): + for g in _igdb_search(query, limit, timeout, system): cover = (g.get("cover") or {}).get("image_id") or "" out.append({ "id": g.get("id"), @@ -1320,8 +1345,8 @@ def search_games_igdb(query, limit=20, timeout=10): return out[:limit] -def get_game_metadata_igdb(title, timeout=10): - games = _igdb_search(title, limit=10, timeout=timeout) +def get_game_metadata_igdb(title, timeout=10, system="games"): + games = _igdb_search(title, limit=10, timeout=timeout, system=system) if not games: return None games.sort(key=lambda g: -_title_score(title, g.get("name") or "")) @@ -1545,8 +1570,8 @@ def movie_metadata_from_wikipedia(page_title, timeout=10): def search_metadata_candidates(system, query, limit=20, timeout=10): """Search candidates the user can link an item to. - Games: IGDB (rich metadata) first, then libretro box art and OpenVGDB - (GameFAQs) box art so covers can be scraped even without an IGDB key. + Games/PS1: IGDB (rich metadata) first, then libretro box art (and the + PSP-only OpenVGDB bundle) so covers can be scraped even without an IGDB key. """ if system == "videos": return search_movies(query, limit, timeout) @@ -1559,13 +1584,15 @@ def search_metadata_candidates(system, query, limit=20, timeout=10): seen.add(key) results.append(r) - for r in search_games_igdb(query, limit, timeout): + # Cap IGDB so libretro/OpenVGDB cover picks still fit in the list. + for r in search_games_igdb(query, max(1, min(limit, 6)), timeout, system): add(r) - for r in _search_libretro(query, limit): + for r in _search_libretro(query, limit, system): r.setdefault("id", None) add(r) - for r in _search_openvgdb(query, limit): - add(r) + if system == "games": + for r in _search_openvgdb(query, limit): + add(r) return results[:limit] @@ -1596,7 +1623,7 @@ def link_metadata(system, candidate, timeout=10): # user still gets synopsis/genre/etc. when a key is configured. title = candidate.get("title") or "" if title: - meta = get_game_metadata_igdb(title, timeout) + meta = get_game_metadata_igdb(title, timeout, system) if meta: return meta return _empty_meta(title) diff --git a/app/gui.py b/app/gui.py index e49704d..2058a4a 100644 --- a/app/gui.py +++ b/app/gui.py @@ -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//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 diff --git a/app/info.py b/app/info.py index b7656ae..380e79e 100644 --- a/app/info.py +++ b/app/info.py @@ -197,7 +197,7 @@ def get_or_fetch(release_id, system, fetch_title): if system == "videos": meta = covers.get_movie_metadata(fetch_title) else: - meta = covers.get_game_metadata(fetch_title) + meta = covers.get_game_metadata(fetch_title, system=system) except Exception: # noqa: BLE001 - network/API failures must not crash the UI meta = None if meta: