mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-14 13:05:09 +00:00
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user