general store infra
This commit is contained in:
@@ -1,98 +1,158 @@
|
||||
# ttg-store — Teletype Games catalog client for Batocera
|
||||
# warp-engine-batocera-store — a Batocera store engine for WarpEngine sites
|
||||
|
||||
Pulls games from the WarpEngine catalog on <https://teletypegames.org> straight
|
||||
into a Batocera box's ROM folders, with EmulationStation metadata and box art.
|
||||
Triggered from the **Ports** menu on the device, or from the CLI over SSH.
|
||||
Pulls games from a [WarpEngine](https://git.teletypegames.org/tools/warp_engine)
|
||||
catalog straight into a Batocera box's ROM folders, with EmulationStation
|
||||
metadata and box art. Triggered from the **Ports** menu on the device, or from
|
||||
the CLI over SSH.
|
||||
|
||||
This repository is the engine only. It knows the WarpEngine API but nothing
|
||||
about any particular site: the host, the store's name and where its games land
|
||||
all come from a `config.json` that lives in a separate **store repository**.
|
||||
|
||||
```
|
||||
Ports ▸ "Teletype Games Store"
|
||||
warp-engine-batocera-store the engine — store.py, install.sh
|
||||
▲
|
||||
│ config.json
|
||||
│
|
||||
┌─────┴─────┬───────────────┐
|
||||
│ │ │
|
||||
ttg-… my-… other-… store repositories
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
teletypegames.org my.example other.example
|
||||
```
|
||||
|
||||
The reference store is
|
||||
[`ttg-batocera-store`](https://git.teletypegames.org/tools/ttg-batocera-store).
|
||||
|
||||
```
|
||||
Ports ▸ "<store name>"
|
||||
│
|
||||
├─ GET /api/software the whole catalog
|
||||
├─ keep platforms this box can run c64 → c64, tic80 → tic80
|
||||
├─ pick the newest non-dev release that carries the right asset
|
||||
├─ GET /api/download?path=<asset> .prg / .tic into the ROM folder
|
||||
├─ GET /api/image/<id> box art
|
||||
├─ GET <software.imageUrl> box art
|
||||
└─ merge gamelist.xml title, desc, author, image
|
||||
```
|
||||
|
||||
Python 3 standard library only — Batocera ships python3 and no pip.
|
||||
|
||||
Repository: `https://git.teletypegames.org/tools/batocera-store`.
|
||||
The catalog side lives in `teletypegames` (`libs/ruby/warp_engine`).
|
||||
## Installing a store
|
||||
|
||||
## Install
|
||||
|
||||
From a checkout of this repo:
|
||||
The installer takes the store as a parameter, so it works with any config:
|
||||
|
||||
```sh
|
||||
scp -r batocera-store root@batocera:/tmp/
|
||||
ssh root@batocera /tmp/batocera-store/install.sh
|
||||
STORE_CONFIG=https://git.example.org/tools/my-store/raw/branch/master/config.json \
|
||||
bash <(curl -fsSL https://git.teletypegames.org/tools/warp-engine-batocera-store/raw/branch/master/install.sh)
|
||||
```
|
||||
|
||||
Or straight onto the device:
|
||||
From a checkout, with a local config:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.teletypegames.org/tools/batocera-store/raw/branch/master/install.sh | bash
|
||||
scp -r warp-engine-batocera-store root@batocera:/tmp/
|
||||
ssh root@batocera 'STORE_CONFIG=/tmp/my-config.json /tmp/warp-engine-batocera-store/install.sh'
|
||||
```
|
||||
|
||||
The installer drops `ttg-store` in `/userdata/system/ttg-store/`, writes a
|
||||
default config, creates the Ports entry and runs the first sync. Restart
|
||||
EmulationStation (`batocera-es-swissknife --restart`) to see the games.
|
||||
It reads `store.id` from the config, installs the engine and the config into
|
||||
`/userdata/system/batocera-store/<id>/`, writes a `<id>-store` launcher and a
|
||||
Ports entry, and runs the first sync. Restart EmulationStation
|
||||
(`batocera-es-swissknife --restart`) to see the games.
|
||||
|
||||
Installer knobs, all environment variables: `STORE_CONFIG` (required),
|
||||
`BATOCERA_STORE_ROOT`, `BATOCERA_PORTS_DIR`, `BATOCERA_PORT_NAME`,
|
||||
`ENGINE_RAW_BASE`.
|
||||
|
||||
## Writing a store repository
|
||||
|
||||
Three files:
|
||||
|
||||
```
|
||||
my-batocera-store/
|
||||
├── config.json the store: URL, name, subfolder, platform mapping
|
||||
├── install.sh a wrapper that hands that config to the engine installer
|
||||
└── README.md
|
||||
```
|
||||
|
||||
`install.sh` is small enough to quote in full:
|
||||
|
||||
```sh
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
ENGINE_RAW_BASE="${ENGINE_RAW_BASE:-https://git.teletypegames.org/tools/warp-engine-batocera-store/raw/branch/master}"
|
||||
export STORE_CONFIG="${STORE_CONFIG:-https://git.example.org/tools/my-batocera-store/raw/branch/master/config.json}"
|
||||
export BATOCERA_PORT_NAME="My Store"
|
||||
curl -fsSL "$ENGINE_RAW_BASE/install.sh" | bash
|
||||
```
|
||||
|
||||
Pick a `store.id` and a `paths.subfolder` nobody else uses — they are what keeps
|
||||
two stores on the same box from deleting each other's games.
|
||||
|
||||
## Use
|
||||
|
||||
**On the device.** *Ports ▸ Teletype Games Store*. The script downloads
|
||||
anything new, then restarts EmulationStation so the games show up. Ports
|
||||
scripts get no console in Batocera, so the output goes to
|
||||
`/userdata/system/ttg-store/ttg-store.log`.
|
||||
**On the device.** *Ports ▸ "\<store name\>"*. The script downloads anything
|
||||
new, then restarts EmulationStation so the games show up. Ports scripts get no
|
||||
console in Batocera, so the output goes to `store.log` in the store home.
|
||||
|
||||
**Over SSH.**
|
||||
**Over SSH**, through the launcher the installer wrote:
|
||||
|
||||
```sh
|
||||
ttg-store list # compatible catalog entries; * installed, ^ update available
|
||||
ttg-store sync # download everything new, refresh gamelists
|
||||
ttg-store sync blessingofra # just one title (never prunes)
|
||||
ttg-store -n sync # dry run
|
||||
ttg-store remove c64demo # uninstall
|
||||
ttg-store config # effective configuration
|
||||
S=/userdata/system/batocera-store/example-store
|
||||
|
||||
$S list # compatible catalog entries; * installed, ^ update available
|
||||
$S sync # download everything new, refresh gamelists
|
||||
$S sync blessingofra # just one title (never prunes)
|
||||
$S -n sync # dry run
|
||||
$S remove c64:demo # uninstall (bare name works too)
|
||||
$S config # effective configuration
|
||||
```
|
||||
|
||||
Global flags go **before** the subcommand: `ttg-store --roms-root /tmp/roms sync`.
|
||||
Global flags go **before** the subcommand: `$S --roms-root /tmp/roms sync`.
|
||||
|
||||
## Where things land
|
||||
|
||||
```
|
||||
/userdata/system/ttg-store/ ttg-store, config.json, state.json, catalog.json, ttg-store.log
|
||||
/userdata/roms/c64/teletypegames/ blessingofra-2.0.0.prg, rabbit-1.0.0.prg, …
|
||||
/userdata/roms/c64/teletypegames/images/ blessingofra.png, …
|
||||
/userdata/roms/c64/gamelist.xml our entries merged in
|
||||
/userdata/roms/ports/Teletype Games Store.sh
|
||||
/userdata/system/batocera-store/
|
||||
├── example-store launcher for the store below
|
||||
└── example/
|
||||
├── store.py, config.json the engine and the store that selected it
|
||||
└── state.json, catalog.json, store.log
|
||||
|
||||
/userdata/roms/c64/example/ blessingofra-2.0.0.prg, rabbit-1.0.0.prg, …
|
||||
/userdata/roms/c64/example/images/ blessingofra.png, …
|
||||
/userdata/roms/c64/gamelist.xml our entries merged in
|
||||
/userdata/roms/ports/Example Store.sh
|
||||
```
|
||||
|
||||
Everything installed lives under the `teletypegames/` subfolder of a system, so
|
||||
a prune can never reach ROMs you put there yourself. The first time a
|
||||
`gamelist.xml` is touched it is copied to `gamelist.xml.ttg-backup`; on every
|
||||
merge only `<game>`/`<folder>` nodes under `teletypegames/` are rewritten —
|
||||
your own entries keep their play counts, favourites and scraped media.
|
||||
Everything a store installs lives under its own `paths.subfolder`, so a prune
|
||||
can never reach ROMs you put there yourself — or another store's games. The
|
||||
first time a `gamelist.xml` is touched it is copied to
|
||||
`gamelist.xml.<store id>-backup`; on every merge only `<game>`/`<folder>` nodes
|
||||
under that store's subfolder are rewritten, so your own entries keep their play
|
||||
counts, favourites and scraped media, and two stores can share one gamelist.
|
||||
|
||||
## Configuration
|
||||
|
||||
`/userdata/system/ttg-store/config.json`, written by `ttg-store config --write`:
|
||||
`config.json` in the store home, template in `config.example.json`:
|
||||
|
||||
| Key | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `base_url` | `https://teletypegames.org` | WarpEngine host |
|
||||
| `roms_root` | `/userdata/roms` | where systems live |
|
||||
| `subfolder` | `teletypegames` | our subfolder inside each system |
|
||||
| `folder_name` | `Teletype Games` | display name of that folder in ES |
|
||||
| `statuses` | `["released", "archived"]` | catalog `status` values to install |
|
||||
| `owner_id` | `null` | restrict to one publisher (`/api/software?owner_id=`) |
|
||||
| `only` / `exclude` | `[]` | software-name allow / deny lists |
|
||||
| `store.id` | `warp` | slug: names the store home, log prefix and gamelist backup |
|
||||
| `store.name` | `WarpEngine Store` | display name for the Ports entry |
|
||||
| `store.base_url` | — | WarpEngine host |
|
||||
| `store.api.catalog` | `/api/software` | catalog endpoint, if the engine is mounted elsewhere |
|
||||
| `store.api.download` | `/api/download` | download endpoint |
|
||||
| `paths.roms_root` | `/userdata/roms` | where systems live |
|
||||
| `paths.subfolder` | `warp` | this store's subfolder inside each system |
|
||||
| `emulationstation.folder_name` | `WarpEngine Store` | display name of that folder in ES |
|
||||
| `emulationstation.restart` | `true` | restart EmulationStation after a change |
|
||||
| `catalog.statuses` | `["released", "archived"]` | catalog `status` values to install |
|
||||
| `catalog.owner_id` | `null` | restrict to one publisher (`/api/software?owner_id=`) |
|
||||
| `catalog.only` / `catalog.exclude` | `[]` | software-name allow / deny lists |
|
||||
| `platforms` | c64, tic80 | platform → system, asset kind, extension, enabled |
|
||||
| `prune` | `true` | remove games that left the catalog or the filters |
|
||||
| `restart_es` | `true` | restart EmulationStation after a change |
|
||||
| `timeout` | `30` | HTTP timeout, seconds |
|
||||
| `insecure` | `false` | skip TLS verification (self-hosted test instances) |
|
||||
| `behavior.prune` | `true` | remove games that left the catalog or the filters |
|
||||
| `behavior.timeout` | `30` | HTTP timeout, seconds |
|
||||
| `behavior.insecure` | `false` | skip TLS verification (self-hosted test instances) |
|
||||
|
||||
### Platform mapping
|
||||
|
||||
@@ -106,20 +166,20 @@ system can boot directly:
|
||||
|
||||
The other WarpEngine platforms (`ebitengine`, `love`, `godot`, `bevy`,
|
||||
`phaser`) ship `html` and per-OS zip archives, not a ROM a Batocera system
|
||||
launches, so they are not mapped. Adding one is a config edit:
|
||||
launches, so they are not mapped by default. Adding one is a config edit:
|
||||
|
||||
```json
|
||||
"platforms": { "godot": { "system": "godot", "kind": "linux_x64", "ext": ".zip", "enabled": true } }
|
||||
```
|
||||
|
||||
A platform is also skipped when the box has no ROM folder for its system —
|
||||
`ttg-store list` reports that as `skipped <name>: no '<system>' ROM folder`.
|
||||
`list` reports that as `skipped <name>: no '<system>' ROM folder`.
|
||||
|
||||
## How a sync stays safe next to EmulationStation
|
||||
|
||||
EmulationStation keeps gamelists in memory and writes them back when it exits,
|
||||
so a merge done while it runs can be clobbered. Launched from the Ports menu
|
||||
(`sync-from-es`), the tool therefore downloads first, then hands the gamelist
|
||||
(`sync-from-es`), the engine therefore downloads first, then hands the gamelist
|
||||
merge to a detached `apply-gamelists --wait-pid <es-pid>` child that waits for
|
||||
the old ES process to die before writing. From SSH, with no ES running, the
|
||||
merge happens inline.
|
||||
@@ -128,3 +188,10 @@ Re-running a sync is idempotent: files already present are left alone, and a
|
||||
release bump (`1.1` → `2.0.0`) deletes the old asset before fetching the new
|
||||
one. `GET /api/download` is used rather than `/file/`, so downloads count
|
||||
towards the catalog's statistics.
|
||||
|
||||
## State
|
||||
|
||||
`state.json` records what this store installed, keyed `<system>:<name>` so the
|
||||
same software name on two systems stays two entries. A `version: 1` state file
|
||||
written by the pre-split `ttg-store` is migrated to that key format on first
|
||||
run.
|
||||
|
||||
Reference in New Issue
Block a user