ytptube/app/library/downloads/hooks.py

120 lines
4.3 KiB
Python

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