Merge pull request #251 from arabcoders/dev
migrate the entire app to use cli options for ytdlp. Closes #250
This commit is contained in:
commit
ec1f2f448a
11 changed files with 277 additions and 143 deletions
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
|
|
|
|||
Loading…
Reference in a new issue