mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 20:15:10 +00:00
docs: expand config reference and genericize service names
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"` |
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user