mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 12:05:07 +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`). |
|
| `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`. |
|
| `profile`, `proxy`, `no_proxy`, `cdm_type` | - | no | As on `list-titles`. |
|
||||||
| `credentials`, `cookies`, `cache` | - | 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"`).
|
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.
|
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.
|
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`
|
`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
|
(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
|
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
|
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"]`,
|
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
|
server drops an unknown codec name. An empty array is a valid value and stays
|
||||||
empty.
|
empty.
|
||||||
|
|
||||||
|
|||||||
@@ -192,10 +192,17 @@ class _CapturedCtx(Exception):
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("params", "expected"),
|
("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]
|
ctx = exc.value.args[0]
|
||||||
p = ctx.params
|
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:
|
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": [],
|
"a_lang": [],
|
||||||
"acodec": [Audio.Codec.EC3],
|
"acodec": [Audio.Codec.EC3],
|
||||||
"forced_subs": True,
|
"forced_subs": True,
|
||||||
|
"exact_lang": True,
|
||||||
}
|
}
|
||||||
data = _sent_create_data(parent_params, {"lang": 3, "a_lang": "x"})
|
data = _sent_create_data(parent_params, {"lang": 3, "a_lang": "x"})
|
||||||
assert data["dl_params"] == {
|
assert data["dl_params"] == {
|
||||||
@@ -624,6 +625,7 @@ def test_dl_selection_travels_nested_and_a_service_option_cannot_override_it() -
|
|||||||
"a_lang": [],
|
"a_lang": [],
|
||||||
"acodec": ["EC3"],
|
"acodec": ["EC3"],
|
||||||
"forced_subs": True,
|
"forced_subs": True,
|
||||||
|
"exact_lang": True,
|
||||||
}
|
}
|
||||||
assert data["lang"] == 3
|
assert data["lang"] == 3
|
||||||
assert data["service_params"] == {"lang": 3, "a_lang": "x"}
|
assert data["service_params"] == {"lang": 3, "a_lang": "x"}
|
||||||
|
|||||||
@@ -162,6 +162,9 @@ def _create_with_real_ctx(monkeypatch, data):
|
|||||||
return svc
|
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):
|
def test_nested_dl_params_reach_the_parent_ctx(monkeypatch):
|
||||||
from unshackle.core.tracks import Audio
|
from unshackle.core.tracks import Audio
|
||||||
|
|
||||||
@@ -172,6 +175,7 @@ def test_nested_dl_params_reach_the_parent_ctx(monkeypatch):
|
|||||||
"a_lang": ["es-419"],
|
"a_lang": ["es-419"],
|
||||||
"acodec": ["EC3", "NOPE", "AAC"],
|
"acodec": ["EC3", "NOPE", "AAC"],
|
||||||
"forced_subs": True,
|
"forced_subs": True,
|
||||||
|
"exact_lang": True,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
svc = _create_with_real_ctx(monkeypatch, data)
|
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["a_lang"] == ["es-419"]
|
||||||
assert svc.parent_params["acodec"] == [Audio.Codec.EC3, Audio.Codec.AAC]
|
assert svc.parent_params["acodec"] == [Audio.Codec.EC3, Audio.Codec.AAC]
|
||||||
assert svc.parent_params["forced_subs"] is True
|
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):
|
def test_absent_dl_params_fall_back_to_the_dl_defaults(monkeypatch):
|
||||||
svc = _create_with_real_ctx(monkeypatch, {})
|
svc = _create_with_real_ctx(monkeypatch, {})
|
||||||
selection = {k: svc.parent_params[k] for k in ("lang", "v_lang", "a_lang", "acodec", "forced_subs")}
|
selection = {k: svc.parent_params[k] for k in (*DL_DEFAULTS,)}
|
||||||
assert selection == {"lang": ["orig"], "v_lang": [], "a_lang": [], "acodec": [], "forced_subs": False}
|
assert selection == DL_DEFAULTS
|
||||||
|
|
||||||
|
|
||||||
def test_an_empty_lang_list_is_kept(monkeypatch):
|
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",
|
"dl_params",
|
||||||
[
|
[
|
||||||
"en",
|
"en",
|
||||||
{"lang": "en", "v_lang": "ja", "a_lang": {"x": 1}, "acodec": "EC3", "forced_subs": "yes"},
|
{"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},
|
{"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):
|
def test_malformed_dl_params_are_ignored(monkeypatch, dl_params):
|
||||||
svc = _create_with_real_ctx(monkeypatch, {"dl_params": 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")}
|
selection = {k: svc.parent_params[k] for k in (*DL_DEFAULTS,)}
|
||||||
assert selection == {"lang": ["orig"], "v_lang": [], "a_lang": [], "acodec": [], "forced_subs": False}
|
assert selection == DL_DEFAULTS
|
||||||
|
|
||||||
|
|
||||||
def test_profile_reaches_the_parent_ctx_params(monkeypatch):
|
def test_profile_reaches_the_parent_ctx_params(monkeypatch):
|
||||||
|
|||||||
@@ -500,6 +500,7 @@ def perform_download(
|
|||||||
"v_lang": params.get("v_lang", []),
|
"v_lang": params.get("v_lang", []),
|
||||||
"a_lang": params.get("a_lang", []),
|
"a_lang": params.get("a_lang", []),
|
||||||
"forced_subs": params.get("forced_subs", False),
|
"forced_subs": params.get("forced_subs", False),
|
||||||
|
"exact_lang": params.get("exact_lang", False),
|
||||||
}
|
}
|
||||||
# Hand-built context: record parameter sources so service dl overrides
|
# Hand-built context: record parameter sources so service dl overrides
|
||||||
# apply to defaults but never clobber client-sent values.
|
# 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:
|
except click.BadParameter:
|
||||||
log.warning(f"Ignoring unknown dl_params.acodec value: {sanitize_log(name)}")
|
log.warning(f"Ignoring unknown dl_params.acodec value: {sanitize_log(name)}")
|
||||||
params["forced_subs"] = raw.get("forced_subs") is True
|
params["forced_subs"] = raw.get("forced_subs") is True
|
||||||
|
params["exact_lang"] = raw.get("exact_lang") is True
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -726,7 +726,7 @@ async def list_tracks(request: web.Request) -> web.Response:
|
|||||||
dl_params:
|
dl_params:
|
||||||
type: object
|
type: object
|
||||||
description: |
|
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`.
|
form as on `/api/session/create`. The service reads it from `ctx.parent.params`.
|
||||||
credentials:
|
credentials:
|
||||||
type: object
|
type: object
|
||||||
@@ -1831,6 +1831,9 @@ async def session_create(request: web.Request) -> web.Response:
|
|||||||
forced_subs:
|
forced_subs:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
exact_lang:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
server_cdm:
|
server_cdm:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: |
|
description: |
|
||||||
|
|||||||
@@ -1073,8 +1073,9 @@ class RemoteService:
|
|||||||
}
|
}
|
||||||
if params.get("acodec") is not None:
|
if params.get("acodec") is not None:
|
||||||
dl_params["acodec"] = [c.name for c in params["acodec"]]
|
dl_params["acodec"] = [c.name for c in params["acodec"]]
|
||||||
if "forced_subs" in params:
|
for key in ("forced_subs", "exact_lang"):
|
||||||
dl_params["forced_subs"] = bool(params["forced_subs"])
|
if key in params:
|
||||||
|
dl_params[key] = bool(params[key])
|
||||||
if dl_params:
|
if dl_params:
|
||||||
create_data["dl_params"] = dl_params
|
create_data["dl_params"] = dl_params
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user