"""Load (or refresh) the `providers` rows the setup flow renders. python scripts/seed-providers.py # local docker compose PB_BASE=https://amber.petruzalekr.cz \ PB_ADMIN_EMAIL=… PB_ADMIN_PASS=… python scripts/seed-providers.py Reads `scripts/providers.seed.json` and **upserts by `slug`**, so running it twice is the same as running it once. That matters more than it sounds: prices and click-paths are edited in the admin UI, and a seeder that inserted blindly would either duplicate every row or quietly overwrite a correction somebody made last week. `--force` is the switch for "yes, put the file back over the top". Without `--force` an existing row is left alone and reported as `kept`, which makes this safe to run after any deploy — the collection gets populated on a fresh instance and untouched on one that is already looked after. Authenticates one of two ways, in this order: PB_ADMIN_TOKEN an existing superuser token, used as-is PB_ADMIN_EMAIL/PB_ADMIN_PASS a superuser sign-in (local default below) The token path exists because that is what the production instance is reachable with: `secrets/amber.env` holds `PB_ADMIN_TOKEN` and no superuser password, and inventing one to satisfy this script would be the wrong way round. Never reads that file itself, so nothing here needs access to the family's credentials, and never prints the token. """ import json, os, pathlib, sys, urllib.error, urllib.request BASE = os.environ.get("PB_BASE", "http://localhost:8090").rstrip("/") ADMIN_TOKEN = os.environ.get("PB_ADMIN_TOKEN", "").strip() ADMIN_EMAIL = os.environ.get("PB_ADMIN_EMAIL", "admin@myanime.local") ADMIN_PASS = os.environ.get("PB_ADMIN_PASS", "Sup3rSecret!123") FORCE = "--force" in sys.argv SEED = pathlib.Path(__file__).with_name("providers.seed.json") def req(method, path, token=None, body=None): data = json.dumps(body).encode() if body is not None else None r = urllib.request.Request(BASE + path, data=data, method=method) r.add_header("Content-Type", "application/json") if token: r.add_header("Authorization", token) try: with urllib.request.urlopen(r) as resp: return resp.status, json.loads(resp.read() or "{}") except urllib.error.HTTPError as e: return e.code, json.loads(e.read() or "{}") def main(): rows = json.loads(SEED.read_text(encoding="utf-8")) slugs = [r["slug"] for r in rows] if len(set(slugs)) != len(slugs): sys.exit(f"duplicate slug in {SEED.name}: {slugs}") # The page keys its per-service wiring off these four strings; a typo here # would render a card that leads nowhere, so it is checked rather than # trusted. known = {"torbox", "prehrajto", "webshare", "tmdb"} unknown = set(slugs) - known if unknown: sys.exit(f"unknown slug(s) {sorted(unknown)} — the page has no wiring for these") if ADMIN_TOKEN: token = ADMIN_TOKEN else: st, auth = req("POST", "/api/collections/_superusers/auth-with-password", body={"identity": ADMIN_EMAIL, "password": ADMIN_PASS}) if st != 200: sys.exit(f"superuser auth failed against {BASE}: HTTP {st} {auth}") token = auth["token"] # Prove the token is really a superuser BEFORE reading anything, against an # endpoint only a superuser may touch. # # Listing records cannot answer this. PocketBase applies a collection's listRule # as a *filter*, so a stale token comes back HTTP 200 with zero rows — identical # to a collection that is simply empty. The seeder then decides every provider is # missing and tries to create all four, and the first thing you see is # "[FAIL] prehrajto: HTTP 403 Only superusers can perform this action", which # points at the row instead of at the token. Measured, not assumed. st, who = req("GET", "/api/collections?perPage=1", token) if st != 200: sys.exit(f"HTTP {st} against {BASE}: this is not a valid superuser token. " "Refresh PB_ADMIN_TOKEN in secrets/amber.env and do not work " "around it.") st, existing = req("GET", "/api/collections/providers/records?perPage=200", token) if st != 200: sys.exit(f"cannot list providers (is the migration applied?): HTTP {st} {existing}") by_slug = {i["slug"]: i for i in existing.get("items", [])} for row in rows: slug = row["slug"] cur = by_slug.get(slug) if cur and not FORCE: print(f"[kept] {slug} — already present, --force to overwrite") continue if cur: st, res = req("PATCH", f"/api/collections/providers/records/{cur['id']}", token, row) verb = "updated" else: st, res = req("POST", "/api/collections/providers/records", token, row) verb = "created" if st not in (200, 201): sys.exit(f"[FAIL] {slug}: HTTP {st} {res}") print(f"[{verb}] {slug}") print(f"\n{len(rows)} provider(s) in {BASE}. " "Prices are editable in the admin UI — this file is only the starting point.") if __name__ == "__main__": main()