mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 20:15:10 +00:00
feat(api): forward exact_lang to the service context
The remote-dl client sends exact_lang in dl_params, and REST download jobs set it in the service ctx, so a service can skip the manifests of close-match languages.
This commit is contained in:
@@ -316,7 +316,7 @@ Show the video, audio, and subtitle tracks for a title. For series, you can scop
|
||||
| `part` | int/string | no | Part index of a split episode (combined with `season` and `episode`). |
|
||||
| `profile`, `proxy`, `no_proxy`, `cdm_type` | - | no | As on `list-titles`. |
|
||||
| `credentials`, `cookies`, `cache` | - | no | As on `list-titles`. |
|
||||
| `dl_params` | object | no | The `dl` track selection that the service reads from `ctx.parent.params`: `lang`, `v_lang`, `a_lang`, `acodec` and `forced_subs`, as on [`POST /api/session/create`](#post-apisessioncreate). Some services select the manifests they fetch from these values. |
|
||||
| `dl_params` | object | no | The `dl` track selection that the service reads from `ctx.parent.params`: `lang`, `v_lang`, `a_lang`, `acodec`, `forced_subs` and `exact_lang`, as on [`POST /api/session/create`](#post-apisessioncreate). Some services select the manifests they fetch from these values. |
|
||||
|
||||
When you give both `season` and `episode`, the server combines them into a `"{season}x{episode}"` selector. `part` narrows that to `"{season}x{episode}.{part}"`, one part of a [split episode](../creating-a-service.md#split-episodes). Without `part` you get every part of the episode. The server reads `part` only when you give both `season` and `episode`, and `wanted` takes precedence over all three. To scope a part by range, put it in `wanted` instead (`"S01E01.2"`).
|
||||
|
||||
@@ -958,7 +958,7 @@ the newlines must go out at once.
|
||||
|
||||
Make a remote session for a service and title. Authentication runs asynchronously in the background. This call returns immediately with `status: "authenticating"`, and you then poll the prompt endpoint. The body accepts `service` and `title_id` (both required). It also accepts a broad set of optional keys, because the body allows `additionalProperties`. These are `credentials` (`{username, password, extra?}`), `cookies` (base64 of zlib-compressed Netscape cookie file), `proxy`, `no_proxy`, `profile`, `cache` (map of forwarded cache files, keyed by the file path relative to the service cache directory with `/` separators and no `.json` suffix, so a nested key such as `session_web/<sha1>` lands in a subdirectory), `client_region`, `proxy_region`, `cdm_type`, `range_`, `vcodec`, `quality`, `best_available`, `dl_params`, `client`, and any service CLI options. `proxy_region` is the country code the client resolved `proxy` from; the server matches it against its own accounts. `client` is a freeform object the dashboard shows as sent (the CLI puts `version`, `code_hash`, `platform` and a redacted `argv` in it); the server ignores it above 4096 bytes of JSON.
|
||||
|
||||
`dl_params` is an object with the `dl` track selection: `lang`, `v_lang` and `a_lang` (arrays of language strings), `acodec` (an array of audio codec names or `dl -a` aliases such as `ddp`) and `forced_subs` (boolean). The service reads these values from `ctx.parent.params`. The server uses the `dl` default for each absent or malformed value, and drops an unknown codec name.
|
||||
`dl_params` is an object with the `dl` track selection: `lang`, `v_lang` and `a_lang` (arrays of language strings), `acodec` (an array of audio codec names or `dl -a` aliases such as `ddp`), and `forced_subs` and `exact_lang` (booleans). The service reads these values from `ctx.parent.params`. The server uses the `dl` default for each absent or malformed value, and drops an unknown codec name.
|
||||
|
||||
Service CLI options also travel in a nested `service_params` object, which wins over a flat key with the same name. `profile` at the top level always means the credentials profile, never a service's own `--profile` option.
|
||||
|
||||
|
||||
@@ -432,11 +432,11 @@ object the dashboard shows as sent), and the track-selection hints `range_`, `vc
|
||||
|
||||
`dl_params` holds the `dl` track selection: `lang`, `v_lang` and `a_lang`
|
||||
(arrays of language strings), `acodec` (an array of audio codec names, such as
|
||||
`EC3`), and `forced_subs` (boolean). The server gives these values to the
|
||||
`EC3`), and `forced_subs` and `exact_lang` (booleans). The server gives these values to the
|
||||
service in `ctx.parent.params`, never as service options, so a service option
|
||||
with the same name, such as an own `--lang`, does not get them. The server uses
|
||||
the `dl` default for each absent or malformed value: `lang` is `["orig"]`,
|
||||
`v_lang`, `a_lang` and `acodec` are `[]`, and `forced_subs` is `false`. The
|
||||
`v_lang`, `a_lang` and `acodec` are `[]`, and `forced_subs` and `exact_lang` are `false`. The
|
||||
server drops an unknown codec name. An empty array is a valid value and stays
|
||||
empty.
|
||||
|
||||
|
||||
@@ -192,10 +192,17 @@ class _CapturedCtx(Exception):
|
||||
@pytest.mark.parametrize(
|
||||
("params", "expected"),
|
||||
[
|
||||
({}, (["orig"], [], [], [], False)),
|
||||
({}, (["orig"], [], [], [], False, False)),
|
||||
(
|
||||
{"lang": ["es-419"], "v_lang": ["en"], "a_lang": ["ja"], "acodec": "AAC,EC3", "forced_subs": True},
|
||||
(["es-419"], ["en"], ["ja"], ["AAC", "EC3"], True),
|
||||
{
|
||||
"lang": ["es-419"],
|
||||
"v_lang": ["en"],
|
||||
"a_lang": ["ja"],
|
||||
"acodec": "AAC,EC3",
|
||||
"forced_subs": True,
|
||||
"exact_lang": True,
|
||||
},
|
||||
(["es-419"], ["en"], ["ja"], ["AAC", "EC3"], True, True),
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -224,7 +231,8 @@ def test_perform_download_puts_the_lang_selection_in_the_service_ctx(
|
||||
|
||||
ctx = exc.value.args[0]
|
||||
p = ctx.params
|
||||
assert (p["lang"], p["v_lang"], p["a_lang"], [c.name for c in p["acodec"]], p["forced_subs"]) == expected
|
||||
selection = (p["lang"], p["v_lang"], p["a_lang"], [c.name for c in p["acodec"]], p["forced_subs"], p["exact_lang"])
|
||||
assert selection == expected
|
||||
|
||||
|
||||
def test_perform_download_passes_latest_episodes_to_dl_result(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
||||
@@ -616,6 +616,7 @@ def test_dl_selection_travels_nested_and_a_service_option_cannot_override_it() -
|
||||
"a_lang": [],
|
||||
"acodec": [Audio.Codec.EC3],
|
||||
"forced_subs": True,
|
||||
"exact_lang": True,
|
||||
}
|
||||
data = _sent_create_data(parent_params, {"lang": 3, "a_lang": "x"})
|
||||
assert data["dl_params"] == {
|
||||
@@ -624,6 +625,7 @@ def test_dl_selection_travels_nested_and_a_service_option_cannot_override_it() -
|
||||
"a_lang": [],
|
||||
"acodec": ["EC3"],
|
||||
"forced_subs": True,
|
||||
"exact_lang": True,
|
||||
}
|
||||
assert data["lang"] == 3
|
||||
assert data["service_params"] == {"lang": 3, "a_lang": "x"}
|
||||
|
||||
@@ -162,6 +162,9 @@ def _create_with_real_ctx(monkeypatch, data):
|
||||
return svc
|
||||
|
||||
|
||||
DL_DEFAULTS = {"lang": ["orig"], "v_lang": [], "a_lang": [], "acodec": [], "forced_subs": False, "exact_lang": False}
|
||||
|
||||
|
||||
def test_nested_dl_params_reach_the_parent_ctx(monkeypatch):
|
||||
from unshackle.core.tracks import Audio
|
||||
|
||||
@@ -172,6 +175,7 @@ def test_nested_dl_params_reach_the_parent_ctx(monkeypatch):
|
||||
"a_lang": ["es-419"],
|
||||
"acodec": ["EC3", "NOPE", "AAC"],
|
||||
"forced_subs": True,
|
||||
"exact_lang": True,
|
||||
}
|
||||
}
|
||||
svc = _create_with_real_ctx(monkeypatch, data)
|
||||
@@ -180,12 +184,13 @@ def test_nested_dl_params_reach_the_parent_ctx(monkeypatch):
|
||||
assert svc.parent_params["a_lang"] == ["es-419"]
|
||||
assert svc.parent_params["acodec"] == [Audio.Codec.EC3, Audio.Codec.AAC]
|
||||
assert svc.parent_params["forced_subs"] is True
|
||||
assert svc.parent_params["exact_lang"] is True
|
||||
|
||||
|
||||
def test_absent_dl_params_fall_back_to_the_dl_defaults(monkeypatch):
|
||||
svc = _create_with_real_ctx(monkeypatch, {})
|
||||
selection = {k: svc.parent_params[k] for k in ("lang", "v_lang", "a_lang", "acodec", "forced_subs")}
|
||||
assert selection == {"lang": ["orig"], "v_lang": [], "a_lang": [], "acodec": [], "forced_subs": False}
|
||||
selection = {k: svc.parent_params[k] for k in (*DL_DEFAULTS,)}
|
||||
assert selection == DL_DEFAULTS
|
||||
|
||||
|
||||
def test_an_empty_lang_list_is_kept(monkeypatch):
|
||||
@@ -204,14 +209,14 @@ def test_a_service_lang_option_never_gets_the_dl_lang(monkeypatch):
|
||||
"dl_params",
|
||||
[
|
||||
"en",
|
||||
{"lang": "en", "v_lang": "ja", "a_lang": {"x": 1}, "acodec": "EC3", "forced_subs": "yes"},
|
||||
{"lang": [1], "v_lang": [None], "a_lang": ["en", None], "acodec": [3], "forced_subs": 1},
|
||||
{"lang": "en", "v_lang": "ja", "a_lang": {"x": 1}, "acodec": "EC3", "forced_subs": "yes", "exact_lang": "yes"},
|
||||
{"lang": [1], "v_lang": [None], "a_lang": ["en", None], "acodec": [3], "forced_subs": 1, "exact_lang": 1},
|
||||
],
|
||||
)
|
||||
def test_malformed_dl_params_are_ignored(monkeypatch, dl_params):
|
||||
svc = _create_with_real_ctx(monkeypatch, {"dl_params": dl_params})
|
||||
selection = {k: svc.parent_params[k] for k in ("lang", "v_lang", "a_lang", "acodec", "forced_subs")}
|
||||
assert selection == {"lang": ["orig"], "v_lang": [], "a_lang": [], "acodec": [], "forced_subs": False}
|
||||
selection = {k: svc.parent_params[k] for k in (*DL_DEFAULTS,)}
|
||||
assert selection == DL_DEFAULTS
|
||||
|
||||
|
||||
def test_profile_reaches_the_parent_ctx_params(monkeypatch):
|
||||
|
||||
@@ -500,6 +500,7 @@ def perform_download(
|
||||
"v_lang": params.get("v_lang", []),
|
||||
"a_lang": params.get("a_lang", []),
|
||||
"forced_subs": params.get("forced_subs", False),
|
||||
"exact_lang": params.get("exact_lang", False),
|
||||
}
|
||||
# Hand-built context: record parameter sources so service dl overrides
|
||||
# apply to defaults but never clobber client-sent values.
|
||||
|
||||
@@ -3277,6 +3277,7 @@ def forwarded_dl_params(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
except click.BadParameter:
|
||||
log.warning(f"Ignoring unknown dl_params.acodec value: {sanitize_log(name)}")
|
||||
params["forced_subs"] = raw.get("forced_subs") is True
|
||||
params["exact_lang"] = raw.get("exact_lang") is True
|
||||
return params
|
||||
|
||||
|
||||
|
||||
@@ -726,7 +726,7 @@ async def list_tracks(request: web.Request) -> web.Response:
|
||||
dl_params:
|
||||
type: object
|
||||
description: |
|
||||
The dl track selection (lang, v_lang, a_lang, acodec, forced_subs), in the same
|
||||
The dl track selection (lang, v_lang, a_lang, acodec, forced_subs, exact_lang), in the same
|
||||
form as on `/api/session/create`. The service reads it from `ctx.parent.params`.
|
||||
credentials:
|
||||
type: object
|
||||
@@ -1831,6 +1831,9 @@ async def session_create(request: web.Request) -> web.Response:
|
||||
forced_subs:
|
||||
type: boolean
|
||||
default: false
|
||||
exact_lang:
|
||||
type: boolean
|
||||
default: false
|
||||
server_cdm:
|
||||
type: boolean
|
||||
description: |
|
||||
|
||||
@@ -1073,8 +1073,9 @@ class RemoteService:
|
||||
}
|
||||
if params.get("acodec") is not None:
|
||||
dl_params["acodec"] = [c.name for c in params["acodec"]]
|
||||
if "forced_subs" in params:
|
||||
dl_params["forced_subs"] = bool(params["forced_subs"])
|
||||
for key in ("forced_subs", "exact_lang"):
|
||||
if key in params:
|
||||
dl_params[key] = bool(params[key])
|
||||
if dl_params:
|
||||
create_data["dl_params"] = dl_params
|
||||
|
||||
|
||||
Reference in New Issue
Block a user