Merge pull request #251 from arabcoders/dev

migrate the entire app to use cli options for ytdlp. Closes #250
This commit is contained in:
Abdulmohsen 2025-04-06 02:31:30 +03:00 committed by GitHub
commit ec1f2f448a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 277 additions and 143 deletions

View file

@ -15,7 +15,7 @@ from .config import Config
from .Events import EventBus, Events from .Events import EventBus, Events
from .ffprobe import ffprobe from .ffprobe import ffprobe
from .ItemDTO import ItemDTO from .ItemDTO import ItemDTO
from .Utils import extract_info from .Utils import extract_info, load_cookies
from .YTDLPOpts import YTDLPOpts from .YTDLPOpts import YTDLPOpts
LOG = logging.getLogger("Download") LOG = logging.getLogger("Download")
@ -49,7 +49,6 @@ class Download:
template: str = None template: str = None
template_chapter: str = None template_chapter: str = None
info: ItemDTO = None info: ItemDTO = None
default_ytdl_opts: dict = None
debug: bool = False debug: bool = False
temp_path: str = None temp_path: str = None
cancelled: bool = False cancelled: bool = False
@ -93,9 +92,7 @@ class Download:
self.preset = info.preset self.preset = info.preset
self.info = info self.info = info
self.id = info._id self.id = info._id
self.default_ytdl_opts = config.ytdl_options
self.debug = bool(config.debug) self.debug = bool(config.debug)
self.archive = bool(config.keep_archive)
self.debug_ytdl = bool(config.ytdl_debug) self.debug_ytdl = bool(config.ytdl_debug)
self.cancelled = False self.cancelled = False
self.tmpfilename = None self.tmpfilename = None
@ -143,11 +140,11 @@ class Download:
try: try:
params = ( params = (
YTDLPOpts.get_instance() YTDLPOpts.get_instance()
.preset(self.preset) .preset(self.preset, with_cookies=not self.info.cookies)
.add({"break_on_existing": True}) .add({"break_on_existing": True})
.add_cli(self.info.cli, from_user=True) .add_cli(args=self.info.cli, from_user=True)
.add( .add(
{ config={
"color": "no_color", "color": "no_color",
"paths": { "paths": {
"home": self.download_dir, "home": self.download_dir,
@ -165,19 +162,6 @@ class Download:
.get_all() .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( params.update(
{ {
"progress_hooks": [self._progress_hook], "progress_hooks": [self._progress_hook],
@ -199,8 +183,25 @@ class Download:
with open(cookie_file, "w") as f: with open(cookie_file, "w") as f:
f.write(self.info.cookies) f.write(self.info.cookies)
params["cookiefile"] = f.name 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: if self.is_live or self.is_manifestless:
hasDeletedOptions = False hasDeletedOptions = False
@ -217,7 +218,7 @@ class Download:
) )
self.logger.info( 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}") self.logger.debug(f"Params before passing to yt-dlp. {params}")

View file

@ -22,7 +22,7 @@ from .Events import EventBus, Events, info
from .ItemDTO import Item, ItemDTO from .ItemDTO import Item, ItemDTO
from .Presets import Presets from .Presets import Presets
from .Singleton import Singleton 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 from .YTDLPOpts import YTDLPOpts
LOG = logging.getLogger("DownloadQueue") LOG = logging.getLogger("DownloadQueue")
@ -301,33 +301,38 @@ class DownloadQueue(metaclass=Singleton):
extras=item.extras, 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"): if dlInfo.info.live_in or "is_upcoming" == entry.get("live_status"):
dlInfo.info.status = "not_live" dlInfo.info.status = "not_live"
itemDownload = self.done.put(dlInfo) itemDownload = self.done.put(dlInfo)
NotifyEvent = Events.COMPLETED NotifyEvent = Events.COMPLETED
log_message = f"{dl.title or dl.id or dl._id}: stream is not live yet." log_message = f"{dl.title or dl.id or dl._id}: stream is not live yet."
if dlInfo.info.live_in: if dlInfo.info.live_in:
log_message += f" Will start in {dlInfo.info.live_in}." log_message += f" Will start in {dlInfo.info.live_in}."
await self._notify.emit( await self._notify.emit(
Events.LOG_INFO, Events.LOG_INFO,
data=info(msg=log_message, data=itemDownload.info.serialize()), data=info(msg=log_message, data=itemDownload.info.serialize()),
) )
elif self.config.allow_manifestless is False and is_manifestless is True: elif self.config.allow_manifestless is False and is_manifestless is True:
dlInfo.info.status = "error" dlInfo.info.status = "error"
dlInfo.info.error = "Video is in post-live manifestless mode." dlInfo.info.error = "Video is in post-live manifestless mode."
itemDownload = self.done.put(dlInfo) itemDownload = self.done.put(dlInfo)
NotifyEvent = Events.COMPLETED NotifyEvent = Events.COMPLETED
else: else:
NotifyEvent = Events.ADDED NotifyEvent = Events.ADDED
itemDownload = self.queue.put(dlInfo) itemDownload = self.queue.put(dlInfo)
self.event.set() 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"): if eventType.startswith("url"):
return await self.add(item=item.new_with(url=entry.get("url")), already=already) 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: if _preset.template and not item.template:
item.template = _preset.template item.template = _preset.template
if _preset.cookies and not item.cookies:
item.cookies = _preset.cookies
yt_conf = {} yt_conf = {}
cookie_file = os.path.join(self.config.temp_path, f"c_{uuid.uuid4().hex}.txt") 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) already.add(item.url)
try: 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 = [] logs = []
yt_conf = { yt_conf = {
@ -396,19 +389,34 @@ class DownloadQueue(metaclass=Singleton):
"func": lambda _, msg: logs.append(msg), "func": lambda _, msg: logs.append(msg),
"level": logging.WARNING, "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: if item.cookies:
try: try:
async with await anyio.open_file(cookie_file, "w") as f: async with await anyio.open_file(cookie_file, "w") as f:
await f.write(item.cookies) await f.write(item.cookies)
yt_conf["cookiefile"] = f.name 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) LOG.error(msg)
return {"status": "error", "msg": 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( entry = await asyncio.wait_for(
fut=asyncio.get_running_loop().run_in_executor( fut=asyncio.get_running_loop().run_in_executor(
None, None,
@ -691,18 +699,19 @@ class DownloadQueue(metaclass=Singleton):
if self.event: if self.event:
self.event.set() 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. Check if the url has been downloaded already.
Args: Args:
url (str): The url to check. url (str): The url to check.
file (str | None): The archive file to check.
Returns: Returns:
tuple: A tuple with the status of the operation and the id of the downloaded item. 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 False, None
return is_downloaded(self.config.ytdl_options.get("download_archive", None), url) return is_downloaded(file, url)

View file

@ -7,7 +7,7 @@ from dataclasses import dataclass, field
from .Singleton import Singleton from .Singleton import Singleton
LOG = logging.getLogger("EventsSubscriber") LOG = logging.getLogger("Events")
def error(msg: str, data: dict | None = None) -> dict: def error(msg: str, data: dict | None = None) -> dict:
@ -152,6 +152,16 @@ class Events:
Events.CLI_OUTPUT, 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) @dataclass(kw_only=True)
class Event: class Event:
@ -226,9 +236,16 @@ class EventBus(metaclass=Singleton):
_listeners: dict[str, list[str, EventListener]] = {} _listeners: dict[str, list[str, EventListener]] = {}
"""The listeners for the events.""" """The listeners for the events."""
debug: bool = False
"""Whether to log debug messages or not."""
def __init__(self): def __init__(self):
EventBus._instance = self EventBus._instance = self
from .config import Config
self.debug = Config.get_instance().debug
@staticmethod @staticmethod
def get_instance() -> "EventBus": def get_instance() -> "EventBus":
""" """
@ -353,7 +370,9 @@ class EventBus(metaclass=Singleton):
return [] return []
ev = Event(event=event, data=data) 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 = [] tasks = []
for handler in self._listeners[event].values(): for handler in self._listeners[event].values():

View file

@ -46,6 +46,7 @@ from .Utils import (
get_file_sidecar, get_file_sidecar,
get_files, get_files,
get_mime_type, get_mime_type,
load_cookies,
read_logfile, read_logfile,
validate_url, validate_url,
validate_uuid, validate_uuid,
@ -530,8 +531,8 @@ class HttpAPI(Common):
opts = {} opts = {}
if self.config.ytdl_options.get("proxy", None): if ytdlp_proxy := self.config.get_ytdlp_args().get("proxy", None):
opts["proxy"] = self.config.ytdl_options.get("proxy", None) opts["proxy"] = ytdlp_proxy
ytdlp_opts = YTDLPOpts.get_instance().preset(name=preset, with_cookies=True).add(opts).get_all() ytdlp_opts = YTDLPOpts.get_instance().preset(name=preset, with_cookies=True).add(opts).get_all()
@ -608,7 +609,7 @@ class HttpAPI(Common):
id, id,
extract_info( extract_info(
config={ config={
"proxy": self.config.ytdl_options.get("proxy", None), "proxy": self.config.get_ytdlp_args().get("proxy", None),
"simulate": True, "simulate": True,
"dump_single_json": 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) return web.json_response(data={"error": str(e)}, status=web.HTTPForbidden.status_code)
try: try:
ytdlp_args = self.config.get_ytdlp_args()
opts = { opts = {
"proxy": self.config.ytdl_options.get("proxy", None), "proxy": ytdlp_args.get("proxy", None),
"headers": { "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}") "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: async with httpx.AsyncClient(**opts) as client:
LOG.debug(f"Fetching thumbnail from '{url}'.") LOG.debug(f"Fetching thumbnail from '{url}'.")
response = await client.request(method="GET", url=url) response = await client.request(method="GET", url=url)
@ -1473,14 +1474,14 @@ class HttpAPI(Common):
}, },
) )
ytdlp_args = self.config.get_ytdlp_args()
opts = { opts = {
"proxy": self.config.ytdl_options.get("proxy", None), "proxy": ytdlp_args.get("proxy", None),
"headers": { "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: async with httpx.AsyncClient(**opts) as client:
response = await client.request(method="GET", url=backend, follow_redirects=True) response = await client.request(method="GET", url=backend, follow_redirects=True)
@ -1616,7 +1617,9 @@ class HttpAPI(Common):
Response: The response object 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: if not cookie_file:
return web.json_response(data={"message": "No cookie file provided."}, status=web.HTTPForbidden.status_code) return web.json_response(data={"message": "No cookie file provided."}, status=web.HTTPForbidden.status_code)
@ -1627,32 +1630,24 @@ class HttpAPI(Common):
) )
try: try:
import http.cookiejar _, cookies = load_cookies(cookie_file)
except ValueError as e:
cookies = http.cookiejar.MozillaCookieJar(cookie_file, None, None) LOG.error(str(e))
cookies.load() return web.json_response(data={"message": str(e)}, status=web.HTTPInternalServerError.status_code)
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,
)
url = "https://www.youtube.com/account" url = "https://www.youtube.com/account"
try: try:
opts = { opts = {
"proxy": self.config.ytdl_options.get("proxy", None), "proxy": ytdlp_args.get("proxy", None),
"headers": { "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}") "user_agent", request.headers.get("User-Agent", f"YTPTube/{self.config.version}")
), ),
}, },
"cookies": cookies, "cookies": cookies,
} }
logging.getLogger("httpx").setLevel(logging.WARNING)
async with httpx.AsyncClient(**opts) as client: async with httpx.AsyncClient(**opts) as client:
LOG.debug(f"Checking '{url}' redirection.") LOG.debug(f"Checking '{url}' redirection.")
response = await client.request(method="GET", url=url, follow_redirects=False) response = await client.request(method="GET", url=url, follow_redirects=False)

View file

@ -236,13 +236,22 @@ class HttpSocket(Common):
@ws_event @ws_event
async def archive_item(self, _: str, data: dict): 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 return
if not isinstance(self.config.ytdl_options, dict): from .YTDLPOpts import YTDLPOpts
self.config.ytdl_options = {}
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: if not file:
return return
@ -267,8 +276,9 @@ class HttpSocket(Common):
if not previouslyArchived: if not previouslyArchived:
async with await anyio.open_file(manual_archive, "a") as f: async with await anyio.open_file(manual_archive, "a") as f:
await f.write(f"{idDict['archive_id']} - at: {datetime.now(UTC).isoformat()}\n") 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 @ws_event
async def connect(self, sid: str, _=None): async def connect(self, sid: str, _=None):

View file

@ -368,9 +368,6 @@ class Notification(metaclass=Singleton):
if "form" == target.request.type.lower(): if "form" == target.request.type.lower():
reqBody["data"]["data"] = self._encoder.encode(reqBody["data"]["data"]) 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) response = await self._client.request(**reqBody)
respData = {"url": target.request.url, "status": response.status_code, "text": response.text} respData = {"url": target.request.url, "status": response.status_code, "text": response.text}

View file

@ -12,6 +12,7 @@ import shlex
import socket import socket
import uuid import uuid
from functools import lru_cache from functools import lru_cache
from http.cookiejar import MozillaCookieJar
from typing import Any from typing import Any
import yt_dlp import yt_dlp
@ -28,7 +29,6 @@ REMOVE_KEYS: list = [
"progress_hooks": "--progress_hooks", "progress_hooks": "--progress_hooks",
"postprocessor_hooks": "--postprocessor_hooks", "postprocessor_hooks": "--postprocessor_hooks",
"post_hooks": "--post_hooks", "post_hooks": "--post_hooks",
"download_archive": "--download_archive",
}, },
{ {
"quiet": "-q, --quiet", "quiet": "-q, --quiet",
@ -38,7 +38,6 @@ REMOVE_KEYS: list = [
"simulate": "--simulate", "simulate": "--simulate",
"noprogress": "--no-progress", "noprogress": "--no-progress",
"wait_for_video": "--wait-for-video", "wait_for_video": "--wait-for-video",
"mark_watched": "--mark-watched",
"color": "--color", "color": "--color",
"verbose": "-v, --verbose", "verbose": "-v, --verbose",
"debug_printtraffic": "--print-traffic", "debug_printtraffic": "--print-traffic",
@ -52,9 +51,6 @@ REMOVE_KEYS: list = [
"print_to_file": "--print-to-file", "print_to_file": "--print-to-file",
"cookiesfrombrowser": "--cookies-from-browser", "cookiesfrombrowser": "--cookies-from-browser",
}, },
{
"cookiefile": "--cookies",
},
] ]
YTDLP_INFO_CLS: yt_dlp.YoutubeDL = None 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, "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

View file

@ -4,7 +4,7 @@ from pathlib import Path
from .config import Config from .config import Config
from .Presets import Presets from .Presets import Presets
from .Singleton import Singleton 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") LOG = logging.getLogger("YTDLPOpts")
@ -16,6 +16,12 @@ class YTDLPOpts(metaclass=Singleton):
_preset_opts: dict = {} _preset_opts: dict = {}
"""The preset options.""" """The preset options."""
_item_cli: list = []
"""The item cli options."""
_preset_cli: str = ""
"""The preset cli options."""
_instance = None _instance = None
"""The instance of the class.""" """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": 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:
args (str): The cli options to add 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): if not args or len(args) < 2 or not isinstance(args, str):
return self 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)) self._item_cli.append(args)
if len(removed_options) > 0:
LOG.warning("Removed the following options: '%s'.", ", ".join(removed_options))
return self return self
@ -108,13 +115,9 @@ class YTDLPOpts(metaclass=Singleton):
if preset.cli: if preset.cli:
try: try:
removed_options = [] arg_converter(args=preset.cli, level=True)
self._preset_opts = arg_converter(args=preset.cli, level=True, removed_options=removed_options) self._preset_opts = {}
if len(removed_options) > 0: self._preset_cli = preset.cli
LOG.warning(
"Removed the following options '%s' from preset '%s'.", ", ".join(removed_options), preset.name
)
except Exception as e: except Exception as e:
msg = f"Invalid cli options for preset '{preset.name}'. '{e!s}'." msg = f"Invalid cli options for preset '{preset.name}'. '{e!s}'."
raise ValueError(msg) from e raise ValueError(msg) from e
@ -128,6 +131,8 @@ class YTDLPOpts(metaclass=Singleton):
with open(file, "w") as f: with open(file, "w") as f:
f.write(preset.cookies) f.write(preset.cookies)
load_cookies(file)
self._preset_opts["cookiefile"] = str(file) self._preset_opts["cookiefile"] = str(file)
if preset.format: if preset.format:
@ -155,20 +160,55 @@ class YTDLPOpts(metaclass=Singleton):
dict: The options 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["paths"] = {"home": self._config.download_path, "temp": self._config.temp_path}
default_opts["outtmpl"] = { default_opts["outtmpl"] = {
"default": self._config.output_template, "default": self._config.output_template,
"chapter": self._config.output_template_chapter, "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: if not keep:
self.presets_opts = {} self.presets_opts = {}
self._item_opts = {} self._item_opts = {}
self._item_cli = []
self._preset_cli = ""
if "format" in data and data["format"] in ["not_set", "default"]: if "format" in data:
data["format"] = None 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 return data

View file

@ -11,7 +11,7 @@ from pathlib import Path
import coloredlogs import coloredlogs
from dotenv import load_dotenv from dotenv import load_dotenv
from .Utils import FileLogFormatter, arg_converter from .Utils import FileLogFormatter, arg_converter, load_cookies
from .version import APP_VERSION from .version import APP_VERSION
@ -112,9 +112,6 @@ class Config:
__instance = None __instance = None
"The instance of the class." "The instance of the class."
ytdl_options: dict = {}
"The options to use for yt-dlp."
new_version_available: bool = False new_version_available: bool = False
"A new version of the application is available." "A new version of the application is available."
@ -154,6 +151,9 @@ class Config:
ytdlp_cli: str = "" ytdlp_cli: str = ""
"""The command line options to use for yt-dlp.""" """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] = [ pictures_backends: list[str] = [
"https://unsplash.it/1920/1080?random", "https://unsplash.it/1920/1080?random",
"https://picsum.photos/1920/1080", "https://picsum.photos/1920/1080",
@ -178,6 +178,7 @@ class Config:
"new_version_available", "new_version_available",
"started", "started",
"ytdlp_cli", "ytdlp_cli",
"_ytdlp_cli_mutable",
) )
"The variables that are immutable." "The variables that are immutable."
@ -328,22 +329,20 @@ class Config:
except Exception as e: except Exception as e:
LOG.error(f"Error starting debugpy server at '0.0.0.0:{self.debugpy_port}'. {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") opts_file: str = os.path.join(self.config_path, "ytdlp.cli")
if os.path.exists(opts_file) and os.path.getsize(opts_file) > 2: if os.path.exists(opts_file) and os.path.getsize(opts_file) > 2:
LOG.info(f"Loading yt-dlp custom options from '{opts_file}'.") LOG.info(f"Loading yt-dlp custom options from '{opts_file}'.")
with open(opts_file) as f: with open(opts_file) as f:
self.ytdlp_cli = f.read().strip() self.ytdlp_cli = f.read().strip()
if self.ytdlp_cli: if self.ytdlp_cli:
self._ytdlp_cli_mutable = self.ytdlp_cli
try: try:
removed_options = [] removed_options = []
self.ytdl_options = arg_converter( ytdl_options = arg_converter(args=self.ytdlp_cli, level=True, removed_options=removed_options)
args=self.ytdlp_cli,
level=1,
removed_options=removed_options,
)
try: try:
LOG.debug("Parsed yt-dlp cli options '%s'.", self.ytdl_options) LOG.debug("Parsed yt-dlp cli options '%s'.", ytdl_options)
except Exception: except Exception:
pass pass
@ -359,17 +358,28 @@ class Config:
else: else:
LOG.info(f"No yt-dlp custom options found at '{opts_file}'.") 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: if self.keep_archive:
LOG.info("keep archive option is enabled.") 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: if not ytdl_options.get("cookiefile", None):
LOG.info(f"Using cookies from '{cookiesFile}' as default.") cookies_file: str = os.path.join(self.config_path, "cookies.txt")
self.ytdl_options["cookiefile"] = cookiesFile 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: if self.temp_keep:
LOG.info("Keep temp files option is enabled.") LOG.info("Keep temp files option is enabled.")
@ -412,6 +422,9 @@ class Config:
self.started = time.time() self.started = time.time()
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.INFO)
def _get_attributes(self) -> dict: def _get_attributes(self) -> dict:
attrs: dict = {} attrs: dict = {}
vClass: str = self.__class__ vClass: str = self.__class__
@ -426,6 +439,13 @@ class Config:
return attrs 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: def frontend(self) -> dict:
""" """
Returns configuration variables relevant to the frontend. Returns configuration variables relevant to the frontend.
@ -435,10 +455,16 @@ class Config:
""" """
data = {k: getattr(self, k) for k in self._frontend_vars} 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 return data
@staticmethod @staticmethod

View file

@ -58,6 +58,7 @@
import "@xterm/xterm/css/xterm.css" import "@xterm/xterm/css/xterm.css"
import { Terminal } from "@xterm/xterm" import { Terminal } from "@xterm/xterm"
import { FitAddon } from "@xterm/addon-fit" import { FitAddon } from "@xterm/addon-fit"
import { useStorage } from '@vueuse/core'
const terminal = ref() const terminal = ref()
const terminalFit = ref() const terminalFit = ref()
@ -67,6 +68,8 @@ const command_input = ref()
const isLoading = ref(false) const isLoading = ref(false)
const config = useConfigStore() const config = useConfigStore()
const socket = useSocketStore() const socket = useSocketStore()
const bg_enable = useStorage('random_bg', true)
const bg_opacity = useStorage('random_bg_opacity', 0.85)
watch(() => isLoading.value, async value => { watch(() => isLoading.value, async value => {
if (value) { if (value) {
@ -185,11 +188,17 @@ onMounted(async () => {
socket.off('cli_output', writer) socket.off('cli_output', writer)
socket.on('cli_close', loader) socket.on('cli_close', loader)
socket.on('cli_output', writer) socket.on('cli_output', writer)
if (bg_enable.value) {
document.querySelector('body').setAttribute("style", `opacity: 1.0`)
}
}) })
onUnmounted(() => { onUnmounted(() => {
socket.off('cli_close', loader) socket.off('cli_close', loader)
socket.off('cli_output', writer) socket.off('cli_output', writer)
window.removeEventListener('resize', reSizeTerminal) window.removeEventListener('resize', reSizeTerminal)
if (bg_enable.value) {
document.querySelector('body').setAttribute("style", `opacity: ${bg_opacity.value}`)
}
}); });
</script> </script>

View file

@ -97,6 +97,7 @@ div.logbox pre {
import moment from 'moment' import moment from 'moment'
import { request } from '~/utils/index' import { request } from '~/utils/index'
import { ref, onMounted, nextTick } from 'vue' import { ref, onMounted, nextTick } from 'vue'
import { useStorage } from '@vueuse/core'
let scrollTimeout = null let scrollTimeout = null
@ -113,6 +114,8 @@ const logContainer = ref(null)
const bottomMarker = ref(null) const bottomMarker = ref(null)
const autoScroll = ref(true) const autoScroll = ref(true)
const textWrap = 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 query = ref(useRoute().query.filter ?? '')
const toggleFilter = ref(false) const toggleFilter = ref(false)
@ -254,19 +257,25 @@ onMounted(async () => {
bottomMarker.value.scrollIntoView({ behavior: 'smooth' }) bottomMarker.value.scrollIntoView({ behavior: 'smooth' })
} }
}) })
}) })
if (bg_enable.value) {
document.querySelector('body').setAttribute("style", `opacity: 1.0`)
}
}) })
onUnmounted(() => { onUnmounted(() => {
socket.emit('unsubscribe', 'log_lines') socket.emit('unsubscribe', 'log_lines')
socket.off('log_lines') socket.off('log_lines')
if (bg_enable.value) {
document.querySelector('body').setAttribute("style", `opacity: ${bg_opacity.value}`)
}
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
socket.emit('unsubscribe', 'log_lines') socket.emit('unsubscribe', 'log_lines')
socket.off('log_lines') socket.off('log_lines')
if (scrollTimeout) clearTimeout(scrollTimeout) if (scrollTimeout) clearTimeout(scrollTimeout)
}) })
useHead({ title: 'Logs' }) useHead({ title: 'Logs' })