From e804d09f0a6bd2a3e944d58253ac3d0ddd75be39 Mon Sep 17 00:00:00 2001 From: imSp4rky Date: Mon, 24 Aug 2026 16:37:48 -0600 Subject: [PATCH] feat(dl): add --cdm to pin one CDM device for a run Overrides the cdm config mapping (and its quality/DRM sub-entries) for the whole run; the REST API cdm field now flows through the same param. --- docs/guide/cli-reference.md | 1 + docs/guide/downloading.md | 2 ++ docs/guide/drm-and-cdm.md | 1 + docs/reference/configuration/drm.md | 5 +++-- unshackle/commands/dl.py | 10 ++++++++++ unshackle/core/api/download_manager.py | 5 +---- unshackle/core/api/routes.py | 3 +++ 7 files changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/guide/cli-reference.md b/docs/guide/cli-reference.md index b416d039..10c5dc06 100644 --- a/docs/guide/cli-reference.md +++ b/docs/guide/cli-reference.md @@ -186,6 +186,7 @@ order the service used, then renumbers the episodes into the order you asked for | Flag | Description | |---|---| | `--cdm-only` / `--vaults-only` | Use only the CDM, or only Key Vaults, for key acquisition. | +| `--cdm ` | Use the named CDM device from the `cdm` config mapping for this run, ignoring the service/default mapping. | | `--skip-dl` | Skip the download but still retrieve keys. | | `--export` | Export track info and keys to a JSON file in the exports directory. | diff --git a/docs/guide/downloading.md b/docs/guide/downloading.md index 403bc93c..e1e14aff 100644 --- a/docs/guide/downloading.md +++ b/docs/guide/downloading.md @@ -751,6 +751,7 @@ By default, unshackle checks your **key vaults** first and only asks a **CDM** t a content key when the vault misses. You can force one side or the other: - `--cdm-only`: only use the CDM (skip vaults). +- `--cdm `: use the named CDM device for this run, ignoring the `cdm` config mapping (including its quality and Widevine/PlayReady sub-entries). - `--vaults-only`: only use key vaults and never license through the CDM. A missing content key fails. @@ -945,6 +946,7 @@ authoritative list. | `--workers` / `--downloads` / `--slow` | | Concurrency and pacing. | | `--list` / `--list-titles` / `--skip-dl` | | Dry runs. | | `--cdm-only` / `--vaults-only` | | Content key source control. | +| `--cdm ` | | Pin one CDM device, bypassing the `cdm` config mapping. | | `--export` | | Export track info and keys to JSON. | | `--tmdb` / `--imdb` / `--tvdb` / `--anilist` / `--enrich` | | Metadata overrides. | | `--tvdb-order` | | Renumber episodes to a TVDB season order. | diff --git a/docs/guide/drm-and-cdm.md b/docs/guide/drm-and-cdm.md index d3de9475..0a52c20e 100644 --- a/docs/guide/drm-and-cdm.md +++ b/docs/guide/drm-and-cdm.md @@ -498,6 +498,7 @@ control this trade-off on the `dl` command: |------|--------| | `--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. | +| `--cdm ` | Use the named CDM device for this run instead of the one the `cdm` config maps for the service. | See [key vaults](vaults.md) for how to configure vault backends. diff --git a/docs/reference/configuration/drm.md b/docs/reference/configuration/drm.md index 21bf4d92..e01076d6 100644 --- a/docs/reference/configuration/drm.md +++ b/docs/reference/configuration/drm.md @@ -9,8 +9,9 @@ the [DRM & CDM guide](../../guide/drm-and-cdm.md). Device files themselves live - **Type:** `dict`  ยท  **Default:** `{}` Maps a service tag to the CDM (Widevine/PlayReady device) to use, with a `default` fallback. -Resolution is case-insensitive: a per-request API override wins, then the per-service entry, -then `default`. +Resolution is case-insensitive: an override (`dl --cdm `, or the `cdm` field of an API +job) wins, then the per-service entry, then `default`. An override pins one device for the whole +run and skips any quality or Widevine/PlayReady sub-entries. ```yaml cdm: diff --git a/unshackle/commands/dl.py b/unshackle/commands/dl.py index 04fccca7..7ab3b38e 100644 --- a/unshackle/commands/dl.py +++ b/unshackle/commands/dl.py @@ -833,6 +833,13 @@ class dl: default=None, help="Only use CDM, or only use Key Vaults for retrieval of Decryption Keys.", ) + @click.option( + "--cdm", + "cdm_name", + type=str, + default=None, + help="Use this CDM device for the run, overriding the cdm config mapping.", + ) @click.option("--no-proxy", is_flag=True, default=False, help="Force disable all proxy use.") @click.option( "--no-proxy-download", @@ -1201,6 +1208,7 @@ class dl: f"Applied service-specific '{config_key}' overrides for {self.service}: {override_value}" ) + self.cdm_override = ctx.params.get("cdm_name") cdm_only = ctx.params.get("cdm_only") if cdm_only: @@ -1504,6 +1512,8 @@ class dl: self.service_anime = bool(getattr(service, "ANIME", False)) self.service_daily = bool(getattr(service, "DAILY", False)) self.server_cdm = getattr(service, "_server_cdm", False) + if self.server_cdm and self.cdm_override: + self.log.warning("--cdm is ignored: this remote service licenses with the server CDM") self._remote_service = service if hasattr(service, "_server_cdm") else None start_time = time.time() diff --git a/unshackle/core/api/download_manager.py b/unshackle/core/api/download_manager.py index 56abc63d..80a3a00f 100644 --- a/unshackle/core/api/download_manager.py +++ b/unshackle/core/api/download_manager.py @@ -440,6 +440,7 @@ def perform_download( "no_proxy": params.get("no_proxy", False), "no_proxy_download": params.get("no_proxy_download", False), "profile": params.get("profile"), + "cdm_name": params.get("cdm"), "repack": params.get("repack", False), "tag": params.get("tag"), "tmdb_id": params.get("tmdb_id"), @@ -484,10 +485,6 @@ def perform_download( daily=params.get("daily", False), output_dir=Path(params["output_dir"]) if params.get("output_dir") else None, ) - # Per-request CDM override (a device name in the WVDs dir); get_cdm() takes it first. - if params.get("cdm"): - dl_instance.cdm_override = params["cdm"] - # Per-request credential ("user:pass"); feed it into the map get_credentials() reads so a # client can authenticate without anything being persisted to disk. Without a profile, # get_credentials() falls back to "default", so store it there too rather than dropping it diff --git a/unshackle/core/api/routes.py b/unshackle/core/api/routes.py index 47b1854f..5ce0a249 100644 --- a/unshackle/core/api/routes.py +++ b/unshackle/core/api/routes.py @@ -757,6 +757,9 @@ async def download(request: web.Request) -> web.Response: cdm_only: type: boolean description: Only use CDM for content key retrieval (true) or only vaults (false) (default - None) + cdm: + type: string + description: CDM device name on the server to license with, overriding the cdm config mapping. Requires serve.cdm_overrides to allow it (default - None) proxy: type: string description: Proxy URI or country code (default - None)