docs: expand config reference and genericize service names

This commit is contained in:
imSp4rky
2026-07-06 22:50:38 -06:00
parent 774c1a4555
commit 84d9b92530
21 changed files with 384 additions and 211 deletions
+3 -3
View File
@@ -59,7 +59,7 @@ serve:
username: alice
devices: ["my_widevine_device"] # Widevine (.wvd) devices this key may use
playready_devices: ["my_playready_prd"] # PlayReady (.prd) devices this key may use
services: ["AMZN", "NF"] # optional per-key service allowlist
services: ["EXAMPLE1", "EXAMPLE2"] # optional per-key service allowlist
a-secret-key-for-bob:
username: bob
devices: ["my_widevine_device"]
@@ -217,11 +217,11 @@ The effective allowlist for a request is:
```yaml title="Restricting services"
serve:
api_secret: "admin-key"
services: ["AMZN", "NF", "DSNP"] # global ceiling for all keys
services: ["EXAMPLE1", "EXAMPLE2", "EXAMPLE3"] # global ceiling for all keys
users:
limited-key:
username: limited
services: ["AMZN"] # this key ends up allowed only AMZN
services: ["EXAMPLE1"] # this key ends up allowed only EXAMPLE1
```
Endpoints that name a service (listing services, searching, listing titles/tracks, downloading, profiles, history, and remote sessions) all filter against this allowlist. A request for a service outside the key's allowlist is rejected as an invalid service. Service tags are matched after normalization, so casing and aliases resolve consistently.
+26 -26
View File
@@ -130,8 +130,8 @@ Search a service for titles by query string. The service must implement `search(
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `service` | string | yes | — | Service tag. |
| `query` | string | yes | — | Search query. |
| `service` | string | yes | - | Service tag. |
| `query` | string | yes | - | Search query. |
| `profile` | string | no | `null` | Credential/cookie profile to use. |
| `proxy` | string | no | `null` | Proxy URI or country code. |
| `no_proxy` | boolean | no | `false` | Force-disable all proxy use. |
@@ -164,7 +164,7 @@ Search a service for titles by query string. The service must implement `search(
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | — | Search completed. |
| `200` | - | Search completed. |
| `400` | `INVALID_PARAMETERS` | Missing `query`. |
| `400` | `INVALID_SERVICE` | Unknown or disallowed service. |
| `400` | `INVALID_PROXY` | Proxy could not be resolved. |
@@ -222,7 +222,7 @@ Each serialized title carries `type` (`"episode"`, `"movie"`, or `"other"`), `na
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | — | Titles returned. |
| `200` | - | Titles returned. |
| `400` | `INVALID_INPUT` | Missing `service` or `title_id`. |
| `400` | `INVALID_SERVICE` | Unknown or disallowed service. |
| `401` | `AUTH_FAILED` | Authentication with the service failed. |
@@ -242,7 +242,7 @@ List the video, audio, and subtitle tracks for a title. For series, you can scop
| `wanted` | string | no | Episode/season range (e.g. `"S01E01-S01E03"`). |
| `season` | int/string | no | Season number (combined with `episode`). |
| `episode` | int/string | no | Episode number (combined with `season`). |
| `profile`, `proxy`, `no_proxy`, `cdm_type` | — | no | As on `list-titles`. |
| `profile`, `proxy`, `no_proxy`, `cdm_type` | - | no | As on `list-titles`. |
When both `season` and `episode` are given, they are combined into a `"{season}x{episode}"` selector.
@@ -297,7 +297,7 @@ The `drm` array on a track is a list of `{type, pssh?, kids?, content_keys?, lic
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | — | Tracks returned. |
| `200` | - | Tracks returned. |
| `400` | `INVALID_INPUT` | Missing `service` or `title_id`. |
| `400` | `INVALID_PARAMETERS` | Invalid `wanted` range. |
| `400` | `INVALID_SERVICE` | Unknown or disallowed service. |
@@ -378,7 +378,7 @@ Create a download job. Requires `service` and `title_id`; every other field is a
| `best_available` | boolean | `false` | Fall back to best available. |
| `repack` | boolean | `false` | Add REPACK tag. |
| `tag` | string | `null` | Release group tag. |
| `tmdb_id` / `imdb_id` / `animeapi_id` | — | `null` | External ID overrides for tagging. |
| `tmdb_id` / `imdb_id` / `animeapi_id` | - | `null` | External ID overrides for tagging. |
| `enrich` | boolean | `false` | Override title/year from external source. |
| `output_dir` | string | `null` | Override output directory. |
| `no_cache` / `reset_cache` | boolean | `false` | Title cache controls. |
@@ -395,7 +395,7 @@ Create a download job. Requires `service` and `title_id`; every other field is a
| Status | Error code | Meaning |
| --- | --- | --- |
| `202` | — | Job queued. |
| `202` | - | Job queued. |
| `400` | `INVALID_INPUT` | Missing `service`/`title_id` or malformed JSON. |
| `400` | `INVALID_PARAMETERS` | A parameter failed validation. |
| `400` | `INVALID_SERVICE` | Unknown or disallowed service. |
@@ -409,8 +409,8 @@ List download jobs, with optional filtering and sorting.
| Param | Values | Default | Description |
| --- | --- | --- | --- |
| `status` | `queued`, `downloading`, `completed`, `failed`, `cancelled` | — | Filter by status. |
| `service` | service tag | — | Filter by service. |
| `status` | `queued`, `downloading`, `completed`, `failed`, `cancelled` | - | Filter by status. |
| `service` | service tag | - | Filter by service. |
| `sort_by` | `created_time`, `started_time`, `completed_time`, `progress`, `status`, `service` | `created_time` | Sort field. |
| `sort_order` | `asc`, `desc` | `desc` | Sort direction. |
| `full` | `true`, `false` | `false` | Include full per-job details. |
@@ -454,7 +454,7 @@ With `full=true`, each job additionally includes `parameters` (with secrets reda
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | — | Jobs returned. |
| `200` | - | Jobs returned. |
| `400` | `INVALID_PARAMETERS` | Invalid `sort_by` or `sort_order`. |
### `GET /api/download/jobs/{job_id}`
@@ -463,7 +463,7 @@ Fetch a single job with full details (equivalent to a `full=true` list entry).
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | — | Job returned. |
| `200` | - | Job returned. |
| `404` | `JOB_NOT_FOUND` | No such job. |
### `DELETE /api/download/jobs/{job_id}`
@@ -478,8 +478,8 @@ Cancel or remove a job. The behaviour depends on the job's current state:
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | — | Job cancelled. |
| `204` | — | Terminal job removed (empty body). |
| `200` | - | Job cancelled. |
| `204` | - | Terminal job removed (empty body). |
| `400` | `INVALID_PARAMETERS` | Job is in a state that cannot be cancelled. |
| `404` | `JOB_NOT_FOUND` | No such job. |
@@ -512,7 +512,7 @@ Enqueue a **new** job that reuses a terminal job's service, title, and parameter
| Status | Error code | Meaning |
| --- | --- | --- |
| `202` | — | New job queued. |
| `202` | - | New job queued. |
| `404` | `JOB_NOT_FOUND` | No such job. |
| `409` | `CONFLICT` | Job is not in a terminal state. |
| `400` | `INVALID_SERVICE` | Service is no longer allowed. |
@@ -530,7 +530,7 @@ Move a queued job to the front of the download queue.
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | — | Job moved to front. |
| `200` | - | Job moved to front. |
| `404` | `JOB_NOT_FOUND` | No such job. |
| `409` | `CONFLICT` | Job is not queued. |
@@ -593,7 +593,7 @@ Read the persisted history of jobs that reached a terminal state, newest first.
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| `limit` | int (≥ 1) | `100` | Maximum entries to return. |
| `service` | string | — | Case-insensitive service filter. |
| `service` | string | - | Case-insensitive service filter. |
=== "Response `200`"
@@ -619,7 +619,7 @@ Read the persisted history of jobs that reached a terminal state, newest first.
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | — | History returned. |
| `200` | - | History returned. |
| `400` | `INVALID_PARAMETERS` | Invalid `limit`. |
### `DELETE /api/history/{job_id}`
@@ -628,7 +628,7 @@ Remove a single persisted history entry. Returns **`204 No Content`** (empty bod
| Status | Error code | Meaning |
| --- | --- | --- |
| `204` | — | Entry removed (empty body). |
| `204` | - | Entry removed (empty body). |
| `404` | `NOT_FOUND` | No such entry (or outside your allowlist). |
### `POST /api/maintenance/clear-cache` and `POST /api/maintenance/clear-temp`
@@ -648,7 +648,7 @@ The `409` guard is about active file I/O, not an arbitrary lock: an in-flight jo
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | — | Directory cleared. |
| `200` | - | Directory cleared. |
| `409` | `CONFLICT` | A download is currently active. |
### `POST /api/maintenance/refresh-services`
@@ -718,7 +718,7 @@ Create a session for a service and title. Authentication runs asynchronously in
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | — | Session created; authentication in progress. |
| `200` | - | Session created; authentication in progress. |
| `400` | `INVALID_INPUT` | Missing `service` or `title_id`. |
| `400` | `INVALID_SERVICE` | Unknown or disallowed service. |
@@ -752,7 +752,7 @@ Poll for the authentication status and any pending interactive prompt (OTP, PIN,
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | — | Status returned. |
| `200` | - | Status returned. |
| `403` | `FORBIDDEN` | Request IP differs from the session creator. |
| `404` | `SESSION_NOT_FOUND` | No such session. |
@@ -768,7 +768,7 @@ Submit a response to a pending prompt. Body: `{ "response": "..." }` (required).
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | — | Response accepted. |
| `200` | - | Response accepted. |
| `400` | `INVALID_INPUT` | Missing `response`, or no prompt is pending. |
| `404` | `SESSION_NOT_FOUND` | No such session. |
@@ -810,7 +810,7 @@ Get tracks and chapters for a specific title within the session. Body: `{ "title
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | — | Tracks returned. |
| `200` | - | Tracks returned. |
| `400` | `INVALID_INPUT` | Missing or unknown `title_id`. |
| `404` | `SESSION_NOT_FOUND` | No such session. |
@@ -830,7 +830,7 @@ Resolve the download URL, DRM info, headers, cookies, and raw track data for sel
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | — | Segment info returned. |
| `200` | - | Segment info returned. |
| `400` | `INVALID_INPUT` | Missing `track_ids`. |
| `404` | `TRACK_NOT_FOUND` | An unknown track ID was requested. |
@@ -876,7 +876,7 @@ Acquire DRM keys. Two modes are supported via the `mode` field.
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | — | License/keys returned. |
| `200` | - | License/keys returned. |
| `400` | `INVALID_INPUT` | Missing challenge/PSSH, or no CDM device configured for the key. |
| `400` | `INVALID_PARAMETERS` | Unsupported `drm_type`. |
| `404` | `TRACK_NOT_FOUND` | Unknown track ID. |
+1 -1
View File
@@ -139,7 +139,7 @@ uncategorised exception bubbles up, the server runs it through a classifier that
inspects the exception type and message and maps it to the most appropriate code.
The classification is keyword-based, checked in order:
| If the error mentions… | It becomes | Retryable |
| If the error mentions... | It becomes | Retryable |
|---|---|---|
| auth, login, credential, unauthorized, forbidden, token | `AUTH_FAILED` (401) | no |
| connection, timeout, network, unreachable, socket, dns, resolve (or `ConnectionError`, `TimeoutError`, `URLError`, `SSLError`) | `NETWORK_ERROR` (503) | yes |
+1 -1
View File
@@ -211,7 +211,7 @@ every request except `/api/health`:
curl -X POST http://127.0.0.1:8786/api/search \
-H "X-Secret-Key: your-api-secret" \
-H "Content-Type: application/json" \
-d '{"service": "NF", "query": "example"}'
-d '{"service": "EXAMPLE", "query": "example"}'
```
A missing or unknown key returns `401`. The full model, including per-key service
+2 -2
View File
@@ -86,7 +86,7 @@ curl -X POST http://127.0.0.1:8786/api/download \
-H "X-Secret-Key: your-api-secret" \
-H "Content-Type: application/json" \
-d '{
"service": "NF",
"service": "EXAMPLE",
"title_id": "81234567",
"quality": ["1080"],
"range": ["HDR10"]
@@ -228,7 +228,7 @@ state, and this is important because the response differs:
|---|---|---|
| `queued` or `downloading` | The job is cancelled. A running download's worker process is terminated. | `200` with `{"status": "success", "message": "Job cancelled"}` |
| `completed`, `failed`, or `cancelled` (terminal) | The job is **removed** from the manager entirely. | `204 No Content`, empty body |
| Not found | — | `404` with error code `JOB_NOT_FOUND` |
| Not found | - | `404` with error code `JOB_NOT_FOUND` |
In other words, `DELETE` on a live job *cancels* it, while `DELETE` on an already
finished job *deletes it from the list*. This dual meaning is deliberate: one verb
+3 -3
View File
@@ -161,7 +161,7 @@ If the key is missing or wrong, you get a `401` with a plain body:
```
!!! warning "Two error shapes exist"
The authentication middleware returns the short `{"status": 401, "message": "..."}` shape shown above. Every **other** error the API produces uses the richer structured shape (`error_code`, `timestamp`, `details`, …) described in [Error handling](#error-handling). A robust client should handle both.
The authentication middleware returns the short `{"status": 401, "message": "..."}` shape shown above. Every **other** error the API produces uses the richer structured shape (`error_code`, `timestamp`, `details`, ...) described in [Error handling](#error-handling). A robust client should handle both.
The `tag` value for the service you want is the identifier you pass as `service` in the calls below.
@@ -225,8 +225,8 @@ The request body accepts the full download parameter set. A few of the most usef
| Field | Default | Meaning |
| --- | --- | --- |
| `service` | — | Service tag (required) |
| `title_id` | — | Title identifier (required) |
| `service` | - | Service tag (required) |
| `title_id` | - | Title identifier (required) |
| `profile` | `null` | Named credential/cookie profile to use |
| `quality` | `[]` | Resolution(s), e.g. `[1080]`; empty means best available |
| `vcodec` | `null` | Video codec, e.g. `"H265"` |
+12 -12
View File
@@ -65,7 +65,7 @@ remote_services:
server_cdm: false
# Optional: per-service config overrides applied locally (title_map, cdm, etc.)
services:
AMZN:
EXAMPLE:
title_map:
"0ABCDEF": "The Show (Renamed Locally)"
```
@@ -87,13 +87,13 @@ remote_services:
If exactly one server is configured, unshackle uses it implicitly:
```bash
unshackle dl AMZN 0ABCDEF
unshackle dl EXAMPLE 0ABCDEF
```
If **more than one** server is configured, you must pick one with `--server`:
```bash
unshackle dl --server my_server AMZN 0ABCDEF
unshackle dl --server my_server EXAMPLE 0ABCDEF
```
With no `remote_services` configured at all, unshackle raises a clear error telling
@@ -280,10 +280,10 @@ All routes are exposed even in `--remote-only` server mode. Paths use the
The response returns **before** authentication finishes:
```json
{ "session_id": "…uuid4…", "service": "AMZN", "status": "authenticating" }
{ "session_id": "...uuid4...", "service": "EXAMPLE", "status": "authenticating" }
```
Authentication runs on a background thread (`asyncio.to_thread(authenticate, …)`).
Authentication runs on a background thread (`asyncio.to_thread(authenticate, ...)`).
The session starts in `AUTHENTICATING`; the client must poll the prompt endpoint
until it reaches `authenticated` (or `failed`).
@@ -306,11 +306,11 @@ layer.
{ "status": "authenticated" }
{ "status": "authenticating" }
{ "status": "pending_input", "prompt": "Enter code: " }
{ "status": "failed", "error": "…message…" }
{ "status": "failed", "error": "...message..." }
```
A missing session returns `404 SESSION_NOT_FOUND`; an IP mismatch returns
`403 FORBIDDEN`. `POST /api/session/{id}/prompt` takes `{ "response": "…" }` and
`403 FORBIDDEN`. `POST /api/session/{id}/prompt` takes `{ "response": "..." }` and
returns `{ "status": "accepted" }`; posting with no pending prompt is an
`INVALID_INPUT` error.
@@ -364,7 +364,7 @@ and base64-encoding, **excluding** `titles_*` files) and returns them under a
`cache` key so the client can persist refreshed tokens:
```json
{ "status": "ok", "cache": { "tokens": "…base64(zlib(bytes))…" } }
{ "status": "ok", "cache": { "tokens": "...base64(zlib(bytes))..." } }
```
### Session info response
@@ -373,8 +373,8 @@ and base64-encoding, **excluding** `titles_*` files) and returns them under a
```json
{
"session_id": "…",
"service": "AMZN",
"session_id": "...",
"service": "EXAMPLE",
"valid": true,
"expires_in": 300,
"track_count": 12,
@@ -410,7 +410,7 @@ session API and a good template for any client.
## Troubleshooting
!!! example "\"Could not connect to remote server … Is it running?\""
!!! example "\"Could not connect to remote server ... Is it running?\""
The client couldn't reach the URL. Confirm `unshackle serve` is running on the
server, the `url`/port in `remote_services` are correct (default port `8786`),
and any firewall or reverse proxy allows the connection.
@@ -426,6 +426,6 @@ session API and a good template for any client.
prompts you, check that credentials/cookies forwarded from the client are valid
for the service.
!!! example "\"Multiple remote services configured. Use --server …\""
!!! example "\"Multiple remote services configured. Use --server ...\""
You have more than one entry under `remote_services`. Pass `--server <name>`
to pick one.