diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py index a062983e..afeabb02 100644 --- a/app/library/DownloadQueue.py +++ b/app/library/DownloadQueue.py @@ -761,7 +761,7 @@ class DownloadQueue(metaclass=Singleton): """ Monitor the queue and pool for stale downloads and cancel them if needed. """ - if self.is_paused() or self.queue.empty(): + if self.is_paused(): return if not self.queue.empty(): diff --git a/app/library/LogWrapper.py b/app/library/LogWrapper.py index 3a8df8f1..d27e3155 100644 --- a/app/library/LogWrapper.py +++ b/app/library/LogWrapper.py @@ -1,4 +1,6 @@ import logging +import weakref +from collections.abc import Callable from dataclasses import dataclass @@ -8,6 +10,7 @@ class LogTarget: A data class that represents a logging target with its level and type. Attributes: + name (str): The name of the logging target. target: The logging target, which can be a logging.Logger instance or a callable. level (int): The logging level for the target. logger (bool): True if the target is a logging.Logger instance, False otherwise. @@ -15,7 +18,8 @@ class LogTarget: """ - target: "logging.Logger|callable" + name: str | None = None + target: logging.Logger|Callable level: int logger: bool @@ -55,17 +59,73 @@ class LogWrapper: """ targets: list[LogTarget] = [] + """A list of dictionaries where each dictionary represents a logging target with its level and type.""" - def add_target(self, target: "logging.Logger|callable", level: int = logging.DEBUG): + def __init__(self): + self.targets: list[LogTarget] = [] + weakref.finalize(self, LogWrapper.cleanup, self) + + def add_target(self, target: logging.Logger | Callable, level: int = logging.DEBUG): """ Adds a new logging target with the specified logging level. Args: - target (logging.Logger|callable): The logging target, which can be a logging.Logger instance or a callable. + target (logging.Logger|Callable): The logging target, which can be a logging.Logger instance or a Callable. level (int): The logging level for the target. Defaults to logging.DEBUG. """ - self.targets.append(LogTarget(target=target, level=level, logger=isinstance(target, logging.Logger))) + if not isinstance(target, logging.Logger | Callable): + msg = "Target must be a logging.Logger instance or a callable." + raise TypeError(msg) + + self.targets.append( + LogTarget( + name=target.name if isinstance(target, logging.Logger) else None, + target=target, + level=level, + logger=isinstance(target, logging.Logger), + ) + ) + + def cleanup(self, name: str = "yt-dlp"): + """ + Remove automatic handlers + + Args: + name (str): The name of the logger to clean up. Defaults to "ytdlp". + + """ + mgr = logging.root.manager + to_drop = [] + + for tgt in list(self.targets): + # only consider real Logger targets whose name + if tgt.logger and tgt.name and tgt.name.startswith(name): + name = tgt.name + logger = tgt.target + + # 1) detach all handlers. + for h in list(logger.handlers): + logger.removeHandler(h) + + # 2) restore default logger settings + logger.propagate = True + logger.setLevel(logging.NOTSET) + + # 3) purge logger & its children from the internal registry + for key in [k for k in mgr.loggerDict if k == name or k.startswith(name + ".")]: + mgr.loggerDict.pop(key, None) + + to_drop.append(tgt) + + # drop those targets from our wrapper + self.targets = [t for t in self.targets if t not in to_drop] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + self.cleanup() def has_targets(self): """ diff --git a/app/library/Utils.py b/app/library/Utils.py index 084d3460..78d5e5a9 100644 --- a/app/library/Utils.py +++ b/app/library/Utils.py @@ -109,6 +109,7 @@ def extract_info( no_archive: bool = False, follow_redirect: bool = False, sanitize_info: bool = False, + **kwargs, ) -> dict: """ Extracts video information from the given URL. @@ -120,12 +121,14 @@ def extract_info( no_archive (bool): Disable download archive. follow_redirect (bool): Follow URL redirects. sanitize_info (bool): Sanitize the extracted information + **kwargs: Additional arguments. Returns: dict: Video information. """ - log_wrapper = LogWrapper() + is_recursive = bool(kwargs.get("log_wrapper")) + log_wrapper = kwargs.get("log_wrapper") or LogWrapper() params: dict = { **config, @@ -141,32 +144,37 @@ def extract_info( for key in keys_to_remove: params.pop(key, None) - id = None - idDict = get_archive_id(url=url) - if idDict.get("id"): - id = f".{idDict['id']}" - - log_wrapper.add_target(target=logging.getLogger(f"yt-dlp{id}"), level=logging.DEBUG if debug else logging.WARNING) - if debug: params["verbose"] = True else: params["quiet"] = True - if "callback" in params: - if isinstance(params["callback"], dict): - log_wrapper.add_target( - target=params["callback"]["func"], - level=params["callback"]["level"] or logging.ERROR, - ) - else: - log_wrapper.add_target(target=params["callback"], level=logging.ERROR) - del params["callback"] + if not is_recursive: + archive_id = None + idDict = get_archive_id(url=url) + if idDict.get("id"): + archive_id = f".{idDict['id']}" + + _logger = logging.getLogger(f"yt-dlp{archive_id}") + _logger.propagate = False + log_wrapper.add_target(target=_logger, level=logging.DEBUG if debug else logging.WARNING) + + if "callback" in params: + if isinstance(params["callback"], dict): + log_wrapper.add_target( + target=params["callback"]["func"], + level=params["callback"]["level"] or logging.ERROR, + ) + else: + log_wrapper.add_target(target=params["callback"], level=logging.ERROR) if log_wrapper.has_targets(): - if "logger" in params: + if "logger" in params and not is_recursive: log_wrapper.add_target(target=params["logger"], level=logging.DEBUG) + if "callback" in params: + del params["callback"] + params["logger"] = log_wrapper if no_archive and "download_archive" in params: @@ -182,6 +190,7 @@ def extract_info( no_archive=no_archive, follow_redirect=follow_redirect, sanitize_info=sanitize_info, + log_wrapper=log_wrapper, ) if not data: