Compare commits

...
5 Commits
Author SHA1 Message Date
quoije 733e3c9089 Fix UI freeze on first cover load (decode+scale off the GUI thread)
build / Windows x64 (Wine) (push) Canceled after 17m46s
build / Linux x86_64 (push) Failing after 8m22s
2026-09-13 23:20:08 -04:00
quoije dc7d6c9720 Rebrand to psppt (psp-preserve -> psppt) 2026-09-13 23:08:22 -04:00
quoije 5c5bb6e5d6 fix line break 2026-09-13 22:57:08 -04:00
quoije 35db22e3c5 Add grrr meme to README (resized to 156x148) 2026-09-13 22:55:46 -04:00
quoije b0cdac5483 Put contents_directory on EXE (COLLECT reads it from the exe) 2026-09-13 22:33:04 -04:00
17 changed files with 118 additions and 85 deletions
+11 -11
View File
@@ -22,19 +22,19 @@ jobs:
pip install -r requirements-build.txt pip install -r requirements-build.txt
- name: Build - name: Build
run: pyinstaller --clean --noconfirm psp-preserve.spec run: pyinstaller --clean --noconfirm psppt.spec
- name: Package tarball - name: Package tarball
run: | run: |
cp packaging/install.sh dist/psp-preserve/ cp packaging/install.sh dist/psppt/
cp packaging/psp-preserve.desktop dist/psp-preserve/ cp packaging/psppt.desktop dist/psppt/
cp img/app-icon.png dist/psp-preserve/ cp img/app-icon.png dist/psppt/
tar -czf psp-preserve-linux-x86_64.tar.gz -C dist psp-preserve tar -czf psppt-linux-x86_64.tar.gz -C dist psppt
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: psp-preserve-linux-x86_64 name: psppt-linux-x86_64
path: psp-preserve-linux-x86_64.tar.gz path: psppt-linux-x86_64.tar.gz
windows: windows:
name: Windows x64 (Wine) name: Windows x64 (Wine)
@@ -48,13 +48,13 @@ jobs:
winpython -m pip install -r requirements-build.txt winpython -m pip install -r requirements-build.txt
- name: Build - name: Build
run: winpython -m PyInstaller --clean --noconfirm psp-preserve.spec run: winpython -m PyInstaller --clean --noconfirm psppt.spec
- name: Package zip - name: Package zip
run: | run: |
cd dist && zip -rq ../psp-preserve-windows-x64.zip psp-preserve cd dist && zip -rq ../psppt-windows-x64.zip psppt
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: psp-preserve-windows-x64 name: psppt-windows-x64
path: psp-preserve-windows-x64.zip path: psppt-windows-x64.zip
+1 -1
View File
@@ -3,5 +3,5 @@
# Port-forward 80 and 443 on your router to this host. # Port-forward 80 and 443 on your router to this host.
hostname.com { hostname.com {
reverse_proxy psp-preserve:8000 reverse_proxy psppt:8000
} }
+12 -8
View File
@@ -1,6 +1,10 @@
# PSP Preservation Tool # psppt
the name is terrible, i know. the name is terrible, i know.
![grrr](img/grrr.png)
![AI Generated](https://raw.githubusercontent.com/dotnetdave/ai-usage-badges/refs/heads/main/badges/png/1x/ai-generated.png) ![AI Generated](https://raw.githubusercontent.com/dotnetdave/ai-usage-badges/refs/heads/main/badges/png/1x/ai-generated.png)
A cross-platform (Linux + Windows) desktop GUI — PySide6 — for preserving PSP A cross-platform (Linux + Windows) desktop GUI — PySide6 — for preserving PSP
@@ -11,7 +15,7 @@ save-data with a real PSP, can pull missing discs from a Redump 7z archive
collection, downloads PSN content from NoPayStation, and converts PlayStation 1 collection, downloads PSN content from NoPayStation, and converts PlayStation 1
ISO/CUE/BIN rips into PSP `EBOOT.PBP` homebrew. ISO/CUE/BIN rips into PSP `EBOOT.PBP` homebrew.
![PSP Preservation Tool](img/qt-gui.png) ![psppt](img/qt-gui.png)
* Dark theme with a blue accent — the Library tab in cover view. * * Dark theme with a blue accent — the Library tab in cover view. *
## Features ## Features
@@ -69,8 +73,8 @@ see the note inside).
python3 -m venv .venv-build python3 -m venv .venv-build
. .venv-build/bin/activate . .venv-build/bin/activate
pip install -r requirements-build.txt pip install -r requirements-build.txt
pyinstaller --clean --noconfirm psp-preserve.spec pyinstaller --clean --noconfirm psppt.spec
# output: dist/psp-preserve/ → run dist/psp-preserve/psp-preserve # output: dist/psppt/ → run dist/psppt/psppt
``` ```
Notes: Notes:
@@ -178,7 +182,7 @@ Discs are matched by hash first (most precise), then serial, then filename.
to drawn art when they're absent. to drawn art when they're absent.
- `data/nps/` — NoPayStation TSV lists (cached here on first store scan in dev; - `data/nps/` — NoPayStation TSV lists (cached here on first store scan in dev;
a packaged build stores them under the state dir instead). a packaged build stores them under the state dir instead).
- State dir (`~/.local/state/psp-preserve/` on Linux, `%LOCALAPPDATA%\PSPPreserve` - State dir (`~/.local/state/psppt/` on Linux, `%LOCALAPPDATA%\psppt`
on Windows, or `$PSP_STATE_DIR`): on Windows, or `$PSP_STATE_DIR`):
- `library.sqlite` — the catalog + library database. - `library.sqlite` — the catalog + library database.
- `covers/` — downloaded cover cache. - `covers/` — downloaded cover cache.
@@ -192,13 +196,13 @@ bundled with the app; user-writable data lives in the state dir.
## Configuration ## Configuration
Everything is driven by environment variables, with GUI settings (stored in Everything is driven by environment variables, with GUI settings (stored in
QSettings under `PSPPreserve`) taking precedence where noted. QSettings under `psppt`) taking precedence where noted.
| Variable | Default | Purpose | | Variable | Default | Purpose |
|---|---|---| |---|---|---|
| `PSP_DATA_DIR` | `./data` | data root (DATs, credentials, manual covers) | | `PSP_DATA_DIR` | `./data` | data root (DATs, credentials, manual covers) |
| `PSP_DATS_DIR` | `$PSP_DATA_DIR/dats` | DAT folder | | `PSP_DATS_DIR` | `$PSP_DATA_DIR/dats` | DAT folder |
| `PSP_STATE_DIR` | `~/.local/state/psp-preserve` | local state root | | `PSP_STATE_DIR` | `~/.local/state/psppt` | local state root |
| `PSP_DB_PATH` | `$PSP_STATE_DIR/library.sqlite` | SQLite library | | `PSP_DB_PATH` | `$PSP_STATE_DIR/library.sqlite` | SQLite library |
| `PSP_COVERS_DIR` | `$PSP_STATE_DIR/covers` | cover cache | | `PSP_COVERS_DIR` | `$PSP_STATE_DIR/covers` | cover cache |
| `PSP_GAMES_ROOTS` | `/data/umd_games` | comma-separated game disc roots | | `PSP_GAMES_ROOTS` | `/data/umd_games` | comma-separated game disc roots |
@@ -423,7 +427,7 @@ Token protection: set `PSP_API_TOKEN`; the client sends
macvlan bridge so it gets its own LAN IP (e.g. `192.168.1.100`, plain HTTP on 8000): macvlan bridge so it gets its own LAN IP (e.g. `192.168.1.100`, plain HTTP on 8000):
```bash ```bash
# in /path/to/psp-preserve/ # in /path/to/psppt/
cp .env.example .env # then edit: LAN IP, share paths, API token cp .env.example .env # then edit: LAN IP, share paths, API token
docker compose up -d --build docker compose up -d --build
``` ```
+1 -1
View File
@@ -1 +1 @@
"""PSP Preservation Tool application package.""" """psppt application package."""
+2 -2
View File
@@ -251,7 +251,7 @@ def _library(conn, system=None, refresh=False):
class Handler(BaseHTTPRequestHandler): class Handler(BaseHTTPRequestHandler):
server_version = "PSPPreserve/0.1" server_version = "psppt/0.1"
def do_GET(self): def do_GET(self):
parsed = urlparse(self.path) parsed = urlparse(self.path)
@@ -262,7 +262,7 @@ class Handler(BaseHTTPRequestHandler):
try: try:
if path == "/": if path == "/":
_json(self, { _json(self, {
"name": "PSP Preservation Tool API", "name": "psppt API",
"endpoints": [ "endpoints": [
"/api/summary", "/api/summary",
"/api/releases?system=games|videos&q=&limit=&offset=", "/api/releases?system=games|videos&q=&limit=&offset=",
+2 -2
View File
@@ -42,10 +42,10 @@ def _default_state_dir():
""" """
if os.name == "nt": if os.name == "nt":
base = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA") or str(Path.home()) base = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA") or str(Path.home())
return Path(base) / "PSPPreserve" return Path(base) / "psppt"
xdg = os.environ.get("XDG_STATE_HOME") xdg = os.environ.get("XDG_STATE_HOME")
base = Path(xdg) if xdg else Path.home() / ".local" / "state" base = Path(xdg) if xdg else Path.home() / ".local" / "state"
return base / "psp-preserve" return base / "psppt"
STATE_DIR = Path(os.environ.get("PSP_STATE_DIR", str(_default_state_dir()))) STATE_DIR = Path(os.environ.get("PSP_STATE_DIR", str(_default_state_dir())))
+2 -2
View File
@@ -62,7 +62,7 @@ _WIKI_SUMMARY = "https://en.wikipedia.org/api/rest_v1/page/summary/"
_PNG_MAGIC = b"\x89PNG\r\n\x1a\n" _PNG_MAGIC = b"\x89PNG\r\n\x1a\n"
_JPEG_MAGIC = b"\xff\xd8\xff" _JPEG_MAGIC = b"\xff\xd8\xff"
_MAX_HOPS = 3 # follow libretro symlink text bodies at most this many times _MAX_HOPS = 3 # follow libretro symlink text bodies at most this many times
_UA = {"User-Agent": "psp-preserve/0.1 (https://github.com; local tool)"} _UA = {"User-Agent": "psppt/0.1 (https://github.com; local tool)"}
_igdb_token_state = {"token": "", "expires": 0.0} # IGDB OAuth2 token cache _igdb_token_state = {"token": "", "expires": 0.0} # IGDB OAuth2 token cache
_igdb_lock = threading.Lock() _igdb_lock = threading.Lock()
_igdb_last = 0.0 # monotonic clock of the last IGDB API call _igdb_last = 0.0 # monotonic clock of the last IGDB API call
@@ -610,7 +610,7 @@ def _fetch_screenscraper(name, timeout, system="games"):
params = { params = {
"devid": devid, "devid": devid,
"devpassword": devpassword, "devpassword": devpassword,
"softname": creds.get("screenscraper_softname") or "psp-preserve", "softname": creds.get("screenscraper_softname") or "psppt",
"output": "json", "output": "json",
"systemeid": _platform(system)["ss_id"], "systemeid": _platform(system)["ss_id"],
"romtype": "rom", "romtype": "rom",
+51 -22
View File
@@ -1,4 +1,4 @@
"""Native Linux GUI for the PSP Preservation Tool (PySide6 / Qt). """Native Linux GUI for psppt (PySide6 / Qt).
Run with:: Run with::
@@ -117,14 +117,16 @@ def _thread_running(thread):
class CoverLoader(QObject): class CoverLoader(QObject):
loaded = Signal(int, int, str) # gen, item index, cached image path loaded = Signal(int, int, QImage) # gen, item index, fitted cover image
failed = Signal(int, int, str) # gen, item index, name (no cover found) failed = Signal(int, int, str) # gen, item index, name (no cover found)
finished = Signal() finished = Signal()
def __init__(self, items, gen): def __init__(self, items, gen, icon_w=150, icon_h=220):
super().__init__() super().__init__()
self.items = items self.items = items
self.gen = gen self.gen = gen
self.icon_w = icon_w
self.icon_h = icon_h
self._cancel = threading.Event() self._cancel = threading.Event()
def cancel(self): def cancel(self):
@@ -148,7 +150,11 @@ class CoverLoader(QObject):
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
path = None path = None
if path is not None: if path is not None:
self.loaded.emit(self.gen, i, str(path)) img = QImage(str(path))
if img.isNull():
self.failed.emit(self.gen, i, self.items[i]["name"])
else:
self.loaded.emit(self.gen, i, _fit_image(img, self.icon_w, self.icon_h))
else: else:
self.failed.emit(self.gen, i, self.items[i]["name"]) self.failed.emit(self.gen, i, self.items[i]["name"])
finally: finally:
@@ -276,6 +282,25 @@ def _fit_pixmap(pm, w, h):
return canvas return canvas
def _fit_image(img, w, h):
"""Scale a QImage into a letterboxed ``w x h`` ARGB image (thread-safe).
Decoding + scaling happen off the GUI thread in the cover loaders; the
main thread only converts the result to a QPixmap (cheap).
"""
canvas = QImage(w, h, QImage.Format_ARGB32)
canvas.fill(Qt.transparent)
if img.isNull():
return canvas
scaled = img.scaled(w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation)
x = (w - scaled.width()) // 2
y = (h - scaled.height()) // 2
painter = QPainter(canvas)
painter.drawImage(x, y, scaled)
painter.end()
return canvas
def _cover_pixmap(rid, system, max_w, max_h): def _cover_pixmap(rid, system, max_w, max_h):
"""Return a scaled cover pixmap for an item, or None if unavailable.""" """Return a scaled cover pixmap for an item, or None if unavailable."""
if rid is None: if rid is None:
@@ -429,7 +454,7 @@ class CoverShowcase(QWidget):
for loader in self._loaders: for loader in self._loaders:
loader.cancel() loader.cancel()
self.thread = QThread(self) self.thread = QThread(self)
self.loader = CoverLoader(self.items, gen) self.loader = CoverLoader(self.items, gen, self.ICON_W, self.ICON_H)
self.loader.moveToThread(self.thread) self.loader.moveToThread(self.thread)
self.thread.started.connect(self.loader.run) self.thread.started.connect(self.loader.run)
self.loader.loaded.connect(self._on_loaded) self.loader.loaded.connect(self._on_loaded)
@@ -454,12 +479,12 @@ class CoverShowcase(QWidget):
painter.end() painter.end()
return QIcon(pm) return QIcon(pm)
def _on_loaded(self, gen, index, path): def _on_loaded(self, gen, index, image):
if gen != self._generation: if gen != self._generation:
return return
item = self.list.item(index) item = self.list.item(index)
if item is not None: if item is not None:
item.setIcon(self._fit_icon(QPixmap(path))) item.setIcon(QIcon(QPixmap.fromImage(image)))
if 0 <= index < len(self.items): if 0 <= index < len(self.items):
self.cover_loaded.emit(self.items[index]) self.cover_loaded.emit(self.items[index])
@@ -1117,7 +1142,7 @@ class SettingsPage(QWidget):
iso_category, ps1_category, psp_category, ps1_dir, ps1_output_dir, ps1_base_pbp, iso_category, ps1_category, psp_category, ps1_dir, ps1_output_dir, ps1_base_pbp,
nps_dir="", parent=None): nps_dir="", parent=None):
super().__init__(parent) super().__init__(parent)
s = QSettings("PSPPreserve", "PSPPreserve") s = QSettings("psppt", "psppt")
outer = QVBoxLayout(self) outer = QVBoxLayout(self)
outer.setContentsMargins(16, 16, 16, 16) outer.setContentsMargins(16, 16, 16, 16)
@@ -1289,7 +1314,7 @@ class SettingsPage(QWidget):
note = QLabel( note = QLabel(
"Your library, covers, and credentials live here — outside the app " "Your library, covers, and credentials live here — outside the app "
"bundle, so they survive upgrades. On Linux this is the " "bundle, so they survive upgrades. On Linux this is the "
"~/.local/state/psp-preserve folder." "~/.local/state/psppt folder."
) )
note.setWordWrap(True) note.setWordWrap(True)
note.setStyleSheet("color: #9a9ba5;") note.setStyleSheet("color: #9a9ba5;")
@@ -1444,7 +1469,7 @@ class SettingsPage(QWidget):
def _save(self): def _save(self):
sources.save({key: edit.text() for key, edit in self.fields.items()}) sources.save({key: edit.text() for key, edit in self.fields.items()})
covers.clear_miss_cache() covers.clear_miss_cache()
s = QSettings("PSPPreserve", "PSPPreserve") s = QSettings("psppt", "psppt")
s.setValue("auto_scan", self.auto_scan.isChecked()) s.setValue("auto_scan", self.auto_scan.isChecked())
s.setValue("scan_workers", self.workers_spin.value()) s.setValue("scan_workers", self.workers_spin.value())
s.setValue("auto_sync_saves", self.auto_sync.isChecked()) s.setValue("auto_sync_saves", self.auto_sync.isChecked())
@@ -1479,14 +1504,16 @@ class StoreScanWorker(QObject):
class StoreCoverLoader(QObject): class StoreCoverLoader(QObject):
loaded = Signal(int, int, str) # gen, grid row, cached image path loaded = Signal(int, int, QImage) # gen, grid row, fitted cover image
finished = Signal() finished = Signal()
def __init__(self, tasks, gen, quick=True): def __init__(self, tasks, gen, quick=True, icon_w=150, icon_h=220):
super().__init__() super().__init__()
self.tasks = tasks # list of (grid_row, item_dict) self.tasks = tasks # list of (grid_row, item_dict)
self.gen = gen self.gen = gen
self.quick = quick self.quick = quick
self.icon_w = icon_w
self.icon_h = icon_h
self._cancel = threading.Event() self._cancel = threading.Event()
def cancel(self): def cancel(self):
@@ -1510,7 +1537,9 @@ class StoreCoverLoader(QObject):
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
path = None path = None
if path is not None: if path is not None:
self.loaded.emit(self.gen, row, str(path)) img = QImage(str(path))
if not img.isNull():
self.loaded.emit(self.gen, row, _fit_image(img, self.icon_w, self.icon_h))
finally: finally:
pool.shutdown(wait=True, cancel_futures=True) pool.shutdown(wait=True, cancel_futures=True)
self.finished.emit() self.finished.emit()
@@ -2336,7 +2365,7 @@ class StorePage(QWidget):
if self._closing or not tasks: if self._closing or not tasks:
return return
thread = QThread(self) thread = QThread(self)
loader = StoreCoverLoader(tasks, gen, quick=True) loader = StoreCoverLoader(tasks, gen, quick=True, icon_w=self.ICON_W, icon_h=self.ICON_H)
loader.moveToThread(thread) loader.moveToThread(thread)
thread.started.connect(loader.run) thread.started.connect(loader.run)
loader.loaded.connect(self._on_cover_loaded) loader.loaded.connect(self._on_cover_loaded)
@@ -2346,12 +2375,12 @@ class StorePage(QWidget):
self._loaders.append(loader) self._loaders.append(loader)
thread.start() thread.start()
def _on_cover_loaded(self, gen, row, path): def _on_cover_loaded(self, gen, row, image):
if gen != self._generation: if gen != self._generation:
return return
item = self.grid.item(row) item = self.grid.item(row)
if item is not None: if item is not None:
item.setIcon(QIcon(_fit_pixmap(QPixmap(path), self.ICON_W, self.ICON_H))) item.setIcon(QIcon(QPixmap.fromImage(image)))
it = item.data(Qt.UserRole) it = item.data(Qt.UserRole)
shown = getattr(self.details, "_item", None) shown = getattr(self.details, "_item", None)
if it is not None and shown is not None and it.get("path") == shown.get("path"): if it is not None and shown is not None and it.get("path") == shown.get("path"):
@@ -4287,7 +4316,7 @@ class Ps1Page(QWidget):
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setWindowTitle("psp preservation tool ~ quoije") self.setWindowTitle("psppt ~ quoije")
self.resize(1240, 780) self.resize(1240, 780)
self._rows = [] self._rows = []
self._renames = [] self._renames = []
@@ -4310,7 +4339,7 @@ class MainWindow(QMainWindow):
self._library_workers = [] self._library_workers = []
self._library_loading = False self._library_loading = False
self.settings = QSettings("PSPPreserve", "PSPPreserve") self.settings = QSettings("psppt", "psppt")
self._games_dir = self._first_existing(config.GAMES_ROOTS) or self.settings.value("games_dir", "") or "" self._games_dir = self._first_existing(config.GAMES_ROOTS) or self.settings.value("games_dir", "") or ""
self._videos_dir = self._first_existing(config.VIDEO_ROOTS) or self.settings.value("videos_dir", "") or "" self._videos_dir = self._first_existing(config.VIDEO_ROOTS) or self.settings.value("videos_dir", "") or ""
self._store_dir = str(self.settings.value("store_dir", str(config.STORE_ROOT)) or "") self._store_dir = str(self.settings.value("store_dir", str(config.STORE_ROOT)) or "")
@@ -4353,7 +4382,7 @@ class MainWindow(QMainWindow):
pm = QPixmap(str(icon_path)) pm = QPixmap(str(icon_path))
pm = pm.scaled(28, 28, Qt.KeepAspectRatio, Qt.SmoothTransformation) pm = pm.scaled(28, 28, Qt.KeepAspectRatio, Qt.SmoothTransformation)
logo.setPixmap(pm) logo.setPixmap(pm)
logo.setToolTip("PSP Preservation Tool") logo.setToolTip("psppt")
self.tabs.setCornerWidget(logo, Qt.TopRightCorner) self.tabs.setCornerWidget(logo, Qt.TopRightCorner)
self.queue = queue.TransferQueue(self) self.queue = queue.TransferQueue(self)
@@ -5676,11 +5705,11 @@ def main():
n = ingest.ensure_catalog() n = ingest.ensure_catalog()
if n: if n:
log.info("ingested %d DAT(s) into the catalog", n) log.info("ingested %d DAT(s) into the catalog", n)
log.info("starting psp-preserve (db: %s)", config.DB_PATH) log.info("starting psppt (db: %s)", config.DB_PATH)
app = QApplication(sys.argv) app = QApplication(sys.argv)
style.apply_theme(app) style.apply_theme(app)
app.setApplicationName("PSP Preservation Tool") app.setApplicationName("psppt")
app.setDesktopFileName("psp-preserve") app.setDesktopFileName("psppt")
icon_path = config.PROJECT_ROOT / "img" / "app-icon.png" icon_path = config.PROJECT_ROOT / "img" / "app-icon.png"
if icon_path.is_file(): if icon_path.is_file():
app.setWindowIcon(QIcon(str(icon_path))) app.setWindowIcon(QIcon(str(icon_path)))
+1 -1
View File
@@ -1,4 +1,4 @@
"""PSP Preservation Tool entry point (native GUI). """psppt entry point (native GUI).
Run with:: Run with::
+1 -1
View File
@@ -151,7 +151,7 @@ def fetch_items(content_type: str, url: str = "", timeout: int = 60, force: bool
return items return items
try: try:
req = urllib.request.Request(url, headers={"User-Agent": "psp-preserve/0.1"}) req = urllib.request.Request(url, headers={"User-Agent": "psppt/0.1"})
with urllib.request.urlopen(req, timeout=timeout) as resp: with urllib.request.urlopen(req, timeout=timeout) as resp:
data = resp.read() data = resp.read()
text = data.decode("utf-8", errors="replace") text = data.decode("utf-8", errors="replace")
+1 -1
View File
@@ -155,7 +155,7 @@ def scrape(limit=None, workers=8, rate=3.0):
log.info("downloading OpenVGDB dump (%s)…", OVGDB_ZIP_URL) log.info("downloading OpenVGDB dump (%s)…", OVGDB_ZIP_URL)
tmpdir = Path(tempfile.mkdtemp(prefix="openvgdb-")) tmpdir = Path(tempfile.mkdtemp(prefix="openvgdb-"))
dump_zip = tmpdir / "openvgdb.zip" dump_zip = tmpdir / "openvgdb.zip"
req = urllib.request.Request(OVGDB_ZIP_URL, headers={"User-Agent": "psp-preserve/0.1"}) req = urllib.request.Request(OVGDB_ZIP_URL, headers={"User-Agent": "psppt/0.1"})
with urllib.request.urlopen(req, timeout=60) as r, open(dump_zip, "wb") as f: with urllib.request.urlopen(req, timeout=60) as r, open(dump_zip, "wb") as f:
f.write(r.read()) f.write(r.read())
+2 -2
View File
@@ -284,7 +284,7 @@ class RemoteStoreSource(Source):
self.token = token or "" self.token = token or ""
def _headers(self): def _headers(self):
headers = {"User-Agent": "psp-preserve/0.1"} headers = {"User-Agent": "psppt/0.1"}
if self.token: if self.token:
headers["Authorization"] = f"Bearer {self.token}" headers["Authorization"] = f"Bearer {self.token}"
return headers return headers
@@ -347,7 +347,7 @@ class ArchiveOrgSource(Source):
return f"https://archive.org/download/{self.item}" return f"https://archive.org/download/{self.item}"
def _headers(self): def _headers(self):
headers = {"User-Agent": "psp-preserve/0.1"} headers = {"User-Agent": "psppt/0.1"}
cookie = "; ".join( cookie = "; ".join(
p for p in ( p for p in (
f"logged-in-sig={self.sig}" if self.sig else "", f"logged-in-sig={self.sig}" if self.sig else "",
+2 -2
View File
@@ -1,7 +1,7 @@
services: services:
psp-preserve: psppt:
build: . build: .
container_name: psp-preserve container_name: psppt
# Attach to Unraid's macvlan bridge so the API gets its own LAN IP. # Attach to Unraid's macvlan bridge so the API gets its own LAN IP.
# Plain HTTP for now; see Caddyfile to add Let's Encrypt TLS later. # Plain HTTP for now; see Caddyfile to add Let's Encrypt TLS later.
networks: networks:
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+18 -18
View File
@@ -1,15 +1,15 @@
#!/bin/sh #!/bin/sh
# Install the PSP Preservation Tool to a system prefix. # Install psppt to a system prefix.
# #
# FHS-friendly layout: # FHS-friendly layout:
# $PREFIX/bin/psp-preserve symlink -> payload binary # $PREFIX/bin/psppt symlink -> payload binary
# $PREFIX/lib/psp-preserve/ app payload (binary + lib/) # $PREFIX/lib/psppt/ app payload (binary + lib/)
# $PREFIX/share/applications/ desktop entry # $PREFIX/share/applications/ desktop entry
# $PREFIX/share/icons/hicolor/256x256/apps/ app icon # $PREFIX/share/icons/hicolor/256x256/apps/ app icon
# #
# The PyInstaller onedir build keeps the executable and its `lib/` contents # The PyInstaller onedir build keeps the executable and its `lib/` contents
# directory side by side, so both live under $PREFIX/lib/psp-preserve/ and a # directory side by side, so both live under $PREFIX/lib/psppt/ and a
# symlink exposes the binary at $PREFIX/bin/psp-preserve. # symlink exposes the binary at $PREFIX/bin/psppt.
# #
# Usage (run from inside the extracted tarball): # Usage (run from inside the extracted tarball):
# sudo ./install.sh # install to /usr/local (default) # sudo ./install.sh # install to /usr/local (default)
@@ -29,11 +29,11 @@ for arg in "$@"; do
done done
SRC_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) SRC_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
LIBDIR="$PREFIX/lib/psp-preserve" LIBDIR="$PREFIX/lib/psppt"
BINDIR="$PREFIX/bin" BINDIR="$PREFIX/bin"
APPDIR="$PREFIX/share/applications" APPDIR="$PREFIX/share/applications"
ICONDIR="$PREFIX/share/icons/hicolor/256x256/apps" ICONDIR="$PREFIX/share/icons/hicolor/256x256/apps"
APP_BIN="$LIBDIR/psp-preserve" APP_BIN="$LIBDIR/psppt"
require_root() { require_root() {
if [ "$(id -u)" -ne 0 ]; then if [ "$(id -u)" -ne 0 ]; then
@@ -43,9 +43,9 @@ require_root() {
} }
uninstall() { uninstall() {
rm -f "$BINDIR/psp-preserve" rm -f "$BINDIR/psppt"
rm -f "$APPDIR/psp-preserve.desktop" rm -f "$APPDIR/psppt.desktop"
rm -f "$ICONDIR/psp-preserve.png" rm -f "$ICONDIR/psppt.png"
rm -rf "$LIBDIR" rm -rf "$LIBDIR"
echo "Uninstalled from $PREFIX" echo "Uninstalled from $PREFIX"
} }
@@ -59,7 +59,7 @@ fi
require_root require_root
# The tarball ships these next to this script. # The tarball ships these next to this script.
for f in psp-preserve lib psp-preserve.desktop app-icon.png; do for f in psppt lib psppt.desktop app-icon.png; do
if [ ! -e "$SRC_DIR/$f" ]; then if [ ! -e "$SRC_DIR/$f" ]; then
echo "missing $f (run install.sh from inside the extracted tarball)" >&2 echo "missing $f (run install.sh from inside the extracted tarball)" >&2
exit 1 exit 1
@@ -69,13 +69,13 @@ done
mkdir -p "$LIBDIR" "$BINDIR" "$APPDIR" "$ICONDIR" mkdir -p "$LIBDIR" "$BINDIR" "$APPDIR" "$ICONDIR"
rm -rf "$LIBDIR" rm -rf "$LIBDIR"
mkdir -p "$LIBDIR" mkdir -p "$LIBDIR"
cp -a "$SRC_DIR/psp-preserve" "$LIBDIR/psp-preserve" cp -a "$SRC_DIR/psppt" "$LIBDIR/psppt"
cp -a "$SRC_DIR/lib" "$LIBDIR/lib" cp -a "$SRC_DIR/lib" "$LIBDIR/lib"
install -m 0644 "$SRC_DIR/psp-preserve.desktop" "$APPDIR/psp-preserve.desktop" install -m 0644 "$SRC_DIR/psppt.desktop" "$APPDIR/psppt.desktop"
install -m 0644 "$SRC_DIR/app-icon.png" "$ICONDIR/psp-preserve.png" install -m 0644 "$SRC_DIR/app-icon.png" "$ICONDIR/psppt.png"
ln -sfn "$APP_BIN" "$BINDIR/psp-preserve" ln -sfn "$APP_BIN" "$BINDIR/psppt"
echo "Installed to $PREFIX" echo "Installed to $PREFIX"
echo " binary: $BINDIR/psp-preserve -> $APP_BIN" echo " binary: $BINDIR/psppt -> $APP_BIN"
echo " payload: $LIBDIR" echo " payload: $LIBDIR"
echo " desktop: $APPDIR/psp-preserve.desktop" echo " desktop: $APPDIR/psppt.desktop"
@@ -1,9 +1,9 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=PSP Preservation Tool Name=psppt
Comment=Preserve and manage your PSP library Comment=Preserve and manage your PSP library
Exec=psp-preserve Exec=psppt
Icon=psp-preserve Icon=psppt
Terminal=false Terminal=false
Categories=Utility;AudioVideo; Categories=Utility;AudioVideo;
StartupWMClass=psp-preserve StartupWMClass=psppt
+7 -7
View File
@@ -1,13 +1,13 @@
# -*- mode: python ; coding: utf-8 -*- # -*- mode: python ; coding: utf-8 -*-
"""PyInstaller spec: build the PSP Preservation Tool (onedir). """PyInstaller spec: build psppt (onedir).
Bundles the read-only resources (DATs, PS1 loader, offline cover bundles, Bundles the read-only resources (DATs, PS1 loader, offline cover bundles,
icons) under ``lib/`` next to the executable. User-writable data icons) under ``lib/`` next to the executable. User-writable data
(credentials, manual covers, PS1 templates, NPS cache, library DB, cover (credentials, manual covers, PS1 templates, NPS cache, library DB, cover
cache) lives in the OS state dir at runtime — see ``app/config.py``. cache) lives in the OS state dir at runtime — see ``app/config.py``.
Usage: pyinstaller --clean --noconfirm psp-preserve.spec Usage: pyinstaller --clean --noconfirm psppt.spec
Output: dist/psp-preserve/ Output: dist/psppt/
""" """
from PyInstaller.utils.hooks import collect_dynamic_libs, collect_submodules from PyInstaller.utils.hooks import collect_dynamic_libs, collect_submodules
@@ -24,7 +24,7 @@ datas = [
("data/covers/UMD_GAME_COVER_TEMPLATE.png", "data/covers"), ("data/covers/UMD_GAME_COVER_TEMPLATE.png", "data/covers"),
("img/app-icon.png", "img"), ("img/app-icon.png", "img"),
("img/chevron.png", "img"), ("img/chevron.png", "img"),
("packaging/psp-preserve.desktop", "."), ("packaging/psppt.desktop", "."),
] ]
a = Analysis( a = Analysis(
@@ -47,13 +47,14 @@ exe = EXE(
a.scripts, a.scripts,
[], [],
exclude_binaries=True, exclude_binaries=True,
name="psp-preserve", name="psppt",
debug=False, debug=False,
bootloader_ignore_signals=False, bootloader_ignore_signals=False,
strip=False, strip=False,
upx=False, upx=False,
console=False, console=False,
disable_windowed_traceback=False, disable_windowed_traceback=False,
contents_directory="lib",
) )
coll = COLLECT( coll = COLLECT(
@@ -62,6 +63,5 @@ coll = COLLECT(
a.datas, a.datas,
strip=False, strip=False,
upx=False, upx=False,
name="psp-preserve", name="psppt",
contents_directory="lib",
) )