"""Download settings and file-type handling.""" from __future__ import annotations from dataclasses import dataclass, field from pathlib import Path from sticker_downloader.errors import InvalidFileType STATIC = "webp" ANIMATED = "tgs" VIDEO = "webm" THUMBNAIL = "png" #: Every type the downloader knows how to save, in a stable display order. ALL_FILE_TYPES: tuple[str, ...] = (STATIC, ANIMATED, VIDEO, THUMBNAIL) FILE_TYPE_HELP: dict[str, str] = { STATIC: "static stickers", ANIMATED: "animated stickers (Lottie)", VIDEO: "video stickers", THUMBNAIL: "PNG thumbnails", } DEFAULT_OUTPUT_DIR = Path("downloads") DEFAULT_URL_FILE = Path("urls.txt") DEFAULT_CONCURRENCY = 8 DEFAULT_RETRIES = 3 METADATA_FILE_NAME = "pack.json" PACK_THUMBNAIL_STEM = "_pack_thumbnail" @dataclass(frozen=True) class DownloadConfig: """Everything that shapes a download run.""" file_types: frozenset[str] = field(default_factory=lambda: frozenset(ALL_FILE_TYPES)) output_dir: Path = DEFAULT_OUTPUT_DIR concurrency: int = DEFAULT_CONCURRENCY retries: int = DEFAULT_RETRIES overwrite: bool = False write_metadata: bool = True pack_thumbnail: bool = True emoji_names: bool = False dry_run: bool = False def __post_init__(self) -> None: unknown = sorted(self.file_types - set(ALL_FILE_TYPES)) if unknown: raise InvalidFileType(f"unknown file type(s): {', '.join(unknown)}") if not self.file_types: raise InvalidFileType("at least one file type is required") if self.concurrency < 1: raise ValueError("concurrency must be at least 1") if self.retries < 1: raise ValueError("retries must be at least 1") @property def sorted_file_types(self) -> list[str]: """The requested types in canonical order, for stable output.""" return [file_type for file_type in ALL_FILE_TYPES if file_type in self.file_types] def wants(self, file_type: str) -> bool: return file_type in self.file_types def parse_file_types(raw: str | None) -> frozenset[str]: """Turn user input such as ``"webp, png"``, ``"all"`` or ``""`` into a type set. Empty input and ``"all"`` both mean "every supported type", matching what the interactive prompt advertises. """ text = (raw or "").strip().lower() if not text or text == "all": return frozenset(ALL_FILE_TYPES) requested = [part.strip().lstrip(".") for part in text.replace(" ", ",").split(",")] selected = {part for part in requested if part} if not selected: raise InvalidFileType("no file types given") unknown = sorted(selected - set(ALL_FILE_TYPES)) if unknown: raise InvalidFileType( f"unknown file type(s): {', '.join(unknown)}. " f"Supported: {', '.join(ALL_FILE_TYPES)}, or 'all'." ) return frozenset(selected)