Files
2026-07-27 15:16:01 +02:00

89 lines
2.8 KiB
Python

"""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)