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 id: str = None
download_dir: str = None download_dir: str = None
temp_dir: str = None temp_dir: str = None
output_template: str = None template: str = None
output_template_chapter: str = None template_chapter: str = None
ytdl_opts: dict = None ytdl_opts: dict = None
info: ItemDTO = None info: ItemDTO = None
default_ytdl_opts: dict = None default_ytdl_opts: dict = None
@ -72,10 +72,10 @@ class Download:
self.download_dir = info.download_dir self.download_dir = info.download_dir
self.temp_dir = info.temp_dir self.temp_dir = info.temp_dir
self.output_template_chapter = info.output_template_chapter self.template = info.template
self.output_template = info.output_template self.template_chapter = info.template_chapter
self.preset = info.preset 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.info = info
self.id = info._id self.id = info._id
self.default_ytdl_opts = config.ytdl_options self.default_ytdl_opts = config.ytdl_options
@ -120,7 +120,7 @@ class Download:
{ {
"color": "no_color", "color": "no_color",
"paths": {"home": self.download_dir, "temp": self.tempPath}, "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, "noprogress": True,
"break_on_existing": True, "break_on_existing": True,
"progress_hooks": [self._progress_hook], "progress_hooks": [self._progress_hook],
@ -136,9 +136,9 @@ class Download:
params["verbose"] = True params["verbose"] = True
params["noprogress"] = False params["noprogress"] = False
if self.info.ytdlp_cookies: if self.info.cookies:
try: try:
data = jsonCookie(json.loads(self.info.ytdlp_cookies)) data = jsonCookie(json.loads(self.info.cookies))
if not data: if not data:
LOG.warning( LOG.warning(
f"The cookie string that was provided for {self.info.title} is empty or not in expected spec." 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, entry: dict,
preset: str, preset: str,
folder: str, folder: str,
ytdlp_config: dict = {}, config: dict = {},
ytdlp_cookies: str = "", cookies: str = "",
output_template: str = "", template: str = "",
already=None, already=None,
): ):
""" """
@ -144,9 +144,9 @@ class DownloadQueue(metaclass=Singleton):
entry (dict): The entry to add to the download queue. entry (dict): The entry to add to the download queue.
preset (str): The preset to use for the download. preset (str): The preset to use for the download.
folder (str): The folder to save the download to. folder (str): The folder to save the download to.
ytdlp_config (dict): The yt-dlp configuration to use for the download. config (dict): The yt-dlp configuration to use for the download.
ytdlp_cookies (str): The cookies to use for the download. cookies (str): The cookies to use for the download.
output_template (str): The output template to use for the download. template (str): The output template to use for the download.
already (set): The set of already downloaded items. already (set): The set of already downloaded items.
Returns: Returns:
@ -180,9 +180,9 @@ class DownloadQueue(metaclass=Singleton):
entry=etr, entry=etr,
preset=preset, preset=preset,
folder=folder, folder=folder,
ytdlp_config=ytdlp_config, config=config,
ytdlp_cookies=ytdlp_cookies, cookies=cookies,
output_template=output_template, template=template,
already=already, already=already,
) )
) )
@ -249,10 +249,10 @@ class DownloadQueue(metaclass=Singleton):
folder=folder, folder=folder,
download_dir=download_dir, download_dir=download_dir,
temp_dir=self.config.temp_path, temp_dir=self.config.temp_path,
ytdlp_cookies=ytdlp_cookies, cookies=cookies,
ytdlp_config=ytdlp_config, config=config,
output_template=output_template if output_template else self.config.output_template, template=template if template else self.config.output_template,
output_template_chapter=self.config.output_template_chapter, template_chapter=self.config.output_template_chapter,
datetime=formatdate(time.time()), datetime=formatdate(time.time()),
error=error, error=error,
is_live=is_live, is_live=is_live,
@ -263,7 +263,7 @@ class DownloadQueue(metaclass=Singleton):
for property, value in entry.items(): for property, value in entry.items():
if property.startswith("playlist"): 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)) 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")), url=str(entry.get("url")),
preset=preset, preset=preset,
folder=folder, folder=folder,
ytdlp_config=ytdlp_config, config=config,
ytdlp_cookies=ytdlp_cookies, cookies=cookies,
output_template=output_template, template=template,
already=already, already=already,
) )
@ -304,25 +304,25 @@ class DownloadQueue(metaclass=Singleton):
url: str, url: str,
preset: str, preset: str,
folder: str, folder: str,
ytdlp_config: dict = {}, config: dict = {},
ytdlp_cookies: str = "", cookies: str = "",
output_template: str = "", template: str = "",
already=None, already=None,
): ):
ytdlp_config = ytdlp_config if ytdlp_config else {} config = config if config else {}
folder = str(folder) if folder else "" folder = str(folder) if folder else ""
filePath = calcDownloadPath(basePath=self.config.download_path, folder=folder) filePath = calcDownloadPath(basePath=self.config.download_path, folder=folder)
LOG.info( 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: try:
ytdlp_config = json.loads(ytdlp_config) config = json.loads(config)
except Exception as e: 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)}"} return {"status": "error", "msg": f"Failed to parse json yt-dlp config. {str(e)}"}
already = set() if already is None else already already = set() if already is None else already
@ -347,7 +347,7 @@ class DownloadQueue(metaclass=Singleton):
fut=asyncio.get_running_loop().run_in_executor( fut=asyncio.get_running_loop().run_in_executor(
None, None,
ExtractInfo, ExtractInfo,
get_opts(preset, mergeConfig(self.config.ytdl_options, ytdlp_config)), get_opts(preset, mergeConfig(self.config.ytdl_options, config)),
url, url,
bool(self.config.ytdl_debug), bool(self.config.ytdl_debug),
), ),
@ -377,9 +377,9 @@ class DownloadQueue(metaclass=Singleton):
entry=entry, entry=entry,
preset=preset, preset=preset,
folder=folder, folder=folder,
ytdlp_config=ytdlp_config, config=config,
ytdlp_cookies=ytdlp_cookies, cookies=cookies,
output_template=output_template, template=template,
already=already, already=already,
) )

View file

@ -40,6 +40,9 @@ class Events:
TEST = "test" TEST = "test"
TASKS_ADD = "task_add" TASKS_ADD = "task_add"
TASK_DISPATCHED = "task_dispatched"
TASK_FINISHED = "task_finished"
TASK_ERROR = "task_error"
@dataclass(kw_only=True) @dataclass(kw_only=True)
@ -169,10 +172,13 @@ class EventsSubscriber(metaclass=Singleton):
tasks = [] tasks = []
for id, callback in self._listeners[event].items(): for id, callback in self._listeners[event].items():
try: try:
if "data" not in kwargs or not isinstance(kwargs["data"], Event): if args and isinstance(args[0], Event):
data = Event(id=id, data={"args": args if args else [], **kwargs}) data = args[0]
else: elif "data" in kwargs and isinstance(kwargs["data"], Event):
data = kwargs["data"] data = kwargs["data"]
else:
data = Event(id=id, data={"args": args if args else [], **kwargs})
tasks.append(asyncio.create_task(callback(event, data))) tasks.append(asyncio.create_task(callback(event, data)))
except Exception as e: except Exception as e:
LOG.error(f"Failed to emit event '{event}' to '{id}'. Error message '{str(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.routes = web.RouteTableDef()
self.cache = Cache() 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: def route(method: str, path: str) -> Awaitable:
""" """
@ -980,7 +980,6 @@ class HttpAPI(common):
) )
except Exception as e: except Exception as e:
LOG.error(f"Error fetching thumbnail from '{url}'. '{e}'.") LOG.error(f"Error fetching thumbnail from '{url}'. '{e}'.")
LOG.exception(e)
return web.json_response( return web.json_response(
data={"error": "failed to retrieve the thumbnail."}, status=web.HTTPInternalServerError.status_code 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) 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 def ws_event(func): # type: ignore
""" """
@ -77,6 +77,7 @@ class HttpSocket(common):
# self.sio.on("*", es.emit) # self.sio.on("*", es.emit)
async def handle_event(_: str, data: Event): async def handle_event(_: str, data: Event):
LOG.debug(f"Event received. '{data}'")
await self.add(**data.data) await self.add(**data.data)
EventsSubscriber.get_instance().subscribe(Events.ADD_URL, "socket_add_url", handle_event) 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 email.utils import formatdate
from typing import Any from typing import Any
@dataclass(kw_only=True) @dataclass(kw_only=True)
class ItemDTO: class ItemDTO:
""" """
@ -14,22 +15,21 @@ class ItemDTO:
_id: str = field(default_factory=lambda: str(uuid.uuid4()), init=False) _id: str = field(default_factory=lambda: str(uuid.uuid4()), init=False)
error: str|None = None error: str | None = None
id: str id: str
title: str title: str
url: str url: str
quality: str | None = None quality: str | None = None
format: str | None = None format: str | None = None
thumbnail: str | None = None
preset: str = "default" preset: str = "default"
folder: str folder: str
download_dir: str | None = None download_dir: str | None = None
temp_dir: str | None = None temp_dir: str | None = None
status: str | None = None status: str | None = None
ytdlp_cookies: str | None = None cookies: str | None = None
ytdlp_config: dict = field(default_factory=dict) config: dict = field(default_factory=dict)
output_template: str | None = None template: str | None = None
output_template_chapter: str | None = None template_chapter: str | None = None
timestamp: float = time.time_ns() timestamp: float = time.time_ns()
is_live: bool | None = None is_live: bool | None = None
datetime: str = field(default_factory=lambda: str(formatdate(time.time()))) datetime: str = field(default_factory=lambda: str(formatdate(time.time())))
@ -49,19 +49,41 @@ class ItemDTO:
speed: str | None = None speed: str | None = None
eta: 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: def serialize(self) -> dict:
deprecated: tuple = ( deprecated: tuple = (
"thumbnail", "thumbnail",
"quality", "quality",
"format", "format",
"ytdlp_cookies",
"ytdlp_config",
"output_template",
"output_template_chapter",
) )
if self.thumbnail and "thumbnail" not in self.extras: if self.thumbnail and "thumbnail" not in self.extras:
self.extras["thumbnail"] = self.thumbnail 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() dump = self.__dict__.copy()
for f in deprecated: for f in deprecated:
dump.pop(f) dump.pop(f, None)
return dump return dump

View file

@ -1,14 +1,15 @@
import asyncio import asyncio
import datetime from datetime import datetime
import json import json
import logging import logging
import os import os
import time import time
from dataclasses import dataclass, field
from typing import Any, List from typing import Any, List
import httpx import httpx
from aiocron import Cron, crontab from aiocron import Cron, crontab
from dataclasses import dataclass, field from aiohttp import web
from .config import Config from .config import Config
from .Emitter import Emitter from .Emitter import Emitter
@ -81,16 +82,12 @@ class Tasks(metaclass=Singleton):
self._loop: asyncio.AbstractEventLoop = loop or asyncio.get_event_loop() self._loop: asyncio.AbstractEventLoop = loop or asyncio.get_event_loop()
self._emitter: Emitter = emitter or Emitter.get_instance() 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: try:
if "600" != oct(os.stat(self._file).st_mode)[-3:]: os.chmod(self._file, 0o600)
os.chmod(self._file, 0o600)
except Exception: except Exception:
pass pass
if os.path.getsize(self._file) > 10:
self.load()
def handle_event(_, e: Event): def handle_event(_, e: Event):
self.save(**e.data) self.save(**e.data)
@ -109,6 +106,24 @@ class Tasks(metaclass=Singleton):
Tasks._instance = Tasks() Tasks._instance = Tasks()
return Tasks._instance 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]: def getTasks(self) -> List[Task]:
"""Return the tasks.""" """Return the tasks."""
return [job.task for job in self._jobs] return [job.task for job in self._jobs]
@ -266,7 +281,7 @@ class Tasks(metaclass=Singleton):
None None
""" """
try: try:
timeNow = datetime.datetime().isoformat() timeNow = datetime.now().isoformat()
started = time.time() started = time.time()
if not task.url: if not task.url:
LOG.error(f"Failed to dispatch task '{task.id}: {task.name}'. No URL found.") 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) preset: str = str(task.preset or self._default_preset)
folder: str = task.folder if task.folder else "" folder: str = task.folder if task.folder else ""
ytdlp_cookies: str = str(task.cookies) if task.cookies else "" cookies: str = str(task.cookies) if task.cookies else ""
output_template: str = task.template if task.template else "" template: str = task.template if task.template else ""
ytdlp_config = task.config if task.config else {} config = task.config if task.config else {}
if isinstance(ytdlp_config, str) and ytdlp_config: if isinstance(config, str) and config:
try: try:
ytdlp_config = json.loads(ytdlp_config) config = json.loads(config)
except Exception as e: except Exception as e:
LOG.error(f"Failed to parse json yt-dlp config for '{task.name}'. {str(e)}") LOG.error(f"Failed to parse json yt-dlp config for '{task.name}'. {str(e)}")
return return
await self.emitter.info(f"Task '{task.name}' dispatched at '{timeNow}'.")
LOG.info(f"Task '{task.id}: {task.name}' dispatched at '{timeNow}'.") LOG.info(f"Task '{task.id}: {task.name}' dispatched at '{timeNow}'.")
await self._emitter.emit( tasks = []
event=Events.ADD_URL, tasks.append(self._emitter.info(f"Task '{task.name}' dispatched at '{timeNow}'."))
data=Event( tasks.append(
id=task.id, self._emitter.emit(
data={ event=Events.ADD_URL,
"url": task.url, data=Event(
"preset": preset, id=task.id,
"folder": folder, data={
"ytdlp_cookies": ytdlp_cookies, "url": task.url,
"ytdlp_config": ytdlp_config, "preset": preset,
"output_template": output_template, "folder": folder,
}, "cookies": cookies,
), "config": config,
local=True, "template": template,
},
),
local=True,
)
) )
timeNow = datetime.datetime().isoformat() await asyncio.wait_for(asyncio.gather(*tasks), timeout=None)
timeNow = datetime.now().isoformat()
ended = time.time() ended = time.time()
LOG.info(f"Task '{task.id}: {task.name}' completed at '{timeNow}' took '{ended - started:.2f}' seconds.") 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: 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)}'.") 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 .DownloadQueue import DownloadQueue
from .encoder import Encoder from .encoder import Encoder
from .config import Config
LOG = logging.getLogger("common") 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. This class is used to share common methods between the socket and the API gateways.
""" """
queue: DownloadQueue def __init__(
encoder: Encoder self,
queue: DownloadQueue | None = None,
def __init__(self, queue: DownloadQueue, encoder: Encoder): encoder: Encoder | None = None,
config: Config | None = None,
):
super().__init__() super().__init__()
self.queue = queue self.queue = queue or DownloadQueue.get_instance()
self.encoder = encoder self.encoder = encoder or Encoder()
config = config or Config.get_instance()
self.default_preset = config.default_preset
async def add( 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]: ) -> 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, url=url,
preset=preset if preset else "default", preset=preset if preset else "default",
folder=folder, folder=folder,
ytdlp_cookies=ytdlp_cookies, cookies=cookies,
ytdlp_config=ytdlp_config if isinstance(ytdlp_config, dict) else {}, config=config if isinstance(config, dict) else {},
output_template=output_template, template=template,
) )
return status
def format_item(self, item: dict) -> dict: def format_item(self, item: dict) -> dict:
""" """
Format the item to be added to the download queue. Format the item to be added to the download queue.
@ -72,9 +91,9 @@ class common:
"url": url, "url": url,
"preset": preset, "preset": preset,
"folder": folder, "folder": folder,
"ytdlp_cookies": cookies, "cookies": cookies,
"ytdlp_config": config if isinstance(config, dict) else {}, "config": config if isinstance(config, dict) else {},
"output_template": template, "template": template,
} }
return item return item

View file

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

View file

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

View file

@ -113,13 +113,13 @@
</label> </label>
<div class="control has-icons-left"> <div class="control has-icons-left">
<input type="text" class="input" id="output_template" placeholder="The output template to use" <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> <span class="icon is-small is-left"><i class="fa-solid fa-file" /></span>
</div> </div>
<span class="help"> <span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span>The output template to use, if not set, it will defaults to <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> </span>
</div> </div>
</div> </div>

View file

@ -83,7 +83,7 @@ div.is-centered {
</p> </p>
<p> <p>
<span class="icon"><i class="fa-solid fa-file" /></span> <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>
<p> <p>
<span class="icon"><i class="fa-solid fa-tv" /></span> <span class="icon"><i class="fa-solid fa-tv" /></span>