feat(remote): cap server CDM licensing by height per API key and service

A session above the cap licenses with the client's own device, and a track above it in an uncapped -q run is handed back to the client. Also names the fallback in the ccextractor crash warning.
This commit is contained in:
imSp4rky
2026-09-24 16:50:26 -06:00
parent 91db1fa0b1
commit 7d31153c7f
21 changed files with 1087 additions and 47 deletions
+4 -3
View File
@@ -44,7 +44,7 @@ All are `GET`.
| Path | Returns |
|------|---------|
| `/api/dashboard/status` | Version, `code_hash`, bind host and port, `mode` (`full`, `api_only`, `remote_only`), uptime, `requests_total`, `requests_rejected` (401s), `requests_by_key` (label → count), loaded service count, session count, `max_sessions` (`null` when the operator configured no limit), `session_ttl`, job counts by status. The service count includes services that failed to import - use `/api/dashboard/services` for the number that loaded. |
| `/api/dashboard/sessions` | Every live remote session: `id`, `owner` (username or masked key), `creator_ip`, `service`, `title_id`, `titles`, `tracks`, `title` (the display name of the title the client is on: the last one it asked tracks for, else the first resolved title), `auth_status`, `auth_error`, `server_account` (the server profile lent to the remote session, else `null`), `client`, `actions`, `created_at` and `last_accessed` (ISO 8601) with `created_ts` and `last_accessed_ts` (Unix epoch, the same clock as log `ts`), `age_seconds`, `idle_seconds`, `log_seq`. `client` is whatever the remote client sent as `client` in its session create request. The CLI sends `version`, `code_hash` (the commit the client runs, `null` when its source cannot be read), `platform` and `argv`. `argv` is the command line the user ran: proxy and URL userinfo, secret query parameters and credential values become `***`, home and install paths shorten as in the logs, and the line is cut at 3000 characters. An older client sends less or nothing and the field is `{}`. `log_seq` is the last sequence number in the remote session's service log - poll `/api/dashboard/sessions/{id}/logs` when it changes. `actions` is the session's request log, newest last, capped at 500: `ts`, `method`, `action` (`titles`, `tracks`, `segments`, `license`, `prompt`, …), `query`, `status`, `ms`, `bytes_in`, `bytes_out`. |
| `/api/dashboard/sessions` | Every live remote session: `id`, `owner` (username or masked key), `creator_ip`, `service`, `title_id`, `titles`, `tracks`, `title` (the display name of the title the client is on: the last one it asked tracks for, else the first resolved title), `auth_status`, `auth_error`, `server_account` (the server profile lent to the remote session, else `null`), `server_cdm` (`false` when the client's own device licenses the remote session), `server_cdm_max_height` (the tallest video the server CDM licenses live, else `null`), `client`, `actions`, `created_at` and `last_accessed` (ISO 8601) with `created_ts` and `last_accessed_ts` (Unix epoch, the same clock as log `ts`), `age_seconds`, `idle_seconds`, `log_seq`. `client` is whatever the remote client sent as `client` in its session create request. The CLI sends `version`, `code_hash` (the commit the client runs, `null` when its source cannot be read), `platform` and `argv`. `argv` is the command line the user ran: proxy and URL userinfo, secret query parameters and credential values become `***`, home and install paths shorten as in the logs, and the line is cut at 3000 characters. An older client sends less or nothing and the field is `{}`. `log_seq` is the last sequence number in the remote session's service log - poll `/api/dashboard/sessions/{id}/logs` when it changes. `actions` is the session's request log, newest last, capped at 500: `ts`, `method`, `action` (`titles`, `tracks`, `segments`, `license`, `prompt`, …), `query`, `status`, `ms`, `bytes_in`, `bytes_out`. |
| `/api/dashboard/jobs` | Every download job with full detail, regardless of owner. Empty in `--remote-only` mode. |
| `/api/dashboard/logs` | `{"seq": N, "records": [...]}`: the last 1000 log records. `?since=<seq>` returns only newer records; `?level=WARNING` sets the minimum level; `?logger=serve` keeps one logger and its children (`aiohttp.access` is the noisiest). `ts` is a Unix epoch in seconds. |
| `/api/dashboard/sessions/{id}/logs` | One remote session's service log: `{"session_id", "last_seq", "records": [...]}`. Each record has `seq`, `ts`, `level`, `message`. `?since=<seq>` returns only newer records. 404 when the remote session is unknown. |
@@ -118,11 +118,11 @@ never keeps it alive or makes it look active.
```
GET /api/dashboard/keys
[{"id": "3ed054be9371", "role": "user", "label": "tier1", "services": ["EXAMPLE", "DEMO"],
"server_cdm": false, "server_accounts": false, "server_proxy": false,
"server_cdm": false, "server_cdm_max_height": null, "server_accounts": false, "server_proxy": false,
"tier": "bot", "rate_limit": 600, "window_used": 412,
"requests": 41300, "rejected": 4, "bytes_out": 103079215104, "last_seen": 1756909188.2},
{"id": "7c1d0f52a884", "role": "dashboard", "label": "dash…", "services": [],
"server_cdm": false, "server_accounts": false, "server_proxy": false,
"server_cdm": false, "server_cdm_max_height": null, "server_accounts": false, "server_proxy": false,
"tier": null, "rate_limit": null, "window_used": 0,
"requests": 8140, "rejected": 0, "bytes_out": 41200311, "last_seen": 1756909350.9}]
```
@@ -136,6 +136,7 @@ here, so use `id` as the identity and `label` only for display.
| `role` | Which key this is: `user` (a `serve.users` entry), `admin` (`serve.api_secret`) or `dashboard` (`serve.dashboard.key`). Identity, not capability - read the grant fields for what the API key may do. |
| `services` | The effective allowlist, the global list intersected with the API key's. `null` means unrestricted; `[]` means the API key reaches no service route at all. |
| `server_cdm`, `server_accounts` | `false`, `true`, or the list of service tags the grant covers. |
| `server_cdm_max_height` | The configured limit on live server CDM licences: a height, a map of service tag to height, or `null` for no limit. |
| `server_proxy` | Boolean. Only a literal `true` grants it, and an API key with no `users` entry does not get it. |
| `rate_limit` | Requests per hour, from the API key's own value or its tier's. `null` means no limit. |
| `window_used` | Requests counted in the current hourly window. |
+3
View File
@@ -506,6 +506,7 @@ profile, never a service's own `--profile` option.
| `400` | `INVALID_PARAMETERS` | A parameter failed validation. |
| `400` | `INVALID_SERVICE` | Unknown or disallowed service. |
| `403` | `FORBIDDEN` | A gated parameter is not permitted. |
| `403` | `SERVER_CDM_CAPPED` | The API key has a `server_cdm_max_height` for the service, and the job has no `quality` at or under it, sets `best_available`, or has `HYBRID` in `range`. |
### `GET /api/download/jobs`
@@ -690,6 +691,7 @@ Enqueue a **new** job that reuses a terminal job's service, title, and parameter
| `409` | `CONFLICT` | Job is not in a terminal state. |
| `400` | `INVALID_SERVICE` | Service is no longer allowed. |
| `403` | `FORBIDDEN` | A gated parameter is no longer permitted. |
| `403` | `SERVER_CDM_CAPPED` | The job is no longer inside the `server_cdm_max_height` of the API key. |
### `POST /api/download/jobs/{job_id}/priority`
@@ -1220,6 +1222,7 @@ The server serialises every structured error raised inside a handler to the same
| `INVALID_PARAMETERS` | 400 |
| `AUTH_FAILED` | 401 |
| `FORBIDDEN` | 403 |
| `SERVER_CDM_CAPPED` | 403 |
| `GEOFENCE` | 403 |
| `NOT_FOUND` | 404 |
| `NO_CONTENT` | 404 |
+1
View File
@@ -100,6 +100,7 @@ but the defaults hold for the overwhelming majority of responses.
| `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 remote session IP binding. |
| `SERVER_CDM_CAPPED` | 403 | The server CDM does not license live above the `server_cdm_max_height` of the API key. `details.reason` is `server_cdm_max_height` (with `details.max_height`) or `client_device`, for a remote session where the client's own device licenses. A remote client licenses the track with its own device. |
| `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. |
+9
View File
@@ -268,6 +268,15 @@ There are two ways DRM keys get resolved, chosen by the client's `server_cdm` fl
already answered. The client proves it, and a server key waits as the next
candidate.
!!! note "A height limit on the server CDM"
An API key with `server_cdm_max_height` gets the server CDM only up to that height. The
session create response tells the client who licenses the session in `server_cdm` and gives
the limit in `server_cdm_max_height`. In a server CDM session, the batch licence response
lists each track the server refused to license live under `capped_tracks`. A single-track
server CDM licence answers `403 SERVER_CDM_CAPPED` for such a track. In both cases the client
licenses those tracks in proxy mode with its own local CDM. See
[`server_cdm_max_height`](../../reference/configuration/services.md).
---
## Remote session lifecycle and expiry
+27
View File
@@ -154,6 +154,32 @@ client that asks anyway gets a `FORBIDDEN` error. Because a download job always
an API key without `server_cdm` for that service also cannot submit or retry `/api/download`
jobs. Keys that have no `users` entry, such as `api_secret`, keep server CDM access.
`server_cdm_max_height` limits the live licences the server's device makes for that API key.
Set it to a height in pixels for every service, or to a map of service tag to height with an
optional `default` entry. A service the map does not name or sets to `null` gets the `default`, and
with no `default` it has no limit. The server compares the limit with the lowest `-q` that selects
the track: the track height, or the 16:9 height from the width when that is lower. The server reads
the remote client's `-q` when the remote session starts:
- With no `-q`, or a `-q` at or below the limit, the server CDM licenses the remote session. The server
refuses a live licence for a video track taller than the limit, and the client licenses that
track with its own local CDM.
- With a `-q` above the limit, the client's own local CDM licenses every track of the remote session.
The service sees the DRM system and security level of the client's device. A client with no
local CDM gets a `SERVER_CDM_CAPPED` error.
- A download job must pass a `quality` at or below the limit, and must not set `best_available` or
a `HYBRID` range. `HYBRID` always keeps the lowest Dolby Vision track, at any quality. A job with
no `quality`, with a higher one, with `best_available`, or with `HYBRID` gets a `SERVER_CDM_CAPPED`
error. The server checks the values the job runs with, after it applies the service defaults
and the `serve:` download defaults.
The limit protects the server's device, not the content keys. For a Widevine track, a server vault
still supplies a content key at every height. A PlayReady track never reads the vault, so above
the limit it always goes to the client's device. A licence for a lower track can carry the
content key of a higher track when the service shares one content key between them. With server
accounts enabled, a client that reports a stronger device than it holds makes the server's
account request those manifests; the licence server still refuses its challenge.
`admin` is a boolean that lets the API key run the maintenance endpoints (clear-cache, clear-temp, refresh-services). It is `false` unless the entry sets it. Keys that have no `users` entry, such as `api_secret`, keep that access.
A `tier` names an entry under `serve.tiers`, which holds the settings that several API keys
@@ -242,6 +268,7 @@ serve:
tier: bot # 600 requests/hour; 429 with Retry-After once over
e5f6a7b8:
server_cdm: true # this key may have the server do the licensing
server_cdm_max_height: 1080 # above 1080p the client licenses with its own device
c9d0e1f2:
services: [EXAMPLE1, EXAMPLE2]
server_cdm: [EXAMPLE1] # the server licenses only EXAMPLE1; EXAMPLE2 needs a local CDM