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.
|
|
|
|
|
|
2026-08-12 17:24:25 +00:00
|
|
|
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("/")
|
2026-08-12 17:24:25 +00:00
|
|
|
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")
|
|
|
|
|
|
2026-08-12 17:24:25 +00:00
|
|
|
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:
|
2026-08-12 17:24:25 +00:00
|
|
|
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()
|