Polish settings, add matching defaults, drop pkg2zip, externalize infra config
- Rework Settings tab into grouped sections - Add preferred-region matching and full-hash-by-default setting - Replace combo-box square arrow with a proper chevron - Fix UMD Refresh button (clicked bool was clearing the view) - Remove pkg2zip.exe fallback (pure-Python decryptor is the primary path) - Move LAN IP/share paths to .env; use example defaults in config
This commit is contained in:
+4
-91
@@ -9,14 +9,12 @@ folder — no Wine or ``pkg2zip`` required.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from . import config
|
||||
|
||||
@@ -167,31 +165,6 @@ def fetch_items(content_type: str, url: str = "", timeout: int = 60, force: bool
|
||||
raise
|
||||
|
||||
|
||||
def find_pkg2zip(configured_path: str = "") -> Optional[Path]:
|
||||
"""Locate pkg2zip: configured path, then the bundled binary, then PATH."""
|
||||
if configured_path:
|
||||
p = Path(configured_path)
|
||||
return p if p.is_file() else None
|
||||
bundled = config.DATA_DIR / "bin" / "pkg2zip.exe"
|
||||
if bundled.is_file():
|
||||
return bundled
|
||||
found = shutil.which("pkg2zip")
|
||||
return Path(found) if found else None
|
||||
|
||||
|
||||
def _pkg2zip_cmd(pkg2zip: Path) -> List[str]:
|
||||
"""Command prefix for pkg2zip, wrapping the bundled .exe in Wine on Linux."""
|
||||
if pkg2zip.suffix.lower() == ".exe" and os.name != "nt":
|
||||
wine = shutil.which("wine") or shutil.which("wine64")
|
||||
if not wine:
|
||||
raise RuntimeError(
|
||||
"The bundled pkg2zip is a Windows binary and Wine is not installed — "
|
||||
"install Wine (or point Settings → Store at a native pkg2zip)."
|
||||
)
|
||||
return [wine, str(pkg2zip)]
|
||||
return [str(pkg2zip)]
|
||||
|
||||
|
||||
def _safe_folder(value: str) -> str:
|
||||
import re
|
||||
cleaned = re.sub(r"[^A-Za-z0-9]", "", value or "").upper()
|
||||
@@ -211,12 +184,11 @@ def _ensure_keys_bin(folder, kind):
|
||||
pass
|
||||
|
||||
|
||||
def extract_pkg(pkg_path, title_id: str, dest_dir, pkg2zip: Path = None,
|
||||
def extract_pkg(pkg_path, title_id: str, dest_dir,
|
||||
kind_hint: str = None, progress_cb=None, cancel_event=None) -> Path:
|
||||
"""Decrypt/extract a ``.pkg`` into ``dest_dir/<TITLE_ID>/``.
|
||||
|
||||
Uses the bundled pure-Python decryptor (no Wine / pkg2zip needed); falls
|
||||
back to pkg2zip only if that fails and a binary is available. Returns the
|
||||
Uses the bundled pure-Python decryptor (no Wine or pkg2zip). Returns the
|
||||
destination game folder path.
|
||||
"""
|
||||
from . import pkg as pkgmod
|
||||
@@ -227,7 +199,6 @@ def extract_pkg(pkg_path, title_id: str, dest_dir, pkg2zip: Path = None,
|
||||
if dest.is_dir() and any(dest.iterdir()):
|
||||
return dest
|
||||
|
||||
# 1. Pure-Python decryptor (primary).
|
||||
try:
|
||||
_kind, _tid, folder = pkgmod.extract(pkg_path, dest_dir, kind_hint,
|
||||
progress_cb, cancel_event)
|
||||
@@ -236,62 +207,4 @@ def extract_pkg(pkg_path, title_id: str, dest_dir, pkg2zip: Path = None,
|
||||
except TransferCancelled:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
py_exc = exc
|
||||
log.warning("python pkg decrypt failed (%s); trying pkg2zip fallback", exc)
|
||||
|
||||
# 2. pkg2zip fallback (needs a binary; usually Wine + the bundled .exe).
|
||||
if pkg2zip is None:
|
||||
pkg2zip = find_pkg2zip()
|
||||
if pkg2zip is None:
|
||||
raise RuntimeError(
|
||||
f"could not decrypt {pkg_path.name} (no pkg2zip available): {py_exc}"
|
||||
)
|
||||
|
||||
folder_name = _safe_folder(title_id)
|
||||
tmp = Path(dest_dir).with_name(".nps-tmp-" + folder_name)
|
||||
if tmp.exists():
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
tmp.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if progress_cb is not None:
|
||||
progress_cb("Extracting", 0.0, "")
|
||||
try:
|
||||
cmd = _pkg2zip_cmd(Path(pkg2zip)) + ["-x", str(pkg_path)]
|
||||
env = dict(os.environ)
|
||||
env["WINEDEBUG"] = "-all" # silence Wine's console chatter
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=str(tmp), stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, bufsize=1, env=env,
|
||||
)
|
||||
assert proc.stdout is not None
|
||||
for _line in proc.stdout:
|
||||
if cancel_event is not None and cancel_event.is_set():
|
||||
proc.kill()
|
||||
raise TransferCancelled()
|
||||
proc.wait()
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"pkg2zip failed (exit {proc.returncode}) for {pkg_path.name}")
|
||||
finally:
|
||||
if cancel_event is not None and cancel_event.is_set():
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
raise TransferCancelled()
|
||||
|
||||
eboots = list(tmp.rglob("EBOOT.PBP"))
|
||||
if not eboots:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
raise RuntimeError(f"pkg2zip produced no EBOOT.PBP from {pkg_path.name}")
|
||||
src_folder = eboots[0].parent
|
||||
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
for p in sorted(src_folder.rglob("*")):
|
||||
if not p.is_file():
|
||||
continue
|
||||
rel = p.relative_to(src_folder)
|
||||
target = dest / rel
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(p, target)
|
||||
if progress_cb is not None:
|
||||
progress_cb("Extracting", 1.0, "")
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
return dest
|
||||
raise RuntimeError(f"could not decrypt {pkg_path.name}: {exc}") from exc
|
||||
|
||||
Reference in New Issue
Block a user