mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-22 00:56:35 +03:00
Full lastfm-model rebuild of the jellyfin skill against the 12.0-era OpenAPI spec, core-dev authorization guidance, and server source: - Document the researched auth sequence end to end: complete pre-token Authorization: MediaBrowser Client/Device/DeviceId/Version header required by POST /Users/AuthenticateByName (400 "Error processing request." without it), AccessToken returned, then Token= on the same header (legacy X-Emby-Token deprecated, disableable since 10.11, targeted for removal at 12.0). - Extend scripts/jellyfin: new `login` subcommand demonstrating the pre-token header and printing session exports (password via stdin/prompt/env only), seasons/episodes TV navigation, next-up --series-id, browse --user-id (userId is required on non-API-key auth per the ItemsController guard), modern Token= header transport with X-Emby-Token fallback, 503 Retry-After handling, search Id/deprecated-ItemId fallback. - Add 5 cited reference files (auth/sessions, endpoint catalog, user-scoping matrix, gotchas field guide, worked recipes) plus quick-connect; all cite api.jellyfin.org and live-verified sources. - Upgrade relocated scripts/test_jellyfin_cli.py to the double-runner standard: 24 tests (was 8) covering help, argument errors, dry-run, mocked login header sequence, TV navigation, search-id fallback, and jq-executed pipeline-consumability chains; zero egress proven via proxy-trap rerun. - Add evals/evals.json (6 cases incl. emby-install-not-for-jellyfin negative probe); rewrite SKILL.md (224 lines) and README; sync root README blurb and skill-triggers row; regenerate marketplace.json and llms.txt (description-embedding artifacts). Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
90 lines
5.4 KiB
Markdown
90 lines
5.4 KiB
Markdown
# Jellyfin user scoping — the userId matrix
|
|
|
|
The single most common Jellyfin integration failure after authentication: a request that
|
|
authenticates fine but 404s or 400s because **which user** the query is about was never
|
|
stated. Jellyfin's data model is per-user at the library level; most read endpoints need
|
|
you to say whose library you are looking at.
|
|
|
|
## Why user id is everywhere
|
|
|
|
A Jellyfin library is not one global catalog. Views, playstate, favorites, resume points,
|
|
and parental-control visibility all attach to a user. The authentication layer may or may
|
|
not imply a user:
|
|
|
|
- **User access token** (from `AuthenticateByName`): implies one user. Recent servers fall
|
|
back to that user when `userId` is omitted on some endpoints.
|
|
- **API key** (from Dashboard → API Keys): implies NO user. `AuthorizationInfo.User` stays
|
|
null and the request gets administrator role. Every user-scoped concept must be named
|
|
explicitly with a `userId` parameter — including `/UserViews`, which is meaningless
|
|
without one.
|
|
|
|
The bundled CLI always sends `userId` explicitly on user-scoped commands regardless of
|
|
which credential type it holds. That is the cross-version-safe baseline.
|
|
|
|
## The userId requirement matrix
|
|
|
|
| Endpoint | User token, userId omitted | API key |
|
|
| --- | --- | --- |
|
|
| `GET /Items` | **400** — body `userId is required` (servers enforce `if (!isApiKey && user is null) return BadRequest("userId is required")`) | Optional — omitted means an unrestricted, userless view; pass it anyway for `UserData` in DTOs |
|
|
| `GET /Items/{itemId}` | Defaults to the token's user; `userId` supplies whose `UserData` embeds | Pass explicitly for user data |
|
|
| `GET /UserViews` | Parameter accepted; pass it | REQUIRED to be meaningful |
|
|
| `GET /Shows/{seriesId}/Seasons` / `Episodes` | ≥10.9 falls back to token user; ≤10.8 crashes on omission — always pass | REQUIRED |
|
|
| `GET /Shows/NextUp` | Same version split as above — always pass | REQUIRED |
|
|
| `GET /Items/Latest` | `userId` scopes "recently added for whom"; always pass | REQUIRED for meaningful results |
|
|
| `GET /Search/Hints` | Optional — "omit to search all" | Optional |
|
|
| `GET /Users/Me` | Works (returns token's user) | **400** `Token is not owned by a user.` |
|
|
| `GET /System/Info`, `/System/Info/Public`, `/Users` | No user concept | Fine |
|
|
|
|
Note the deliberate trap in `/Users/Me`: it is the natural "who am I" endpoint for a user
|
|
token and returns exactly the id you need — but with an API key it is a 400, by design,
|
|
because an API key is nobody.
|
|
|
|
## 400 vs 404: absent vs invalid userId on /Items
|
|
|
|
Two distinct failure signatures, enforced in this order by the items controller
|
|
(verified at master and v10.10.7):
|
|
|
|
1. The user lookup runs first: a supplied-but-nonexistent `userId` throws
|
|
`ResourceNotFoundException` → mapped to **404** (`Error processing request.` body).
|
|
2. Then the guard: an ABSENT `userId` on non-API-key auth returns **400** with the literal
|
|
string body `userId is required` (not the middleware's generic text).
|
|
|
|
So: `400 userId is required` = you forgot the parameter; `404` = the parameter names a user
|
|
that does not exist. Mock both distinctly.
|
|
|
|
## Finding a user id without logging in as one
|
|
|
|
1. **`GET /Users`** (any valid token): array of `UserDto` with `Name` and `Id`. The
|
|
administrator-flavored listing — API keys see everyone.
|
|
2. **`GET /Users/Public`** (no auth): only users flagged visible on login screens.
|
|
3. **After `AuthenticateByName`**: the response's `User.Id` is the documented primary.
|
|
4. **With a user token**: `GET /Users/Me`.
|
|
|
|
```bash
|
|
# Discover user ids with an API key
|
|
curl -s -H 'Authorization: MediaBrowser Token="YOUR_API_KEY"' \
|
|
"http://localhost:8096/Users" | jq -r '.[] | [.Name, .Id] | @tsv'
|
|
# → alice 6eec632a-ff0d-4d09-aad0-bf9e90b14bc6
|
|
```
|
|
|
|
## Scoping errors look like 404s
|
|
|
|
The confusion this reference exists for: `GET /Items` (or `/Shows/NextUp`) called without a
|
|
`userId` under a context where one is required does not answer "you forgot the user" on
|
|
every endpoint and version — older servers crash (NextUp ≤10.8: 500 from an empty-Guid
|
|
`ArgumentException`), and user-token fallbacks silently change results. Symptoms cluster as
|
|
"endpoint exists but returns 400/404/empty" even though the token is perfectly valid. The
|
|
fix is uniform: resolve the user id once, pass it explicitly on every user-scoped call.
|
|
|
|
The bundled CLI mirrors that baseline: `recent`, `next-up`, and `item` require
|
|
`JELLYFIN_USER_ID` or `--user-id` before any network call, refuse to guess an
|
|
administrator, and dry-run previews show the `userId` that would have been sent.
|
|
|
|
## Sources
|
|
|
|
- https://api.jellyfin.org/ — official Jellyfin API reference (ReDoc), version 12.0.0 stable
|
|
- https://api.jellyfin.org/openapi/jellyfin-openapi-stable.json — canonical OpenAPI spec (`/Users/Me` 400 "Token is not owned by a user.", `userId` parameter descriptions across /Items, /Items/Latest, /Shows/*, /Search/Hints)
|
|
- https://github.com/jellyfin/jellyfin — server source: `ItemsController.cs` (userId-required guard and 400/404 ordering), `RequestHelpers.cs` (GetUserId token fallback), `TvShowsController.cs` (NextUp userId version history), `AuthorizationContext.cs` (API key ⇒ User null + admin role)
|
|
- https://mintlify.wiki/jellyfin/jellyfin/api/authentication/overview — official server docs (API key vs user token semantics)
|
|
- https://gist.github.com/nielsvanvelzen/ea047d9028f676185832e51ffaf12a6f — core-developer authorization guide (API-key identity behavior)
|