From d3594ca67c9baf136e7e6bdb027cc678b71e237a Mon Sep 17 00:00:00 2001 From: imSp4rky Date: Mon, 30 Mar 2026 05:20:32 +0000 Subject: [PATCH] fix(remote): forward track selection params to server and improve error display Forward range, vcodec, quality, and best_available from client to server so services fetch the correct manifests (e.g. HDR10 instead of SDR). Exit with error when no video tracks match the requested range filter. Use Rich logger for remote API errors instead of plain click.ClickException. --- unshackle/commands/dl.py | 3 +++ unshackle/core/api/handlers.py | 38 +++++++++++++++++++++++++++++++- unshackle/core/remote_service.py | 23 ++++++++++++++++--- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/unshackle/commands/dl.py b/unshackle/commands/dl.py index a00a1322..6470e6f8 100644 --- a/unshackle/commands/dl.py +++ b/unshackle/commands/dl.py @@ -1561,6 +1561,9 @@ class dl: missing_ranges = [r for r in range_ if not any(x.range == r for x in title.tracks.videos)] for color_range in missing_ranges: self.log.warning(f"Skipping {color_range.name} video tracks as none are available.") + if not title.tracks.videos: + self.log.error(f"There's no {', '.join(r.name for r in range_)} Video Track...") + sys.exit(1) if vbitrate: if any(r == Video.Range.HYBRID for r in range_): diff --git a/unshackle/core/api/handlers.py b/unshackle/core/api/handlers.py index f14f6898..dc5c52c0 100644 --- a/unshackle/core/api/handlers.py +++ b/unshackle/core/api/handlers.py @@ -1231,7 +1231,39 @@ def _create_service_instance( cdm = _resolve_server_cdm(normalized_service, profile, data.get("cdm_type")) ctx.obj = ContextData(config=service_config, cdm=cdm, proxy_providers=proxy_providers, profile=profile) - ctx.params = {"proxy": proxy_param, "no_proxy": data.get("no_proxy", False)} + # Reconstruct track selection params from client data + from unshackle.core.tracks import Video + + range_names = data.get("range_") + range_values = None + if range_names: + range_values = [] + for name in range_names: + try: + range_values.append(Video.Range[name]) + except KeyError: + pass + range_values = range_values or None + + vcodec_names = data.get("vcodec") + vcodec_values = None + if vcodec_names: + vcodec_values = [] + for name in vcodec_names: + try: + vcodec_values.append(Video.Codec[name]) + except KeyError: + pass + vcodec_values = vcodec_values or None + + ctx.params = { + "proxy": proxy_param, + "no_proxy": data.get("no_proxy", False), + "range_": range_values, + "vcodec": vcodec_values, + "quality": data.get("quality"), + "best_available": data.get("best_available", False), + } service_module = Services.load(normalized_service) @@ -1258,6 +1290,10 @@ def _create_service_instance( "client_region", "no_proxy", "cdm_type", + "range_", + "vcodec", + "quality", + "best_available", } service_init_params = inspect.signature(service_module.__init__).parameters diff --git a/unshackle/core/remote_service.py b/unshackle/core/remote_service.py index 30854c39..99e56be8 100644 --- a/unshackle/core/remote_service.py +++ b/unshackle/core/remote_service.py @@ -68,7 +68,8 @@ class RemoteClient: if resp.status_code >= 400: error_msg = result.get("message", resp.text) error_code = result.get("error_code", "UNKNOWN") - raise click.ClickException(f"Server error [{error_code}]: {error_msg}") + log.error(f"Server error [{error_code}]: {error_msg}") + raise SystemExit(1) return result def post(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]: @@ -492,6 +493,20 @@ class RemoteService: create_data["profile"] = profile if no_proxy: create_data["no_proxy"] = True + # Forward track selection params so the server fetches the right manifests + if self.ctx.parent: + range_ = self.ctx.parent.params.get("range_") + if range_: + create_data["range_"] = [r.name for r in range_] + vcodec = self.ctx.parent.params.get("vcodec") + if vcodec: + create_data["vcodec"] = [c.name for c in vcodec] + quality = self.ctx.parent.params.get("quality") + if quality: + create_data["quality"] = list(quality) + if self.ctx.parent.params.get("best_available"): + create_data["best_available"] = True + if self._service_params: create_data.update(self._service_params) @@ -530,7 +545,8 @@ class RemoteService: if status == "failed": error = resp.get("error", "Authentication failed on server") - raise click.ClickException(f"Remote auth failed: {error}") + log.error(f"Remote auth failed: {error}") + raise SystemExit(1) if status == "pending_input": prompt = resp.get("prompt", "Enter input: ") @@ -543,7 +559,8 @@ class RemoteService: time.sleep(poll_interval) - raise click.ClickException("Remote authentication timed out") + log.error("Remote authentication timed out") + raise SystemExit(1) def get_titles(self) -> Titles_T: if self._titles is not None: