diff --git a/docs/dev/creating-a-service.md b/docs/dev/creating-a-service.md index 23472d24..462db4d8 100644 --- a/docs/dev/creating-a-service.md +++ b/docs/dev/creating-a-service.md @@ -32,14 +32,14 @@ is a service**, and the folder name is the service **tag**: ```text services/ └── EXAMPLE/ - ├── __init__.py # required — defines the service class - └── config.yaml # optional — per-service configuration + ├── __init__.py # required - defines the service class + └── config.yaml # optional - per-service configuration ``` Two rules the loader enforces: -1. **The class name must exactly match the folder/tag name.** Folder `AMZN` must - define `class AMZN`. A mismatch raises a `RuntimeError`. +1. **The class name must exactly match the folder/tag name.** Folder `EXAMPLE` must + define `class EXAMPLE`. A mismatch raises a `RuntimeError`. 2. **List order is priority.** The first services path to define a given tag wins; later duplicates are shadowed. Put your own local overrides *last* to use them as fallbacks, or *first* to override a repo. @@ -546,7 +546,7 @@ from unshackle.core.tracks import Chapters, Tracks class MYSVC(Service): """ - MyService — https://myservice.com + MyService - https://myservice.com \b Usage: unshackle dl MYSVC diff --git a/docs/dev/rest-api/authentication.md b/docs/dev/rest-api/authentication.md index 83a678dd..6cf5b644 100644 --- a/docs/dev/rest-api/authentication.md +++ b/docs/dev/rest-api/authentication.md @@ -59,7 +59,7 @@ serve: username: alice devices: ["my_widevine_device"] # Widevine (.wvd) devices this key may use playready_devices: ["my_playready_prd"] # PlayReady (.prd) devices this key may use - services: ["AMZN", "NF"] # optional per-key service allowlist + services: ["EXAMPLE1", "EXAMPLE2"] # optional per-key service allowlist a-secret-key-for-bob: username: bob devices: ["my_widevine_device"] @@ -217,11 +217,11 @@ The effective allowlist for a request is: ```yaml title="Restricting services" serve: api_secret: "admin-key" - services: ["AMZN", "NF", "DSNP"] # global ceiling for all keys + services: ["EXAMPLE1", "EXAMPLE2", "EXAMPLE3"] # global ceiling for all keys users: limited-key: username: limited - services: ["AMZN"] # this key ends up allowed only AMZN + services: ["EXAMPLE1"] # this key ends up allowed only EXAMPLE1 ``` Endpoints that name a service (listing services, searching, listing titles/tracks, downloading, profiles, history, and remote sessions) all filter against this allowlist. A request for a service outside the key's allowlist is rejected as an invalid service. Service tags are matched after normalization, so casing and aliases resolve consistently. diff --git a/docs/dev/rest-api/endpoints.md b/docs/dev/rest-api/endpoints.md index f752ef5b..0951e5b7 100644 --- a/docs/dev/rest-api/endpoints.md +++ b/docs/dev/rest-api/endpoints.md @@ -130,8 +130,8 @@ Search a service for titles by query string. The service must implement `search( | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | -| `service` | string | yes | — | Service tag. | -| `query` | string | yes | — | Search query. | +| `service` | string | yes | - | Service tag. | +| `query` | string | yes | - | Search query. | | `profile` | string | no | `null` | Credential/cookie profile to use. | | `proxy` | string | no | `null` | Proxy URI or country code. | | `no_proxy` | boolean | no | `false` | Force-disable all proxy use. | @@ -164,7 +164,7 @@ Search a service for titles by query string. The service must implement `search( | Status | Error code | Meaning | | --- | --- | --- | -| `200` | — | Search completed. | +| `200` | - | Search completed. | | `400` | `INVALID_PARAMETERS` | Missing `query`. | | `400` | `INVALID_SERVICE` | Unknown or disallowed service. | | `400` | `INVALID_PROXY` | Proxy could not be resolved. | @@ -222,7 +222,7 @@ Each serialized title carries `type` (`"episode"`, `"movie"`, or `"other"`), `na | Status | Error code | Meaning | | --- | --- | --- | -| `200` | — | Titles returned. | +| `200` | - | Titles returned. | | `400` | `INVALID_INPUT` | Missing `service` or `title_id`. | | `400` | `INVALID_SERVICE` | Unknown or disallowed service. | | `401` | `AUTH_FAILED` | Authentication with the service failed. | @@ -242,7 +242,7 @@ List the video, audio, and subtitle tracks for a title. For series, you can scop | `wanted` | string | no | Episode/season range (e.g. `"S01E01-S01E03"`). | | `season` | int/string | no | Season number (combined with `episode`). | | `episode` | int/string | no | Episode number (combined with `season`). | -| `profile`, `proxy`, `no_proxy`, `cdm_type` | — | no | As on `list-titles`. | +| `profile`, `proxy`, `no_proxy`, `cdm_type` | - | no | As on `list-titles`. | When both `season` and `episode` are given, they are combined into a `"{season}x{episode}"` selector. @@ -297,7 +297,7 @@ The `drm` array on a track is a list of `{type, pssh?, kids?, content_keys?, lic | Status | Error code | Meaning | | --- | --- | --- | -| `200` | — | Tracks returned. | +| `200` | - | Tracks returned. | | `400` | `INVALID_INPUT` | Missing `service` or `title_id`. | | `400` | `INVALID_PARAMETERS` | Invalid `wanted` range. | | `400` | `INVALID_SERVICE` | Unknown or disallowed service. | @@ -378,7 +378,7 @@ Create a download job. Requires `service` and `title_id`; every other field is a | `best_available` | boolean | `false` | Fall back to best available. | | `repack` | boolean | `false` | Add REPACK tag. | | `tag` | string | `null` | Release group tag. | -| `tmdb_id` / `imdb_id` / `animeapi_id` | — | `null` | External ID overrides for tagging. | +| `tmdb_id` / `imdb_id` / `animeapi_id` | - | `null` | External ID overrides for tagging. | | `enrich` | boolean | `false` | Override title/year from external source. | | `output_dir` | string | `null` | Override output directory. | | `no_cache` / `reset_cache` | boolean | `false` | Title cache controls. | @@ -395,7 +395,7 @@ Create a download job. Requires `service` and `title_id`; every other field is a | Status | Error code | Meaning | | --- | --- | --- | -| `202` | — | Job queued. | +| `202` | - | Job queued. | | `400` | `INVALID_INPUT` | Missing `service`/`title_id` or malformed JSON. | | `400` | `INVALID_PARAMETERS` | A parameter failed validation. | | `400` | `INVALID_SERVICE` | Unknown or disallowed service. | @@ -409,8 +409,8 @@ List download jobs, with optional filtering and sorting. | Param | Values | Default | Description | | --- | --- | --- | --- | -| `status` | `queued`, `downloading`, `completed`, `failed`, `cancelled` | — | Filter by status. | -| `service` | service tag | — | Filter by service. | +| `status` | `queued`, `downloading`, `completed`, `failed`, `cancelled` | - | Filter by status. | +| `service` | service tag | - | Filter by service. | | `sort_by` | `created_time`, `started_time`, `completed_time`, `progress`, `status`, `service` | `created_time` | Sort field. | | `sort_order` | `asc`, `desc` | `desc` | Sort direction. | | `full` | `true`, `false` | `false` | Include full per-job details. | @@ -454,7 +454,7 @@ With `full=true`, each job additionally includes `parameters` (with secrets reda | Status | Error code | Meaning | | --- | --- | --- | -| `200` | — | Jobs returned. | +| `200` | - | Jobs returned. | | `400` | `INVALID_PARAMETERS` | Invalid `sort_by` or `sort_order`. | ### `GET /api/download/jobs/{job_id}` @@ -463,7 +463,7 @@ Fetch a single job with full details (equivalent to a `full=true` list entry). | Status | Error code | Meaning | | --- | --- | --- | -| `200` | — | Job returned. | +| `200` | - | Job returned. | | `404` | `JOB_NOT_FOUND` | No such job. | ### `DELETE /api/download/jobs/{job_id}` @@ -478,8 +478,8 @@ Cancel or remove a job. The behaviour depends on the job's current state: | Status | Error code | Meaning | | --- | --- | --- | -| `200` | — | Job cancelled. | -| `204` | — | Terminal job removed (empty body). | +| `200` | - | Job cancelled. | +| `204` | - | Terminal job removed (empty body). | | `400` | `INVALID_PARAMETERS` | Job is in a state that cannot be cancelled. | | `404` | `JOB_NOT_FOUND` | No such job. | @@ -512,7 +512,7 @@ Enqueue a **new** job that reuses a terminal job's service, title, and parameter | Status | Error code | Meaning | | --- | --- | --- | -| `202` | — | New job queued. | +| `202` | - | New job queued. | | `404` | `JOB_NOT_FOUND` | No such job. | | `409` | `CONFLICT` | Job is not in a terminal state. | | `400` | `INVALID_SERVICE` | Service is no longer allowed. | @@ -530,7 +530,7 @@ Move a queued job to the front of the download queue. | Status | Error code | Meaning | | --- | --- | --- | -| `200` | — | Job moved to front. | +| `200` | - | Job moved to front. | | `404` | `JOB_NOT_FOUND` | No such job. | | `409` | `CONFLICT` | Job is not queued. | @@ -593,7 +593,7 @@ Read the persisted history of jobs that reached a terminal state, newest first. | Param | Type | Default | Description | | --- | --- | --- | --- | | `limit` | int (≥ 1) | `100` | Maximum entries to return. | -| `service` | string | — | Case-insensitive service filter. | +| `service` | string | - | Case-insensitive service filter. | === "Response `200`" @@ -619,7 +619,7 @@ Read the persisted history of jobs that reached a terminal state, newest first. | Status | Error code | Meaning | | --- | --- | --- | -| `200` | — | History returned. | +| `200` | - | History returned. | | `400` | `INVALID_PARAMETERS` | Invalid `limit`. | ### `DELETE /api/history/{job_id}` @@ -628,7 +628,7 @@ Remove a single persisted history entry. Returns **`204 No Content`** (empty bod | Status | Error code | Meaning | | --- | --- | --- | -| `204` | — | Entry removed (empty body). | +| `204` | - | Entry removed (empty body). | | `404` | `NOT_FOUND` | No such entry (or outside your allowlist). | ### `POST /api/maintenance/clear-cache` and `POST /api/maintenance/clear-temp` @@ -648,7 +648,7 @@ The `409` guard is about active file I/O, not an arbitrary lock: an in-flight jo | Status | Error code | Meaning | | --- | --- | --- | -| `200` | — | Directory cleared. | +| `200` | - | Directory cleared. | | `409` | `CONFLICT` | A download is currently active. | ### `POST /api/maintenance/refresh-services` @@ -718,7 +718,7 @@ Create a session for a service and title. Authentication runs asynchronously in | Status | Error code | Meaning | | --- | --- | --- | -| `200` | — | Session created; authentication in progress. | +| `200` | - | Session created; authentication in progress. | | `400` | `INVALID_INPUT` | Missing `service` or `title_id`. | | `400` | `INVALID_SERVICE` | Unknown or disallowed service. | @@ -752,7 +752,7 @@ Poll for the authentication status and any pending interactive prompt (OTP, PIN, | Status | Error code | Meaning | | --- | --- | --- | -| `200` | — | Status returned. | +| `200` | - | Status returned. | | `403` | `FORBIDDEN` | Request IP differs from the session creator. | | `404` | `SESSION_NOT_FOUND` | No such session. | @@ -768,7 +768,7 @@ Submit a response to a pending prompt. Body: `{ "response": "..." }` (required). | Status | Error code | Meaning | | --- | --- | --- | -| `200` | — | Response accepted. | +| `200` | - | Response accepted. | | `400` | `INVALID_INPUT` | Missing `response`, or no prompt is pending. | | `404` | `SESSION_NOT_FOUND` | No such session. | @@ -810,7 +810,7 @@ Get tracks and chapters for a specific title within the session. Body: `{ "title | Status | Error code | Meaning | | --- | --- | --- | -| `200` | — | Tracks returned. | +| `200` | - | Tracks returned. | | `400` | `INVALID_INPUT` | Missing or unknown `title_id`. | | `404` | `SESSION_NOT_FOUND` | No such session. | @@ -830,7 +830,7 @@ Resolve the download URL, DRM info, headers, cookies, and raw track data for sel | Status | Error code | Meaning | | --- | --- | --- | -| `200` | — | Segment info returned. | +| `200` | - | Segment info returned. | | `400` | `INVALID_INPUT` | Missing `track_ids`. | | `404` | `TRACK_NOT_FOUND` | An unknown track ID was requested. | @@ -876,7 +876,7 @@ Acquire DRM keys. Two modes are supported via the `mode` field. | Status | Error code | Meaning | | --- | --- | --- | -| `200` | — | License/keys returned. | +| `200` | - | License/keys returned. | | `400` | `INVALID_INPUT` | Missing challenge/PSSH, or no CDM device configured for the key. | | `400` | `INVALID_PARAMETERS` | Unsupported `drm_type`. | | `404` | `TRACK_NOT_FOUND` | Unknown track ID. | diff --git a/docs/dev/rest-api/errors.md b/docs/dev/rest-api/errors.md index 32b8ceee..8f708793 100644 --- a/docs/dev/rest-api/errors.md +++ b/docs/dev/rest-api/errors.md @@ -139,7 +139,7 @@ uncategorised exception bubbles up, the server runs it through a classifier that inspects the exception type and message and maps it to the most appropriate code. The classification is keyword-based, checked in order: -| If the error mentions… | It becomes | Retryable | +| If the error mentions... | It becomes | Retryable | |---|---|---| | auth, login, credential, unauthorized, forbidden, token | `AUTH_FAILED` (401) | no | | connection, timeout, network, unreachable, socket, dns, resolve (or `ConnectionError`, `TimeoutError`, `URLError`, `SSLError`) | `NETWORK_ERROR` (503) | yes | diff --git a/docs/dev/rest-api/index.md b/docs/dev/rest-api/index.md index 0f5dee19..d6f9c40c 100644 --- a/docs/dev/rest-api/index.md +++ b/docs/dev/rest-api/index.md @@ -211,7 +211,7 @@ every request except `/api/health`: curl -X POST http://127.0.0.1:8786/api/search \ -H "X-Secret-Key: your-api-secret" \ -H "Content-Type: application/json" \ - -d '{"service": "NF", "query": "example"}' + -d '{"service": "EXAMPLE", "query": "example"}' ``` A missing or unknown key returns `401`. The full model, including per-key service diff --git a/docs/dev/rest-api/job-lifecycle.md b/docs/dev/rest-api/job-lifecycle.md index f300996c..56b9c761 100644 --- a/docs/dev/rest-api/job-lifecycle.md +++ b/docs/dev/rest-api/job-lifecycle.md @@ -86,7 +86,7 @@ curl -X POST http://127.0.0.1:8786/api/download \ -H "X-Secret-Key: your-api-secret" \ -H "Content-Type: application/json" \ -d '{ - "service": "NF", + "service": "EXAMPLE", "title_id": "81234567", "quality": ["1080"], "range": ["HDR10"] @@ -228,7 +228,7 @@ state, and this is important because the response differs: |---|---|---| | `queued` or `downloading` | The job is cancelled. A running download's worker process is terminated. | `200` with `{"status": "success", "message": "Job cancelled"}` | | `completed`, `failed`, or `cancelled` (terminal) | The job is **removed** from the manager entirely. | `204 No Content`, empty body | -| Not found | — | `404` with error code `JOB_NOT_FOUND` | +| Not found | - | `404` with error code `JOB_NOT_FOUND` | In other words, `DELETE` on a live job *cancels* it, while `DELETE` on an already finished job *deletes it from the list*. This dual meaning is deliberate: one verb diff --git a/docs/dev/rest-api/quickstart.md b/docs/dev/rest-api/quickstart.md index f0f02323..da02a045 100644 --- a/docs/dev/rest-api/quickstart.md +++ b/docs/dev/rest-api/quickstart.md @@ -161,7 +161,7 @@ If the key is missing or wrong, you get a `401` with a plain body: ``` !!! warning "Two error shapes exist" - The authentication middleware returns the short `{"status": 401, "message": "..."}` shape shown above. Every **other** error the API produces uses the richer structured shape (`error_code`, `timestamp`, `details`, …) described in [Error handling](#error-handling). A robust client should handle both. + The authentication middleware returns the short `{"status": 401, "message": "..."}` shape shown above. Every **other** error the API produces uses the richer structured shape (`error_code`, `timestamp`, `details`, ...) described in [Error handling](#error-handling). A robust client should handle both. The `tag` value for the service you want is the identifier you pass as `service` in the calls below. @@ -225,8 +225,8 @@ The request body accepts the full download parameter set. A few of the most usef | Field | Default | Meaning | | --- | --- | --- | -| `service` | — | Service tag (required) | -| `title_id` | — | Title identifier (required) | +| `service` | - | Service tag (required) | +| `title_id` | - | Title identifier (required) | | `profile` | `null` | Named credential/cookie profile to use | | `quality` | `[]` | Resolution(s), e.g. `[1080]`; empty means best available | | `vcodec` | `null` | Video codec, e.g. `"H265"` | diff --git a/docs/dev/rest-api/remote-sessions.md b/docs/dev/rest-api/remote-sessions.md index a65905bf..56a89ecf 100644 --- a/docs/dev/rest-api/remote-sessions.md +++ b/docs/dev/rest-api/remote-sessions.md @@ -65,7 +65,7 @@ remote_services: server_cdm: false # Optional: per-service config overrides applied locally (title_map, cdm, etc.) services: - AMZN: + EXAMPLE: title_map: "0ABCDEF": "The Show (Renamed Locally)" ``` @@ -87,13 +87,13 @@ remote_services: If exactly one server is configured, unshackle uses it implicitly: ```bash -unshackle dl AMZN 0ABCDEF +unshackle dl EXAMPLE 0ABCDEF ``` If **more than one** server is configured, you must pick one with `--server`: ```bash -unshackle dl --server my_server AMZN 0ABCDEF +unshackle dl --server my_server EXAMPLE 0ABCDEF ``` With no `remote_services` configured at all, unshackle raises a clear error telling @@ -280,10 +280,10 @@ All routes are exposed even in `--remote-only` server mode. Paths use the The response returns **before** authentication finishes: ```json -{ "session_id": "…uuid4…", "service": "AMZN", "status": "authenticating" } +{ "session_id": "...uuid4...", "service": "EXAMPLE", "status": "authenticating" } ``` -Authentication runs on a background thread (`asyncio.to_thread(authenticate, …)`). +Authentication runs on a background thread (`asyncio.to_thread(authenticate, ...)`). The session starts in `AUTHENTICATING`; the client must poll the prompt endpoint until it reaches `authenticated` (or `failed`). @@ -306,11 +306,11 @@ layer. { "status": "authenticated" } { "status": "authenticating" } { "status": "pending_input", "prompt": "Enter code: " } -{ "status": "failed", "error": "…message…" } +{ "status": "failed", "error": "...message..." } ``` A missing session returns `404 SESSION_NOT_FOUND`; an IP mismatch returns -`403 FORBIDDEN`. `POST /api/session/{id}/prompt` takes `{ "response": "…" }` and +`403 FORBIDDEN`. `POST /api/session/{id}/prompt` takes `{ "response": "..." }` and returns `{ "status": "accepted" }`; posting with no pending prompt is an `INVALID_INPUT` error. @@ -364,7 +364,7 @@ and base64-encoding, **excluding** `titles_*` files) and returns them under a `cache` key so the client can persist refreshed tokens: ```json -{ "status": "ok", "cache": { "tokens": "…base64(zlib(bytes))…" } } +{ "status": "ok", "cache": { "tokens": "...base64(zlib(bytes))..." } } ``` ### Session info response @@ -373,8 +373,8 @@ and base64-encoding, **excluding** `titles_*` files) and returns them under a ```json { - "session_id": "…", - "service": "AMZN", + "session_id": "...", + "service": "EXAMPLE", "valid": true, "expires_in": 300, "track_count": 12, @@ -410,7 +410,7 @@ session API and a good template for any client. ## Troubleshooting -!!! example "\"Could not connect to remote server … Is it running?\"" +!!! example "\"Could not connect to remote server ... Is it running?\"" The client couldn't reach the URL. Confirm `unshackle serve` is running on the server, the `url`/port in `remote_services` are correct (default port `8786`), and any firewall or reverse proxy allows the connection. @@ -426,6 +426,6 @@ session API and a good template for any client. prompts you, check that credentials/cookies forwarded from the client are valid for the service. -!!! example "\"Multiple remote services configured. Use --server …\"" +!!! example "\"Multiple remote services configured. Use --server ...\"" You have more than one entry under `remote_services`. Pass `--server ` to pick one. diff --git a/docs/dev/service-architecture.md b/docs/dev/service-architecture.md index 40f645cf..59598417 100644 --- a/docs/dev/service-architecture.md +++ b/docs/dev/service-architecture.md @@ -25,9 +25,9 @@ your service, in order: ```text Service.get_titles → which movies / episodes / songs exist │ -Service.get_tracks → fetch a manifest for one title … +Service.get_tracks → fetch a manifest for one title ... │ -manifest.to_tracks → … parsed into Video / Audio / Subtitle tracks +manifest.to_tracks → ... parsed into Video / Audio / Subtitle tracks │ Track.download → fetch + merge segments (core-driven) │ @@ -310,7 +310,7 @@ A typical `get_tracks` therefore looks like fetching the manifest text with - **HLS**: set `track.OnSegmentFilter(segment)` to drop unwanted segments (ads/bumpers). - **DASH**: pass a `period_filter` callable to `to_tracks` to drop whole periods. - Amazon caching-breadth periods and non-`content` segment types are dropped automatically. + Caching-breadth periods some services emit and non-`content` segment types are dropped automatically. ### Format gotchas worth knowing diff --git a/docs/getting-started/configuration-file.md b/docs/getting-started/configuration-file.md index 2ab2d7d3..32d99792 100644 --- a/docs/getting-started/configuration-file.md +++ b/docs/getting-started/configuration-file.md @@ -63,11 +63,11 @@ tag: MYGRP # Default Widevine/PlayReady device to use, with per-service overrides cdm: default: my_device_l3 - AMZN: my_device_l1 + EXAMPLE: my_device_l1 # Per-service login details (service tag -> "username:password") credentials: - AMZN: my_email@example.com:hunter2 + EXAMPLE: my_email@example.com:hunter2 # Default HTTP headers merged into every request headers: @@ -94,6 +94,41 @@ For the full list with types and defaults, see the [Configuration Reference](../ !!! warning "Unknown keys are silently ignored" unshackle does not validate your config against a schema. If you misspell a key, it is simply skipped and the default is used. You will not get an error. Double-check key names (and their nesting) if a setting does not seem to apply. +## Setting download defaults (`dl:`) + +If you always pass the same `dl` flags (a language, a resolution, a codec), put them under a +`dl:` key once and unshackle applies them to every download. Any flag from +[Downloading](../guide/downloading.md) works here; the key is the flag's long name with dashes +turned into underscores (`--best-available` → `best_available`). + +```yaml title="unshackle.yaml" +dl: + lang: [en] # -l en + quality: [1080] # -q 1080 + vcodec: [H265] # -v H265 + sub_format: srt # convert subtitles to SRT + downloads: 2 # two tracks at once +``` + +You can still override any of these on the command line for a one-off; an explicit flag +always beats the config default. You can also scope defaults to a single service by nesting a +`dl:` block under it: + +```yaml title="Per-service defaults" +dl: + lang: [en] # default for everything +services: + EXAMPLE: + dl: + lang: [en, ja] # Example downloads English + Japanese +``` + +!!! tip "A few keys are named after the flag's internal name" + Most keys are obvious, but set these exact ones: `range` (`-r`), `list` (`--list`), + `tmdb_id` (`--tmdb`), `imdb_id` (`--imdb`), `no_atmos` (`--noatmos`), and `output_dir` + (`-o`). The [Configuration Reference](../reference/configuration.md#dl) has the full list + and every available key. + ## The directory layout The `directories` key controls where unshackle reads and writes its various files. Each directory has a sensible default, and you can override most of them by giving a new path. Paths support `~` for your home directory. diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 861c8766..43881be6 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -79,7 +79,7 @@ full set of valid variables, including `resolution`, `video`, `audio`, `hdr`, ## 3. Add a service -The service tag you pass to `dl` (like `NF`) maps to a **service module**, a small +The service tag you pass to `dl` (like `EXAMPLE`) maps to a **service module**, a small plugin that knows how to talk to one streaming platform. unshackle ships without any bundled services, so you add the one(s) you want. @@ -90,14 +90,14 @@ that exact same name. === "Local folder" - Drop a service folder into your services directory. A service tagged `NF` + Drop a service folder into your services directory. A service tagged `EXAMPLE` lives at: ``` - /NF/__init__.py # defines a class named NF + /EXAMPLE/__init__.py # defines a class named EXAMPLE ``` - Once the folder is in place, the tag `NF` is immediately available to `dl`, + Once the folder is in place, the tag `EXAMPLE` is immediately available to `dl`, `search`, and the other service-aware commands. === "Git repository" @@ -140,7 +140,7 @@ covers everything else: ```yaml title="unshackle.yaml" cdm: default: my_device # the .wvd file's name, without the extension - NF: my_other_device # override for a specific service + EXAMPLE: my_other_device # override for a specific service ``` PlayReady works the same way with `.prd` files created and managed by the @@ -157,14 +157,14 @@ Services that require a login read either **cookies** or **credentials**. - **Cookies**: export the service's cookies to a Netscape-format text file and place it in your cookies directory. unshackle looks for, in order: `cookies/{SERVICE}.txt`, then `cookies/{SERVICE}/{profile}.txt`, then - `cookies/{SERVICE}/default.txt`. So a file at `cookies/NF.txt` is picked up - automatically for the `NF` service. + `cookies/{SERVICE}/default.txt`. So a file at `cookies/EXAMPLE.txt` is picked up + automatically for the `EXAMPLE` service. - **Credentials**: store a username and password per service in your config: ```yaml title="unshackle.yaml" credentials: - NF: "email@example.com:your-password" + EXAMPLE: "email@example.com:your-password" ``` Use the `-p/--profile` flag to switch between multiple accounts for the same @@ -177,14 +177,14 @@ If you have a URL or ID already, skip this. Otherwise, search the service for a title and note the `id` it prints, since that is what you feed to `dl`: ```shell -unshackle search NF "The Office" +unshackle search EXAMPLE "My Show" ``` You can also list what a service exposes for a given title without downloading: ```shell -unshackle dl --list-titles NF 81234567 # show seasons/episodes -unshackle dl --list NF 81234567 # show available tracks +unshackle dl --list-titles EXAMPLE 81234567 # show seasons/episodes +unshackle dl --list EXAMPLE 81234567 # show available tracks ``` ## 7. Run your first download @@ -196,13 +196,13 @@ unshackle dl ``` - **`dl`** carries every quality, language, track, and output flag. -- **`<SERVICE-TAG>`** picks which service to talk to (`NF`, `DSNP`, …). +- **`<SERVICE-TAG>`** picks which service to talk to (e.g. `EXAMPLE`). - **`<TITLE>`** is the URL, ID, or slug the service understands. A good first command asks for 1080p with English audio and subtitles: ```shell title="Your first download" -unshackle dl -q 1080 -l en NF 81234567 +unshackle dl -q 1080 -l en EXAMPLE 81234567 ``` unshackle will fetch the title, select the tracks matching your flags, acquire @@ -224,13 +224,13 @@ keys through your CDM (and any key vaults), then decrypt, mux, and tag the resul !!! example "A few realistic variations" ```shell # A whole first season in the best available quality - unshackle dl -w S01 NF 81234567 + unshackle dl -w S01 EXAMPLE 81234567 # 2160p HDR10 with the original-language audio plus English subtitles - unshackle dl -q 2160 -r HDR10 -l orig -sl en NF 81234567 + unshackle dl -q 2160 -r HDR10 -l orig -sl en EXAMPLE 81234567 # Just the newest episode of an ongoing show - unshackle dl --latest-episode NF 81234567 + unshackle dl --latest-episode EXAMPLE 81234567 ``` See [Downloading](../guide/downloading.md) for the complete flag reference, including @@ -243,7 +243,7 @@ By default, finished files are written to the `downloads` directory folder alongside the project). Override it per run with `-o`: ```shell -unshackle dl -q 1080 -o /mnt/media/incoming NF 81234567 +unshackle dl -q 1080 -o /mnt/media/incoming EXAMPLE 81234567 ``` - **Movies** are written as a single `.mkv` file named from your `movies` template. diff --git a/docs/guide/cli-reference.md b/docs/guide/cli-reference.md index 741a9e2f..a0571a07 100644 --- a/docs/guide/cli-reference.md +++ b/docs/guide/cli-reference.md @@ -45,16 +45,16 @@ unshackle dl [OPTIONS] SERVICE [SERVICE ARGS...] ``` - `OPTIONS`: every flag below, parsed at the `dl` level. -- `SERVICE`: a service tag (e.g. `NF`, `DSNP`). Tags are case-insensitive and honour each service's aliases (`disney+`, `DSNP`, etc. all resolve to the same tag). +- `SERVICE`: a service tag (e.g. `EXAMPLE1`, `EXAMPLE2`). Tags are case-insensitive and honour each service's aliases (`example+`, `EXAMPLE2`, etc. all resolve to the same tag). - `SERVICE ARGS`: the title, URL, or ID, plus any service-specific options. These belong to the service, not to `dl`. !!! example "Typical downloads" ```shell - # Best available 1080p SDR of a Netflix title - unshackle dl -q 1080 NF 81234567 + # Best available 1080p SDR of a title + unshackle dl -q 1080 EXAMPLE 81234567 # 4K HDR10, English audio + subs, from a URL - unshackle dl -q 2160 -r HDR10 -l en DSNP "https://www.disneyplus.com/…" + unshackle dl -q 2160 -r HDR10 -l en EXAMPLE "https://www.example.com/..." # A season range, HEVC video, EC3 audio unshackle dl -w S01-S03 -v H.265 -a EC3 SERVICE TITLE-ID @@ -67,16 +67,16 @@ unshackle dl [OPTIONS] SERVICE [SERVICE ARGS...] | Flag | Default | Description | |---|---|---| -| `-p`, `--profile` | — | Profile for credentials and cookies. | +| `-p`, `--profile` | - | Profile for credentials and cookies. | | `-q`, `--quality` | best | Resolution(s), comma-separated, e.g. `1080,720`. `-q 1080` also matches anamorphic tracks by 16:9 canvas. | | `-v`, `--vcodec` | any | Video codec(s). Accepts names or values: `AVC`/`H.264`, `HEVC`/`H.265`, `VC1`, `VP8`, `VP9`, `AV1`. | | `-a`, `--acodec` | any | Audio codec(s), comma-separated. Accepts `AAC`, `AC3`/`DD`, `EC3`/`DD+`/`eac3`/`ddp`, `AC4`, `OPUS`, `OGG`/`vorbis`, `DTS`, `ALAC`, `FLAC`. | | `-vb`, `--vbitrate` | highest | Exact video bitrate in kbps. | | `-ab`, `--abitrate` | highest | Exact audio bitrate in kbps. | -| `-vb-range`, `--vbitrate-range` | — | Video bitrate range in kbps, e.g. `6000-7000`; picks highest within. Mutually exclusive with `--vbitrate`. | -| `-ab-range`, `--abitrate-range` | — | Audio bitrate range in kbps, e.g. `128-256`. Mutually exclusive with `--abitrate`. | +| `-vb-range`, `--vbitrate-range` | - | Video bitrate range in kbps, e.g. `6000-7000`; picks highest within. Mutually exclusive with `--vbitrate`. | +| `-ab-range`, `--abitrate-range` | - | Audio bitrate range in kbps, e.g. `128-256`. Mutually exclusive with `--abitrate`. | | `-r`, `--range` | `SDR` | Colour range(s): `SDR`, `HLG`, `HDR10`, `HDR10P` (HDR10+), `DV`, `HYBRID`. | -| `-c`, `--channels` | — | Audio channels; matches sub-layouts (5.1 ≈ 6.0). | +| `-c`, `--channels` | - | Audio channels; matches sub-layouts (5.1 ≈ 6.0). | | `-naa`, `--noatmos` | off | Exclude Dolby Atmos audio tracks. | | `--worst` | off | Pick the lowest bitrate within the requested quality. **Requires `-q`.** | | `--best-available` | off | Continue with the best available if a requested resolution/language is absent, instead of failing. | @@ -91,13 +91,13 @@ unshackle dl [OPTIONS] SERVICE [SERVICE ARGS...] | Flag | Default | Description | |---|---|---| | `-l`, `--lang` | `orig` | Language(s) for **both** video and audio. `orig` = the title's original language; e.g. `orig,en`. | -| `-vl`, `--v-lang` | — | Video-only language (overrides `-l` for video). | -| `-al`, `--a-lang` | — | Audio-only language (overrides `-l` for audio). | +| `-vl`, `--v-lang` | - | Video-only language (overrides `-l` for video). | +| `-al`, `--a-lang` | - | Audio-only language (overrides `-l` for audio). | | `-sl`, `--s-lang` | `all` | Subtitle language(s). | -| `--require-subs` | — | Required subtitle langs; keeps **all** subs only if these exist. **Cannot combine with `--s-lang`.** | +| `--require-subs` | - | Required subtitle langs; keeps **all** subs only if these exist. **Cannot combine with `--s-lang`.** | | `-fs`, `--forced-subs` | off | Include forced subtitle tracks. | | `--exact-lang` | off | Exact matching only: `-l es-419` matches `es-419`, not `es-ES`. | -| `--sub-format` | — | Output subtitle format (`SRT`/`srt`, `VTT`/`webvtt`, `ASS`/`ssa`, `TTML`, `SMI`, …), or `original` to keep the source format. | +| `--sub-format` | - | Output subtitle format (`SRT`/`srt`, `VTT`/`webvtt`, `ASS`/`ssa`, `TTML`, `SMI`, ...), or `original` to keep the source format. | The special language tokens `orig`, `all`, and `best` are honoured everywhere a language is expected. @@ -175,7 +175,7 @@ Keep only certain track types, or skip certain track types. Attachments are alwa | `--no-cache` | off | Bypass the title cache. | | `--reset-cache` | off | Clear the title cache. | | `--list` | off | List available/would-be-downloaded tracks; do not download. | -| `--slow` | — | Inter-title delay. Bare `--slow` = 60-120s; `--slow 20-40` = custom range (minimum 20s). | +| `--slow` | - | Inter-title delay. Bare `--slow` = 60-120s; `--slow 20-40` = custom range (minimum 20s). | !!! warning "Some flags cannot be combined" `--require-subs` and `--s-lang`; `--select-titles` and `--wanted`; `--worst` requires `--quality`; `--vbitrate` and `--vbitrate-range` (and the audio equivalents) are mutually exclusive. @@ -200,8 +200,8 @@ The query syntax is defined per service. Results are printed as a tree of titles !!! example ```shell - unshackle search NF "The Office" - unshackle search -p myprofile NF "breaking bad" + unshackle search EXAMPLE "My Show" + unshackle search -p myprofile EXAMPLE "Another Show" unshackle search --proxy nordvpn:ca SERVICE "query" ``` @@ -306,7 +306,7 @@ Clear an environment directory. The directory is emptied and recreated, and the unshackle env check unshackle env info unshackle env clear cache - unshackle env clear cache NF # one service only + unshackle env clear cache EXAMPLE # one service only unshackle env clear temp ``` @@ -356,10 +356,10 @@ Create service tables on vaults that use tables, for every installed service, wh !!! example ```shell unshackle kv copy main backup1 backup2 - unshackle kv copy main backup -s NF + unshackle kv copy main backup -s EXAMPLE unshackle kv sync main mysql_vault - unshackle kv add keys.txt NF main backup - unshackle kv search 0123456789abcdef0123456789abcdef -s NF -v main + unshackle kv add keys.txt EXAMPLE main backup + unshackle kv search 0123456789abcdef0123456789abcdef -s EXAMPLE -v main unshackle kv prepare main mysql_vault ``` @@ -521,7 +521,7 @@ Losslessly set the video range flag to full or limited at the bitstream level. | Option | Default | Description | |---|---|---| -| `--full` / `--limited` | — | Full (0-255) or limited (16-235) range. | +| `--full` / `--limited` | - | Full (0-255) or limited (16-235) range. | | `-p`, `--preview` | off | Preview instead of writing a file. | ### `util test PATH` diff --git a/docs/guide/downloading.md b/docs/guide/downloading.md index 1fd25e67..d94ff670 100644 --- a/docs/guide/downloading.md +++ b/docs/guide/downloading.md @@ -26,23 +26,23 @@ unshackle dl [OPTIONS] SERVICE [SERVICE ARGUMENTS] - `unshackle`: the root command. - `dl`: the download command. **All of the options on this page are parsed here**, so they go *before* the service tag. -- `SERVICE`: a service tag such as `NF`, `DSNP`, `AMZN`. Service tags are - case-insensitive and honour each service's aliases (for example `disney+` resolves to +- `SERVICE`: a service tag such as `EXAMPLE1`, `EXAMPLE2`, `EXAMPLE3`. Service tags are + case-insensitive and honour each service's aliases (for example `example+` resolves to its real tag). - `SERVICE ARGUMENTS`: usually a title ID or URL. This positional argument belongs to the **service**, not to `dl`, so it comes *after* the tag. ```shell title="A minimal download" -unshackle dl NF 81234567 +unshackle dl EXAMPLE 81234567 ``` ```shell title="Flags belong to dl, the title belongs to the service" -unshackle dl -q 1080 -v H.265 -r HDR10 --lang en DSNP 'https://www.disneyplus.com/…' +unshackle dl -q 1080 -v H.265 -r HDR10 --lang en EXAMPLE 'https://www.example.com/...' ``` !!! tip "See what a service accepts" Because the positional argument is defined by each service, run - `unshackle dl SERVICE --help` (e.g. `unshackle dl NF --help`) to see that service's + `unshackle dl SERVICE --help` (e.g. `unshackle dl EXAMPLE --help`) to see that service's own argument and any service-specific options. ## The download flow @@ -75,11 +75,11 @@ downloaded. `--skip-dl` runs the license step but skips the actual segment downl list. Without it, unshackle picks the **best available** resolution. ```shell title="Single resolution" -unshackle dl -q 1080 NF 81234567 +unshackle dl -q 1080 EXAMPLE 81234567 ``` ```shell title="Multiple resolutions in one run" -unshackle dl -q 2160,1080,720 NF 81234567 +unshackle dl -q 2160,1080,720 EXAMPLE 81234567 ``` !!! note "16:9 canvas matching" @@ -97,7 +97,7 @@ By default a missing requested resolution is an error. Two flags change that: | `--worst` | Within the specified quality, pick the **lowest** bitrate rendition. **Requires `-q/--quality`.** | ```shell title="Never fail on a missing resolution" -unshackle dl -q 2160 --best-available AMZN 0ABC123 +unshackle dl -q 2160 --best-available EXAMPLE 0ABC123 ``` ## Video codec and color range @@ -117,9 +117,9 @@ either enum **names** or their **values**, comma-separated. | `AV1` | `AV1` | ```shell title="Either spelling works" -unshackle dl -v HEVC NF 81234567 -unshackle dl -v H.265 NF 81234567 -unshackle dl -v hevc,avc NF 81234567 +unshackle dl -v HEVC EXAMPLE 81234567 +unshackle dl -v H.265 EXAMPLE 81234567 +unshackle dl -v hevc,avc EXAMPLE 81234567 ``` ### Color range @@ -136,7 +136,7 @@ unshackle dl -v hevc,avc NF 81234567 | `HYBRID` | Fetch both an HDR10/HDR10+ base and a DV track, then merge them | ```shell title="Grab HDR10 and Dolby Vision in one run" -unshackle dl -q 2160 -r HDR10,DV DSNP '…' +unshackle dl -q 2160 -r HDR10,DV EXAMPLE '...' ``` !!! warning "HYBRID requires dovi_tool" @@ -215,7 +215,7 @@ Names, values, and a few aliases are accepted: `eac3` and `ddp` both resolve to and `vorbis` resolves to `OGG`. ```shell title="Prefer Dolby Digital Plus, fall back to AAC" -unshackle dl -a EC3,AAC NF 81234567 +unshackle dl -a EC3,AAC EXAMPLE 81234567 ``` ### Channels and Atmos @@ -225,7 +225,7 @@ unshackle dl -a EC3,AAC NF 81234567 - `-naa` / `--noatmos`: exclude Dolby Atmos audio tracks from selection. ```shell title="5.1 audio, no Atmos" -unshackle dl -c 5.1 --noatmos AMZN 0ABC123 +unshackle dl -c 5.1 --noatmos EXAMPLE 0ABC123 ``` ## Languages @@ -236,7 +236,7 @@ unshackle dl -c 5.1 --noatmos AMZN 0ABC123 `orig`, the title's original language. ```shell title="Original language plus English" -unshackle dl -l orig,en NF 81234567 +unshackle dl -l orig,en EXAMPLE 81234567 ``` The special token `orig` is resolved to the title's actual original language everywhere @@ -247,7 +247,7 @@ it is used. You can override each stream type independently: - `-al` / `--a-lang`: language for **audio only** (overrides `-l` for audio). ```shell title="English audio over the original video" -unshackle dl -al en NF 81234567 +unshackle dl -al en EXAMPLE 81234567 ``` ### Exact vs fuzzy matching @@ -269,7 +269,7 @@ matches only `es-419`, not `es-ES`. every available subtitle language is downloaded. ```shell title="Only English and Spanish subtitles" -unshackle dl -sl en,es NF 81234567 +unshackle dl -sl en,es EXAMPLE 81234567 ``` ### Requiring subtitles @@ -299,7 +299,7 @@ download on the presence of a specific subtitle track. | `original` | Keep the source format, no conversion | ```shell title="Convert subtitles to SRT" -unshackle dl --sub-format srt NF 81234567 +unshackle dl --sub-format srt EXAMPLE 81234567 ``` !!! note "SDH stripping happens by default" @@ -321,24 +321,24 @@ with `-` to exclude it. === "Whole seasons" ```shell - unshackle dl -w S01 NF 81234567 - unshackle dl -w S01-S03 NF 81234567 - unshackle dl -w S01,S03,S05 NF 81234567 + unshackle dl -w S01 EXAMPLE 81234567 + unshackle dl -w S01-S03 EXAMPLE 81234567 + unshackle dl -w S01,S03,S05 EXAMPLE 81234567 ``` === "Specific episodes" ```shell - unshackle dl -w S01E01 NF 81234567 - unshackle dl -w S01E01-S01E05 NF 81234567 - unshackle dl -w S01E01-S02E03 NF 81234567 + unshackle dl -w S01E01 EXAMPLE 81234567 + unshackle dl -w S01E01-S01E05 EXAMPLE 81234567 + unshackle dl -w S01E01-S02E03 EXAMPLE 81234567 ``` === "Exclusions" ```shell # Seasons 1 through 5, but not season 3 - unshackle dl -w S01-S05,-S03 NF 81234567 + unshackle dl -w S01-S05,-S03 EXAMPLE 81234567 ``` ### Other selection flags @@ -350,7 +350,7 @@ with `-` to exclude it. | `--list-titles` | List the titles that would be downloaded, then stop. | ```shell title="Grab just the newest episode" -unshackle dl --latest-episode NF 81234567 +unshackle dl --latest-episode EXAMPLE 81234567 ``` ## Including and excluding track types @@ -380,11 +380,11 @@ Additional track-type flags: than aborting the whole title. Video and audio failures remain fatal. ```shell title="Subtitles only" -unshackle dl -S -sl en NF 81234567 +unshackle dl -S -sl en EXAMPLE 81234567 ``` ```shell title="Everything except chapters" -unshackle dl -nc NF 81234567 +unshackle dl -nc EXAMPLE 81234567 ``` !!! note @@ -403,7 +403,7 @@ Before committing to a long download, inspect what unshackle *would* do: | `--skip-dl` | Skip downloading but still acquire the decryption keys. | ```shell title="See the track selection without downloading" -unshackle dl -q 1080 -v H.265 -r HDR10 --list NF 81234567 +unshackle dl -q 1080 -v H.265 -r HDR10 --list EXAMPLE 81234567 ``` ## Output and muxing @@ -416,7 +416,7 @@ unshackle dl -q 1080 -v H.265 -r HDR10 --list NF 81234567 - `--no-source`: remove the service source tag from the filename and path. ```shell title="Send this download somewhere specific" -unshackle dl -o ~/Videos/incoming NF 81234567 +unshackle dl -o ~/Videos/incoming EXAMPLE 81234567 ``` ### Muxing behaviour @@ -426,7 +426,7 @@ flags change how the output is assembled: | Flag | Behaviour | Default source | | --- | --- | --- | -| `--no-mux` | Do not mux; keep the individual track files. | — | +| `--no-mux` | Do not mux; keep the individual track files. | - | | `--split-audio` | Write a separate output file per audio codec instead of merging all audio. | config `muxing.merge_audio` (on) | | `--merge-video` | Mux all selected video tracks into one file. | config `muxing.merge_video` (off) | @@ -436,7 +436,7 @@ flags change how the output is assembled: - `--repack`: add a `REPACK` tag to the output filename. ```shell title="Custom group tag and a REPACK label" -unshackle dl --tag MYGRP --repack NF 81234567 +unshackle dl --tag MYGRP --repack EXAMPLE 81234567 ``` ## Proxies @@ -445,9 +445,9 @@ unshackle dl --tag MYGRP --repack NF 81234567 configured proxy providers), or a `provider:region` form. ```shell title="Proxy forms" -unshackle dl --proxy us NF 81234567 -unshackle dl --proxy nordvpn:ca NF 81234567 -unshackle dl --proxy 'http://user:pass@host:8080' NF 81234567 +unshackle dl --proxy us EXAMPLE 81234567 +unshackle dl --proxy nordvpn:ca EXAMPLE 81234567 +unshackle dl --proxy 'http://user:pass@host:8080' EXAMPLE 81234567 ``` Two related flags: @@ -468,11 +468,11 @@ Two related flags: | `--reset-cache` | Clear the title cache before fetching. | ```shell title="Two tracks at a time, eight threads each" -unshackle dl --downloads 2 --workers 8 NF 81234567 +unshackle dl --downloads 2 --workers 8 EXAMPLE 81234567 ``` ```shell title="Space out a season download" -unshackle dl -w S01 --slow 30-60 NF 81234567 +unshackle dl -w S01 --slow 30-60 EXAMPLE 81234567 ``` ## Keys, vaults, and export @@ -484,7 +484,7 @@ a key when the vault misses. You can force one side or the other: - `--vaults-only`: only use key vaults (never license via the CDM); a missing key fails. ```shell title="Fetch keys only, no download" -unshackle dl --skip-dl NF 81234567 +unshackle dl --skip-dl EXAMPLE 81234567 ``` `--export` writes a JSON file, into the configured exports directory, containing track @@ -492,7 +492,7 @@ info and the acquired content keys for each title. This is the format consumed b `unshackle import` to reconstruct a download later. ```shell title="Export track info and keys" -unshackle dl --skip-dl --export NF 81234567 +unshackle dl --skip-dl --export EXAMPLE 81234567 ``` !!! note "Region is recorded only with a proxy" @@ -509,10 +509,10 @@ tagging and naming: | `--tmdb` | `--tmdb 27205` | Use this TMDB ID instead of automatic lookup. | | `--imdb` | `--imdb tt1375666` | Use this IMDb ID. | | `--animeapi` | `--animeapi mal:12345` | Resolve via AnimeAPI (`mal:`/`anilist:` prefix; defaults to MAL). | -| `--enrich` | — | Override the show title and year from an external source. **Requires** one of `--tmdb`, `--imdb`, or `--animeapi`. | +| `--enrich` | - | Override the show title and year from an external source. **Requires** one of `--tmdb`, `--imdb`, or `--animeapi`. | ```shell title="Force the right IMDb match and enrich the title" -unshackle dl --imdb tt1375666 --enrich NF 81234567 +unshackle dl --imdb tt1375666 --enrich EXAMPLE 81234567 ``` ## Configuration defaults diff --git a/docs/guide/drm-and-cdm.md b/docs/guide/drm-and-cdm.md index 4b8438b3..99d851c0 100644 --- a/docs/guide/drm-and-cdm.md +++ b/docs/guide/drm-and-cdm.md @@ -48,7 +48,7 @@ license-server handshake; the rest are special cases. |--------|-------------|--------------|------------| | **Widevine** (Google) | `.wvd` | CDM challenge → license server → CDM parses → keys | shaka-packager / mp4decrypt | | **PlayReady** (Microsoft) | `.prd` | CDM challenge (WRM header) → license server → CDM parses → keys | shaka-packager / mp4decrypt | -| **ClearKey (CENC)**, W3C EME `org.w3.clearkey` | none | JSON `{"kids":[…],"type":"temporary"}` → license server returns a JWK Set. No CDM. | shaka-packager / mp4decrypt | +| **ClearKey (CENC)**, W3C EME `org.w3.clearkey` | none | JSON `{"kids":[...],"type":"temporary"}` → license server returns a JWK Set. No CDM. | shaka-packager / mp4decrypt | | **ClearKey (HLS)**, AES-128 `EXT-X-KEY` | none | Key fetched directly from a URI or `data:` URL. No CDM, no challenge. | in-process AES-CBC | | **MonaLisa** (proprietary, WASM) | `.mld` | Ticket from the service API; keys extracted locally via WASM. No license server. | ML-Worker binary + AES-ECB, per segment | @@ -201,15 +201,15 @@ fallback: ```yaml title="unshackle.yaml" cdm: default: nexus_6p_26830_l3 - AMZN: chromecdm_l3 - NF: sm_g935f_l1 + EXAMPLE1: chromecdm_l3 + EXAMPLE2: sm_g935f_l1 ``` Lookups are case-insensitive. On the command line you can override the configured device for a single run: ```console -$ unshackle dl -w chromecdm_l3 AMZN B0ABCDEF +$ unshackle dl -w chromecdm_l3 EXAMPLE1 B0ABCDEF ``` !!! tip "The device name is the file name" @@ -451,7 +451,7 @@ returns the keys as a JWK Set rather than a DRM-wrapped blob. unshackle builds a EME request of the form: ```json -{"kids": ["<base64url-kid>", …], "type": "temporary"} +{"kids": ["<base64url-kid>", ...], "type": "temporary"} ``` It sends this to the manifest's license URL (or to whatever a service's ClearKey diff --git a/docs/guide/output-and-naming.md b/docs/guide/output-and-naming.md index 3c8665d1..4514108b 100644 --- a/docs/guide/output-and-naming.md +++ b/docs/guide/output-and-naming.md @@ -20,8 +20,8 @@ When a download finishes muxing, unshackle builds the final path from three piec !!! example "A typical episode path" ```text /home/user/Downloads/ - └─ Show Name S01 1080p AMZN WEB-DL DDP5.1 H.264/ - └─ Show.Name.S01E01.Pilot.1080p.AMZN.WEB-DL.DDP5.1.H.264-TAG.mkv + └─ Show Name S01 1080p EXAMPLE WEB-DL DDP5.1 H.264/ + └─ Show.Name.S01E01.Pilot.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG.mkv ``` The folder name comes from the `series` folder template and the file from the `series` output template. @@ -51,7 +51,7 @@ The recognized kinds are: | `movies` | Movies | *(required for movie downloads to be named, no built-in default)* | | `series` | TV episodes | *(required for episode downloads to be named)* | | `songs` | Music tracks | `"{track_number}. {title}"` | -| `albums` | Album **folders** only (see below) | — | +| `albums` | Album **folders** only (see below) | - | !!! note `movies` and `series` have no hardcoded filename fallback. If you download those title types, define their templates. `songs` falls back to `"{track_number}. {title}"` when omitted. @@ -80,7 +80,7 @@ unshackle looks at the characters you place *between* variables to decide the fi movies: "{title}.{year}.{quality}.{source}.WEB-DL.{audio_full}.{video}-{tag}" ``` ```text - The.Movie.2024.1080p.NF.WEB-DL.DDP5.1.H.264-TAG.mkv + The.Movie.2024.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG.mkv ``` === "Readable style (spaces)" @@ -90,7 +90,7 @@ unshackle looks at the characters you place *between* variables to decide the fi movies: "{title} ({year}) {quality} {source} WEB-DL {audio_full} {video}" ``` ```text - The Movie (2024) 1080p NF WEB-DL DDP5.1 H.264.mkv + The Movie (2024) 1080p EXAMPLE WEB-DL DDP5.1 H.264.mkv ``` ## Template variables @@ -103,7 +103,7 @@ Every variable below is valid in both output and folder templates. Values are de |---|---|---| | `title` | Title name (movie/show/song name; `$` is rendered as `S`) | `The Show` | | `year` | Release year | `2024` | -| `source` | Service tag / class name (empty with `--no-source`) | `NF` | +| `source` | Service tag / class name (empty with `--no-source`) | `EXAMPLE` | | `quality` | Resolution with scan suffix | `1080p`, `2160p`, `576i` | | `resolution` | Resolution number only | `1080` | | `video` | Video codec | `H.264`, `H.265` | @@ -249,13 +249,13 @@ tag_imdb_tmdb: true You can override the tag per run without editing config: ```bash -unshackle dl --tag OTHERGROUP AMZN "..." +unshackle dl --tag OTHERGROUP EXAMPLE "..." ``` The `--repack` flag adds the `REPACK` marker (surfaced through the `{repack}` variable) to the filename for that run: ```bash -unshackle dl --repack NF "..." +unshackle dl --repack EXAMPLE "..." ``` !!! note "Empty tags collapse cleanly" @@ -349,7 +349,7 @@ The fallback supports two placeholders: `{i}` (chapter number, starting at 1) an ``` ```text The Movie (2024)/ - └─ The.Movie.2024.1080p.NF.WEB-DL.DDP5.1.Atmos.H.265.HDR-TAG.mkv + └─ The.Movie.2024.1080p.EXAMPLE.WEB-DL.DDP5.1.Atmos.H.265.HDR-TAG.mkv ``` === "TV series" @@ -362,7 +362,7 @@ The fallback supports two placeholders: `{i}` (chapter number, starting at 1) an ``` ```text The Show (2023)/Season S02/ - └─ The.Show.S02E04.The.Reckoning.2160p.AMZN.WEB-DL.DDP5.1.H.265-TAG.mkv + └─ The.Show.S02E04.The.Reckoning.2160p.EXAMPLE.WEB-DL.DDP5.1.H.265-TAG.mkv ``` === "Music" diff --git a/docs/guide/proxies-and-vpn.md b/docs/guide/proxies-and-vpn.md index fd5505ec..799ed548 100644 --- a/docs/guide/proxies-and-vpn.md +++ b/docs/guide/proxies-and-vpn.md @@ -31,15 +31,15 @@ Proxying is controlled at download time by three options on the `dl` command: | `--no-proxy-download` | Use the proxy for the manifest, licence, and authentication, but bypass it for the **segment downloads** themselves. | ```shell title="Explicit proxy URI" -unshackle dl --proxy http://user:pass@1.2.3.4:8080 NF 81234567 +unshackle dl --proxy http://user:pass@1.2.3.4:8080 EXAMPLE 81234567 ``` ```shell title="Resolve a US server from your configured providers" -unshackle dl --proxy us NF 81234567 +unshackle dl --proxy us EXAMPLE 81234567 ``` ```shell title="Target a specific provider" -unshackle dl --proxy nordvpn:us NF 81234567 +unshackle dl --proxy nordvpn:us EXAMPLE 81234567 ``` !!! tip "Set a default proxy in config" @@ -55,7 +55,7 @@ proxy where it matters (passing the geo-check for the manifest and the licence) pulling the actual audio and video segments over your normal connection for full speed. ```shell title="Proxy the manifest and licence, download segments direct" -unshackle dl --proxy gb --no-proxy-download ITV 10a1234 +unshackle dl --proxy gb --no-proxy-download EXAMPLE 10a1234 ``` !!! warning "When this is unsafe" @@ -78,8 +78,8 @@ flowchart TD D -- no --> F["Ask every provider in order,<br/>first match wins"] ``` -1. **An explicit URI.** Anything shaped like `http://…`, `https://…`, or a `socks…` - URI is used exactly as given. unshackle logs `Using explicit Proxy: …` and does no +1. **An explicit URI.** Anything shaped like `http://...`, `https://...`, or a `socks...` + URI is used exactly as given. unshackle logs `Using explicit Proxy: ...` and does no lookup. 2. **A provider-prefixed query**: `provider:query`, for example `nordvpn:us` or `gluetun:windscribe:us`. unshackle finds the provider whose name matches the prefix @@ -154,7 +154,7 @@ proxy_providers: malformed URI is rejected up front rather than failing mid-download. ```shell -unshackle dl --proxy de2 NF 81234567 +unshackle dl --proxy de2 EXAMPLE 81234567 ``` ## Gluetun @@ -187,11 +187,11 @@ gluetun:<vpn-provider>:<region> ``` ```shell title="A Windscribe US exit via Gluetun" -unshackle dl --proxy gluetun:windscribe:us NF 81234567 +unshackle dl --proxy gluetun:windscribe:us EXAMPLE 81234567 ``` ```shell title="A NordVPN Germany exit via Gluetun" -unshackle dl --proxy gluetun:nordvpn:de NF 81234567 +unshackle dl --proxy gluetun:nordvpn:de EXAMPLE 81234567 ``` After unshackle strips the leading `gluetun:`, the provider receives exactly @@ -437,7 +437,7 @@ The returned proxy is HTTPS on **port 89** (NordVPN disabled its plain-HTTP prox port 80 in 2021). ```shell -unshackle dl --proxy nordvpn:us:seattle NF 81234567 +unshackle dl --proxy nordvpn:us:seattle EXAMPLE 81234567 ``` ## Proton VPN @@ -521,7 +521,7 @@ that binary is found on your `PATH`. If you query Hola but the binary is missing provider raises an error telling you to install it. ```shell title="Install hola-proxy, then just query a country" -unshackle dl --proxy hola:us NF 81234567 +unshackle dl --proxy hola:us EXAMPLE 81234567 ``` **Query forms:** a two-letter country code (e.g. `us`, `gb`). unshackle asks `hola-proxy` diff --git a/docs/guide/subtitles.md b/docs/guide/subtitles.md index ab39249a..ec505170 100644 --- a/docs/guide/subtitles.md +++ b/docs/guide/subtitles.md @@ -82,7 +82,7 @@ By default (`--s-lang all`) unshackle keeps every subtitle language the service To narrow it down, pass a comma-separated list of language tags: ```shell title="English and Spanish subtitles only" -unshackle dl --s-lang en,es NF 81234567 +unshackle dl --s-lang en,es EXAMPLE 81234567 ``` Language matching is fuzzy by default: `en` will match `en-US`, `en-GB`, and similar @@ -93,7 +93,7 @@ For strict matching, where `es-419` matches only `es-419` and never `es-ES`, add `--exact-lang`: ```shell title="Only Latin-American Spanish, no other Spanish variants" -unshackle dl --s-lang es-419 --exact-lang DSNP 'https://www.disneyplus.com/…' +unshackle dl --s-lang es-419 --exact-lang EXAMPLE 'https://www.example.com/...' ``` ### Requiring languages before downloading @@ -104,7 +104,7 @@ errors out. This is useful for batch jobs where a title without your must-have l should fail loudly rather than silently produce a partial result. ```shell title="Fail unless both English and French subs exist, then grab everything" -unshackle dl --require-subs en,fr AMZN B0ABCDEFGH +unshackle dl --require-subs en,fr EXAMPLE B0ABCDEFGH ``` !!! warning "`--require-subs` and `--s-lang` are mutually exclusive" @@ -118,7 +118,7 @@ to display even when you are watching in your own language. They are **excluded default**. Add `-fs` / `--forced-subs` to include them: ```shell title="Include forced tracks alongside full subtitles" -unshackle dl --s-lang en --forced-subs NF 81234567 +unshackle dl --s-lang en --forced-subs EXAMPLE 81234567 ``` ## Track types: forced, SDH, and CC @@ -181,7 +181,7 @@ TTML to WebVTT for muxing, as noted above). To force a specific output format, u `--sub-format`: ```shell title="Convert every subtitle to SRT" -unshackle dl --s-lang en,es --sub-format srt NF 81234567 +unshackle dl --s-lang en,es --sub-format srt EXAMPLE 81234567 ``` `--sub-format` accepts the common format names and aliases, including `srt`, `vtt`, @@ -189,7 +189,7 @@ unshackle dl --s-lang en,es --sub-format srt NF 81234567 all conversion: ```shell title="Keep whatever the service delivered" -unshackle dl --sub-format original DSNP 'https://www.disneyplus.com/…' +unshackle dl --sub-format original EXAMPLE 'https://www.example.com/...' ``` Conversion only runs when the source format differs from the target, so @@ -218,9 +218,9 @@ formats involved: | Backend | Best at | Requires | | --- | --- | --- | | `subtitleedit` | Highest fidelity; preserves positioning and italics | SubtitleEdit installed | -| `subby` | Native SRT output with clean-up | — | -| `pysubs2` | Best fidelity for styled SSA/ASS | — (pure Python, always available) | -| `pycaption` | Last-resort fallback; flattens styling | — | +| `subby` | Native SRT output with clean-up | - | +| `pysubs2` | Best fidelity for styled SSA/ASS | - (pure Python, always available) | +| `pycaption` | Last-resort fallback; flattens styling | - | With `conversion_method: auto`, unshackle ranks these automatically per conversion. Setting it to a specific value pins that backend as the first choice, falling back to diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md index 6f79df58..383850e3 100644 --- a/docs/guide/troubleshooting.md +++ b/docs/guide/troubleshooting.md @@ -64,7 +64,7 @@ The debug log is created during a `dl` run (and by [`import`](cli-reference.md#i <logs directory>/unshackle_debug_<service>_<timestamp>.jsonl ``` -For example, `unshackle_debug_NETFLIX_20260703-142530.jsonl`. The `<service>` tag and the timestamp mean each download gets its own file, so runs never overwrite each other. +For example, `unshackle_debug_EXAMPLE_20260703-142530.jsonl`. The `<service>` tag and the timestamp mean each download gets its own file, so runs never overwrite each other. !!! tip "Find your logs directory" The logs directory defaults to a `logs` folder inside the package data directory, but it can be relocated with the `directories.logs` config key. To see the exact resolved path on your machine, run [`unshackle env info`](#env-info) and read the **Directories** table. @@ -99,16 +99,16 @@ Because each line is independent JSON, `jq` is the natural tool: ```console # Show only error-level entries -jq 'select(.level == "ERROR")' unshackle_debug_NETFLIX_20260703-142530.jsonl +jq 'select(.level == "ERROR")' unshackle_debug_EXAMPLE_20260703-142530.jsonl # List every operation and whether it succeeded -jq -c '{operation, success}' unshackle_debug_NETFLIX_20260703-142530.jsonl +jq -c '{operation, success}' unshackle_debug_EXAMPLE_20260703-142530.jsonl # Pull the traceback out of the first error -jq -r 'select(.error) | .error.traceback[]' unshackle_debug_NETFLIX_20260703-142530.jsonl +jq -r 'select(.error) | .error.traceback[]' unshackle_debug_EXAMPLE_20260703-142530.jsonl # Show only DRM / licensing operations -jq 'select(.operation | startswith("drm_"))' unshackle_debug_NETFLIX_20260703-142530.jsonl +jq 'select(.operation | startswith("drm_"))' unshackle_debug_EXAMPLE_20260703-142530.jsonl ``` The session's binary-tool versions (Shaka Packager, mp4decrypt, mkvmerge, FFmpeg, FFprobe) are logged near the start of a `dl` run, which is often the fastest way to confirm a muxing or decryption failure is really a wrong-version problem. @@ -168,7 +168,7 @@ unshackle redacts logged strings so that debug output is safe to share. Three pa Path redaction is controlled by the `redact_paths` config key, which is **on by default**: ```yaml -redact_paths: true # default — mask local base directories in logged paths +redact_paths: true # default - mask local base directories in logged paths ``` Set `redact_paths: false` if you are debugging a path problem locally and want to see full, unmasked paths in the console and logs. Secret and URL redaction in the JSON debug log are always applied and are not affected by this toggle. @@ -223,7 +223,7 @@ Stale cache or leftover temp files can cause confusing behaviour (an old cached ```console unshackle env clear cache # clear the whole cache directory -unshackle env clear cache NETFLIX # clear just one service's cache subdirectory +unshackle env clear cache EXAMPLE # clear just one service's cache subdirectory unshackle env clear temp # clear the temp working directory ``` @@ -287,7 +287,7 @@ The debug log is tuned for **developers troubleshooting pipeline flow** (maximum ```console # Read the pipeline's high-level flow, one line per stage - jq 'select(.level == "INFO")' unshackle_debug_NETFLIX_20260703-142530.jsonl + jq 'select(.level == "INFO")' unshackle_debug_EXAMPLE_20260703-142530.jsonl ``` !!! warning "No raw dumps" diff --git a/docs/guide/vaults.md b/docs/guide/vaults.md index a78a7c1a..f62926b8 100644 --- a/docs/guide/vaults.md +++ b/docs/guide/vaults.md @@ -13,7 +13,7 @@ Every key is stored 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. -- **Service**: the canonical service tag (for example `DSNP`, not `DisneyPlus`) that namespaces the key. Each service gets its own logical table. +- **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. @@ -242,7 +242,7 @@ $ unshackle kv --help Add `KID:KEY` pairs to one or more vaults for a given service. The file must contain one pair per line as `HEX:HEX` (32 hex characters each); lines that do not match are ignored. ```console -$ unshackle kv add keys.txt DSNP local shared +$ unshackle kv add keys.txt EXAMPLE local shared ``` ```text title="keys.txt" @@ -250,7 +250,7 @@ $ unshackle kv add keys.txt DSNP local shared 0e7cc1a4e2f9411db0f24a3fbb8f8b21:00112233445566778899aabbccddeeff ``` -The service argument (`DSNP` above) is normalized to its canonical tag automatically. Keys that already exist in a vault are reported as skipped. +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 diff --git a/docs/index.md b/docs/index.md index 2ab8380a..b71f6f46 100644 --- a/docs/index.md +++ b/docs/index.md @@ -71,11 +71,11 @@ Every download is three parts: the `dl` command, a **service tag**, and the service's title argument (a URL, ID, or slug). ```shell title="Download a 1080p title with English audio and subtitles" -unshackle dl -q 1080 -l en NF 81234567 +unshackle dl -q 1080 -l en EXAMPLE 81234567 ``` The `dl` command carries all the quality, language, and output flags; the service -tag (`NF` above) selects which streaming service to talk to, and the value after it +tag (`EXAMPLE` above) selects which streaming service to talk to, and the value after it is passed to that service. See the [Quickstart](getting-started/quickstart.md) to go from a fresh install to your first file. diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 4f34e023..04f6c8d6 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -79,11 +79,11 @@ directories: | `data` | path | `unshackle/` | ❌ protected | Base for the data subdirectories above. | | `core_dir` | path | `unshackle/core` | ❌ protected | Package core. | | `namespace_dir` | path | `unshackle/` | ❌ protected | Package root. | -| `app_dirs` | — | `AppDirs("unshackle", False)` | ❌ protected | Internal AppDirs instance. | +| `app_dirs` | - | `AppDirs("unshackle", False)` | ❌ protected | Internal AppDirs instance. | !!! note "The `services` directory is special" `services` may be a **list**, and each entry can be either a local directory or a - **repository spec**: a git URL (`https://…`, `ssh://…`, `git@…`, or anything ending in + **repository spec**: a git URL (`https://...`, `ssh://...`, `git@...`, or anything ending in `.git`) or `owner/repo` shorthand. Repo specs are cloned and updated automatically; plain paths are used as-is. **List order is priority**: the first source to define a service tag wins, so put your own local service directories **last** to make them fallbacks. @@ -142,8 +142,8 @@ runtime. - **Type:** `dict` keyed by service tag  ·  **Default:** `{}` -Per-service configuration, keyed by the canonical service tag (for example `AMZN`, `NF`, -`DSNP`). unshackle reads several well-known sub-keys out of each service's block: +Per-service configuration, keyed by the canonical service tag (for example `EXAMPLE1`, `EXAMPLE2`, +`EXAMPLE3`). unshackle reads several well-known sub-keys out of each service's block: - **`proxy_map`**: a dict mapping `"provider:query"` or `"query"` to a proxy value, used to auto-select a proxy for that service. @@ -163,11 +163,11 @@ the service as `self.config`. ```yaml services: - AMZN: + EXAMPLE1: proxy_map: default: us title_map: - "The Grand Tour: The Movie": "The Grand Tour Presents The Movie" + "My Show: The Movie": "My Show Presents The Movie" ``` ### `credentials` @@ -180,8 +180,8 @@ objects; the credential's SHA-1 is also used as an account hash for cache keys. ```yaml credentials: - NF: user@example.com:hunter2 - AMZN: + EXAMPLE2: user@example.com:hunter2 + EXAMPLE1: - primary@example.com:pw1 - secondary@example.com:pw2 ``` @@ -230,13 +230,46 @@ CLI commands that run against the remote server. - **Type:** `dict`  ·  **Default:** `{}` -Configuration for the `serve` command (the built-in REST API server). +Configuration for the `serve` command (the built-in REST API server). The full server guide +is the [REST API](../dev/rest-api/index.md) section; these are the config keys. + +| Sub-key | Type | Default | Description | +|---------|------|---------|-------------| +| `api_secret` | str | *(unset)* | Master secret accepted in the `X-Secret-Key` header. | +| `users` | dict | `{}` | Per-user API keys and their allowlists (see below). | +| `services` | list | *(unset)* | Global service allowlist. Omit to allow all. | +| `remote_only` | bool | `false` | Run without a local `output_template` (CORS/Cloudflare-friendly). | +| `session_ttl` | int (s) | `300` | Lifetime of an interactive auth session. | +| `max_sessions` | int | `100` | Maximum concurrent sessions. | +| `history_limit` | int | `100` | How many finished jobs to retain in history. | +| `compression_level` | int | `1` | gzip level for responses. | +| `cdm_overrides` | dict | *(unset)* | Allowed per-request CDM overrides. | +| `allow_job_credentials` | bool | `false` | Permit clients to supply credentials per job. | +| `devices` | list | *(auto)* | Widevine devices offered; auto-filled from `directories.wvds`. | +| `playready_devices` | list | *(auto)* | PlayReady devices; auto-filled from `directories.prds`. | +| `country` / `locations` | str / dict | *(unset)* | Advertised region metadata. | + +Each entry under `users` is keyed by that user's API key and may set its own `services`, +`devices`, and `playready_devices` allowlists, narrowing the global ones. + +```yaml +serve: + api_secret: change-me + remote_only: true + services: [EXAMPLE1, EXAMPLE2] + # server-wide download defaults (same keys as the `dl:` block) + downloads: 3 + best_available: true + users: + a1b2c3d4: # this user's API key + services: [EXAMPLE1] # may only use EXAMPLE1 +``` !!! tip "Declaring `dl` defaults under `serve`" - You can nest `dl` flags (`downloads`, `workers`, `best_available`, and so on) inside - `serve`. The point is to raise concurrency and behaviour defaults **server-wide** (every - request the server handles picks them up) without having to change each client call - individually. + You can set any [`dl`](#dl) flag key (`downloads`, `workers`, `best_available`, and so on) + directly inside `serve`. The point is to raise concurrency and behaviour defaults + **server-wide** (every request the server handles picks them up) without having to change + each client call individually. --- @@ -246,15 +279,105 @@ Configuration for the `serve` command (the built-in REST API server). - **Type:** `dict`  ·  **Default:** `{}` -Default options forwarded to the `dl` command. Anything you would otherwise pass as a `dl` -flag can be given a persistent default here. See [Downloading](../guide/downloading.md) for -the flags themselves. +Persistent defaults for the `dl` command. This block is used as Click's `default_map`, so +**any** `dl` flag can be given a default here and it applies to every download without you +typing it. See [Downloading](../guide/downloading.md) and the +[CLI Reference](../guide/cli-reference.md) for what each flag does. + +**Key naming.** A key is the flag's long name with dashes replaced by underscores +(`--best-available` → `best_available`, `--sub-format` → `sub_format`). A few keys are named +after the flag's internal destination rather than its visible name; set these exact keys: + +| Flag | Config key | +|------|-----------| +| `-r` / `--range` | `range` | +| `--list` | `list` | +| `--tmdb` | `tmdb_id` | +| `--imdb` | `imdb_id` | +| `--animeapi` | `animeapi_id` | +| `-naa` / `--noatmos` | `no_atmos` | +| `-o` / `--output` | `output_dir` | +| `--cdm-only` / `--vaults-only` | `cdm_only` | + +**Precedence** (highest first): an explicit CLI flag or environment variable → a per-service +[`services.<TAG>.dl`](#services-auth) override → this global `dl:` block → the built-in +default. In other words, config only fills in options you did not set on the command line. + +Common keys (a useful subset; every `dl` flag works): + +| Key | Type | Default | Sets | +|-----|------|---------|------| +| `lang` | list | `["orig"]` | Video/audio language(s); `orig` = original language. | +| `a_lang` / `v_lang` | list | `[]` | Audio- / video-only language override. | +| `s_lang` | list | `["all"]` | Subtitle language(s). | +| `quality` | list | `[]` (best) | Resolution(s), e.g. `[1080]`. | +| `vcodec` | list | `[]` (any) | Video codec(s), e.g. `[H265]`. | +| `acodec` | list | `[]` (any) | Audio codec(s), e.g. `[EC3]`. | +| `range` | list | `["SDR"]` | Colour range(s): `SDR`, `HDR10`, `HDR10+`, `DV`. | +| `channels` | float | *(unset)* | Audio channels, e.g. `6` for 5.1. | +| `sub_format` | str | *(unset)* | Convert subtitles to this format (`srt`, `vtt`, `original`, ...). | +| `forced_subs` | bool | `false` | Include forced subtitle tracks. | +| `no_subs` / `no_audio` / `no_chapters` | bool | `false` | Skip that track type. | +| `downloads` | int | `1` | Tracks downloaded concurrently. | +| `workers` | int | *(downloader default)* | Threads per track. | +| `slow` | str | *(unset)* | Inter-title delay, e.g. `"20-40"`. | +| `best_available` | bool | `false` | Fall back to best quality if the request is unavailable. | +| `proxy` | str | *(unset)* | Default proxy URI or 2-letter country. | +| `no_folder` | bool | `false` | Do not create a per-title folder. | + +```yaml title="Global download defaults" +dl: + lang: [en] + quality: [1080] + range: [HDR10] + vcodec: [H265] + sub_format: srt + downloads: 2 +``` + +!!! tip "Per-service overrides" + Nest a `dl` block under a service in [`services`](#services-auth) to override these defaults + for just that service. It uses the same keys and wins over the global `dl:` block (but still + loses to an explicit CLI flag). + + ```yaml + dl: + lang: [en] # global default + services: + EXAMPLE1: + dl: + lang: [en, ja] # EXAMPLE1 downloads both; everything else stays English + range: [DV] + ``` + +!!! note "`serve` uses these same keys" + The [`serve`](#serve) block accepts the same `dl` option keys at its top level to set + server-wide download defaults for every request it handles. ### `subtitle` - **Type:** `dict`  ·  **Default:** `{}` -Subtitle handling options. See [Subtitles](../guide/subtitles.md). +Subtitle handling: SDH stripping, format conversion, and whether subtitles are muxed in or +written as sidecar files. See [Subtitles](../guide/subtitles.md) for the full guide. + +| Sub-key | Type | Default | Description | +|---------|------|---------|-------------| +| `strip_sdh` | bool | `true` | Strip SDH/CC cues (hearing-impaired annotations) into a clean track. | +| `sdh_method` | str | `"auto"` | SDH-stripping backend to use. | +| `preserve_formatting` | bool | `true` | Keep styling/positioning when converting or stripping. | +| `convert_before_strip` | bool | `true` | Convert to the working format before stripping SDH. | +| `conversion_method` | str | `"auto"` | Subtitle conversion backend. | +| `preferred_conversion_method` | str | *(unset)* | Preferred converter when several are available. | +| `output_mode` | str | `"mux"` | `mux` embeds subtitles in the MKV; `sidecar` writes separate files. | +| `sidecar_format` | str | `"srt"` | Format for sidecar files when `output_mode: sidecar`. | + +```yaml +subtitle: + strip_sdh: true + output_mode: sidecar + sidecar_format: srt +``` ### `audio` @@ -284,6 +407,21 @@ audio: Matroska (MKV) muxing options. +| Sub-key | Type | Default | Description | +|---------|------|---------|-------------| +| `set_title` | bool | `true` | Write a human-readable title into the MKV container. | +| `default_language` | dict | `{}` | Force which language is flagged *default* per track type, e.g. `{audio: en, subtitle: en}`. | +| `merge_audio` | bool | `true` | Merge all audio into one file. `--split-audio` on the CLI flips this off. | +| `merge_video` | bool | `false` | Merge all video tracks into one file. `--merge-video` on the CLI flips this on. | + +```yaml +muxing: + set_title: true + default_language: + audio: en + subtitle: en +``` + ### `language_tags` - **Type:** `dict`  ·  **Default:** `{}` @@ -381,7 +519,7 @@ then `default`. ```yaml cdm: default: chromecdm_l3 - NF: android_l1 + EXAMPLE2: android_l1 ``` A value may also be a **nested dict** for advanced selection by quality, DRM system, or @@ -389,7 +527,7 @@ profile: ```yaml cdm: - AMZN: + EXAMPLE1: ">=1080": android_l1 # by track height "<1080": chromecdm_l3 widevine: android_l1 # by DRM system @@ -515,13 +653,13 @@ the full provider guide. Recognised providers and their exit ports: | Provider | Config key | Credentials | Proxy scheme/port | |----------|-----------|-------------|-------------------| | Basic (static) | `basic` | country → URI(s) | as specified | -| NordVPN | `nordvpn` | 48-char **service** credentials | `https://…:89` | -| Surfshark | `surfsharkvpn` | 48-char **service** credentials | `https://…:443` | -| Windscribe | `windscribevpn` | service credentials | `https://…:443` | -| ExpressVPN | `expressvpn` | browser cookies / token cache | `https://cat:…@…:443` | -| ProtonVPN | `protonvpn` | TV login or exported cookies | `https://…:4443` (or `:443` Secure Core) | +| NordVPN | `nordvpn` | 48-char **service** credentials | `https://...:89` | +| Surfshark | `surfsharkvpn` | 48-char **service** credentials | `https://...:443` | +| Windscribe | `windscribevpn` | service credentials | `https://...:443` | +| ExpressVPN | `expressvpn` | browser cookies / token cache | `https://cat:...@...:443` | +| ProtonVPN | `protonvpn` | TV login or exported cookies | `https://...:4443` (or `:443` Secure Core) | | Gluetun | `gluetun` | per-VPN keys/creds | `http://localhost:{port}` (local Docker) | -| Hola | *(none, auto)* | none | `http://…:{peer}` | +| Hola | *(none, auto)* | none | `http://...:{peer}` | ```yaml proxy_providers: @@ -696,12 +834,12 @@ headers: cdm: default: chromecdm_l3 - NF: android_l1 + EXAMPLE2: android_l1 decryption: shaka credentials: - NF: user@example.com:hunter2 + EXAMPLE2: user@example.com:hunter2 output_template: movies: "{title}.{year}.{quality?}.{source}-{tag}"