Tell the two API-key setup failures apart in verify_api.py

Ran Phase 0 against a live key and hit both of the ways a fresh Google Cloud
project can be wrong, in sequence. Google reports them as the same 403
`forbidden`, so the first version of this script printed reason='forbidden'
three times and buried the one sentence that said what to do.

The distinguishing signal is in error.details[].reason, not
error.errors[].reason:

  SERVICE_DISABLED         YouTube Data API v3 is not enabled on the project.
                           Carries an activationUrl naming the project number.
  API_KEY_SERVICE_BLOCKED  The API is enabled, but this key's API restrictions
                           exclude it.

They are fixed on different console screens, so they are now separate exception
types with separate advice, and a one-call preflight reports either before the
three real checks run and fail identically.

The ordering between them is a trap worth writing down: YouTube Data API v3 does
not appear in a key's API-restriction picker until the API is enabled on the
project, so creating the key and restricting it first yields a key that blocks
the only API it exists for. That is precisely what happened here. §4.1 step 4
now says to enable before restricting.

Nothing has yet reached YouTube's own privacy check, so whether the brother's
subscriptions are readable is still untested — every call so far failed at the
key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Tom Flux
2026-08-12 15:45:15 +01:00
co-authored by Claude Opus 5
parent 339232c7f9
commit f3d70f1c87
2 changed files with 123 additions and 12 deletions
+28 -7
View File
@@ -211,6 +211,9 @@ short of §4.2.
4. Restrict the key: Application restrictions → *None* (it is called from a server, so referrer and
Android/iOS restrictions do not apply; an IP restriction is optional and breaks if susan's
residential IP rotates). API restrictions → **YouTube Data API v3 only**.
**Do step 2 before this step.** YouTube Data API v3 does not appear in the API-restriction picker
until it is enabled on the project, so restricting first produces a key that blocks the one API it
exists for — see §16, which is exactly what happened.
5. Paste it into the ytstream admin UI. It is stored in the `setting` table like
`jellyfin_api_key` already is — never in the repo, never in a systemd unit.
@@ -778,6 +781,11 @@ Things already paid for once. All of these are verified.
spend an afternoon trying to scrape it; the API is the only route (§4).
- **`pkill -f 'ytstream.py'` kills the shell that runs it**, because the command string contains its
own pattern. Bracket it: `pkill -f 'ytstrea[m].py'`.
- **Google returns 403 `forbidden` for two unrelated setup mistakes**, and the useful signal is in
`error.details[].reason`, not `error.errors[].reason`. `SERVICE_DISABLED` means the API is not
enabled on the project (and carries an `activationUrl` naming the project number);
`API_KEY_SERVICE_BLOCKED` means the API is enabled but *this key's* restrictions exclude it. They
are fixed on different console screens. `tools/verify_api.py` distinguishes them and prints the fix.
---
@@ -800,12 +808,24 @@ Things already paid for once. All of these are verified.
- **`tools/verify_api.py` written and self-tested** (ISO-8601 duration parser unit-checked against six
cases; argparse and import verified). It runs all three API checks in one command.
### Blocked on one thing only
### Key created, project 510818173753 — two setup steps deep, one to go
**There is no Google API key on this machine** — I searched for `AIza`-shaped strings across
`/opt/*`, `/home/susan`, the settings table and the config trees, and there is none. Creating one
needs a browser and Tom's Google account (§4.1, ~5 minutes, free, no billing). Until then these three
remain unverified:
Progress on the key itself, all of it diagnosed from the error bodies:
1. **Key created** and reaching Google — it authenticates, so the key string is good.
2. **`SERVICE_DISABLED`** — YouTube Data API v3 was not enabled on project `510818173753`. Fixed by
enabling it.
3. **`API_KEY_SERVICE_BLOCKED`** ← *current state.* The API is now enabled, but the key's own API
restrictions exclude it, so every method returns "Requests to this API youtube method … are
blocked". Fix at
`https://console.cloud.google.com/apis/credentials?project=510818173753` → the key → API
restrictions → *Don't restrict key*, or tick YouTube Data API v3.
The ordering trap is worth remembering rather than rediscovering: the API must be enabled **before**
the key can be restricted to it, because it is absent from the picker until then. §4.1 step 4 now
says so.
Until the key answers, these three assumptions remain unverified:
| # | Assumption | Consequence if it fails |
|---|---|---|
@@ -814,8 +834,9 @@ remain unverified:
| 3 | `videos.list` returns parseable `contentDetails.duration` | No `<durationinseconds>` in NFOs without a yt-dlp extraction per video. Degrades, does not block. |
Only #1 is a genuine blocker, and it also carries the number that sets `subsync_max_new`. Two
prerequisites, both external: **Tom creates the API key**, and **his brother unchecks "Keep all my
subscriptions private"**. Then one command closes Phase 0:
external prerequisites remain: **the key's API restriction** (above), and **his brother unchecking
"Keep all my subscriptions private"** — note that nothing so far has tested the second, because every
call has failed at the key before reaching YouTube's privacy check. Then one command closes Phase 0:
```sh
python3 /opt/ytstream/tools/verify_api.py --key AIza...