mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-05-16 21:59:26 +00:00
docs: update docs to match current codebase
This commit is contained in:
532
docs/API.md
532
docs/API.md
@@ -1,6 +1,8 @@
|
||||
# REST API Documentation
|
||||
|
||||
The unshackle REST API allows you to control downloads, search services, and manage jobs remotely. Start the server with `unshackle serve` and access the interactive Swagger UI at `http://localhost:8786/api/docs/`.
|
||||
The unshackle REST API allows you to control downloads, search services, drive remote downloads from a thin client, and (optionally) co-host the pywidevine/pyplayready CDM. Start the server with `unshackle serve` and access the interactive Swagger UI at `http://localhost:8786/api/docs/`.
|
||||
|
||||
The server is built on **aiohttp** (not FastAPI). Implementation lives in `unshackle/commands/serve.py` and `unshackle/core/api/` (`routes.py`, `handlers.py`, `session_store.py`, `input_bridge.py`, `download_manager.py`, `download_worker.py`).
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -8,32 +10,109 @@ The unshackle REST API allows you to control downloads, search services, and man
|
||||
# Start the server (no authentication)
|
||||
unshackle serve --no-key
|
||||
|
||||
# Start with authentication
|
||||
unshackle serve # Requires api_secret in unshackle.yaml
|
||||
# Start with authentication (api_secret in unshackle.yaml)
|
||||
unshackle serve
|
||||
|
||||
# Serve only the REST API (no pywidevine/pyplayready CDM)
|
||||
unshackle serve --api-only
|
||||
|
||||
# Serve only the remote-dl session endpoints (CORS/Cloudflare friendly)
|
||||
unshackle serve --remote-only
|
||||
|
||||
# Disable just one CDM
|
||||
unshackle serve --no-widevine
|
||||
unshackle serve --no-playready
|
||||
|
||||
# Verbose error responses (tracebacks/stderr in JSON)
|
||||
unshackle serve --debug-api
|
||||
```
|
||||
|
||||
`serve` flags:
|
||||
|
||||
| Flag | Description |
|
||||
| --- | --- |
|
||||
| `-h, --host` | Bind host (default `127.0.0.1`) |
|
||||
| `-p, --port` | Bind port (default `8786`) |
|
||||
| `--caddy` | Also launch Caddy using `Caddyfile` next to the unshackle config |
|
||||
| `--api-only` | REST API only; skip the bundled pywidevine/pyplayready CDM endpoints |
|
||||
| `--no-widevine` | Disable Widevine CDM endpoints |
|
||||
| `--no-playready` | Disable PlayReady CDM endpoints |
|
||||
| `--no-key` | Disable API key authentication entirely |
|
||||
| `--debug-api` | Include tracebacks/stderr in error responses |
|
||||
| `--debug` | Enable DEBUG-level logging for API operations |
|
||||
| `--remote-only` | Expose only `/api/health`, `/api/services`, `/api/search`, and `/api/session/*` (implies `--api-only`) |
|
||||
|
||||
## Authentication
|
||||
|
||||
When `api_secret` is set in `unshackle.yaml`, all API requests require authentication via:
|
||||
|
||||
- **Header**: `X-API-Key: your-secret-key-here`
|
||||
- **Query parameter**: `?api_key=your-secret-key-here`
|
||||
|
||||
Use `--no-key` to disable authentication entirely (not recommended for public-facing servers).
|
||||
When `api_secret` is set in `unshackle.yaml`, all API requests require the **`X-Secret-Key`** header. There is no query-parameter fallback. `/api/health` is always reachable without authentication. `--no-key` disables auth entirely (not recommended for public-facing servers).
|
||||
|
||||
```yaml
|
||||
# unshackle.yaml
|
||||
serve:
|
||||
api_secret: "your-secret-key-here"
|
||||
api_secret: "your-master-secret" # falls back to global users map below
|
||||
remote_only: false # also toggleable via --remote-only
|
||||
services: ["EXAMPLE1", "EXAMPLE2"] # optional global service allowlist
|
||||
users:
|
||||
user-secret-1:
|
||||
username: alice
|
||||
devices: ["my_widevine_l3"] # Widevine WVD names this user may use
|
||||
playready_devices: ["my_pr_sl2000"] # PlayReady PRD names; defaults to [] (no access)
|
||||
services: ["EXAMPLE1"] # optional per-user allowlist (intersected with global)
|
||||
user-secret-2:
|
||||
username: bob
|
||||
devices: []
|
||||
playready_devices: []
|
||||
```
|
||||
|
||||
### Service allowlists
|
||||
|
||||
`config.serve.services` is the global allowlist; `users.<key>.services` further narrows it per key. The effective set is the intersection. Endpoints affected: `/api/services`, `/api/search`, `/api/list-titles`, `/api/list-tracks`, `/api/download`, and all `/api/session/*` routes.
|
||||
|
||||
### CDM access (server-side decryption)
|
||||
|
||||
There is no separate "tier" flag. Whether the server can return KID:KEY for a session-mode download depends solely on the device lists configured for the calling user key:
|
||||
|
||||
- Empty `devices` and `playready_devices` -> server can only proxy CDM challenges; the client must run its own CDM and parse the license.
|
||||
- Populated lists -> the client may set `mode: "server_cdm"` on `/api/session/{id}/license` and receive `{ "keys": { "<track_id>": { "<KID>": "<KEY>" } } }` instead of raw license bytes.
|
||||
|
||||
Per-service CDM type can be pinned via `config.cdm` (`widevine`/`playready`) or per-service `cdm_type`; otherwise the server picks the type the user has devices for.
|
||||
|
||||
---
|
||||
|
||||
## Endpoint Map
|
||||
|
||||
Standard endpoints (suppressed in `--remote-only` mode are marked R):
|
||||
|
||||
| Method | Path | R |
|
||||
| --- | --- | :-: |
|
||||
| GET | `/api/health` | ok |
|
||||
| GET | `/api/services` | ok |
|
||||
| POST | `/api/search` | ok |
|
||||
| POST | `/api/list-titles` | hidden |
|
||||
| POST | `/api/list-tracks` | hidden |
|
||||
| POST | `/api/download` | hidden |
|
||||
| GET | `/api/download/jobs` | hidden |
|
||||
| GET | `/api/download/jobs/{job_id}` | hidden |
|
||||
| DELETE | `/api/download/jobs/{job_id}` | hidden |
|
||||
| POST | `/api/session/create` | ok |
|
||||
| GET | `/api/session/{session_id}` | ok |
|
||||
| DELETE | `/api/session/{session_id}` | ok |
|
||||
| GET | `/api/session/{session_id}/titles` | ok |
|
||||
| POST | `/api/session/{session_id}/tracks` | ok |
|
||||
| POST | `/api/session/{session_id}/segments` | ok |
|
||||
| POST | `/api/session/{session_id}/license` | ok |
|
||||
| GET | `/api/session/{session_id}/prompt` | ok |
|
||||
| POST | `/api/session/{session_id}/prompt` | ok |
|
||||
|
||||
CDM endpoints (`/{wvd}/...`, `/playready/{prd}/...`) are exposed unless `--api-only` / `--remote-only` / `--no-widevine` / `--no-playready` is set, and use pywidevine / pyplayready's own auth scheme.
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
||||
### GET /api/health
|
||||
|
||||
Health check with version and update information.
|
||||
Health check with version and update information. Always reachable without auth.
|
||||
|
||||
```bash
|
||||
curl http://localhost:8786/api/health
|
||||
@@ -55,13 +134,13 @@ curl http://localhost:8786/api/health
|
||||
|
||||
### GET /api/services
|
||||
|
||||
List all available streaming services.
|
||||
List all available streaming services (filtered by the effective allowlist for the caller).
|
||||
|
||||
```bash
|
||||
curl http://localhost:8786/api/services
|
||||
curl -H "X-Secret-Key: $KEY" http://localhost:8786/api/services
|
||||
```
|
||||
|
||||
Returns an array of services with `tag`, `aliases`, `geofence`, `title_regex`, `url`, and `help` text.
|
||||
Returns `{"services": [...]}`. Each entry has `tag`, `aliases`, `geofence`, `title_regex`, `url` (from `cli.short_help`), `help` (full docstring), and `cli_params` describing the service-level Click parameters.
|
||||
|
||||
---
|
||||
|
||||
@@ -84,8 +163,9 @@ Search for titles from a streaming service.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8786/api/search \
|
||||
-H "X-Secret-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"service": "EXAMPLE", "query": "example show"}'
|
||||
-d '{"service": "EXAMPLE1", "query": "example show"}'
|
||||
```
|
||||
|
||||
```json
|
||||
@@ -107,7 +187,7 @@ curl -X POST http://localhost:8786/api/search \
|
||||
|
||||
### POST /api/list-titles
|
||||
|
||||
Get available titles (seasons/episodes/movies) for a service and title ID.
|
||||
Get available titles (seasons/episodes/movies) for a service and title ID. Disabled in `--remote-only` mode.
|
||||
|
||||
**Required parameters:**
|
||||
| Parameter | Type | Description |
|
||||
@@ -117,31 +197,16 @@ Get available titles (seasons/episodes/movies) for a service and title ID.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8786/api/list-titles \
|
||||
-H "X-Secret-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"service": "EXAMPLE", "title_id": "abc123def456"}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"titles": [
|
||||
{
|
||||
"type": "episode",
|
||||
"name": "Pilot",
|
||||
"series_title": "Example Show",
|
||||
"season": 1,
|
||||
"number": 1,
|
||||
"year": 2024,
|
||||
"id": "abc123def789"
|
||||
}
|
||||
]
|
||||
}
|
||||
-d '{"service": "EXAMPLE1", "title_id": "abc123def456"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /api/list-tracks
|
||||
|
||||
Get video, audio, and subtitle tracks for a title.
|
||||
Get video, audio, and subtitle tracks for a title. Disabled in `--remote-only` mode.
|
||||
|
||||
**Required parameters:**
|
||||
| Parameter | Type | Description |
|
||||
@@ -157,23 +222,13 @@ Get video, audio, and subtitle tracks for a title.
|
||||
| `proxy` | string | `null` | Proxy URI or country code |
|
||||
| `no_proxy` | boolean | `false` | Disable all proxy use |
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8786/api/list-tracks \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"service": "EXAMPLE",
|
||||
"title_id": "abc123def456",
|
||||
"wanted": ["S01E01"]
|
||||
}'
|
||||
```
|
||||
|
||||
Returns video, audio, and subtitle tracks with codec, bitrate, resolution, language, and DRM information.
|
||||
|
||||
---
|
||||
|
||||
### POST /api/download
|
||||
|
||||
Start a download job. Returns immediately with a job ID (HTTP 202).
|
||||
Start a download job. Returns immediately with a job ID (HTTP 202). Disabled in `--remote-only` mode.
|
||||
|
||||
**Required parameters:**
|
||||
| Parameter | Type | Description |
|
||||
@@ -185,8 +240,8 @@ Start a download job. Returns immediately with a job ID (HTTP 202).
|
||||
| Parameter | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `quality` | array[int] | best | Resolution(s) (e.g., `[1080, 2160]`) |
|
||||
| `vcodec` | string or array | any | Video codec(s): `H264`, `H265`/`HEVC`, `VP9`, `AV1`, `VC1` |
|
||||
| `acodec` | string or array | any | Audio codec(s): `AAC`, `AC3`, `EC3`, `AC4`, `OPUS`, `FLAC`, `ALAC`, `DTS` |
|
||||
| `vcodec` | string or array | any | Video codec(s): `H264`, `H265`/`HEVC`, `VP9`, `AV1`, `VC1`, `VP8` |
|
||||
| `acodec` | string or array | any | Audio codec(s): `AAC`, `AC3`, `EC3`, `AC4`, `OPUS`, `FLAC`, `ALAC`, `DTS`, `OGG` |
|
||||
| `vbitrate` | int | highest | Video bitrate in kbps |
|
||||
| `abitrate` | int | highest | Audio bitrate in kbps |
|
||||
| `range` | array[string] | `["SDR"]` | Color range(s): `SDR`, `HDR10`, `HDR10+`, `HLG`, `DV`, `HYBRID` |
|
||||
@@ -247,7 +302,7 @@ Start a download job. Returns immediately with a job ID (HTTP 202).
|
||||
| `no_proxy` | boolean | `false` | Disable all proxy use |
|
||||
| `workers` | int | `null` | Max threads per track download |
|
||||
| `downloads` | int | `1` | Concurrent track downloads |
|
||||
| `slow` | string | `null` | Add delay between titles. `true` for 60-120s, or `MIN-MAX` (e.g. `20-40`) |
|
||||
| `slow` | boolean | `false` | Add 60-120s delay between titles |
|
||||
| `best_available` | boolean | `false` | Continue if requested quality unavailable |
|
||||
| `skip_dl` | boolean | `false` | Skip download, only get decryption keys |
|
||||
| `export` | boolean | `false` | Export manifest, track URLs, keys, and subtitles to JSON in the exports directory |
|
||||
@@ -259,9 +314,10 @@ Start a download job. Returns immediately with a job ID (HTTP 202).
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8786/api/download \
|
||||
-H "X-Secret-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"service": "EXAMPLE",
|
||||
"service": "EXAMPLE1",
|
||||
"title_id": "abc123def456",
|
||||
"wanted": ["S01E01"],
|
||||
"quality": [1080, 2160],
|
||||
@@ -285,25 +341,18 @@ curl -X POST http://localhost:8786/api/download \
|
||||
|
||||
### GET /api/download/jobs
|
||||
|
||||
List all download jobs with optional filtering and sorting.
|
||||
List all download jobs with optional filtering and sorting. Disabled in `--remote-only` mode.
|
||||
|
||||
**Query parameters:**
|
||||
| Parameter | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `status` | string | all | Filter by status: `queued`, `downloading`, `completed`, `failed`, `cancelled` |
|
||||
| `service` | string | all | Filter by service tag |
|
||||
| `sort_by` | string | `created_time` | Sort field: `created_time`, `status`, `service` |
|
||||
| `sort_by` | string | `created_time` | Sort field: `created_time`, `started_time`, `completed_time`, `progress`, `status`, `service` |
|
||||
| `sort_order` | string | `desc` | Sort order: `asc`, `desc` |
|
||||
|
||||
```bash
|
||||
# List all jobs
|
||||
curl http://localhost:8786/api/download/jobs
|
||||
|
||||
# Filter by status
|
||||
curl "http://localhost:8786/api/download/jobs?status=completed"
|
||||
|
||||
# Filter by service
|
||||
curl "http://localhost:8786/api/download/jobs?service=EXAMPLE"
|
||||
curl -H "X-Secret-Key: $KEY" "http://localhost:8786/api/download/jobs?status=completed"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -312,19 +361,15 @@ curl "http://localhost:8786/api/download/jobs?service=EXAMPLE"
|
||||
|
||||
Get detailed information about a specific download job including progress, parameters, and error details.
|
||||
|
||||
```bash
|
||||
curl http://localhost:8786/api/download/jobs/504db959-80b0-446c-a764-7924b761d613
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"job_id": "504db959-80b0-446c-a764-7924b761d613",
|
||||
"status": "completed",
|
||||
"created_time": "2026-02-27T18:00:00.000000",
|
||||
"service": "EXAMPLE",
|
||||
"service": "EXAMPLE1",
|
||||
"title_id": "abc123def456",
|
||||
"progress": 100.0,
|
||||
"parameters": { ... },
|
||||
"parameters": { },
|
||||
"started_time": "2026-02-27T18:00:01.000000",
|
||||
"completed_time": "2026-02-27T18:00:15.000000",
|
||||
"output_files": [],
|
||||
@@ -337,13 +382,339 @@ curl http://localhost:8786/api/download/jobs/504db959-80b0-446c-a764-7924b761d61
|
||||
|
||||
### DELETE /api/download/jobs/{job_id}
|
||||
|
||||
Cancel a queued or running download job.
|
||||
Cancel a queued or running download job. Returns 400 if the job has already terminated.
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8786/api/download/jobs/504db959-80b0-446c-a764-7924b761d613
|
||||
---
|
||||
|
||||
## Remote Service Sessions
|
||||
|
||||
These endpoints back the `RemoteService` adapter in `unshackle/core/remote_service.py`. They let a thin `dl` client (or any consumer) authenticate against a service on the server, fetch titles/tracks/manifests, and either proxy CDM challenges or have the server resolve KID:KEY directly. The `dl` command's `RemoteService` adapter replaces the old `remote_dl` command. These endpoints are the only `/api/*` routes available in `--remote-only` mode (in addition to `health`, `services`, and `search`).
|
||||
|
||||
### POST /api/session/create
|
||||
|
||||
Authenticate against a service and open a session. Body fields:
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `service` | string | Service tag (required) |
|
||||
| `title_id` | string | Title ID/URL (required) |
|
||||
| `credentials` | object | Auth credentials forwarded to `Service.authenticate` |
|
||||
| `cookies` | string | Cookie blob (Netscape or JSON) |
|
||||
| `proxy` | string | Proxy URI or country code |
|
||||
| `no_proxy` | bool | Force-disable proxies |
|
||||
| `profile` | string | Profile name |
|
||||
| `cache` | object | Optional pre-warmed title cache payload |
|
||||
|
||||
If the service requires interactive input during authentication, poll `GET /api/session/{id}/prompt` and submit responses via `POST /api/session/{id}/prompt` until status is `authenticated`.
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{
|
||||
"service": "EXAMPLE1",
|
||||
"title_id": "abc123def456",
|
||||
"credentials": {"username": "alice", "password": "hunter2"},
|
||||
"cookies": "# Netscape HTTP Cookie File\n...",
|
||||
"proxy": "us",
|
||||
"no_proxy": false,
|
||||
"profile": "default",
|
||||
"cache": {}
|
||||
}
|
||||
```
|
||||
|
||||
Returns confirmation on success, or an error if the job has already completed or been cancelled.
|
||||
**Response (202-style; auth runs asynchronously):**
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "f1c4a8b2-9c7e-4d2a-bf91-2d3e4f5a6b7c",
|
||||
"service": "EXAMPLE1",
|
||||
"status": "authenticating"
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/session/{session_id}
|
||||
|
||||
Returns session metadata. 404 if expired or unknown.
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "f1c4a8b2-9c7e-4d2a-bf91-2d3e4f5a6b7c",
|
||||
"service": "EXAMPLE1",
|
||||
"valid": true,
|
||||
"expires_in": 3600,
|
||||
"track_count": 0,
|
||||
"title_count": 0
|
||||
}
|
||||
```
|
||||
|
||||
### DELETE /api/session/{session_id}
|
||||
|
||||
Tears down the session, cancels any pending prompts, and returns any updated per-session cache files (base64-encoded, zlib-compressed) so the client can re-warm next time.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"cache": {
|
||||
"tokens": "eJzLSM3JyVcozy/KSVGo5AIAGgQEvQ=="
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/session/{session_id}/titles
|
||||
|
||||
Returns the resolved titles list.
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "f1c4a8b2-9c7e-4d2a-bf91-2d3e4f5a6b7c",
|
||||
"titles": [
|
||||
{
|
||||
"type": "episode",
|
||||
"name": "Pilot",
|
||||
"series_title": "Example Show",
|
||||
"season": 1,
|
||||
"number": 1,
|
||||
"year": 2024,
|
||||
"id": "ep-0001",
|
||||
"language": "en"
|
||||
},
|
||||
{
|
||||
"type": "movie",
|
||||
"name": "Example Movie",
|
||||
"year": 2024,
|
||||
"id": "mov-0001",
|
||||
"language": "en"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/session/{session_id}/tracks
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{"title_id": "ep-0001"}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"title": {
|
||||
"type": "episode",
|
||||
"name": "Pilot",
|
||||
"series_title": "Example Show",
|
||||
"season": 1,
|
||||
"number": 1,
|
||||
"year": 2024,
|
||||
"id": "ep-0001",
|
||||
"language": "en"
|
||||
},
|
||||
"video": [
|
||||
{
|
||||
"id": "v-1080p-h264",
|
||||
"codec": "H264",
|
||||
"codec_display": "H.264",
|
||||
"bitrate": 6000,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"resolution": "1920x1080",
|
||||
"fps": "23.976",
|
||||
"range": "SDR",
|
||||
"range_display": "SDR",
|
||||
"language": "en",
|
||||
"drm": [
|
||||
{
|
||||
"type": "widevine",
|
||||
"pssh": "AAAAW3Bzc2gAAAAA7e+...",
|
||||
"kids": ["abcdef0123456789abcdef0123456789"],
|
||||
"license_url": "https://license.example.com/widevine"
|
||||
}
|
||||
],
|
||||
"descriptor": "DASH",
|
||||
"url": "https://cdn.example.com/manifest.mpd"
|
||||
}
|
||||
],
|
||||
"audio": [
|
||||
{
|
||||
"id": "a-en-eac3",
|
||||
"codec": "EC3",
|
||||
"codec_display": "Dolby Digital Plus",
|
||||
"bitrate": 640,
|
||||
"channels": "5.1",
|
||||
"language": "en",
|
||||
"atmos": false,
|
||||
"descriptive": false,
|
||||
"drm": null,
|
||||
"descriptor": "DASH",
|
||||
"url": "https://cdn.example.com/manifest.mpd"
|
||||
}
|
||||
],
|
||||
"subtitles": [
|
||||
{
|
||||
"id": "s-en-vtt",
|
||||
"codec": "WebVTT",
|
||||
"language": "en",
|
||||
"forced": false,
|
||||
"sdh": false,
|
||||
"cc": false,
|
||||
"descriptor": "DASH",
|
||||
"url": "https://cdn.example.com/subs/en.vtt"
|
||||
}
|
||||
],
|
||||
"chapters": [
|
||||
{"timestamp": "00:00:00.000", "name": "Chapter 1"}
|
||||
],
|
||||
"attachments": [],
|
||||
"manifests": [
|
||||
{
|
||||
"type": "dash",
|
||||
"url": "https://cdn.example.com/manifest.mpd",
|
||||
"data": "eJzNVk1v2zAM/Ss..."
|
||||
}
|
||||
],
|
||||
"session_headers": {
|
||||
"User-Agent": "Mozilla/5.0 ..."
|
||||
},
|
||||
"session_cookies": {
|
||||
"session": "abc123"
|
||||
},
|
||||
"server_cdm_type": "widevine"
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/session/{session_id}/segments
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{"track_ids": ["v-1080p-h264", "a-en-eac3"]}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"tracks": {
|
||||
"v-1080p-h264": {
|
||||
"descriptor": "DASH",
|
||||
"url": "https://cdn.example.com/manifest.mpd",
|
||||
"drm": [
|
||||
{
|
||||
"type": "widevine",
|
||||
"pssh": "AAAAW3Bzc2gAAAAA7e+...",
|
||||
"kids": ["abcdef0123456789abcdef0123456789"],
|
||||
"license_url": "https://license.example.com/widevine"
|
||||
}
|
||||
],
|
||||
"headers": {"User-Agent": "Mozilla/5.0 ..."},
|
||||
"cookies": {"session": "abc123"},
|
||||
"data": {}
|
||||
},
|
||||
"a-en-eac3": {
|
||||
"descriptor": "DASH",
|
||||
"url": "https://cdn.example.com/manifest.mpd",
|
||||
"drm": null,
|
||||
"headers": {"User-Agent": "Mozilla/5.0 ..."},
|
||||
"cookies": {"session": "abc123"},
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/session/{session_id}/license
|
||||
|
||||
Two modes, selected by the `mode` field.
|
||||
|
||||
**`mode: "proxy"` (default)** -- forward a client-built CDM challenge to the service's license endpoint.
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "proxy",
|
||||
"track_id": "v-1080p-h264",
|
||||
"challenge": "CAESxQEK...",
|
||||
"drm_type": "widevine",
|
||||
"pssh": "AAAAW3Bzc2gAAAAA7e+..."
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{"license": "CAIS3wIK..."}
|
||||
```
|
||||
|
||||
**`mode: "server_cdm"`** -- the server uses its own CDM to license the track and extract keys. Single-track form takes `track_id`; batch form takes `track_ids`. Requires the calling user key to have a matching device (`devices` for Widevine, `playready_devices` for PlayReady) in `unshackle.yaml`.
|
||||
|
||||
Request (batch):
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "server_cdm",
|
||||
"track_ids": ["v-1080p-h264", "a-en-eac3"],
|
||||
"drm_type": "widevine"
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"keys": {
|
||||
"v-1080p-h264": {
|
||||
"abcdef0123456789abcdef0123456789": "00112233445566778899aabbccddeeff"
|
||||
},
|
||||
"a-en-eac3": {
|
||||
"abcdef0123456789abcdef0123456789": "00112233445566778899aabbccddeeff"
|
||||
}
|
||||
},
|
||||
"drm_type": "widevine"
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/session/{session_id}/prompt
|
||||
|
||||
Polled by the client during interactive authentication (OTP, PIN, device codes). Backed by the `InputBridge` in `unshackle/core/api/input_bridge.py`; `Service.request_input()` blocks server-side until the client posts a response.
|
||||
|
||||
Pending input:
|
||||
|
||||
```json
|
||||
{"status": "pending_input", "prompt": "Enter OTP code: "}
|
||||
```
|
||||
|
||||
Other states:
|
||||
|
||||
```json
|
||||
{"status": "authenticating"}
|
||||
```
|
||||
|
||||
```json
|
||||
{"status": "authenticated"}
|
||||
```
|
||||
|
||||
```json
|
||||
{"status": "failed", "error": "Invalid credentials"}
|
||||
```
|
||||
|
||||
### POST /api/session/{session_id}/prompt
|
||||
|
||||
Unblocks the server-side `request_input()` call.
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{"response": "123456"}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{"status": "accepted"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -357,22 +728,25 @@ All endpoints return consistent error responses:
|
||||
"error_code": "INVALID_PARAMETERS",
|
||||
"message": "Invalid vcodec: XYZ. Must be one of: H264, H265, VP9, AV1, VC1, VP8",
|
||||
"timestamp": "2026-02-27T18:00:00.000000+00:00",
|
||||
"details": { ... }
|
||||
"details": { }
|
||||
}
|
||||
```
|
||||
|
||||
Common error codes:
|
||||
- `INVALID_INPUT` - Malformed request body
|
||||
- `INVALID_PARAMETERS` - Invalid parameter values
|
||||
- `MISSING_SERVICE` - Service tag not provided
|
||||
- `INVALID_SERVICE` - Service not found
|
||||
- `SERVICE_ERROR` - Service initialization or runtime error
|
||||
- `AUTH_FAILED` - Authentication failure
|
||||
- `NOT_FOUND` - Job or resource not found
|
||||
- `INTERNAL_ERROR` - Unexpected server error
|
||||
|
||||
- `INVALID_INPUT` -- malformed request body
|
||||
- `INVALID_PARAMETERS` -- invalid parameter values
|
||||
- `MISSING_SERVICE` -- service tag not provided
|
||||
- `INVALID_SERVICE` -- service not found or not in the caller's allowlist
|
||||
- `SERVICE_ERROR` -- service initialization or runtime error
|
||||
- `AUTH_FAILED` -- authentication failure
|
||||
- `NOT_FOUND` / `TRACK_NOT_FOUND` / session not found -- job/session/track/title missing
|
||||
- `INTERNAL_ERROR` -- unexpected server error
|
||||
|
||||
When `--debug-api` is enabled, error responses include additional `debug_info` with tracebacks and stderr output.
|
||||
|
||||
Authentication errors from the auth middleware are returned as `{"status": 401, "message": "..."}` (not the standard error envelope).
|
||||
|
||||
---
|
||||
|
||||
## Download Job Lifecycle
|
||||
@@ -385,3 +759,5 @@ downloading -> cancelled
|
||||
```
|
||||
|
||||
Jobs are retained for 24 hours after completion. The server supports up to 2 concurrent downloads by default.
|
||||
|
||||
Remote sessions are managed by `SessionStore` (`unshackle/core/api/session_store.py`); idle sessions and their `InputBridge` instances are cleaned up by a background loop started/stopped with the app lifecycle.
|
||||
|
||||
Reference in New Issue
Block a user