"""Hook handlers for download progress and postprocessing events.""" import logging import re from typing import TYPE_CHECKING, Any from .utils import DEBUG_MESSAGE_PREFIXES, YTDLP_PROGRESS_FIELDS, create_debug_safe_dict if TYPE_CHECKING: from multiprocessing import Queue class HookHandlers: """Manages yt-dlp hook callbacks for progress tracking and postprocessing.""" def __init__(self, download_id: str, status_queue: "Queue[Any]", logger: logging.Logger, debug: bool = False): """ Initialize hook handlers. Args: download_id: Unique identifier for the download status_queue: Multiprocessing queue for status updates logger: Logger instance for this download debug: Whether to enable debug logging """ self.id = download_id self.status_queue = status_queue self.logger = logger self.debug = debug def progress_hook(self, data: dict[str, Any]) -> None: if self.debug: try: d_safe = create_debug_safe_dict(data) self.logger.debug( "Received a yt-dlp progress update for download '%s'.", self.id, extra={"download": {"download_id": self.id, "hook": "progress", "status": d_safe}}, ) except Exception as e: self.logger.exception( "Failed to create progress hook debug info for download '%s'.", self.id, extra={ "download": {"download_id": self.id, "hook": "progress", "exception_type": type(e).__name__} }, ) self.status_queue.put( { "id": self.id, "action": "progress", **{k: v for k, v in data.items() if k in YTDLP_PROGRESS_FIELDS}, } ) def postprocessor_hook(self, data: dict[str, Any]) -> None: info_dict = data.get("info_dict", {}) filepath = info_dict.get("filepath") status: dict[str, Any] = { "id": self.id, "action": "postprocessing", **{k: v for k, v in data.items() if k in YTDLP_PROGRESS_FIELDS}, "status": "postprocessing", } if filepath: status["filepath"] = filepath if self.debug: try: d_safe = create_debug_safe_dict(data) d_safe["postprocessor"] = data.get("postprocessor") self.logger.debug( "Received a yt-dlp post-processing update for download '%s'.", self.id, extra={"download": {"download_id": self.id, "hook": "postprocessor", "status": d_safe}}, ) except Exception as e: self.logger.exception( "Failed to create postprocessor hook debug info for download '%s'.", self.id, extra={ "download": { "download_id": self.id, "hook": "postprocessor", "exception_type": type(e).__name__, } }, ) self.status_queue.put(status) def post_hook(self, filename: str) -> None: self.status_queue.put({"id": self.id, "status": "finished", "final_name": filename}) class NestedLogger: """ Logger adapter for yt-dlp that adjusts log levels based on message prefixes. yt-dlp logs everything through a custom logger. This adapter maps certain message types to appropriate log levels and strips redundant prefixes. """ def __init__(self, logger: logging.Logger) -> None: self.logger: logging.Logger = logger def debug(self, msg: str) -> None: levelno: int = logging.DEBUG if any(msg.startswith(x) for x in DEBUG_MESSAGE_PREFIXES) else logging.INFO self.logger.log(level=levelno, msg=re.sub(r"^\[(debug|info)\] ", "", msg, flags=re.IGNORECASE)) def error(self, msg: str) -> None: """Log an error message.""" self.logger.error(msg) def warning(self, msg: str) -> None: """Log a warning message.""" self.logger.warning(msg)