feat(dl): allow --slow to accept custom delay range

--slow now supports an optional MIN-MAX range (e.g., --slow 20-40) for custom delay between title downloads. Bare --slow retains the original 60-120s default. Minimum delay enforced at 20 seconds.
This commit is contained in:
imSp4rky
2026-03-30 16:25:29 -06:00
parent c7fd2a904c
commit 47b3390bd0
5 changed files with 51 additions and 9 deletions
+8 -7
View File
@@ -58,7 +58,7 @@ from unshackle.core.utilities import (find_font_with_fallbacks, get_debug_logger
is_close_match, suggest_font_packages, time_elapsed_since)
from unshackle.core.utils import tags
from unshackle.core.utils.click_types import (AUDIO_CODEC_LIST, LANGUAGE_RANGE, QUALITY_LIST, SEASON_RANGE,
ContextData, MultipleChoice, MultipleVideoCodecChoice,
SLOW_DELAY_RANGE, ContextData, MultipleChoice, MultipleVideoCodecChoice,
SubtitleCodecChoice)
from unshackle.core.utils.collections import merge_dict
from unshackle.core.utils.selector import select_multiple
@@ -460,10 +460,11 @@ class dl:
@click.option("-ad", "--audio-description", is_flag=True, default=False, help="Download audio description tracks.")
@click.option(
"--slow",
is_flag=True,
default=False,
help="Add a 60-120 second delay between each Title download to act more like a real device. "
"This is recommended if you are downloading high-risk titles or streams.",
type=SLOW_DELAY_RANGE,
default=None,
help="Add a delay between each Title download to act more like a real device. "
"Use --slow for a 60-120s delay, or --slow MIN-MAX (e.g., --slow 20-40) for a custom range. "
"Minimum delay is 20 seconds.",
)
@click.option(
"--list",
@@ -1017,7 +1018,7 @@ class dl:
no_chapters: bool,
no_video: bool,
audio_description: bool,
slow: bool,
slow: Optional[tuple[int, int]],
list_: bool,
list_titles: bool,
skip_dl: bool,
@@ -1393,7 +1394,7 @@ class dl:
)
if slow and i != 0:
delay = random.randint(60, 120)
delay = random.randint(slow[0], slow[1])
with console.status(f"Delaying by {delay} seconds..."):
time.sleep(delay)
+1 -1
View File
@@ -289,7 +289,7 @@ def _perform_download(
no_chapters=params.get("no_chapters", False),
no_video=params.get("no_video", False),
audio_description=params.get("audio_description", False),
slow=params.get("slow", False),
slow=params.get("slow", None),
list_=False,
list_titles=False,
skip_dl=params.get("skip_dl", False),
+1 -1
View File
@@ -44,7 +44,7 @@ DEFAULT_DOWNLOAD_PARAMS = {
"no_chapters": False,
"no_video": False,
"audio_description": False,
"slow": False,
"slow": None,
"split_audio": None,
"skip_dl": False,
"export": None,
+18
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import re
from pathlib import Path
import click
@@ -29,6 +30,23 @@ class Services(click.MultiCommand):
# Click-specific methods
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
"""Preprocess --slow to support optional range value before Click parses args."""
processed = []
i = 0
while i < len(args):
if args[i] == "--slow":
if i + 1 < len(args) and re.match(r"^\d+-\d+$", args[i + 1]):
processed.append(f"--slow={args[i + 1]}")
i += 2
else:
processed.append("--slow=60-120")
i += 1
else:
processed.append(args[i])
i += 1
return super().parse_args(ctx, processed)
def list_commands(self, ctx: click.Context) -> list[str]:
"""Returns a list of all available Services as command names for Click.
+23
View File
@@ -360,9 +360,32 @@ class MultipleChoice(click.Choice):
return super(self).shell_complete(ctx, param, incomplete)
class SlowDelayRange(click.ParamType):
"""Parses a delay range string like '20-40' into a tuple of (min, max) seconds."""
name = "delay_range"
def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> tuple[int, int]:
if isinstance(value, tuple):
return value
match = re.match(r"^(\d+)-(\d+)$", str(value))
if not match:
self.fail(f"'{value}' is not a valid range. Use format: MIN-MAX (e.g., 20-40)", param, ctx)
low, high = int(match.group(1)), int(match.group(2))
if low < 20:
self.fail(f"Minimum delay must be at least 20 seconds, got {low}", param, ctx)
if low > high:
self.fail(f"Min ({low}) cannot be greater than max ({high})", param, ctx)
return (low, high)
SEASON_RANGE = SeasonRange()
LANGUAGE_RANGE = LanguageRange()
QUALITY_LIST = QualityList()
AUDIO_CODEC_LIST = AudioCodecList(Audio.Codec)
SLOW_DELAY_RANGE = SlowDelayRange()
# VIDEO_CODEC_CHOICE will be created dynamically when imported