mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 20:15:10 +00:00
docs: phase 2 of the prose style across docs and docstrings
This commit is contained in:
@@ -100,8 +100,8 @@ Declared at class level to configure framework behaviour:
|
||||
| `VAULT_TAG` | `Optional[str]` | Overrides the key-vault namespace so sibling services can share one vault. Default `None` (use the service's own tag). |
|
||||
| `AUTH_METHODS` | `Optional[tuple[str, ...]]` | Auth methods accepted (`"cookies"` / `"credentials"`). When `None`, the REST `/services` endpoint infers them from `authenticate()`. |
|
||||
| `NO_SUBTITLES` | `bool` | Set `True` on a service with no subtitle tracks to skip subtitle handling entirely. |
|
||||
| `ANIME` | `bool` | Set `True` when the catalog is anime, so metadata lookups prefer AniList. A title's own `anime` flag overrides it. |
|
||||
| `DAILY` | `bool` | Set `True` when the catalog is daily/date-based (talk shows, news, sports), so episodes are named by air date. A title's own `daily` flag overrides it. |
|
||||
| `ANIME` | `bool` | Set `True` when the catalogue is anime, so metadata lookups prefer AniList. A title's own `anime` flag overrides it. |
|
||||
| `DAILY` | `bool` | Set `True` when the catalogue is daily/date-based (talk shows, news, sports), so episodes are named by air date. A title's own `daily` flag overrides it. |
|
||||
|
||||
!!! note "`NO_SUBTITLES` is a convention, not a base-class attribute"
|
||||
`dl.py` uses `hasattr` to find `NO_SUBTITLES`, and the `Service` base class
|
||||
@@ -130,7 +130,7 @@ will use throughout the service:
|
||||
| `self.config` | This service's `config.yaml` contents (a dict), or `None` if absent. |
|
||||
| `self.log` | A `logging.Logger` named after your class. |
|
||||
| `self.session` | A prepared `requests.Session` with config headers and a retry adapter (5 retries, backoff 0.2, retries on 429/500/502/503/504). |
|
||||
| `self.cache` | A `Cacher` for arbitrary key/value data (tokens, etc.). |
|
||||
| `self.cache` | A `Cacher` for arbitrary key/value data, for example authentication tokens. |
|
||||
| `self.title_cache` | A region/account-aware `TitleCacher` used by `get_titles_cached`. |
|
||||
| `self.cache_dir` | `config.directories.cache / <ClassName>`. |
|
||||
| `self.track_request` | A `TrackRequest` built from the CLI `--vcodec` / `--range` / `--best-available` flags. |
|
||||
|
||||
@@ -171,7 +171,7 @@ Find titles in a service by query string. The service must have a `search()` met
|
||||
| `400` | `INVALID_PARAMETERS` | Missing `query`. |
|
||||
| `400` | `INVALID_SERVICE` | Unknown or disallowed service. |
|
||||
| `400` | `INVALID_PROXY` | Proxy could not be resolved. |
|
||||
| `502` | `SERVICE_ERROR` | Service failed to initialize, or does not support search. |
|
||||
| `502` | `SERVICE_ERROR` | Service failed to initialise, or does not support search. |
|
||||
|
||||
### `POST /api/list-titles`
|
||||
|
||||
@@ -235,7 +235,7 @@ These keys are sent only when the title carries them, so a title without them se
|
||||
| Key | Type | On | Meaning |
|
||||
| --- | --- | --- | --- |
|
||||
| `part` | integer | episode | Part index of a [split episode](../creating-a-service.md#split-episodes), counting from 1. |
|
||||
| `air_date` | string | episode | ISO air date of [dated content](../../guide/downloading.md#daily-and-date-based-content). unshackle names the episode by date instead of `SxxExx`. |
|
||||
| `air_date` | string | episode | ISO air date of [daily and date-based content](../../guide/downloading.md#daily-and-date-based-content). unshackle names the episode by date instead of `SxxExx`. |
|
||||
| `absolute` | integer | episode | Absolute episode number across all seasons. |
|
||||
| `daily` | boolean | episode | The episode is daily/date-based. Set from the title, or from the service's `DAILY` class attribute. |
|
||||
| `anime` | boolean | episode, movie | The title is anime, so metadata lookups prefer AniList. Set from the title, or from the service's `ANIME` class attribute. |
|
||||
@@ -268,7 +268,7 @@ Show the video, audio, and subtitle tracks for a title. For series, you can scop
|
||||
| --- | --- | --- | --- |
|
||||
| `service` | string | yes | Service tag. |
|
||||
| `title_id` | string | yes | Title identifier. |
|
||||
| `wanted` | string / string[] | no | Episode/season range (e.g. `"S01E01-S01E03"`), or a music track selector (e.g. `"1-5"`, `"1,3,7"`, or `"2x3"` for disc 2 track 3). A list of selectors is accepted too. |
|
||||
| `wanted` | string / string[] | no | Episode/season range (e.g. `"S01E01-S01E03"`), or a song selector (e.g. `"1-5"`, `"1,3,7"`, or `"2x3"` for disc 2 track 3). A list of selectors is accepted too. |
|
||||
| `season` | int/string | no | Season number (combined with `episode`). |
|
||||
| `episode` | int/string | no | Episode number (combined with `season`). |
|
||||
| `part` | int/string | no | Part index of a split episode (combined with `season` and `episode`). |
|
||||
@@ -383,7 +383,7 @@ Make a download job. It requires `service` and `title_id`. Every other field is
|
||||
| `vbitrate` / `abitrate` | int | `null` | Video / audio bitrate in kbps (positive). |
|
||||
| `vbitrate_range` / `abitrate_range` | string | `null` | `"MIN-MAX"` bitrate window. |
|
||||
| `range` | string[] | `["SDR"]` | Dynamic range(s). |
|
||||
| `channels` | number | `null` | Audio channel count. |
|
||||
| `channels` | number | `null` | Audio channel layout. |
|
||||
| `no_atmos` | boolean | `false` | Exclude Atmos tracks. |
|
||||
| `wanted` | string / string[] | `[]` | Episode/season selectors, as a list or as one comma-separated string. Accepts the part form, `"S01E01.2"`, and the air-date form, `"2026-08-11"` or `"2026-08-01:2026-08-31"`. For a music release, a selector is a track number, `"1-5"` or `"1,3,7"`, or `"{disc}x{track}"` such as `"2x3"`. |
|
||||
| `latest_episode` | boolean | `false` | Only the newest episode. |
|
||||
@@ -411,7 +411,7 @@ Make a download job. It requires `service` and `title_id`. Every other field is
|
||||
| `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` / `tvdb_id` / `anilist_id` | - | `null` | External ID overrides. Each resolves its metadata directly instead of by a title search, and is used for tagging. Set `enrich` to also take the title, year and original language. Give at most one of `tmdb_id`, `imdb_id` and `tvdb_id`, since unshackle resolves the others from it. Sending two returns `400`. `anilist_id` still combines with one of them. `tmdb_id` and `tvdb_id` must be positive integers, `imdb_id` must look like `tt1375666`, and `anilist_id` must be a positive integer or a string like `mal:12345`, or the request returns `400`. An ID whose provider is unconfigured, such as `tmdb_id` with no `tmdb_api_key`, fails the job rather than returning `400`. `anilist_id` needs no key. |
|
||||
| `tmdb_id` / `imdb_id` / `tvdb_id` / `anilist_id` | - | `null` | External ID overrides. Each resolves its metadata directly instead of by a title search, and is used for tagging. Set `enrich` to also take the title, year and original language. Give at most one of `tmdb_id`, `imdb_id` and `tvdb_id`, since unshackle resolves the others from it. Sending two returns `400`. `anilist_id` still combines with one of them. `tmdb_id` and `tvdb_id` must be positive integers, `imdb_id` must look like `tt1375666`, and `anilist_id` must be a positive integer or a string like `mal:12345`, or the request returns `400`. An ID whose provider is unconfigured, such as `tmdb_id` with no `tmdb_api_key`, fails the job rather than returning `400`. `anilist_id` needs no API key. |
|
||||
| `tvdb_order` | `official`, `dvd`, `absolute`, `alternate`, `regional` | `null` | Renumber episodes to a TVDB season order. Falls back to the `tvdb_order` config option. |
|
||||
| `enrich` | boolean | `false` | Overwrite title, year and original language with the external source's. Needs one of `tmdb_id`, `imdb_id`, `tvdb_id` or `anilist_id`. Without one the job fails instead of returning `400`. |
|
||||
| `daily` | boolean | `false` | Treat the title as daily/date-based content and fill missing episode air dates from TVDB. The fill needs `enrich` and a TVDB ID. An air date the service already set is kept. |
|
||||
@@ -527,9 +527,9 @@ The `data` field of every event is the same job object that `GET /api/download/j
|
||||
| `snapshot` | Immediately, as the first event. Gives the job's current state. |
|
||||
| `status` | The job leaves the queue and starts to download. |
|
||||
| `progress` | The worker's progress record changed. The server reads that record every 0.5 seconds, so you get at most two of these events each second, and none while the record stays the same. |
|
||||
| `completed` | The job finished. The server then closes the stream. |
|
||||
| `failed` | The job failed. The server then closes the stream. |
|
||||
| `cancelled` | The job was cancelled. The server then closes the stream. |
|
||||
| `completed` | The job finished. The server then closes the event stream. |
|
||||
| `failed` | The job failed. The server then closes the event stream. |
|
||||
| `cancelled` | The job was cancelled. The server then closes the event stream. |
|
||||
|
||||
The server sends a `: keep-alive` comment each 15 seconds while the job is quiet, to keep proxies from closing an idle connection. Ignore these lines.
|
||||
|
||||
@@ -862,7 +862,7 @@ Poll for the authentication status and any pending interactive prompt (OTP, PIN,
|
||||
| Status | Error code | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `200` | - | Status returned. |
|
||||
| `403` | `FORBIDDEN` | Request IP differs from the session creator. |
|
||||
| `403` | `FORBIDDEN` | Request IP differs from the remote session creator. |
|
||||
| `404` | `SESSION_NOT_FOUND` | No such session. |
|
||||
|
||||
### `POST /api/session/{session_id}/prompt`
|
||||
@@ -986,7 +986,7 @@ Get the content keys for the DRM. The `mode` field selects one of two modes.
|
||||
| Status | Error code | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `200` | - | License/keys returned. |
|
||||
| `400` | `INVALID_INPUT` | Missing challenge/PSSH, or no CDM device configured for the key. |
|
||||
| `400` | `INVALID_INPUT` | Missing challenge/PSSH, or no CDM device configured for the API key. |
|
||||
| `400` | `INVALID_PARAMETERS` | Unsupported `drm_type`. |
|
||||
| `404` | `TRACK_NOT_FOUND` | Unknown track ID. |
|
||||
| `404` | `NO_CONTENT` | Server CDM produced no keys. |
|
||||
|
||||
+11
-11
@@ -99,13 +99,13 @@ but the defaults hold for the overwhelming majority of responses.
|
||||
| `INVALID_PROXY` | 400 | The supplied proxy specification could not be resolved or is malformed. |
|
||||
| `INVALID_PARAMETERS` | 400 | One or more download or query parameters failed validation (bad codec, bitrate, sort field, and so on). |
|
||||
| `AUTH_FAILED` | 401 | Authentication with the streaming service failed (bad credentials or cookies). |
|
||||
| `FORBIDDEN` | 403 | The action is not allowed. Raised by server-side gates (per-key restrictions on CDM or credential overrides) and by session IP binding. |
|
||||
| `GEOFENCE` | 403 | The content is not available in the applicable region. |
|
||||
| `FORBIDDEN` | 403 | The action is not allowed. Raised by server-side gates (per-key restrictions on CDM or credential overrides) and by remote session IP binding. |
|
||||
| `GEOFENCE` | 403 | The title is not available in the applicable region. |
|
||||
| `NOT_FOUND` | 404 | A requested resource (title, history entry, and so on) does not exist. |
|
||||
| `NO_CONTENT` | 404 | The request was valid but produced nothing: no matching titles, tracks, episodes, or keys. |
|
||||
| `JOB_NOT_FOUND` | 404 | The referenced download job does not exist. |
|
||||
| `SESSION_NOT_FOUND` | 404 | The remote-dl session does not exist or has expired. |
|
||||
| `TRACK_NOT_FOUND` | 404 | A referenced track ID is not present in the session. |
|
||||
| `TRACK_NOT_FOUND` | 404 | A referenced track ID is not present in the remote session. |
|
||||
| `CONFLICT` | 409 | The target resource is in a state that disallows the action (retrying a running job, prioritising a non-queued job, clearing cache during an active download). |
|
||||
| `RATE_LIMITED` | 429 | The streaming service is rate-limiting requests. Retryable. |
|
||||
|
||||
@@ -141,14 +141,14 @@ The classification is keyword-based, checked in order:
|
||||
|
||||
| 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 |
|
||||
| geofence, region, "not available in", territory | `GEOFENCE` (403) | no |
|
||||
| "not found", 404, "does not exist", "invalid id" | `NOT_FOUND` (404) | no |
|
||||
| rate limit, too many requests, 429, throttle | `RATE_LIMITED` (429) | yes |
|
||||
| drm, license, widevine, playready, decrypt | `DRM_ERROR` (502) | no |
|
||||
| "service unavailable", 503, maintenance, "temporarily unavailable" | `SERVICE_UNAVAILABLE` (503) | yes |
|
||||
| invalid, malformed, validation (or `ValueError`, `ValidationError`) | `INVALID_INPUT` (400) | no |
|
||||
| `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 |
|
||||
| `geofence`, `region`, `not available in`, `territory` | `GEOFENCE` (403) | no |
|
||||
| `not found`, `404`, `does not exist`, `invalid id` | `NOT_FOUND` (404) | no |
|
||||
| `rate limit`, `too many requests`, `429`, `throttle` | `RATE_LIMITED` (429) | yes |
|
||||
| `drm`, `license`, `widevine`, `playready`, `decrypt` | `DRM_ERROR` (502) | no |
|
||||
| `service unavailable`, `503`, `maintenance`, `temporarily unavailable` | `SERVICE_UNAVAILABLE` (503) | yes |
|
||||
| `invalid`, `malformed`, `validation` (or `ValueError`, `ValidationError`) | `INVALID_INPUT` (400) | no |
|
||||
| anything else | `INTERNAL_ERROR` (500) | no |
|
||||
|
||||
Categorised errors include a `reason` tag inside `details` (such as
|
||||
|
||||
@@ -148,7 +148,7 @@ request and response in full.
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/health` | Liveness and version check. Never requires a key. |
|
||||
| `GET` | `/api/health` | Liveness and version check. Never requires an API key. |
|
||||
| `GET` | `/api/services` | List the services available on this server. |
|
||||
| `POST` | `/api/search` | Run a service's search for a query. |
|
||||
| `POST` | `/api/list-titles` | List the titles behind a service title ID. |
|
||||
@@ -190,7 +190,7 @@ request and response in full.
|
||||
| Method | Path | Purpose |
|
||||
|---|---|---|
|
||||
| `POST` | `/api/session/create` | Start a server-side, authenticated service session. |
|
||||
| `GET` | `/api/session/{session_id}` | Session info and time-to-live. |
|
||||
| `GET` | `/api/session/{session_id}` | Remote session info and time-to-live. |
|
||||
| `DELETE` | `/api/session/{session_id}` | Tear down a session. |
|
||||
| `GET` | `/api/session/{session_id}/titles` | Titles for the session's title ID. |
|
||||
| `POST` | `/api/session/{session_id}/tracks` | Tracks (with URLs) for a chosen title. |
|
||||
|
||||
@@ -268,7 +268,7 @@ The server mounts all these routes, even in `--remote-only` mode. Paths use the
|
||||
| `GET` | `/api/session/{id}/prompt` | Poll interactive auth status / pending prompt |
|
||||
| `POST` | `/api/session/{id}/prompt` | Submit an answer to a pending prompt |
|
||||
| `GET` | `/api/session/{id}` | Session info (validity, TTL, counts) |
|
||||
| `DELETE` | `/api/session/{id}` | Close the session, return updated cache, clean up |
|
||||
| `DELETE` | `/api/session/{id}` | Close the remote session, return updated cache, clean up |
|
||||
|
||||
### Create: request and response
|
||||
|
||||
|
||||
@@ -65,8 +65,8 @@ Declared at the top of your service class, these are static descriptors:
|
||||
| `GEOFENCE` | `tuple[str, ...]` | ISO region codes the service requires; empty means no geofence. The first entry is treated as the "main region" for automatic proxy selection. |
|
||||
| `VAULT_TAG` | `Optional[str]` | Overrides the key-vault namespace; defaults to the service's own tag. |
|
||||
| `AUTH_METHODS` | `Optional[tuple[str, ...]]` | Accepted auth methods (`"cookies"` / `"credentials"`). When `None`, the REST `/services` endpoint infers them from `authenticate()`. |
|
||||
| `ANIME` | `bool` | The catalog is anime, so metadata lookups prefer AniList. A title's own `anime` flag overrides it. |
|
||||
| `DAILY` | `bool` | The catalog is daily/date-based, so episodes are named by air date. A title's own `daily` flag overrides it. |
|
||||
| `ANIME` | `bool` | The catalogue is anime, so metadata lookups prefer AniList. A title's own `anime` flag overrides it. |
|
||||
| `DAILY` | `bool` | The catalogue is daily/date-based, so episodes are named by air date. A title's own `daily` flag overrides it. |
|
||||
|
||||
### Required methods
|
||||
|
||||
@@ -105,7 +105,7 @@ up the objects your methods rely on:
|
||||
|
||||
| Attribute | What it is |
|
||||
|---|---|
|
||||
| `self.config` | The per-service config slice (see [Request and session objects](#request-and-session-objects)). |
|
||||
| `self.config` | The per-service config slice (see [Request and HTTP session objects](#request-and-http-session-objects)). |
|
||||
| `self.log` | A `logging.Logger` named after your class. |
|
||||
| `self.session` | A ready-to-use HTTP session (headers, retries, and proxy applied). |
|
||||
| `self.cache` | A generic key/value `Cacher` namespaced to the service. |
|
||||
@@ -114,10 +114,11 @@ up the objects your methods rely on:
|
||||
| `self.credential` | The active `Credential`, or `None` until authenticated. |
|
||||
| `self.track_request` | A `TrackRequest` describing the requested codecs, ranges, and best-available flag (see below). |
|
||||
|
||||
The constructor also resolves proxies: it reads `--proxy` / `--proxy-query` /
|
||||
`--proxy-provider`, consults the per-service `proxy_map`, and, if `GEOFENCE` is set and
|
||||
you did not give an explicit proxy, does a live IP check and auto-fetches a proxy to
|
||||
`GEOFENCE[0]` when it detects a geoblock. See [Proxies and VPN](../guide/proxies-and-vpn.md).
|
||||
The constructor also resolves proxies: it reads `--proxy` and the `proxy_query` and
|
||||
`proxy_provider` context params that `dl` derives from it, consults the per-service
|
||||
`proxy_map`, and, if `GEOFENCE` is set and you did not give an explicit proxy, does a live
|
||||
IP check and auto-fetches a proxy to `GEOFENCE[0]` when it detects a geoblock.
|
||||
See [Proxies and VPN](../guide/proxies-and-vpn.md).
|
||||
|
||||
!!! note "`TrackRequest`"
|
||||
`self.track_request` is a small dataclass built from the CLI: `codecs`
|
||||
@@ -223,7 +224,7 @@ After the service fetches the titles (and after any cache), the framework applie
|
||||
**exact-string** rename (it compares `Episode`s on their show `title`, and `Movie`s and
|
||||
`Song`s on their `name`), so you can change a platform's odd naming to what your
|
||||
library expects. `get_titles_cached()` also cache-wraps the titles, and it is region-
|
||||
and account-aware. See the [TitleCacher](#request-and-session-objects) notes.
|
||||
and account-aware. See the [TitleCacher](#request-and-http-session-objects) notes.
|
||||
|
||||
The ordering here is deliberate on both sides. The framework applies `title_map` **after**
|
||||
the title cache so that editing the map takes effect immediately, without forcing a cache
|
||||
@@ -376,7 +377,7 @@ service to talk to the platform's license server. unshackle recognizes five DRM
|
||||
| `Widevine` | Widevine | CDM challenge → license server → content keys (`.wvd` device) |
|
||||
| `PlayReady` | PlayReady | CDM challenge (WRM header) → license server → content keys (`.prd` device) |
|
||||
| `ClearKeyCENC` | W3C `org.w3.clearkey` over CENC | JSON KID request → JWK set; no CDM |
|
||||
| `ClearKey` | HLS AES-128 | key fetched directly from a URI; no CDM, no challenge |
|
||||
| `ClearKey` | HLS AES-128 | content key fetched directly from a URI; no CDM, no challenge |
|
||||
| `MonaLisa` | Proprietary WASM DRM | keys extracted locally; no license server |
|
||||
|
||||
!!! warning "`ClearKey` vs `ClearKeyCENC`"
|
||||
@@ -422,7 +423,7 @@ extracts the RPU with `dovi_tool`, and injects it onto the base layer, to make a
|
||||
track that carries both HDR10 metadata and the DV RPU. Both are automatic once the
|
||||
pipeline selects the right tracks. A service only needs to give accurate ranges.
|
||||
|
||||
## Request and session objects
|
||||
## Request and HTTP session objects
|
||||
|
||||
Everything a service does over the network flows through objects the base constructor
|
||||
prepared, all derived from the Click context (`ctx`) that `dl` passes in.
|
||||
|
||||
@@ -86,7 +86,7 @@ The top-level keys group loosely into these areas:
|
||||
| Proxies & remote | `proxy_providers`, `remote_services`, `serve`, `services` |
|
||||
| Naming & tagging | `tag`, `output_template`, `chapter_fallback_name` |
|
||||
| External API keys | `tmdb_api_key`, `tvdb_api_key`, `simkl_client_id`, `ipinfo_api_key`, `metadata_providers` |
|
||||
| Behavior & logging | `update_checks`, `redact_paths`, `debug`, `unicode_filenames` |
|
||||
| Behaviour & logging | `update_checks`, `redact_paths`, `debug`, `unicode_filenames` |
|
||||
| Post-download scripts | `post_scripts` |
|
||||
| Paths | `directories`, `filenames` |
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ These must be present for core downloading, decryption, and muxing to work:
|
||||
|
||||
| Tool | Binary looked up | Why it is needed |
|
||||
| --- | --- | --- |
|
||||
| FFmpeg | `ffmpeg` | Media processing: remuxing, conversion, stream handling |
|
||||
| FFmpeg | `ffmpeg` | Media processing: remuxing, conversion, track handling |
|
||||
| FFprobe | `ffprobe` | Media analysis: inspecting tracks and container details |
|
||||
| MKVToolNix | `mkvmerge` | Muxing downloaded tracks into a final MKV |
|
||||
| mkvpropedit | `mkvpropedit` | Editing MKV metadata after muxing |
|
||||
|
||||
@@ -112,7 +112,7 @@ unshackle honours the special language tokens `orig`, `all`, and `best` everywhe
|
||||
|
||||
| Flag | Description |
|
||||
|---|---|
|
||||
| `-w`, `--wanted` | Wanted episodes, e.g. `S01-S05,S07`, `S01E01-S02E03`. Supports exclusions with a leading `-` (e.g. `-S03`). For a [split episode](downloading.md#split-episodes), `.N` picks one part (`S01E01.2`), a range must stay inside the episode (`S01E01.1-S01E01.3`), and `S01E01` on its own takes every part. For [dated content](downloading.md#daily-and-date-based-content), a token can also be an ISO air date (`2026-08-11`) or a date range with a colon (`2026-08-01:2026-08-31`). For a [music release](downloading.md#music-tracks), a token is a track number (`1-5`, `1,3,7`), or `{disc}x{track}` (`2x3`) when the release has more than one disc. |
|
||||
| `-w`, `--wanted` | Wanted episodes, e.g. `S01-S05,S07`, `S01E01-S02E03`. Supports exclusions with a leading `-` (e.g. `-S03`). For a [split episode](downloading.md#split-episodes), `.N` picks one part (`S01E01.2`), a range must stay inside the episode (`S01E01.1-S01E01.3`), and `S01E01` on its own takes every part. For a [dated episode](downloading.md#daily-and-date-based-content), a token can also be an ISO air date (`2026-08-11`) or a date range with a colon (`2026-08-01:2026-08-31`). For a [music release](downloading.md#music-tracks), a token is a track number (`1-5`, `1,3,7`), or `{disc}x{track}` (`2x3`) when the release has more than one disc. |
|
||||
| `--select-titles` | Interactively select what to download: episodes of a series, or films when a title has more than one. **Cannot combine with `-w`.** |
|
||||
| `--latest-episode` | Download only the single most recent episode. |
|
||||
| `--list-titles` | List titles only; do not download. |
|
||||
@@ -152,9 +152,9 @@ Keep only certain track types, or skip certain track types. Attachments are alwa
|
||||
| Flag | Description |
|
||||
|---|---|
|
||||
| `--tmdb` | TMDB ID (integer). Used for the tags. Skips the title search. `--enrich` reads it too. Needs `tmdb_api_key`. |
|
||||
| `--imdb` | IMDb ID, e.g. `tt1375666`. Used for the tags. Skips the title search. `--enrich` reads it too. Needs no key. |
|
||||
| `--imdb` | IMDb ID, e.g. `tt1375666`. Used for the tags. Skips the title search. `--enrich` reads it too. Needs no API key. |
|
||||
| `--tvdb` | TVDB ID (integer). Used for the tags. Skips the series lookup that `--tvdb-order` would otherwise do. `--enrich` reads it too. Needs `tvdb_api_key`. |
|
||||
| `--anilist` | AniList ID (integer), e.g. `--anilist 21`. A MyAnimeList ID is accepted as `mal:12345` and resolved to the AniList entry. Used for the tags. Skips the title search. `--enrich` reads it too. Needs no key. |
|
||||
| `--anilist` | AniList ID (integer), e.g. `--anilist 21`. A MyAnimeList ID is accepted as `mal:12345` and resolved to the AniList entry. Used for the tags. Skips the title search. `--enrich` reads it too. Needs no API key. |
|
||||
| `--enrich` | Overwrite show title, year and original language with the external source's. **Requires** one of `--tmdb`, `--imdb`, `--tvdb`, or `--anilist`. |
|
||||
| `--daily` | Treat the title as daily/date-based content and fill missing episode air dates from TVDB. The fill needs `--enrich` and a TVDB ID. See [Daily and date-based content](downloading.md#daily-and-date-based-content). |
|
||||
| `--tvdb-order` | Renumber episodes to a TVDB season order: `official` (aired), `dvd`, `absolute`, `alternate`, or `regional`. Needs `tvdb_api_key`. |
|
||||
@@ -340,7 +340,7 @@ Clear an environment directory. unshackle empties the directory, makes it again,
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `unshackle env clear cache [SERVICE]` | Clear the cache directory, or just one service's cache subdirectory. |
|
||||
| `unshackle env clear cache [SERVICE]` | Clear the cache directory, or only one service's cache subdirectory. |
|
||||
| `unshackle env clear temp` | Clear the temp directory. |
|
||||
|
||||
!!! example
|
||||
|
||||
@@ -759,9 +759,9 @@ lookups but keep the IDs you give, set
|
||||
| Flag | Example | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `--tmdb` | `--tmdb 27205` | Use this TMDB ID instead of an automatic search. Needs `tmdb_api_key`. |
|
||||
| `--imdb` | `--imdb tt1375666` | Use this IMDb ID instead of an automatic search. Needs no key. |
|
||||
| `--imdb` | `--imdb tt1375666` | Use this IMDb ID instead of an automatic search. Needs no API key. |
|
||||
| `--tvdb` | `--tvdb 73871` | Use this TVDB ID instead of looking the series up. Needs `tvdb_api_key`. |
|
||||
| `--anilist` | `--anilist 21` | Use this AniList ID instead of an automatic search. `mal:12345` is also accepted. Needs no key. |
|
||||
| `--anilist` | `--anilist 21` | Use this AniList ID instead of an automatic search. `mal:12345` is also accepted. Needs no API key. |
|
||||
| `--enrich` | - | Overwrite the show title, year and original language with the external source's. **Requires** one of `--tmdb`, `--imdb`, `--tvdb`, or `--anilist`. |
|
||||
| `--tvdb-order` | `--tvdb-order dvd` | Renumber episodes to a TVDB season order. Needs `tvdb_api_key`. |
|
||||
|
||||
@@ -784,9 +784,9 @@ operates when you set its config key:
|
||||
| TMDB | `tmdb_api_key` | yes | yes, alpha-2 such as `ko` | IMDb, TMDB, TVDB |
|
||||
| TVDB | `tvdb_api_key` | yes | yes, alpha-3 such as `kor` | TMDB, TVDB, sometimes IMDb |
|
||||
| OMDb | `omdb_api_key` | yes | yes, as an English name such as `Korean` | IMDb |
|
||||
| IMDb | *(no key needed)* | yes | yes, alpha-2 such as `ko` | IMDb |
|
||||
| IMDb | *(no config key needed)* | yes | yes, alpha-2 such as `ko` | IMDb |
|
||||
| SIMKL | `simkl_client_id` | yes | no, it publishes a country and no language | IMDb, TMDB, TVDB |
|
||||
| AniList | *(no key needed)* | yes | yes, worked out from the country of origin, such as `ja` | AniList |
|
||||
| AniList | *(no config key needed)* | yes | yes, worked out from the country of origin, such as `ja` | AniList |
|
||||
|
||||
Whatever the tag looks like, unshackle normalises it before use, so `ko`, `kor` and `Korean`
|
||||
all end up as the same language.
|
||||
@@ -832,8 +832,8 @@ with a thin record cannot blank out what the service told you.
|
||||
`--enrich` also fills in the absolute episode number of each episode from TVDB's absolute
|
||||
order, for any series that has one and where the service did not supply it. Anime is the
|
||||
usual beneficiary, but nothing here applies only to anime. This only adds the
|
||||
[`{absolute}`](../reference/configuration/output.md#output_template) naming variable. The
|
||||
season and episode numbers are never changed.
|
||||
[`{absolute}`](../reference/configuration/output.md#output_template) naming variable.
|
||||
`--enrich` never changes the season and episode numbers.
|
||||
|
||||
With `--daily`, `--enrich` also fills in the air date of each episode from TVDB. See
|
||||
[Daily and date-based content](#daily-and-date-based-content).
|
||||
@@ -897,7 +897,7 @@ authoritative list.
|
||||
| `--forced-subs` | `-fs` | Include forced subtitles. |
|
||||
| `--forced-s-lang` | `-fsl` | Forced subtitle language(s); implies `-fs`. `-` excludes. |
|
||||
| `--sub-format` | | Output subtitle format. |
|
||||
| `--wanted` | `-w` | Episode/season range, an air date, or a music track (`1-5`, `2x3`). |
|
||||
| `--wanted` | `-w` | Episode/season range, an air date, or a track number for a music release (`1-5`, `2x3`). |
|
||||
| `--daily` | | Fill missing air dates from TVDB during `--enrich`. |
|
||||
| `--select-titles` | | Interactively pick episodes or films. |
|
||||
| `--latest-episode` | | Only the newest episode. |
|
||||
@@ -911,7 +911,7 @@ authoritative list.
|
||||
| `--proxy` / `--no-proxy` / `--no-proxy-download` | | Proxy control. |
|
||||
| `--workers` / `--downloads` / `--slow` | | Concurrency and pacing. |
|
||||
| `--list` / `--list-titles` / `--skip-dl` | | Dry runs. |
|
||||
| `--cdm-only` / `--vaults-only` | | Key source control. |
|
||||
| `--cdm-only` / `--vaults-only` | | Content key source control. |
|
||||
| `--export` | | Export track info and keys to JSON. |
|
||||
| `--tmdb` / `--imdb` / `--tvdb` / `--anilist` / `--enrich` | | Metadata overrides. |
|
||||
| `--tvdb-order` | | Renumber episodes to a TVDB season order. |
|
||||
|
||||
@@ -30,7 +30,7 @@ A handful of other terms show up throughout:
|
||||
|------|---------|
|
||||
| **PSSH** | Protection System Specific Header. The per-system init data extracted from the file's init segment or the manifest. |
|
||||
| **KID** | Key ID: a 16-byte UUID identifying *which* key a track needs. |
|
||||
| **CEK** | Content Encryption Key: the actual AES key, mapped from its KID. |
|
||||
| **CEK** | Content Encryption Key: the AES content key, mapped from its KID. |
|
||||
| **Challenge** | The license request the CDM produces, sent to the service's license server. |
|
||||
| **License** | The server's response, which the CDM parses to recover the content keys. |
|
||||
|
||||
@@ -496,7 +496,7 @@ control this trade-off on the `dl` command:
|
||||
|
||||
| Flag | Effect |
|
||||
|------|--------|
|
||||
| `--vaults-only` | Never send a license request; use only cached/vault keys. Fails if a key is missing. |
|
||||
| `--vaults-only` | Never send a license request; use only cached/vault keys. Fails if a content key is missing. |
|
||||
| `--cdm-only` | Ignore vaults for sourcing keys and always perform a fresh license request. |
|
||||
|
||||
See [key vaults](vaults.md) for how to configure vault backends.
|
||||
|
||||
@@ -128,7 +128,7 @@ Every variable below is valid in both output and folder templates. unshackle tak
|
||||
| `episode_name` | Episode title | `Pilot` |
|
||||
| `part` | Part index of a [split episode](#split-episodes), empty otherwise | `2` |
|
||||
| `absolute` | Absolute episode number, zero-padded to 3 digits, empty when unknown | `007` |
|
||||
| `date` | ISO air date for daily/dated content | `2024-06-01` |
|
||||
| `date` | ISO air date, empty when the episode has none | `2024-06-01` |
|
||||
|
||||
!!! note "Daily & sports content"
|
||||
When an episode has an air date, unshackle switches to date-based naming automatically. `season` and `season_episode` become the formatted air date, unshackle clears `episode` and `year`, and `{date}` holds the ISO date. The date's internal separator (dots or spaces) follows your `series` template style.
|
||||
@@ -180,7 +180,7 @@ For custom templates there is also a standalone [`{part}`](../reference/configur
|
||||
| `album` | Album name |
|
||||
| `disc` | Disc number (only shown when greater than 1) |
|
||||
| `track_total` / `disc_total` | Totals |
|
||||
| `release_type` | `album`, `single`, `ep`, etc. |
|
||||
| `release_type` | `album`, `single`, `ep`, or another type the service supplies |
|
||||
| `genre` | Genre |
|
||||
| `explicit` | `Explicit` when flagged |
|
||||
| `isrc` / `upc` / `label` | ISRC, UPC and record label |
|
||||
@@ -285,7 +285,7 @@ muxing:
|
||||
When no track has the preferred language, unshackle applies sensible defaults. The video default falls back to the title language, then the original-language track, then the first track. The audio default is the original-language track. The subtitle default is a forced track in the first audio's language.
|
||||
|
||||
!!! note "`default_language` only sets the default-track flag"
|
||||
`default_language` controls **which track carries the MKV `--default-track` flag**. Nothing else. It does not change track *selection* (that stays with `-l`/`--alang` and friends). It also does not touch which track carries the original flag: `--original-flag` still tags the *true* original-audio track. That is the point: you can make your player default to, say, Polish audio on an English-original title without altering the original marker. When the configured language is not present in the manifest, each track type falls back to its normal default rule described above.
|
||||
`default_language` controls **which track carries the MKV `--default-track` flag**. Nothing else. It does not change track *selection* (that stays with `-l`/`--a-lang` and friends). It also does not touch which track carries the original flag: `--original-flag` still tags the *true* original-audio track. That is the point: you can make your player default to, say, Polish audio on an English-original title without altering the original marker. When the configured language is not present in the manifest, each track type falls back to its normal default rule described above.
|
||||
|
||||
!!! warning "`merge_video` collapses only the language dimension"
|
||||
`merge_video` groups video tracks by `(resolution, range, codec)` and merges them so that only **language** differs within a single file, with no re-encode and no concatenation. Different resolutions, ranges (SDR / HDR10 / HDR10+ / DV / HYBRID), and codecs (H.264 / H.265) **always** land in separate files. So `-r HYBRID,DV,HDR10,SDR --merge-video` gives one file *per range*, never a single fused file. English and French video of identical resolution, range, and codec gives one file that holds both video tracks. `merge_audio` works the same way for audio languages.
|
||||
|
||||
@@ -247,7 +247,7 @@ With `conversion_method: auto`, unshackle ranks these automatically per conversi
|
||||
Setting it to a specific value pins that backend as the first choice, falling back to
|
||||
others only if the pin cannot convert the pair.
|
||||
|
||||
#### What each backend actually preserves
|
||||
#### What each backend preserves
|
||||
|
||||
The table below comes from round-tripping a subtitle that carries italics, bold,
|
||||
underline, positioning, and colour through every backend. Each cell shows the
|
||||
|
||||
@@ -199,7 +199,7 @@ The required tools (unshackle will not get far without them) are:
|
||||
| **FFprobe** | Media analysis |
|
||||
| **MKVToolNix** (`mkvmerge`) | MKV muxing |
|
||||
| **mkvpropedit** | MKV metadata |
|
||||
| **Shaka Packager** | DRM decryption |
|
||||
| **shaka-packager** | DRM decryption |
|
||||
|
||||
Optional tools unlock extra capabilities. For example, `mp4decrypt` (alternative decryptor), `dovi_tool` / `HDR10Plus_tool` (Dolby Vision / HDR10+), `SubtitleEdit` / `CCExtractor` (subtitle conversion and CC extraction), `MPV` / `FFplay` (playback preview), and `git` (service repositories) or `docker` (Gluetun VPN, see [Proxies & VPN](proxies-and-vpn.md)).
|
||||
|
||||
|
||||
+29
-29
@@ -1,41 +1,41 @@
|
||||
# Key Vaults
|
||||
|
||||
A **key vault** is a store of DRM **content keys** that unshackle remembers between downloads. Once a title's keys have been recovered, unshackle saves them to your configured vaults. The next time you download something that reuses those same keys, unshackle finds them in a vault instead of performing another license exchange. The download starts faster and puts less load on the service.
|
||||
A **key vault** is a store of DRM **content keys** that unshackle remembers between downloads. Once unshackle has recovered a title's keys, it saves them to your configured vaults. The next time you download something that reuses those same keys, unshackle finds them in a vault instead of performing another license exchange. The download starts faster and puts less load on the service.
|
||||
|
||||
Vaults are entirely optional, but if you download regularly they are one of the biggest quality-of-life improvements you can configure.
|
||||
|
||||
!!! tip "Vaults save license calls even on the very first download"
|
||||
The benefit is not limited to repeat downloads. Within a single run, a service often issues the *same* Key ID and content key for both the video and audio tracks, and for several resolutions or bitrates of the same title. Once unshackle has recovered a key it writes it to your vaults immediately, so the moment the next track presents a KID it has already seen, the key is served from the vault instead of triggering another license exchange. A single fetched key can therefore satisfy many tracks in one run.
|
||||
The benefit is not limited to repeat downloads. Within a single run, a service often issues the *same* Key ID and content key for both the video and audio tracks. The same pair frequently covers several resolutions or bitrates of the same title. Once unshackle has recovered a key it writes it to your vaults immediately. The moment the next track presents a KID it has already seen, the key comes from the vault instead of another license exchange. A single fetched key can therefore satisfy many tracks in one run.
|
||||
|
||||
## What a vault stores
|
||||
|
||||
Every key is stored as a **KID → KEY** pair, grouped by **service**:
|
||||
A vault stores every content key as a **KID → KEY** pair, grouped by **service**:
|
||||
|
||||
- **KID** (Key ID): a 16-byte identifier for a specific encrypted stream, stored as 32 lowercase hex characters with no dashes.
|
||||
- **KEY**: the 16-byte content key that decrypts that stream, also 32 hex characters.
|
||||
- **KID** (Key ID): a 16-byte identifier for a specific encrypted track, stored as 32 lowercase hex characters with no dashes.
|
||||
- **KEY**: the 16-byte content key that decrypts that track, also 32 hex characters.
|
||||
- **Service**: the canonical service tag (for example `EXAMPLE`, not `ExampleService`) that namespaces the key. Each service gets its own logical table.
|
||||
|
||||
!!! note "Why keys are matched by KID"
|
||||
Vaults match keys by KID, never by PSSH/CENC header. A KID does not change unless the underlying video file itself changes, whereas the PSSH box can vary between requests for the very same content. That makes the KID a far more reliable match key.
|
||||
|
||||
!!! info "Null keys are ignored everywhere"
|
||||
A key made entirely of zeroes (`00000000000000000000000000000000`) is treated as "no key". Vault lookups skip it, and every backend raises an error if you try to add one. This prevents a placeholder key from masking a real one.
|
||||
A content key made entirely of zeroes (`00000000000000000000000000000000`) is treated as "no key". Vault lookups skip it, and every backend raises an error if you try to add one. This prevents a placeholder key from masking a real one.
|
||||
|
||||
## How lookups and caching work
|
||||
|
||||
When unshackle needs a key during a download it asks your vaults **in the order they are listed in your config**. It returns the first non-null key it finds and stops searching. If a vault raises a permission error or is unreachable, that vault is skipped and the search continues with the next one.
|
||||
When unshackle needs a content key during a download it asks your vaults **in the order they appear in your config**. It returns the first non-null key it finds and stops searching. If a vault raises a permission error or is unreachable, unshackle skips it and continues the search with the next one.
|
||||
|
||||
When new keys are recovered from a license, unshackle pushes them to **all** configured vaults, so every vault gradually converges on the same set of keys. Two rules apply to pushing:
|
||||
When unshackle recovers new keys from a license, it pushes them to **all** configured vaults, so every vault gradually converges on the same set of keys. Two rules apply to pushing:
|
||||
|
||||
- Keys that already exist in a vault are skipped (existing data is never overwritten or deleted).
|
||||
- A vault marked `no_push: true` receives lookups but is never written to. This is useful for read-only or shared upstream vaults you do not own.
|
||||
- unshackle skips keys that already exist in a vault (it never overwrites or deletes existing data).
|
||||
- A vault marked `no_push: true` receives lookups, but unshackle never writes to it. This is useful for read-only or shared upstream vaults you do not own.
|
||||
|
||||
!!! tip "A good two-vault setup"
|
||||
A common arrangement is a fast local **SQLite** vault plus a shared remote vault (**MySQL**, **HTTP**, or **API**). The local vault answers instantly and works offline; the remote vault lets you share keys across machines or with a group.
|
||||
|
||||
## Configuring vaults
|
||||
|
||||
Vaults are declared under the `key_vaults` list in your unshackle config file. Each entry is a mapping with two required fields, `type` (which backend to use) and `name` (a label you choose), plus whatever settings that backend needs.
|
||||
Declare vaults under the `key_vaults` list in your unshackle config file. Each entry is a mapping with two required fields, `type` (which backend to use) and `name` (a label you choose), plus whatever settings that backend needs.
|
||||
|
||||
```yaml title="unshackle.yaml"
|
||||
key_vaults:
|
||||
@@ -85,10 +85,10 @@ key_vaults:
|
||||
| `path` | yes | Path to the database file. `~` is expanded. The file (and its tables) are created automatically on first use. |
|
||||
| `no_push` | no | If `true`, keys are read but never written. Defaults to `false`. |
|
||||
|
||||
Each service gets its own table (named after the service tag), created on demand with `kid` and `key_` columns. Connections use WAL journaling with a 30-second busy timeout, so the same database file can be used safely from multiple threads.
|
||||
Each service gets its own table (named after the service tag), created on demand with `kid` and `key_` columns. Connections use WAL journaling with a 30-second busy timeout, so multiple threads can safely use the same database file.
|
||||
|
||||
!!! warning "Moving the .db file silently forks your vault"
|
||||
Because the file is created on demand, if you relocate an existing `.db` without updating `path` to match, unshackle will simply create a fresh empty database at the old location, leaving you with two divergent vaults and no error to warn you. Always update the config path whenever you move the file.
|
||||
Because the file is created on demand, if you relocate an existing `.db` without updating `path` to match, unshackle will create a fresh empty database at the old location. That leaves you with two divergent vaults and no error to warn you. Always update the config path whenever you move the file.
|
||||
|
||||
!!! tip "Keep a local SQLite vault even when you share a MySQL one"
|
||||
The SQLite vault is best thought of as an offline-only backup should anything ever happen to a shared `MySQL` vault. Each member of a group should keep their own local SQLite vault alongside the shared database, so no one loses their keys if the shared vault goes down or away.
|
||||
@@ -97,10 +97,10 @@ Each service gets its own table (named after the service tag), created on demand
|
||||
|
||||
## MySQL backend
|
||||
|
||||
A remotely-accessed MySQL/MariaDB database, ideal for sharing keys across several machines. It connects via `pymysql`. MariaDB is recommended over MySQL where you have the choice.
|
||||
A remotely-accessed MySQL/MariaDB database, ideal for sharing keys across several machines. It connects via `pymysql`. Prefer MariaDB over MySQL where you have the choice.
|
||||
|
||||
!!! warning "The vault connects directly to the database server"
|
||||
A `MySQL` vault (like `SQLite`) speaks the database wire protocol directly to the host or IP you configure. It cannot sit behind a PHP API or any other application layer. A common real-world snag: many hosting providers firewall their MySQL server to their own internal network, so a database on shared hosting is frequently unreachable from the outside world even when your credentials are perfectly correct. If connections time out, confirm the server accepts remote connections from your address before suspecting the config.
|
||||
A `MySQL` vault (like `SQLite`) speaks the database wire protocol directly to the host or IP you configure. It cannot sit behind a PHP API or any other application layer. A common real-world snag: many hosting providers firewall their MySQL server to their own internal network. A database on shared hosting is therefore frequently unreachable from the outside world even when your credentials are perfectly correct. If connections time out, confirm the server accepts remote connections from your address before suspecting the config.
|
||||
|
||||
```yaml title="unshackle.yaml"
|
||||
key_vaults:
|
||||
@@ -125,10 +125,10 @@ key_vaults:
|
||||
Any additional fields you provide (such as `password`, `port`, or `ssl`) are passed straight through to `pymysql.connect`, so you can use the full range of its connection options.
|
||||
|
||||
!!! warning "Account security is on you, not on unshackle"
|
||||
unshackle enforces none of this. It is operational policy for a database that may be shared with a group. Never connect with the `root` account, not even for yourself. Give every account a password, and never let two users share a username/password. Restrict each account's access to only the `unshackle` database, and grant normal users just `SELECT` and `INSERT`. Reserve the `CREATE` grant for a single trusted user who bootstraps the service tables (see [`kv prepare`](#kv-prepare-pre-create-service-tables)); everyone else can insert new keys but should not be creating tables.
|
||||
unshackle enforces none of this. It is operational policy for a database that may be shared with a group. Never connect with the `root` account, not even for yourself. Give every account a password, and never let two users share a username/password. Restrict each account's access to only the `unshackle` database, and grant normal users only `SELECT` and `INSERT`. Reserve the `CREATE` grant for a single trusted user who bootstraps the service tables (see [`kv prepare`](#kv-prepare-pre-create-service-tables)); everyone else can insert new keys but should not be creating tables.
|
||||
|
||||
!!! note "Grants determine what the vault can do"
|
||||
On connection the backend reads its own `SHOW GRANTS`. It requires at least `SELECT` to load at all (a missing `SELECT` grant makes the vault fail to load). Writing keys additionally needs `INSERT`, and creating a new service table needs `CREATE`. A read-only account still works fine as a lookup source. It simply cannot push new keys. Because `MySQL` is loaded leniently during downloads, a permission problem will not abort the run.
|
||||
On connection the backend reads its own `SHOW GRANTS`. It requires at least `SELECT` to load at all (a missing `SELECT` grant makes the vault fail to load). Writing keys additionally needs `INSERT`, and creating a new service table needs `CREATE`. A read-only account still works fine as a lookup source. It cannot push new keys. Because `MySQL` is loaded leniently during downloads, a permission problem will not abort the run.
|
||||
|
||||
---
|
||||
|
||||
@@ -139,7 +139,7 @@ A flexible HTTP vault that speaks one of three wire protocols, selected with `ap
|
||||
| `api_mode` | Transport | Authentication | Notes |
|
||||
|------------|-----------|----------------|-------|
|
||||
| `query` (default) | `GET` with query parameters | `username` + `password` | Full read and write. |
|
||||
| `json` | `POST` with a JSON payload | `password` used as a token | Read and write per key; cannot bulk-enumerate keys. |
|
||||
| `json` | `POST` with a JSON payload | `password` used as a token | Read and write per content key; cannot bulk-enumerate keys. |
|
||||
| `decrypt_labs` | `POST` in DecryptLabs format | `api_key` sent as a header | **Read-only**. `no_push` is forced on. |
|
||||
|
||||
=== "query mode"
|
||||
@@ -154,7 +154,7 @@ A flexible HTTP vault that speaks one of three wire protocols, selected with `ap
|
||||
password: "your-password"
|
||||
```
|
||||
|
||||
`username` is required in query mode. Both the username and password are sent as query parameters on each request.
|
||||
Query mode requires `username`. Both the username and password are sent as query parameters on each request.
|
||||
|
||||
=== "json mode"
|
||||
|
||||
@@ -180,7 +180,7 @@ A flexible HTTP vault that speaks one of three wire protocols, selected with `ap
|
||||
api_key: "your-decrypt-labs-key"
|
||||
```
|
||||
|
||||
A read-only lookup source. The key is sent in a `decrypt-labs-api-key` header and the vault refuses all writes.
|
||||
A read-only lookup source. The API key is sent in a `decrypt-labs-api-key` header and the vault refuses all writes.
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
@@ -190,7 +190,7 @@ A flexible HTTP vault that speaks one of three wire protocols, selected with `ap
|
||||
| `api_mode` | no | `query` (default), `json`, or `decrypt_labs`. |
|
||||
| `username` | for `query` | Required in query mode; ignored otherwise. |
|
||||
| `password` | one of these | Password (query mode) or token (json mode). |
|
||||
| `api_key` | one of these | Alternative to `password`; used for `decrypt_labs`. If both are given, `api_key` wins. |
|
||||
| `api_key` | one of these | Alternative to `password`; used for `decrypt_labs`. If you give both, `api_key` wins. |
|
||||
| `no_push` | no | Read-only when `true` (always `true` for `decrypt_labs`). |
|
||||
| `timeout` | no | Request timeout in seconds; defaults to `vault_timeout`. |
|
||||
|
||||
@@ -223,13 +223,13 @@ key_vaults:
|
||||
| `timeout` | no | Request timeout in seconds; defaults to `vault_timeout`. |
|
||||
| `headers` | no | Extra HTTP `headers` sent with every request, for example the Cloudflare Access service-token headers `CF-Access-Client-Id` and `CF-Access-Client-Secret`. |
|
||||
|
||||
The backend talks to endpoints under `uri` (for example `GET {uri}/{service}/{kid}` to look up a single key) and interprets a numeric `code` field in each JSON response to detect errors such as an invalid token, rate limiting, or an invalid service tag.
|
||||
The backend talks to endpoints under `uri` (for example `GET {uri}/{service}/{kid}` to look up a single key). It interprets a numeric `code` field in each JSON response to detect errors such as an invalid token, rate limiting, or an invalid service tag.
|
||||
|
||||
!!! warning "This is unshackle's own protocol, not a generic vault client"
|
||||
The `API` backend expects unshackle's specific HTTP request and response shape. An API or HTTP key-vault endpoint built for a different project or service will generally *not* be compatible. You cannot simply point this at an arbitrary third-party vault API and expect it to work. To bridge another service, put an adapter in front of it that speaks this format, or use the `HTTP` backend if one of its wire protocols matches. When pushing many keys at once it batches requests and automatically shrinks the batch size if the server rejects an over-large request.
|
||||
The `API` backend expects unshackle's specific HTTP request and response shape. An API or HTTP key-vault endpoint built for a different project or service will generally *not* be compatible. You cannot point this at an arbitrary third-party vault API and expect it to work. To bridge another service, put an adapter in front of it that speaks this format, or use the `HTTP` backend if one of its wire protocols matches. When pushing many keys at once it batches requests and automatically shrinks the batch size if the server rejects an over-large request.
|
||||
|
||||
!!! tip "DecryptLabs shortcut"
|
||||
An `API` vault whose `name` contains `decrypt_labs` will have its `token` filled in automatically from a global `decrypt_labs_api_key` in your config if you leave `token` unset. This lets you keep the key in one place.
|
||||
An `API` vault whose `name` contains `decrypt_labs` will have its `token` filled in automatically from a global `decrypt_labs_api_key` in your config if you leave `token` unset. This lets you keep the API key in one place.
|
||||
|
||||
---
|
||||
|
||||
@@ -256,15 +256,15 @@ $ unshackle kv add keys.txt EXAMPLE local shared
|
||||
|
||||
The service argument (`EXAMPLE` above) is normalized to its canonical tag automatically. Keys that already exist in a vault are reported as skipped.
|
||||
|
||||
### `kv search`: find a key by KID
|
||||
### `kv search`: find a content key by KID
|
||||
|
||||
Search your vaults for a KID and print any matching key.
|
||||
Search your vaults for a KID and print any matching content key.
|
||||
|
||||
```console
|
||||
$ unshackle kv search 9a04f07998404286ab92e65be0885f95
|
||||
```
|
||||
|
||||
The KID must be 32 hex characters (dashes are stripped for you). By default every configured vault and every service table is scanned; narrow the search with options:
|
||||
The KID must be 32 hex characters (dashes are stripped for you). By default the search scans every configured vault and every service table; narrow it with options:
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
@@ -276,7 +276,7 @@ The KID must be 32 hex characters (dashes are stripped for you). By default ever
|
||||
|
||||
### `kv copy`: copy keys between vaults
|
||||
|
||||
Copy every key from one or more source vaults into a single destination vault. Existing rows are never altered; only genuinely new KIDs are added, and null keys are skipped.
|
||||
Copy every content key from one or more source vaults into a single destination vault. `kv copy` never alters existing rows; it adds only genuinely new KIDs and skips null keys.
|
||||
|
||||
```console
|
||||
$ unshackle kv copy shared local
|
||||
@@ -297,7 +297,7 @@ $ unshackle kv copy local shared community
|
||||
|
||||
### `kv sync`: mirror vaults both ways
|
||||
|
||||
Ensure two or more vaults each end up with every key the others have. This is effectively a two-way `copy` between each pair, so afterwards all listed vaults hold the same set of keys.
|
||||
Make each of two or more vaults hold every content key the others have. This is effectively a two-way `copy` between each pair, so afterwards all listed vaults hold the same set of keys.
|
||||
|
||||
```console
|
||||
$ unshackle kv sync local shared
|
||||
|
||||
@@ -66,7 +66,7 @@ folder-kind keys.
|
||||
| `tag` | str | `""` | Release-group tag. Fills the `{tag}` template variable and is written into MKV/audio tags. |
|
||||
| `tag_group_name` | bool | `true` | Include the group name (`tag`) in the MKV `Group` tag. |
|
||||
| `tag_imdb_tmdb` | bool | `true` | Write IMDb/TMDB/TVDB external-ID tags into the MKV (needs metadata providers). |
|
||||
| `chapter_fallback_name` | str | `""` | Fallback chapter-name template, e.g. `"Chapter {i:02}"`. |
|
||||
| `chapter_fallback_name` | str | `""` | Fallback template for chapter names, e.g. `"Chapter {i:02}"`. |
|
||||
| `unicode_filenames` | bool | `false` | Allow Unicode in filenames; when `false`, names are ASCII-sanitised. |
|
||||
|
||||
## `tag_rules`
|
||||
@@ -97,7 +97,7 @@ Rule semantics:
|
||||
`year`, `season`, `episode`, and so on) after it evaluates the rules, so a condition cannot
|
||||
use them.
|
||||
- An unknown key in `when` is a mistake: unshackle logs a warning that names the key
|
||||
and skips that rule. A rule without conditions or without a `tag` is also warned about and skipped.
|
||||
and skips that rule. unshackle also warns about a rule without conditions or without a `tag`, and skips it.
|
||||
|
||||
Put the more specific rules first, because unshackle stops at the first match.
|
||||
|
||||
|
||||
@@ -245,9 +245,9 @@ title object already knew: `{title}`, `{title_raw}`, `{title_id}`, `{year}`, `{s
|
||||
comes from the naming context, such as `{quality}`, `{hdr}`, `{artist}` and `{album}`, is
|
||||
empty.
|
||||
|
||||
A resumed `--continue` download that finishes runs `success` as usual. Because unshackle
|
||||
does not wait for your script, it never sees its exit code, so a failing script is not
|
||||
reported anywhere. Your script owns its own error handling and logging.
|
||||
A resumed `--continue-downloads` download that finishes runs `success` as usual. Because
|
||||
unshackle does not wait for your script, it never sees its exit code, so a failing script
|
||||
is not reported anywhere. Your script owns its own error handling and logging.
|
||||
|
||||
A `success` hook always names a file this run wrote. There is no skip-if-exists path in
|
||||
`dl` today, so nothing fires for a title that was not downloaded.
|
||||
|
||||
@@ -61,9 +61,9 @@ credentials:
|
||||
|
||||
Settings for extracting cookies directly from a local Firefox profile. A service block must give
|
||||
`hosts`, a list of cookie hostnames. unshackle ignores an entry shorter than 3 characters, to
|
||||
prevent dumping the whole cookie store. With an optional `local_storage` boolean, unshackle also
|
||||
pulls matching entries from `webappsstore.sqlite`, which only services that keep auth tokens
|
||||
in localStorage rather than in HTTP cookies need. Extraction is read-only.
|
||||
prevent a broad match dumping most of the profile. With an optional `local_storage` boolean,
|
||||
unshackle also pulls matching entries from `webappsstore.sqlite`, which only services that keep
|
||||
auth tokens in localStorage rather than in HTTP cookies need. Extraction is read-only.
|
||||
|
||||
!!! note "Firefox does not need to be closed"
|
||||
The extractor copies **both** `cookies.sqlite` **and** its WAL file into a `0700` temp
|
||||
|
||||
@@ -3760,7 +3760,7 @@ class dl:
|
||||
code) only when the export used ``--proxy``, as an import geofence. Each track records
|
||||
only the licensed DRM system. Content keys live once under the track's ``keys``. ``drm`` may be None
|
||||
(DRM-free track) or a DRM system without ``to_dict``/``content_keys`` (e.g. ClearKey) -
|
||||
the track, manifest, chapter and attachment info is still exported.
|
||||
the export still records the track, manifest, chapter and attachment info.
|
||||
"""
|
||||
with self.EXPORT_LOCK:
|
||||
doc: dict[str, Any] = {}
|
||||
|
||||
@@ -2241,7 +2241,7 @@ def resolve_server_cdm(service: str, profile: Optional[str], cdm_type: Optional[
|
||||
|
||||
Checks the server's own CDM config (``config.cdm[service]``) to
|
||||
determine the CDM type without loading the full CDM object. This
|
||||
ensures that when you use ``server_cdm: true``, the server's CDM
|
||||
makes sure that when you use ``server_cdm: true``, the server's CDM
|
||||
determines device selection (e.g. PlayReady vs Widevine).
|
||||
|
||||
Falls back to a lightweight stub from *cdm_type* only if the configuration
|
||||
|
||||
@@ -81,7 +81,7 @@ class DecryptLabsRemoteCDM:
|
||||
Main Features:
|
||||
- Compatible with both Widevine and PlayReady DRM schemes
|
||||
- Intelligent caching that compares required vs. available keys
|
||||
- Optimized caching for L1/L2 devices (leverages API auto-optimization)
|
||||
- Optimized caching for L1/L2 devices (uses API auto-optimization)
|
||||
- Automatic content key combination for mixed cache/license scenarios
|
||||
- Fallback to license requests when keys are missing
|
||||
|
||||
@@ -367,7 +367,7 @@ class DecryptLabsRemoteCDM:
|
||||
- For other devices: Uses cache retry logic based on CDM session state
|
||||
- With required KIDs set: Only requests license for missing keys
|
||||
- Without required KIDs: Returns any available cached keys
|
||||
- For PlayReady: Combines vault, cached, and license keys seamlessly
|
||||
- For PlayReady: Combines vault, cached, and license keys
|
||||
|
||||
Args:
|
||||
session_id: Session identifier
|
||||
@@ -549,8 +549,8 @@ class DecryptLabsRemoteCDM:
|
||||
Parse license response with the Decrypt Labs API and intelligent content key combination.
|
||||
|
||||
For PlayReady titles with partial cached keys, this method intelligently
|
||||
combines the cached keys with newly obtained license keys, avoiding
|
||||
duplicates while ensuring all required keys are available.
|
||||
combines the cached keys with newly obtained license keys, skipping a
|
||||
license key whose KID is already present.
|
||||
|
||||
The content key combination process:
|
||||
1. Extracts keys from the license response
|
||||
|
||||
@@ -626,11 +626,11 @@ def download(
|
||||
RnetSession streams natively. requests.Session reads the raw socket, except
|
||||
on a content-encoded body: only iter_content decodes such a body.
|
||||
|
||||
Yields these download status updates while the chunks download:
|
||||
Yields these download status updates while the segments download:
|
||||
|
||||
- {total: 123} (there are 123 chunks to download)
|
||||
- {total: None} (there are an unknown number of chunks to download)
|
||||
- {advance: 1} (the downloader finished one chunk)
|
||||
- {total: 123} (there are 123 segments to download)
|
||||
- {total: None} (there are an unknown number of segments to download)
|
||||
- {advance: 1} (the downloader finished one segment)
|
||||
- {downloaded: "10.1 MB/s"} (currently downloading at a rate of 10.1 MB/s)
|
||||
- {file_downloaded: Path(...), written: 1024} (download finished, has the save path and size)
|
||||
|
||||
@@ -1260,11 +1260,11 @@ def requests(
|
||||
reports it as downloaded at its on-disk size without any request, so a second call resumes a
|
||||
batch from the segments the earlier run completed.
|
||||
|
||||
Yields these download status updates while the chunks download:
|
||||
Yields these download status updates while the segments download:
|
||||
|
||||
- {total: 123} (there are 123 chunks to download)
|
||||
- {total: None} (there are an unknown number of chunks to download)
|
||||
- {advance: 1} (the downloader finished one chunk)
|
||||
- {total: 123} (there are 123 segments to download)
|
||||
- {total: None} (there are an unknown number of segments to download)
|
||||
- {advance: 1} (the downloader finished one segment)
|
||||
- {downloaded: "10.1 MB/s"} (currently downloading at a rate of 10.1 MB/s)
|
||||
- {file_downloaded: Path(...), written: 1024} (download finished, has the save path and size)
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ class Episode(Title):
|
||||
return str(self.air_date)
|
||||
|
||||
def folder_season(self) -> str:
|
||||
"""Season folder label: air year for dated content, else SxxExx-style season."""
|
||||
"""Season folder label: air year when the air date parsed to a date, else SxxExx-style season."""
|
||||
if isinstance(self.air_date, date):
|
||||
return f"{self.air_date.year:04}"
|
||||
return f"S{self.season:02}"
|
||||
|
||||
@@ -146,7 +146,7 @@ class Song(Title):
|
||||
|
||||
A song shares the episode key space, its disc reading as the season and its track
|
||||
as the episode, so ``-w 1-5`` takes tracks 1 to 5 and ``-w 2x3`` takes disc 2
|
||||
track 3. A song has no parts and no air date, so it answers to one key only.
|
||||
track 3. A song has no parts and no air date, so it answers to its disc-and-track key only.
|
||||
"""
|
||||
return f"{self.disc}x{self.track}" in wanted
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ class Title:
|
||||
@abstractmethod
|
||||
def get_filename(self, media_info: MediaInfo, folder: bool = False, show_service: bool = True) -> str:
|
||||
"""
|
||||
Get a Filename for this Title with the provided Media Info.
|
||||
Get a Filename for this Title with the provided MediaInfo.
|
||||
All filenames should be sanitized with the sanitize_filename() utility function.
|
||||
|
||||
Parameters:
|
||||
|
||||
@@ -163,8 +163,8 @@ class Subtitle(Track):
|
||||
However, I would not pay much attention to the claims about SDH needing to
|
||||
be in the original language. It is logically not true.
|
||||
|
||||
CC == Closed Captions. Source: Basically every site.
|
||||
SDH = Subtitles for the Deaf or Hard-of-Hearing. Source: Basically every site.
|
||||
CC == Closed Captions. Source: Every site.
|
||||
SDH = Subtitles for the Deaf or Hard-of-Hearing. Source: Every site.
|
||||
HOH = Exact same as SDH. Is a term used in the UK. Source: https://bit.ly/2PGJatz (ICO UK)
|
||||
|
||||
More in-depth information, examples, and stuff to look for can be found in the Parameter
|
||||
|
||||
@@ -918,7 +918,7 @@ class Track:
|
||||
return init_data
|
||||
|
||||
def repackage(self, bsf_v: Optional[str] = None) -> bool:
|
||||
"""Remux the track with ffmpeg ``-c copy``.
|
||||
"""Remux the track with FFmpeg ``-c copy``.
|
||||
|
||||
A given ``bsf_v`` goes into the same pass as ``-bsf:v``, which normalises video VUI
|
||||
colour metadata without a second full-file remux. Repackaging is mandatory. The
|
||||
|
||||
@@ -204,7 +204,11 @@ class Tracks:
|
||||
],
|
||||
warn_only: bool = False,
|
||||
) -> None:
|
||||
"""Add a provided track to its appropriate array and ensuring it is not a duplicate."""
|
||||
"""Add a provided track to its appropriate array.
|
||||
|
||||
A track whose ID is already in the collection raises ValueError. With `warn_only`
|
||||
set, this method skips such a track and logs how many it skipped.
|
||||
"""
|
||||
if isinstance(tracks, Tracks):
|
||||
if tracks.manifest_url and not self.manifest_url:
|
||||
self.manifest_url = tracks.manifest_url
|
||||
|
||||
@@ -415,7 +415,7 @@ class Video(Track):
|
||||
|
||||
Returns None when a rewrite is not necessary: SDR/DV/HYBRID ranges, non-AVC/HEVC codecs, a
|
||||
missing file, or when the bitstream already ships the correct colour metadata. Otherwise
|
||||
returns a ``{h264,hevc}_metadata=...`` string suitable for ffmpeg ``-bsf:v``.
|
||||
returns a ``{h264,hevc}_metadata=...`` string suitable for FFmpeg ``-bsf:v``.
|
||||
"""
|
||||
if not self.path or not self.path.exists():
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user