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 .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}")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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():
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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' })
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue