use same name cross client and server for variables and config files.

This commit is contained in:
ArabCoders 2025-01-28 00:09:46 +03:00
parent 41ddc14af5
commit d063cf0f52
12 changed files with 175 additions and 108 deletions

View file

@ -28,8 +28,8 @@ class Download:
id: str = None
download_dir: str = None
temp_dir: str = None
output_template: str = None
output_template_chapter: str = None
template: str = None
template_chapter: str = None
ytdl_opts: dict = None
info: ItemDTO = None
default_ytdl_opts: dict = None
@ -72,10 +72,10 @@ class Download:
self.download_dir = info.download_dir
self.temp_dir = info.temp_dir
self.output_template_chapter = info.output_template_chapter
self.output_template = info.output_template
self.template = info.template
self.template_chapter = info.template_chapter
self.preset = info.preset
self.ytdl_opts = info.ytdlp_config if info.ytdlp_config else {}
self.ytdl_opts = info.config if info.config else {}
self.info = info
self.id = info._id
self.default_ytdl_opts = config.ytdl_options
@ -120,7 +120,7 @@ class Download:
{
"color": "no_color",
"paths": {"home": self.download_dir, "temp": self.tempPath},
"outtmpl": {"default": self.output_template, "chapter": self.output_template_chapter},
"outtmpl": {"default": self.template, "chapter": self.template_chapter},
"noprogress": True,
"break_on_existing": True,
"progress_hooks": [self._progress_hook],
@ -136,9 +136,9 @@ class Download:
params["verbose"] = True
params["noprogress"] = False
if self.info.ytdlp_cookies:
if self.info.cookies:
try:
data = jsonCookie(json.loads(self.info.ytdlp_cookies))
data = jsonCookie(json.loads(self.info.cookies))
if not data:
LOG.warning(
f"The cookie string that was provided for {self.info.title} is empty or not in expected spec."

View file

@ -132,9 +132,9 @@ class DownloadQueue(metaclass=Singleton):
entry: dict,
preset: str,
folder: str,
ytdlp_config: dict = {},
ytdlp_cookies: str = "",
output_template: str = "",
config: dict = {},
cookies: str = "",
template: str = "",
already=None,
):
"""
@ -144,9 +144,9 @@ class DownloadQueue(metaclass=Singleton):
entry (dict): The entry to add to the download queue.
preset (str): The preset to use for the download.
folder (str): The folder to save the download to.
ytdlp_config (dict): The yt-dlp configuration to use for the download.
ytdlp_cookies (str): The cookies to use for the download.
output_template (str): The output template to use for the download.
config (dict): The yt-dlp configuration to use for the download.
cookies (str): The cookies to use for the download.
template (str): The output template to use for the download.
already (set): The set of already downloaded items.
Returns:
@ -180,9 +180,9 @@ class DownloadQueue(metaclass=Singleton):
entry=etr,
preset=preset,
folder=folder,
ytdlp_config=ytdlp_config,
ytdlp_cookies=ytdlp_cookies,
output_template=output_template,
config=config,
cookies=cookies,
template=template,
already=already,
)
)
@ -249,10 +249,10 @@ class DownloadQueue(metaclass=Singleton):
folder=folder,
download_dir=download_dir,
temp_dir=self.config.temp_path,
ytdlp_cookies=ytdlp_cookies,
ytdlp_config=ytdlp_config,
output_template=output_template if output_template else self.config.output_template,
output_template_chapter=self.config.output_template_chapter,
cookies=cookies,
config=config,
template=template if template else self.config.output_template,
template_chapter=self.config.output_template_chapter,
datetime=formatdate(time.time()),
error=error,
is_live=is_live,
@ -263,7 +263,7 @@ class DownloadQueue(metaclass=Singleton):
for property, value in entry.items():
if property.startswith("playlist"):
dl.output_template = str(dl.output_template).replace(f"%({property})s", str(value))
dl.template = str(dl.template).replace(f"%({property})s", str(value))
dlInfo: Download = Download(info=dl, info_dict=entry, debug=bool(self.config.ytdl_debug))
@ -291,9 +291,9 @@ class DownloadQueue(metaclass=Singleton):
url=str(entry.get("url")),
preset=preset,
folder=folder,
ytdlp_config=ytdlp_config,
ytdlp_cookies=ytdlp_cookies,
output_template=output_template,
config=config,
cookies=cookies,
template=template,
already=already,
)
@ -304,25 +304,25 @@ class DownloadQueue(metaclass=Singleton):
url: str,
preset: str,
folder: str,
ytdlp_config: dict = {},
ytdlp_cookies: str = "",
output_template: str = "",
config: dict = {},
cookies: str = "",
template: str = "",
already=None,
):
ytdlp_config = ytdlp_config if ytdlp_config else {}
config = config if config else {}
folder = str(folder) if folder else ""
filePath = calcDownloadPath(basePath=self.config.download_path, folder=folder)
LOG.info(
f"Adding 'URL: {url}' to 'Folder: {filePath}' with 'Preset: {preset}' 'Naming: {output_template}', 'Cookies: {ytdlp_cookies}' 'YTConfig: {ytdlp_config}'."
f"Adding 'URL: {url}' to 'Folder: {filePath}' with 'Preset: {preset}' 'Naming: {template}', 'Cookies: {cookies}' 'YTConfig: {config}'."
)
if isinstance(ytdlp_config, str):
if isinstance(config, str):
try:
ytdlp_config = json.loads(ytdlp_config)
config = json.loads(config)
except Exception as e:
LOG.error(f"Unable to load '{ytdlp_config=}'. {str(e)}")
LOG.error(f"Unable to load '{config=}'. {str(e)}")
return {"status": "error", "msg": f"Failed to parse json yt-dlp config. {str(e)}"}
already = set() if already is None else already
@ -347,7 +347,7 @@ class DownloadQueue(metaclass=Singleton):
fut=asyncio.get_running_loop().run_in_executor(
None,
ExtractInfo,
get_opts(preset, mergeConfig(self.config.ytdl_options, ytdlp_config)),
get_opts(preset, mergeConfig(self.config.ytdl_options, config)),
url,
bool(self.config.ytdl_debug),
),
@ -377,9 +377,9 @@ class DownloadQueue(metaclass=Singleton):
entry=entry,
preset=preset,
folder=folder,
ytdlp_config=ytdlp_config,
ytdlp_cookies=ytdlp_cookies,
output_template=output_template,
config=config,
cookies=cookies,
template=template,
already=already,
)

View file

@ -40,6 +40,9 @@ class Events:
TEST = "test"
TASKS_ADD = "task_add"
TASK_DISPATCHED = "task_dispatched"
TASK_FINISHED = "task_finished"
TASK_ERROR = "task_error"
@dataclass(kw_only=True)
@ -169,10 +172,13 @@ class EventsSubscriber(metaclass=Singleton):
tasks = []
for id, callback in self._listeners[event].items():
try:
if "data" not in kwargs or not isinstance(kwargs["data"], Event):
data = Event(id=id, data={"args": args if args else [], **kwargs})
else:
if args and isinstance(args[0], Event):
data = args[0]
elif "data" in kwargs and isinstance(kwargs["data"], Event):
data = kwargs["data"]
else:
data = Event(id=id, data={"args": args if args else [], **kwargs})
tasks.append(asyncio.create_task(callback(event, data)))
except Exception as e:
LOG.error(f"Failed to emit event '{event}' to '{id}'. Error message '{str(e)}'.")

View file

@ -63,7 +63,7 @@ class HttpAPI(common):
self.routes = web.RouteTableDef()
self.cache = Cache()
super().__init__(queue=self.queue, encoder=self.encoder)
super().__init__(queue=self.queue, encoder=self.encoder, config=self.config)
def route(method: str, path: str) -> Awaitable:
"""
@ -980,7 +980,6 @@ class HttpAPI(common):
)
except Exception as e:
LOG.error(f"Error fetching thumbnail from '{url}'. '{e}'.")
LOG.exception(e)
return web.json_response(
data={"error": "failed to retrieve the thumbnail."}, status=web.HTTPInternalServerError.status_code
)

View file

@ -52,7 +52,7 @@ class HttpSocket(common):
self.emitter.add_emitter([emit], local=False)
super().__init__(queue=queue, encoder=encoder)
super().__init__(queue=queue, encoder=encoder, config=config)
def ws_event(func): # type: ignore
"""
@ -77,6 +77,7 @@ class HttpSocket(common):
# self.sio.on("*", es.emit)
async def handle_event(_: str, data: Event):
LOG.debug(f"Event received. '{data}'")
await self.add(**data.data)
EventsSubscriber.get_instance().subscribe(Events.ADD_URL, "socket_add_url", handle_event)

View file

@ -5,6 +5,7 @@ from dataclasses import dataclass, field
from email.utils import formatdate
from typing import Any
@dataclass(kw_only=True)
class ItemDTO:
"""
@ -14,22 +15,21 @@ class ItemDTO:
_id: str = field(default_factory=lambda: str(uuid.uuid4()), init=False)
error: str|None = None
error: str | None = None
id: str
title: str
url: str
quality: str | None = None
format: str | None = None
thumbnail: str | None = None
preset: str = "default"
folder: str
download_dir: str | None = None
temp_dir: str | None = None
status: str | None = None
ytdlp_cookies: str | None = None
ytdlp_config: dict = field(default_factory=dict)
output_template: str | None = None
output_template_chapter: str | None = None
cookies: str | None = None
config: dict = field(default_factory=dict)
template: str | None = None
template_chapter: str | None = None
timestamp: float = time.time_ns()
is_live: bool | None = None
datetime: str = field(default_factory=lambda: str(formatdate(time.time())))
@ -49,19 +49,41 @@ class ItemDTO:
speed: str | None = None
eta: str | None = None
# DEPRECATED: These fields are deprecated and will be removed in the future.
thumbnail: str | None = None
ytdlp_cookies: str | None = None
ytdlp_config: dict = field(default_factory=dict)
output_template: str | None = None
output_template_chapter: str | None = None
def serialize(self) -> dict:
deprecated: tuple = (
"thumbnail",
"quality",
"format",
"ytdlp_cookies",
"ytdlp_config",
"output_template",
"output_template_chapter",
)
if self.thumbnail and "thumbnail" not in self.extras:
self.extras["thumbnail"] = self.thumbnail
mapper: dict = {
"cookies": "ytdlp_cookies",
"config": "ytdlp_config",
"template": "output_template",
"template_chapter": "output_template_chapter",
}
for k, v in mapper.items():
if not self.get(k) and self.get(v):
self.__dict__[k] = self.get(v)
dump = self.__dict__.copy()
for f in deprecated:
dump.pop(f)
dump.pop(f, None)
return dump

View file

@ -1,14 +1,15 @@
import asyncio
import datetime
from datetime import datetime
import json
import logging
import os
import time
from dataclasses import dataclass, field
from typing import Any, List
import httpx
from aiocron import Cron, crontab
from dataclasses import dataclass, field
from aiohttp import web
from .config import Config
from .Emitter import Emitter
@ -81,16 +82,12 @@ class Tasks(metaclass=Singleton):
self._loop: asyncio.AbstractEventLoop = loop or asyncio.get_event_loop()
self._emitter: Emitter = emitter or Emitter.get_instance()
if os.path.exists(self._file):
if os.path.exists(self._file) and "600" != oct(os.stat(self._file).st_mode)[-3:]:
try:
if "600" != oct(os.stat(self._file).st_mode)[-3:]:
os.chmod(self._file, 0o600)
os.chmod(self._file, 0o600)
except Exception:
pass
if os.path.getsize(self._file) > 10:
self.load()
def handle_event(_, e: Event):
self.save(**e.data)
@ -109,6 +106,24 @@ class Tasks(metaclass=Singleton):
Tasks._instance = Tasks()
return Tasks._instance
def attach(self, _: web.Application):
"""
Attach the tasks to the aiohttp application.
Args:
_ (web.Application): The aiohttp application.
Returns:
None
"""
self.load()
def shutdown(self):
"""
Shutdown the tasks service.
"""
self.clear()
def getTasks(self) -> List[Task]:
"""Return the tasks."""
return [job.task for job in self._jobs]
@ -266,7 +281,7 @@ class Tasks(metaclass=Singleton):
None
"""
try:
timeNow = datetime.datetime().isoformat()
timeNow = datetime.now().isoformat()
started = time.time()
if not task.url:
LOG.error(f"Failed to dispatch task '{task.id}: {task.name}'. No URL found.")
@ -274,43 +289,48 @@ class Tasks(metaclass=Singleton):
preset: str = str(task.preset or self._default_preset)
folder: str = task.folder if task.folder else ""
ytdlp_cookies: str = str(task.cookies) if task.cookies else ""
output_template: str = task.template if task.template else ""
cookies: str = str(task.cookies) if task.cookies else ""
template: str = task.template if task.template else ""
ytdlp_config = task.config if task.config else {}
if isinstance(ytdlp_config, str) and ytdlp_config:
config = task.config if task.config else {}
if isinstance(config, str) and config:
try:
ytdlp_config = json.loads(ytdlp_config)
config = json.loads(config)
except Exception as e:
LOG.error(f"Failed to parse json yt-dlp config for '{task.name}'. {str(e)}")
return
await self.emitter.info(f"Task '{task.name}' dispatched at '{timeNow}'.")
LOG.info(f"Task '{task.id}: {task.name}' dispatched at '{timeNow}'.")
await self._emitter.emit(
event=Events.ADD_URL,
data=Event(
id=task.id,
data={
"url": task.url,
"preset": preset,
"folder": folder,
"ytdlp_cookies": ytdlp_cookies,
"ytdlp_config": ytdlp_config,
"output_template": output_template,
},
),
local=True,
tasks = []
tasks.append(self._emitter.info(f"Task '{task.name}' dispatched at '{timeNow}'."))
tasks.append(
self._emitter.emit(
event=Events.ADD_URL,
data=Event(
id=task.id,
data={
"url": task.url,
"preset": preset,
"folder": folder,
"cookies": cookies,
"config": config,
"template": template,
},
),
local=True,
)
)
timeNow = datetime.datetime().isoformat()
await asyncio.wait_for(asyncio.gather(*tasks), timeout=None)
timeNow = datetime.now().isoformat()
ended = time.time()
LOG.info(f"Task '{task.id}: {task.name}' completed at '{timeNow}' took '{ended - started:.2f}' seconds.")
await self.emitter.success(f"Task '{task.name}' completed in '{ended - started:.2f}' seconds.")
await self._emitter.success(f"Task '{task.name}' completed in '{ended - started:.2f}' seconds.")
except Exception as e:
timeNow = datetime.datetime().isoformat()
timeNow = datetime.now().isoformat()
LOG.error(f"Task '{task.id}: {task.name}' has failed to execute at '{timeNow}'. '{str(e)}'.")
await self.emitter.error(f"Task '{task.name}' failed to execute at '{timeNow}'. '{str(e)}'.")
await self._emitter.error(f"Task '{task.name}' failed to execute at '{timeNow}'. '{str(e)}'.")

View file

@ -3,6 +3,7 @@ import logging
from .DownloadQueue import DownloadQueue
from .encoder import Encoder
from .config import Config
LOG = logging.getLogger("common")
@ -12,31 +13,49 @@ class common:
This class is used to share common methods between the socket and the API gateways.
"""
queue: DownloadQueue
encoder: Encoder
def __init__(self, queue: DownloadQueue, encoder: Encoder):
def __init__(
self,
queue: DownloadQueue | None = None,
encoder: Encoder | None = None,
config: Config | None = None,
):
super().__init__()
self.queue = queue
self.encoder = encoder
self.queue = queue or DownloadQueue.get_instance()
self.encoder = encoder or Encoder()
config = config or Config.get_instance()
self.default_preset = config.default_preset
async def add(
self, url: str, preset: str, folder: str, ytdlp_cookies: str, ytdlp_config: dict, output_template: str
self, url: str, preset: str, folder: str, cookies: str, config: dict, template: str
) -> dict[str, str]:
if ytdlp_cookies and isinstance(ytdlp_cookies, dict):
ytdlp_cookies = self.encoder.encode(ytdlp_cookies)
"""
Add an item to the download queue.
status = await self.queue.add(
Args:
url (str): The url to be added to the queue.
preset (str): The preset to be used for the download.
folder (str): The folder to save the download to.
cookies (str): The cookies to be used for the download.
config (dict): The yt-dlp config to be used for the download.
template (str): The template to be used for the download.
Returns:
dict[str, str]: The status of the download.
{ "status": "text" }
"""
if cookies and isinstance(cookies, dict):
cookies = self.encoder.encode(cookies)
return await self.queue.add(
url=url,
preset=preset if preset else "default",
folder=folder,
ytdlp_cookies=ytdlp_cookies,
ytdlp_config=ytdlp_config if isinstance(ytdlp_config, dict) else {},
output_template=output_template,
cookies=cookies,
config=config if isinstance(config, dict) else {},
template=template,
)
return status
def format_item(self, item: dict) -> dict:
"""
Format the item to be added to the download queue.
@ -72,9 +91,9 @@ class common:
"url": url,
"preset": preset,
"folder": folder,
"ytdlp_cookies": cookies,
"ytdlp_config": config if isinstance(config, dict) else {},
"output_template": template,
"cookies": cookies,
"config": config if isinstance(config, dict) else {},
"template": template,
}
return item

View file

@ -112,7 +112,7 @@ class Main:
"""
self.socket.attach(self.app)
self.http.attach(self.app)
Tasks.get_instance().load()
Tasks.get_instance().attach(self.app)
def started(_):
LOG.info("=" * 40)

View file

@ -468,9 +468,9 @@ const reQueueItem = item => {
url: item.url,
preset: item.preset,
folder: item.folder,
ytdlp_config: item.ytdlp_config,
ytdlp_cookies: item.ytdlp_cookies,
output_template: item.output_template,
config: item.config,
cookies: item.cookies,
template: item.template,
})
}
</script>

View file

@ -113,13 +113,13 @@
</label>
<div class="control has-icons-left">
<input type="text" class="input" id="output_template" placeholder="The output template to use"
v-model="form.output_template" :disabled="addInProgress">
v-model="form.template" :disabled="addInProgress">
<span class="icon is-small is-left"><i class="fa-solid fa-file" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The output template to use, if not set, it will defaults to
<code>{{ config.app.output_template }}</code></span>
<code>{{ config.app.template }}</code></span>
</span>
</div>
</div>

View file

@ -83,7 +83,7 @@ div.is-centered {
</p>
<p>
<span class="icon"><i class="fa-solid fa-file" /></span>
<span>{{ item.output_template ?? config.app.output_template }}</span>
<span>{{ item.template ?? config.app.output_template }}</span>
</p>
<p>
<span class="icon"><i class="fa-solid fa-tv" /></span>