diff --git a/app/library/Download.py b/app/library/Download.py index 723e5d6a..5b6c51ad 100644 --- a/app/library/Download.py +++ b/app/library/Download.py @@ -15,7 +15,7 @@ from .config import Config from .Events import EventBus, Events from .ffprobe import ffprobe from .ItemDTO import ItemDTO -from .Utils import extract_info +from .Utils import extract_info, load_cookies from .YTDLPOpts import YTDLPOpts LOG = logging.getLogger("Download") @@ -49,7 +49,6 @@ class Download: template: str = None template_chapter: str = None info: ItemDTO = None - default_ytdl_opts: dict = None debug: bool = False temp_path: str = None cancelled: bool = False @@ -93,9 +92,7 @@ class Download: self.preset = info.preset self.info = info self.id = info._id - self.default_ytdl_opts = config.ytdl_options self.debug = bool(config.debug) - self.archive = bool(config.keep_archive) self.debug_ytdl = bool(config.ytdl_debug) self.cancelled = False self.tmpfilename = None @@ -143,11 +140,11 @@ class Download: try: params = ( YTDLPOpts.get_instance() - .preset(self.preset) + .preset(self.preset, with_cookies=not self.info.cookies) .add({"break_on_existing": True}) - .add_cli(self.info.cli, from_user=True) + .add_cli(args=self.info.cli, from_user=True) .add( - { + config={ "color": "no_color", "paths": { "home": self.download_dir, @@ -165,19 +162,6 @@ class Download: .get_all() ) - if not self.info_dict: - self.logger.info(f"Extracting info for '{self.info.url}'.") - info = extract_info( - config=params, - url=self.info.url, - debug=self.debug, - no_archive=not self.archive, - follow_redirect=True, - ) - - if info: - self.info_dict = info - params.update( { "progress_hooks": [self._progress_hook], @@ -199,8 +183,25 @@ class Download: with open(cookie_file, "w") as f: f.write(self.info.cookies) params["cookiefile"] = f.name - except ValueError as e: - self.logger.error(f"Failed to create cookie file for '{self.info.id}: {self.info.title}'. '{e!s}'.") + + load_cookies(cookie_file) + except Exception as e: + err_msg = f"Failed to create cookie file for '{self.info.id}: {self.info.title}'. '{e!s}'." + self.logger.error(err_msg) + raise ValueError(err_msg) from e + + if not self.info_dict: + self.logger.info(f"Extracting info for '{self.info.url}'.") + info = extract_info( + config=params, + url=self.info.url, + debug=self.debug, + no_archive=not params.get("download_archive", False), + follow_redirect=True, + ) + + if info: + self.info_dict = info if self.is_live or self.is_manifestless: hasDeletedOptions = False @@ -217,7 +218,7 @@ class Download: ) self.logger.info( - f'Task id="{self.info.id}" PID="{os.getpid()}" title="{self.info.title}" preset="{self.preset}" started.' + f'Task id="{self.info.id}" PID="{os.getpid()}" title="{self.info.title}" preset="{self.preset}" cookies="{bool(params.get("cookiefile"))}" started.' ) self.logger.debug(f"Params before passing to yt-dlp. {params}") diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py index 5e87678d..df84ba68 100644 --- a/app/library/DownloadQueue.py +++ b/app/library/DownloadQueue.py @@ -22,7 +22,7 @@ from .Events import EventBus, Events, info from .ItemDTO import Item, ItemDTO from .Presets import Presets from .Singleton import Singleton -from .Utils import arg_converter, calc_download_path, extract_info, is_downloaded +from .Utils import arg_converter, calc_download_path, extract_info, is_downloaded, load_cookies from .YTDLPOpts import YTDLPOpts LOG = logging.getLogger("DownloadQueue") @@ -301,33 +301,38 @@ class DownloadQueue(metaclass=Singleton): extras=item.extras, ) - dlInfo: Download = Download(info=dl, info_dict=entry) + try: + dlInfo: Download = Download(info=dl, info_dict=entry) - if dlInfo.info.live_in or "is_upcoming" == entry.get("live_status"): - dlInfo.info.status = "not_live" - itemDownload = self.done.put(dlInfo) - NotifyEvent = Events.COMPLETED - log_message = f"{dl.title or dl.id or dl._id}: stream is not live yet." - if dlInfo.info.live_in: - log_message += f" Will start in {dlInfo.info.live_in}." + if dlInfo.info.live_in or "is_upcoming" == entry.get("live_status"): + dlInfo.info.status = "not_live" + itemDownload = self.done.put(dlInfo) + NotifyEvent = Events.COMPLETED + log_message = f"{dl.title or dl.id or dl._id}: stream is not live yet." + if dlInfo.info.live_in: + log_message += f" Will start in {dlInfo.info.live_in}." - await self._notify.emit( - Events.LOG_INFO, - data=info(msg=log_message, data=itemDownload.info.serialize()), - ) - elif self.config.allow_manifestless is False and is_manifestless is True: - dlInfo.info.status = "error" - dlInfo.info.error = "Video is in post-live manifestless mode." - itemDownload = self.done.put(dlInfo) - NotifyEvent = Events.COMPLETED - else: - NotifyEvent = Events.ADDED - itemDownload = self.queue.put(dlInfo) - self.event.set() + await self._notify.emit( + Events.LOG_INFO, + data=info(msg=log_message, data=itemDownload.info.serialize()), + ) + elif self.config.allow_manifestless is False and is_manifestless is True: + dlInfo.info.status = "error" + dlInfo.info.error = "Video is in post-live manifestless mode." + itemDownload = self.done.put(dlInfo) + NotifyEvent = Events.COMPLETED + else: + NotifyEvent = Events.ADDED + itemDownload = self.queue.put(dlInfo) + self.event.set() - await self._notify.emit(NotifyEvent, data=itemDownload.info.serialize()) + await self._notify.emit(NotifyEvent, data=itemDownload.info.serialize()) - return {"status": "ok"} + return {"status": "ok"} + except Exception as e: + LOG.exception(e) + LOG.error(f"Failed to download item. '{e!s}'") + return {"status": "error", "msg": str(e)} if eventType.startswith("url"): return await self.add(item=item.new_with(url=entry.get("url")), already=already) @@ -363,9 +368,6 @@ class DownloadQueue(metaclass=Singleton): if _preset.template and not item.template: item.template = _preset.template - if _preset.cookies and not item.cookies: - item.cookies = _preset.cookies - yt_conf = {} cookie_file = os.path.join(self.config.temp_path, f"c_{uuid.uuid4().hex}.txt") @@ -380,15 +382,6 @@ class DownloadQueue(metaclass=Singleton): already.add(item.url) try: - downloaded, id_dict = self._is_downloaded(item.url) - if downloaded is True and id_dict: - message = f"This url with ID '{id_dict.get('id')}' has been downloaded already and recorded in archive." - LOG.info(message) - return {"status": "error", "msg": message} - - started = time.perf_counter() - LOG.debug(f"extract_info: checking '{item.url}'.") - logs = [] yt_conf = { @@ -396,19 +389,34 @@ class DownloadQueue(metaclass=Singleton): "func": lambda _, msg: logs.append(msg), "level": logging.WARNING, }, - **YTDLPOpts.get_instance().preset(name=item.preset).add_cli(args=item.cli, from_user=True).get_all(), + **YTDLPOpts.get_instance() + .preset(name=item.preset, with_cookies=not item.cookies) + .add_cli(args=item.cli, from_user=True) + .get_all(), } + downloaded, id_dict = self._is_downloaded(file=yt_conf.get("download_archive", None), url=item.url) + if downloaded is True and id_dict: + message = f"This url with ID '{id_dict.get('id')}' has been downloaded already and recorded in archive." + LOG.info(message) + return {"status": "error", "msg": message} + + started = time.perf_counter() + if item.cookies: try: async with await anyio.open_file(cookie_file, "w") as f: await f.write(item.cookies) yt_conf["cookiefile"] = f.name - except ValueError as e: - msg = f"Failed to create cookie file for '{self.info.id}: {self.info.title}'. '{e!s}'." + + load_cookies(cookie_file) + except Exception as e: + msg = f"Failed to create cookie file for '{item.url}'. '{e!s}'." LOG.error(msg) return {"status": "error", "msg": msg} + LOG.info(f"Checking '{item.url}' with {'cookies' if yt_conf.get('cookiefile') else 'no cookies'}.") + entry = await asyncio.wait_for( fut=asyncio.get_running_loop().run_in_executor( None, @@ -691,18 +699,19 @@ class DownloadQueue(metaclass=Singleton): if self.event: self.event.set() - def _is_downloaded(self, url: str) -> tuple[bool, dict | None]: + def _is_downloaded(self, url: str, file: str | None = None) -> tuple[bool, dict | None]: """ Check if the url has been downloaded already. Args: url (str): The url to check. + file (str | None): The archive file to check. Returns: tuple: A tuple with the status of the operation and the id of the downloaded item. """ - if not url or not self.config.keep_archive: + if not url or not file: return False, None - return is_downloaded(self.config.ytdl_options.get("download_archive", None), url) + return is_downloaded(file, url) diff --git a/app/library/Events.py b/app/library/Events.py index 0fb1afbd..dfd27046 100644 --- a/app/library/Events.py +++ b/app/library/Events.py @@ -7,7 +7,7 @@ from dataclasses import dataclass, field from .Singleton import Singleton -LOG = logging.getLogger("EventsSubscriber") +LOG = logging.getLogger("Events") def error(msg: str, data: dict | None = None) -> dict: @@ -152,6 +152,16 @@ class Events: Events.CLI_OUTPUT, ] + def only_debug() -> list: + """ + High frequency events that should only be logged in debug mode. + + Returns: + list: The list of debug events. + + """ + return [Events.UPDATED] + @dataclass(kw_only=True) class Event: @@ -226,9 +236,16 @@ class EventBus(metaclass=Singleton): _listeners: dict[str, list[str, EventListener]] = {} """The listeners for the events.""" + debug: bool = False + """Whether to log debug messages or not.""" + def __init__(self): EventBus._instance = self + from .config import Config + + self.debug = Config.get_instance().debug + @staticmethod def get_instance() -> "EventBus": """ @@ -353,7 +370,9 @@ class EventBus(metaclass=Singleton): return [] ev = Event(event=event, data=data) - LOG.debug(f"Emitting event '{ev.id}: {ev.event}'.", extra={"data": data}) + + if self.debug or event not in Events.only_debug(): + LOG.debug(f"Emitting event '{ev.id}: {ev.event}'.", extra={"data": data}) tasks = [] for handler in self._listeners[event].values(): diff --git a/app/library/HttpAPI.py b/app/library/HttpAPI.py index b2a3a470..926eac03 100644 --- a/app/library/HttpAPI.py +++ b/app/library/HttpAPI.py @@ -46,6 +46,7 @@ from .Utils import ( get_file_sidecar, get_files, get_mime_type, + load_cookies, read_logfile, validate_url, validate_uuid, @@ -530,8 +531,8 @@ class HttpAPI(Common): opts = {} - if self.config.ytdl_options.get("proxy", None): - opts["proxy"] = self.config.ytdl_options.get("proxy", None) + if ytdlp_proxy := self.config.get_ytdlp_args().get("proxy", None): + opts["proxy"] = ytdlp_proxy ytdlp_opts = YTDLPOpts.get_instance().preset(name=preset, with_cookies=True).add(opts).get_all() @@ -608,7 +609,7 @@ class HttpAPI(Common): id, extract_info( config={ - "proxy": self.config.ytdl_options.get("proxy", None), + "proxy": self.config.get_ytdlp_args().get("proxy", None), "simulate": True, "dump_single_json": True, }, @@ -1411,16 +1412,16 @@ class HttpAPI(Common): return web.json_response(data={"error": str(e)}, status=web.HTTPForbidden.status_code) try: + ytdlp_args = self.config.get_ytdlp_args() opts = { - "proxy": self.config.ytdl_options.get("proxy", None), + "proxy": ytdlp_args.get("proxy", None), "headers": { - "User-Agent": self.config.ytdl_options.get( + "User-Agent": ytdlp_args.get( "user_agent", request.headers.get("User-Agent", f"YTPTube/{self.config.version}") ), }, } - logging.getLogger("httpx").setLevel(logging.WARNING) async with httpx.AsyncClient(**opts) as client: LOG.debug(f"Fetching thumbnail from '{url}'.") response = await client.request(method="GET", url=url) @@ -1473,14 +1474,14 @@ class HttpAPI(Common): }, ) + ytdlp_args = self.config.get_ytdlp_args() opts = { - "proxy": self.config.ytdl_options.get("proxy", None), + "proxy": ytdlp_args.get("proxy", None), "headers": { - "User-Agent": self.config.ytdl_options.get("user_agent", f"YTPTube/{self.config.version}"), + "User-Agent": ytdlp_args.get("user_agent", f"YTPTube/{self.config.version}"), }, } - logging.getLogger("httpx").setLevel(logging.WARNING) async with httpx.AsyncClient(**opts) as client: response = await client.request(method="GET", url=backend, follow_redirects=True) @@ -1616,7 +1617,9 @@ class HttpAPI(Common): Response: The response object """ - cookie_file = self.config.ytdl_options.get("cookiefile", None) + ytdlp_args = self.config.get_ytdlp_args() + + cookie_file = ytdlp_args.get("cookiefile", None) if not cookie_file: return web.json_response(data={"message": "No cookie file provided."}, status=web.HTTPForbidden.status_code) @@ -1627,32 +1630,24 @@ class HttpAPI(Common): ) try: - import http.cookiejar - - cookies = http.cookiejar.MozillaCookieJar(cookie_file, None, None) - cookies.load() - except Exception as e: - LOG.exception(e) - LOG.error(f"failed to load cookies from '{cookie_file}'. '{e!s}'.") - return web.json_response( - data={"message": "Failed to load cookies"}, - status=web.HTTPInternalServerError.status_code, - ) + _, cookies = load_cookies(cookie_file) + except ValueError as e: + LOG.error(str(e)) + return web.json_response(data={"message": str(e)}, status=web.HTTPInternalServerError.status_code) url = "https://www.youtube.com/account" try: opts = { - "proxy": self.config.ytdl_options.get("proxy", None), + "proxy": ytdlp_args.get("proxy", None), "headers": { - "User-Agent": self.config.ytdl_options.get( + "User-Agent": ytdlp_args.get( "user_agent", request.headers.get("User-Agent", f"YTPTube/{self.config.version}") ), }, "cookies": cookies, } - logging.getLogger("httpx").setLevel(logging.WARNING) async with httpx.AsyncClient(**opts) as client: LOG.debug(f"Checking '{url}' redirection.") response = await client.request(method="GET", url=url, follow_redirects=False) diff --git a/app/library/HttpSocket.py b/app/library/HttpSocket.py index 40191789..a8c39c1e 100644 --- a/app/library/HttpSocket.py +++ b/app/library/HttpSocket.py @@ -236,13 +236,22 @@ class HttpSocket(Common): @ws_event async def archive_item(self, _: str, data: dict): - if not isinstance(data, dict) or "url" not in data or not self.config.keep_archive: + if not isinstance(data, dict) or "url" not in data: return - if not isinstance(self.config.ytdl_options, dict): - self.config.ytdl_options = {} + from .YTDLPOpts import YTDLPOpts - file: str = self.config.ytdl_options.get("download_archive", None) + params = YTDLPOpts.get_instance() + + if "preset" in data and isinstance(data["preset"], str): + params.preset(name=data["preset"]) + + if "cli" in data and isinstance(data["cli"], str) and len(data["cli"]) > 1: + params.add_cli(data["cli"], from_user=True) + + params = params.get_all() + + file: str = params.get("download_archive", None) if not file: return @@ -267,8 +276,9 @@ class HttpSocket(Common): if not previouslyArchived: async with await anyio.open_file(manual_archive, "a") as f: await f.write(f"{idDict['archive_id']} - at: {datetime.now(UTC).isoformat()}\n") - - LOG.info(f"Archiving url '{data['url']}' with id '{idDict['archive_id']}'.") + LOG.info(f"Archiving url '{data['url']}' with id '{idDict['archive_id']}'.") + else: + LOG.info(f"URL '{data['url']}' with id '{idDict['archive_id']}' already archived.") @ws_event async def connect(self, sid: str, _=None): diff --git a/app/library/Notifications.py b/app/library/Notifications.py index 20deb566..a62e1bf3 100644 --- a/app/library/Notifications.py +++ b/app/library/Notifications.py @@ -368,9 +368,6 @@ class Notification(metaclass=Singleton): if "form" == target.request.type.lower(): reqBody["data"]["data"] = self._encoder.encode(reqBody["data"]["data"]) - if not self._debug: - logging.getLogger("httpx").setLevel(logging.WARNING) - response = await self._client.request(**reqBody) respData = {"url": target.request.url, "status": response.status_code, "text": response.text} diff --git a/app/library/Utils.py b/app/library/Utils.py index b8e79806..d0022785 100644 --- a/app/library/Utils.py +++ b/app/library/Utils.py @@ -12,6 +12,7 @@ import shlex import socket import uuid from functools import lru_cache +from http.cookiejar import MozillaCookieJar from typing import Any import yt_dlp @@ -28,7 +29,6 @@ REMOVE_KEYS: list = [ "progress_hooks": "--progress_hooks", "postprocessor_hooks": "--postprocessor_hooks", "post_hooks": "--post_hooks", - "download_archive": "--download_archive", }, { "quiet": "-q, --quiet", @@ -38,7 +38,6 @@ REMOVE_KEYS: list = [ "simulate": "--simulate", "noprogress": "--no-progress", "wait_for_video": "--wait-for-video", - "mark_watched": "--mark-watched", "color": "--color", "verbose": "-v, --verbose", "debug_printtraffic": "--print-traffic", @@ -52,9 +51,6 @@ REMOVE_KEYS: list = [ "print_to_file": "--print-to-file", "cookiesfrombrowser": "--cookies-from-browser", }, - { - "cookiefile": "--cookies", - }, ] YTDLP_INFO_CLS: yt_dlp.YoutubeDL = None @@ -1051,3 +1047,26 @@ async def tail_log(file: str, emitter: callable, sleep_time: float = 0.5): "datetime": dt_match.group(1) if dt_match else None, } ) + + +def load_cookies(file: str) -> tuple[bool, MozillaCookieJar]: + """ + Validate and load a cookie file. + + Args: + file (str): The cookie file path. + + Returns: + bool: True if the cookie file is valid. + + """ + try: + from http.cookiejar import MozillaCookieJar + + cookies = MozillaCookieJar(file, None, None) + cookies.load() + + return (True, cookies) + except Exception as e: + msg = f"Invalid cookie file '{file}'. '{e!s}'" + raise ValueError(msg) from e diff --git a/app/library/YTDLPOpts.py b/app/library/YTDLPOpts.py index 266e9edd..4e4003ba 100644 --- a/app/library/YTDLPOpts.py +++ b/app/library/YTDLPOpts.py @@ -4,7 +4,7 @@ from pathlib import Path from .config import Config from .Presets import Presets from .Singleton import Singleton -from .Utils import REMOVE_KEYS, arg_converter, calc_download_path, merge_dict +from .Utils import REMOVE_KEYS, arg_converter, calc_download_path, load_cookies, merge_dict LOG = logging.getLogger("YTDLPOpts") @@ -16,6 +16,12 @@ class YTDLPOpts(metaclass=Singleton): _preset_opts: dict = {} """The preset options.""" + _item_cli: list = [] + """The item cli options.""" + + _preset_cli: str = "" + """The preset cli options.""" + _instance = None """The instance of the class.""" @@ -38,7 +44,7 @@ class YTDLPOpts(metaclass=Singleton): def add_cli(self, args: str, from_user: int | bool = False) -> "YTDLPOpts": """ - Prase and add yt-dlp cli options to the item options. + Parse and add yt-dlp cli options to the item options. Args: args (str): The cli options to add @@ -51,12 +57,13 @@ class YTDLPOpts(metaclass=Singleton): if not args or len(args) < 2 or not isinstance(args, str): return self - removed_options = [] + try: + arg_converter(args=args, level=from_user) + except Exception as e: + msg = f"Invalid cli options for were given. '{e!s}'." + raise ValueError(msg) from e - self._item_opts.update(arg_converter(args=args, level=from_user, removed_options=removed_options)) - - if len(removed_options) > 0: - LOG.warning("Removed the following options: '%s'.", ", ".join(removed_options)) + self._item_cli.append(args) return self @@ -108,13 +115,9 @@ class YTDLPOpts(metaclass=Singleton): if preset.cli: try: - removed_options = [] - self._preset_opts = arg_converter(args=preset.cli, level=True, removed_options=removed_options) - if len(removed_options) > 0: - LOG.warning( - "Removed the following options '%s' from preset '%s'.", ", ".join(removed_options), preset.name - ) - + arg_converter(args=preset.cli, level=True) + self._preset_opts = {} + self._preset_cli = preset.cli except Exception as e: msg = f"Invalid cli options for preset '{preset.name}'. '{e!s}'." raise ValueError(msg) from e @@ -128,6 +131,8 @@ class YTDLPOpts(metaclass=Singleton): with open(file, "w") as f: f.write(preset.cookies) + load_cookies(file) + self._preset_opts["cookiefile"] = str(file) if preset.format: @@ -155,20 +160,55 @@ class YTDLPOpts(metaclass=Singleton): dict: The options """ - default_opts = self._config.ytdl_options + default_opts = {} default_opts["paths"] = {"home": self._config.download_path, "temp": self._config.temp_path} default_opts["outtmpl"] = { "default": self._config.output_template, "chapter": self._config.output_template_chapter, } - data = merge_dict(self._item_opts, merge_dict(self._preset_opts, default_opts)) + if not isinstance(self._item_cli, list): + self._item_cli = [] + + merge = [] + if self._config._ytdlp_cli_mutable and len(self._config._ytdlp_cli_mutable) > 1: + merge.append(self._config._ytdlp_cli_mutable) + + if self._preset_cli and len(self._preset_cli) > 1: + merge.append(self._preset_cli) + + if len(merge) > 0: + # prepend the cli options to the list + self._item_cli = merge + self._item_cli + + user_cli = {} + + if len(self._item_cli) > 0: + try: + removed_options = [] + user_cli = arg_converter(args="\n".join(self._item_cli), level=True, removed_options=removed_options) + + if len(removed_options) > 0: + LOG.warning("Removed the following options: '%s'.", ", ".join(removed_options)) + except Exception as e: + msg = f"Invalid cli options were given. '{e!s}'." + raise ValueError(msg) from e + + data = merge_dict(user_cli, merge_dict(self._item_opts, merge_dict(self._preset_opts, default_opts))) if not keep: self.presets_opts = {} self._item_opts = {} + self._item_cli = [] + self._preset_cli = "" - if "format" in data and data["format"] in ["not_set", "default"]: - data["format"] = None + if "format" in data: + if data["format"] in ["not_set", "default", "best"]: + data["format"] = None + + if data["format"] == "-best": + data["format"] = data["format"][1:] + + LOG.debug(f"Parsed yt-dlp options are: '{data!s}'.") return data diff --git a/app/library/config.py b/app/library/config.py index 39558720..169c5e8a 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -11,7 +11,7 @@ from pathlib import Path import coloredlogs from dotenv import load_dotenv -from .Utils import FileLogFormatter, arg_converter +from .Utils import FileLogFormatter, arg_converter, load_cookies from .version import APP_VERSION @@ -112,9 +112,6 @@ class Config: __instance = None "The instance of the class." - ytdl_options: dict = {} - "The options to use for yt-dlp." - new_version_available: bool = False "A new version of the application is available." @@ -154,6 +151,9 @@ class Config: ytdlp_cli: str = "" """The command line options to use for yt-dlp.""" + _ytdlp_cli_mutable: str = "" + """The command line options to use for yt-dlp.""" + pictures_backends: list[str] = [ "https://unsplash.it/1920/1080?random", "https://picsum.photos/1920/1080", @@ -178,6 +178,7 @@ class Config: "new_version_available", "started", "ytdlp_cli", + "_ytdlp_cli_mutable", ) "The variables that are immutable." @@ -328,22 +329,20 @@ class Config: except Exception as e: LOG.error(f"Error starting debugpy server at '0.0.0.0:{self.debugpy_port}'. {e}") + ytdl_options = {} opts_file: str = os.path.join(self.config_path, "ytdlp.cli") if os.path.exists(opts_file) and os.path.getsize(opts_file) > 2: LOG.info(f"Loading yt-dlp custom options from '{opts_file}'.") with open(opts_file) as f: self.ytdlp_cli = f.read().strip() if self.ytdlp_cli: + self._ytdlp_cli_mutable = self.ytdlp_cli try: removed_options = [] - self.ytdl_options = arg_converter( - args=self.ytdlp_cli, - level=1, - removed_options=removed_options, - ) + ytdl_options = arg_converter(args=self.ytdlp_cli, level=True, removed_options=removed_options) try: - LOG.debug("Parsed yt-dlp cli options '%s'.", self.ytdl_options) + LOG.debug("Parsed yt-dlp cli options '%s'.", ytdl_options) except Exception: pass @@ -359,17 +358,28 @@ class Config: else: LOG.info(f"No yt-dlp custom options found at '{opts_file}'.") - self.ytdl_options["socket_timeout"] = self.socket_timeout + self._ytdlp_cli_mutable += f"\n--socket-timeout {self.socket_timeout}" if self.keep_archive: LOG.info("keep archive option is enabled.") - self.ytdl_options["download_archive"] = os.path.join(self.config_path, "archive.log") + archive_file: str = os.path.join(self.config_path, "archive.log") + self._ytdlp_cli_mutable += f"\n--download-archive {archive_file}" - cookiesFile: str = os.path.join(self.config_path, "cookies.txt") + if cookies_file := ytdl_options.get("cookiefile", None): + if os.path.exists(cookies_file) and os.path.getsize(cookies_file) > 2: + LOG.info(f"Using cookies from '{cookies_file}'.") + load_cookies(cookies_file) + self._ytdlp_cli_mutable += f"\n--cookies {cookies_file}" + else: + LOG.warning(f"Invalid cookie file '{cookies_file}' specified.") + ytdl_options.pop("cookiefile", None) - if os.path.exists(cookiesFile) and self.ytdl_options.get("cookiefile", None) is None: - LOG.info(f"Using cookies from '{cookiesFile}' as default.") - self.ytdl_options["cookiefile"] = cookiesFile + if not ytdl_options.get("cookiefile", None): + cookies_file: str = os.path.join(self.config_path, "cookies.txt") + if os.path.exists(cookies_file) and os.path.getsize(cookies_file) > 2: + LOG.info(f"Using cookies from '{cookies_file}'.") + load_cookies(cookies_file) + self._ytdlp_cli_mutable += f"\n--cookies {cookies_file}" if self.temp_keep: LOG.info("Keep temp files option is enabled.") @@ -412,6 +422,9 @@ class Config: self.started = time.time() + logging.getLogger("httpx").setLevel(logging.WARNING) + logging.getLogger("httpcore").setLevel(logging.INFO) + def _get_attributes(self) -> dict: attrs: dict = {} vClass: str = self.__class__ @@ -426,6 +439,13 @@ class Config: return attrs + def get_ytdlp_args(self) -> dict: + try: + return arg_converter(args=self._ytdlp_cli_mutable, level=True) + except Exception as e: + msg = f"Invalid ytdlp.cli options for yt-dlp. '{e!s}'." + raise ValueError(msg) from e + def frontend(self) -> dict: """ Returns configuration variables relevant to the frontend. @@ -435,10 +455,16 @@ class Config: """ data = {k: getattr(self, k) for k in self._frontend_vars} - hasCookies = self.ytdl_options.get("cookiefile", None) - data["has_cookies"] = hasCookies is not None and os.path.exists(hasCookies) - data["ytdlp_version"] = Config.ytdlp_version() + ytdlp_args = self.get_ytdlp_args() + + hasCookies = ytdlp_args.get("cookiefile", None) + data["has_cookies"] = hasCookies is not None and os.path.exists(hasCookies) + + if not data.get("keep_archive", False) and ytdlp_args.get("download_archive", None): + data["keep_archive"] = True + + data["ytdlp_version"] = Config.ytdlp_version() return data @staticmethod diff --git a/ui/pages/console.vue b/ui/pages/console.vue index 0bb9cbd5..69a5a57a 100644 --- a/ui/pages/console.vue +++ b/ui/pages/console.vue @@ -58,6 +58,7 @@ import "@xterm/xterm/css/xterm.css" import { Terminal } from "@xterm/xterm" import { FitAddon } from "@xterm/addon-fit" +import { useStorage } from '@vueuse/core' const terminal = ref() const terminalFit = ref() @@ -67,6 +68,8 @@ const command_input = ref() const isLoading = ref(false) const config = useConfigStore() const socket = useSocketStore() +const bg_enable = useStorage('random_bg', true) +const bg_opacity = useStorage('random_bg_opacity', 0.85) watch(() => isLoading.value, async value => { if (value) { @@ -185,11 +188,17 @@ onMounted(async () => { socket.off('cli_output', writer) socket.on('cli_close', loader) socket.on('cli_output', writer) + if (bg_enable.value) { + document.querySelector('body').setAttribute("style", `opacity: 1.0`) + } }) onUnmounted(() => { socket.off('cli_close', loader) socket.off('cli_output', writer) window.removeEventListener('resize', reSizeTerminal) + if (bg_enable.value) { + document.querySelector('body').setAttribute("style", `opacity: ${bg_opacity.value}`) + } }); diff --git a/ui/pages/logs.vue b/ui/pages/logs.vue index efe48e7e..31d8cd82 100644 --- a/ui/pages/logs.vue +++ b/ui/pages/logs.vue @@ -97,6 +97,7 @@ div.logbox pre { import moment from 'moment' import { request } from '~/utils/index' import { ref, onMounted, nextTick } from 'vue' +import { useStorage } from '@vueuse/core' let scrollTimeout = null @@ -113,6 +114,8 @@ const logContainer = ref(null) const bottomMarker = ref(null) const autoScroll = ref(true) const textWrap = ref(true) +const bg_enable = useStorage('random_bg', true) +const bg_opacity = useStorage('random_bg_opacity', 0.85) const query = ref(useRoute().query.filter ?? '') const toggleFilter = ref(false) @@ -254,19 +257,25 @@ onMounted(async () => { bottomMarker.value.scrollIntoView({ behavior: 'smooth' }) } }) - }) + if (bg_enable.value) { + document.querySelector('body').setAttribute("style", `opacity: 1.0`) + } }) onUnmounted(() => { socket.emit('unsubscribe', 'log_lines') socket.off('log_lines') + if (bg_enable.value) { + document.querySelector('body').setAttribute("style", `opacity: ${bg_opacity.value}`) + } }) onBeforeUnmount(() => { socket.emit('unsubscribe', 'log_lines') socket.off('log_lines') if (scrollTimeout) clearTimeout(scrollTimeout) + }) useHead({ title: 'Logs' })