amber-backend/scripts/seed-providers.py

116 lines
5.1 KiB
Python
Raw Normal View History

Tell a new viewer what to buy before asking them to fill in a form Someone with their own accounts had to work out on their own that Amber needs a paid subscription somewhere before it can find anything, and which of three services that means. The Zdroje box already turns their usernames into addon URLs; nothing anywhere told them which usernames to go and get, or what it would cost. Six steps as a new #start tab: change the generated password, choose services, create the accounts and pay, take a free TMDB key, enter it all in Zdroje, download. Steps 1, 5 and 6 hand off to the tabs that already do that work. This screen owns no credentials and no crypto on purpose, since a second encryptor for one blob is how a vault gets corrupted. The password is step 1, not step 6. Changing it re-keys the vault with a fresh salt, so every device already signed in holds a stale key until it authenticates again. Done first, while the vault is empty, there is nothing to re-encrypt and no paired television to strand. State is three fields on the user record and nothing more: skipped, chosen and pwChangedAt. Whether the sources exist is CONFIGS.length and whether TMDB is set is a field in the decrypted config, so storing those again would let two answers disagree. pwChangedAt is the one thing that cannot be derived, because PocketBase records no password-changed timestamp. Prices and click-paths live in a providers collection, editable in the admin UI, with priceCheckedAt rendered beside the number so a stale figure looks stale rather than reading as a promise. Owner supplied the three signup guides; TorBox Free is called out as unusable because it has no API access, which is the only way Amber talks to it. byGo also stops demanding a Czech host. Nothing downstream needed one, and someone who only wants anime was being told to buy a service they had no use for. Verified against PocketBase 0.39.6 from a throwaway data dir with this repo's real migrations, hooks and page: both migrations applied clean, the flow opened itself for a user with no sources, a choice persisted as {"chosen":["prehrajto","torbox"]} and nothing else, guides rendered numbered with working links, skipping raised the banner and survived a reload, resuming came back in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:36:55 +00:00
"""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.
Tell a new viewer what to buy before asking them to fill in a form Someone with their own accounts had to work out on their own that Amber needs a paid subscription somewhere before it can find anything, and which of three services that means. The Zdroje box already turns their usernames into addon URLs; nothing anywhere told them which usernames to go and get, or what it would cost. Six steps as a new #start tab: change the generated password, choose services, create the accounts and pay, take a free TMDB key, enter it all in Zdroje, download. Steps 1, 5 and 6 hand off to the tabs that already do that work. This screen owns no credentials and no crypto on purpose, since a second encryptor for one blob is how a vault gets corrupted. The password is step 1, not step 6. Changing it re-keys the vault with a fresh salt, so every device already signed in holds a stale key until it authenticates again. Done first, while the vault is empty, there is nothing to re-encrypt and no paired television to strand. State is three fields on the user record and nothing more: skipped, chosen and pwChangedAt. Whether the sources exist is CONFIGS.length and whether TMDB is set is a field in the decrypted config, so storing those again would let two answers disagree. pwChangedAt is the one thing that cannot be derived, because PocketBase records no password-changed timestamp. Prices and click-paths live in a providers collection, editable in the admin UI, with priceCheckedAt rendered beside the number so a stale figure looks stale rather than reading as a promise. Owner supplied the three signup guides; TorBox Free is called out as unusable because it has no API access, which is the only way Amber talks to it. byGo also stops demanding a Czech host. Nothing downstream needed one, and someone who only wants anime was being told to buy a service they had no use for. Verified against PocketBase 0.39.6 from a throwaway data dir with this repo's real migrations, hooks and page: both migrations applied clean, the flow opened itself for a user with no sources, a choice persisted as {"chosen":["prehrajto","torbox"]} and nothing else, guides rendered numbered with working links, skipping raised the banner and survived a reload, resuming came back in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:36:55 +00:00
"""
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()
Tell a new viewer what to buy before asking them to fill in a form Someone with their own accounts had to work out on their own that Amber needs a paid subscription somewhere before it can find anything, and which of three services that means. The Zdroje box already turns their usernames into addon URLs; nothing anywhere told them which usernames to go and get, or what it would cost. Six steps as a new #start tab: change the generated password, choose services, create the accounts and pay, take a free TMDB key, enter it all in Zdroje, download. Steps 1, 5 and 6 hand off to the tabs that already do that work. This screen owns no credentials and no crypto on purpose, since a second encryptor for one blob is how a vault gets corrupted. The password is step 1, not step 6. Changing it re-keys the vault with a fresh salt, so every device already signed in holds a stale key until it authenticates again. Done first, while the vault is empty, there is nothing to re-encrypt and no paired television to strand. State is three fields on the user record and nothing more: skipped, chosen and pwChangedAt. Whether the sources exist is CONFIGS.length and whether TMDB is set is a field in the decrypted config, so storing those again would let two answers disagree. pwChangedAt is the one thing that cannot be derived, because PocketBase records no password-changed timestamp. Prices and click-paths live in a providers collection, editable in the admin UI, with priceCheckedAt rendered beside the number so a stale figure looks stale rather than reading as a promise. Owner supplied the three signup guides; TorBox Free is called out as unusable because it has no API access, which is the only way Amber talks to it. byGo also stops demanding a Czech host. Nothing downstream needed one, and someone who only wants anime was being told to buy a service they had no use for. Verified against PocketBase 0.39.6 from a throwaway data dir with this repo's real migrations, hooks and page: both migrations applied clean, the flow opened itself for a user with no sources, a choice persisted as {"chosen":["prehrajto","torbox"]} and nothing else, guides rendered numbered with working links, skipping raised the banner and survived a reload, resuming came back in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:36:55 +00:00
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)
Tell a new viewer what to buy before asking them to fill in a form Someone with their own accounts had to work out on their own that Amber needs a paid subscription somewhere before it can find anything, and which of three services that means. The Zdroje box already turns their usernames into addon URLs; nothing anywhere told them which usernames to go and get, or what it would cost. Six steps as a new #start tab: change the generated password, choose services, create the accounts and pay, take a free TMDB key, enter it all in Zdroje, download. Steps 1, 5 and 6 hand off to the tabs that already do that work. This screen owns no credentials and no crypto on purpose, since a second encryptor for one blob is how a vault gets corrupted. The password is step 1, not step 6. Changing it re-keys the vault with a fresh salt, so every device already signed in holds a stale key until it authenticates again. Done first, while the vault is empty, there is nothing to re-encrypt and no paired television to strand. State is three fields on the user record and nothing more: skipped, chosen and pwChangedAt. Whether the sources exist is CONFIGS.length and whether TMDB is set is a field in the decrypted config, so storing those again would let two answers disagree. pwChangedAt is the one thing that cannot be derived, because PocketBase records no password-changed timestamp. Prices and click-paths live in a providers collection, editable in the admin UI, with priceCheckedAt rendered beside the number so a stale figure looks stale rather than reading as a promise. Owner supplied the three signup guides; TorBox Free is called out as unusable because it has no API access, which is the only way Amber talks to it. byGo also stops demanding a Czech host. Nothing downstream needed one, and someone who only wants anime was being told to buy a service they had no use for. Verified against PocketBase 0.39.6 from a throwaway data dir with this repo's real migrations, hooks and page: both migrations applied clean, the flow opened itself for a user with no sources, a choice persisted as {"chosen":["prehrajto","torbox"]} and nothing else, guides rendered numbered with working links, skipping raised the banner and survived a reload, resuming came back in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:36:55 +00:00
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.")
Tell a new viewer what to buy before asking them to fill in a form Someone with their own accounts had to work out on their own that Amber needs a paid subscription somewhere before it can find anything, and which of three services that means. The Zdroje box already turns their usernames into addon URLs; nothing anywhere told them which usernames to go and get, or what it would cost. Six steps as a new #start tab: change the generated password, choose services, create the accounts and pay, take a free TMDB key, enter it all in Zdroje, download. Steps 1, 5 and 6 hand off to the tabs that already do that work. This screen owns no credentials and no crypto on purpose, since a second encryptor for one blob is how a vault gets corrupted. The password is step 1, not step 6. Changing it re-keys the vault with a fresh salt, so every device already signed in holds a stale key until it authenticates again. Done first, while the vault is empty, there is nothing to re-encrypt and no paired television to strand. State is three fields on the user record and nothing more: skipped, chosen and pwChangedAt. Whether the sources exist is CONFIGS.length and whether TMDB is set is a field in the decrypted config, so storing those again would let two answers disagree. pwChangedAt is the one thing that cannot be derived, because PocketBase records no password-changed timestamp. Prices and click-paths live in a providers collection, editable in the admin UI, with priceCheckedAt rendered beside the number so a stale figure looks stale rather than reading as a promise. Owner supplied the three signup guides; TorBox Free is called out as unusable because it has no API access, which is the only way Amber talks to it. byGo also stops demanding a Czech host. Nothing downstream needed one, and someone who only wants anime was being told to buy a service they had no use for. Verified against PocketBase 0.39.6 from a throwaway data dir with this repo's real migrations, hooks and page: both migrations applied clean, the flow opened itself for a user with no sources, a choice persisted as {"chosen":["prehrajto","torbox"]} and nothing else, guides rendered numbered with working links, skipping raised the banner and survived a reload, resuming came back in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:36:55 +00:00
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()