From 5df691453668995f847cdfb9500edc55aaa1e723 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 23 Sep 2025 01:28:55 +0000 Subject: [PATCH] feat: Add options for required subtitles and best available quality in download command --- unshackle/commands/dl.py | 64 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/unshackle/commands/dl.py b/unshackle/commands/dl.py index c4e044d..8d8caeb 100644 --- a/unshackle/commands/dl.py +++ b/unshackle/commands/dl.py @@ -173,6 +173,12 @@ class dl: help="Language wanted for Audio, overrides -l/--lang for audio tracks.", ) @click.option("-sl", "--s-lang", type=LANGUAGE_RANGE, default=["all"], help="Language wanted for Subtitles.") + @click.option( + "--require-subs", + type=LANGUAGE_RANGE, + default=[], + help="Required subtitle languages. Downloads all subtitles only if these languages exist. Cannot be used with --s-lang.", + ) @click.option("-fs", "--forced-subs", is_flag=True, default=False, help="Include forced subtitle tracks.") @click.option( "--proxy", @@ -263,6 +269,13 @@ class dl: @click.option( "--reset-cache", "reset_cache", is_flag=True, default=False, help="Clear title cache before fetching." ) + @click.option( + "--best-available", + "best_available", + is_flag=True, + default=False, + help="Continue with best available quality if requested resolutions are not available.", + ) @click.pass_context def cli(ctx: click.Context, **kwargs: Any) -> dl: return dl(ctx, **kwargs) @@ -322,6 +335,16 @@ class dl: vault_copy = vault.copy() del vault_copy["type"] + if vault_type.lower() == "api" and "decrypt_labs" in vault_name.lower(): + if "token" not in vault_copy or not vault_copy["token"]: + if config.decrypt_labs_api_key: + vault_copy["token"] = config.decrypt_labs_api_key + else: + self.log.warning( + f"No token provided for DecryptLabs vault '{vault_name}' and no global " + "decrypt_labs_api_key configured" + ) + if vault_type.lower() == "sqlite": try: self.vaults.load_critical(vault_type, **vault_copy) @@ -442,6 +465,7 @@ class dl: v_lang: list[str], a_lang: list[str], s_lang: list[str], + require_subs: list[str], forced_subs: bool, sub_format: Optional[Subtitle.Codec], video_only: bool, @@ -462,6 +486,7 @@ class dl: no_source: bool, workers: Optional[int], downloads: int, + best_available: bool, *_: Any, **__: Any, ) -> None: @@ -469,6 +494,10 @@ class dl: self.search_source = None start_time = time.time() + if require_subs and s_lang != ["all"]: + self.log.error("--require-subs and --s-lang cannot be used together") + sys.exit(1) + # Check if dovi_tool is available when hybrid mode is requested if any(r == Video.Range.HYBRID for r in range_): from unshackle.core.binaries import DoviTool @@ -703,8 +732,14 @@ class dl: res_list = ", ".join([f"{x}p" for x in missing_resolutions[:-1]]) + " or " res_list = f"{res_list}{missing_resolutions[-1]}p" plural = "s" if len(missing_resolutions) > 1 else "" - self.log.error(f"There's no {res_list} Video Track{plural}...") - sys.exit(1) + + if best_available: + self.log.warning( + f"There's no {res_list} Video Track{plural}, continuing with available qualities..." + ) + else: + self.log.error(f"There's no {res_list} Video Track{plural}...") + sys.exit(1) # choose best track by range and quality if any(r == Video.Range.HYBRID for r in range_): @@ -740,7 +775,21 @@ class dl: title.tracks.videos = selected_videos # filter subtitle tracks - if s_lang and "all" not in s_lang: + if require_subs: + missing_langs = [ + lang + for lang in require_subs + if not any(is_close_match(lang, [sub.language]) for sub in title.tracks.subtitles) + ] + + if missing_langs: + self.log.error(f"Required subtitle language(s) not found: {', '.join(missing_langs)}") + sys.exit(1) + + self.log.info( + f"Required languages found ({', '.join(require_subs)}), downloading all available subtitles" + ) + elif s_lang and "all" not in s_lang: missing_langs = [ lang_ for lang_ in s_lang @@ -1637,6 +1686,15 @@ class dl: del cdm_api["name"] del cdm_api["type"] + if "secret" not in cdm_api or not cdm_api["secret"]: + if config.decrypt_labs_api_key: + cdm_api["secret"] = config.decrypt_labs_api_key + else: + raise ValueError( + f"No secret provided for DecryptLabs CDM '{cdm_name}' and no global " + "decrypt_labs_api_key configured" + ) + # All DecryptLabs CDMs use DecryptLabsRemoteCDM return DecryptLabsRemoteCDM(service_name=service, vaults=self.vaults, **cdm_api) else: