completed path migration

This commit is contained in:
arabcoders 2025-06-10 12:29:21 +03:00
parent e42af75137
commit 294ec31e94
6 changed files with 76 additions and 112 deletions

View file

@ -134,15 +134,14 @@ class Notification(metaclass=Singleton):
config: Config = config or Config.get_instance() config: Config = config or Config.get_instance()
self._debug = config.debug self._debug = config.debug
self._file: Path = Path(file) if file else (Path(config.config_path) / "notifications.json") self._file: Path = Path(file) if file else Path(config.config_path).joinpath("notifications.json")
self._client: httpx.AsyncClient = client or httpx.AsyncClient() self._client: httpx.AsyncClient = client or httpx.AsyncClient()
self._encoder: Encoder = encoder or Encoder() self._encoder: Encoder = encoder or Encoder()
self._version = config.version self._version = config.version
if self._file.exists(): if self._file.exists() and "600" != self._file.stat().st_mode:
try: try:
if "600" != self._file.stat().st_mode: self._file.chmod(0o600)
self._file.chmod(0o600)
except Exception: except Exception:
pass pass
@ -213,13 +212,12 @@ class Notification(metaclass=Singleton):
try: try:
try: try:
Notification.validate(target) Notification.validate(target)
target: Target = self.make_target(target)
except ValueError as e: except ValueError as e:
name = target.get("name") or target.get("id") or target.get("request", {}).get("url") or "unknown" name = target.get("name") or target.get("id") or target.get("request", {}).get("url") or "unknown"
LOG.error(f"Invalid notification target '{name}'. '{e!s}'") LOG.error(f"Invalid notification target '{name}'. '{e!s}'")
continue continue
target = self.make_target(target)
self._targets.append(target) self._targets.append(target)
LOG.info( LOG.info(

View file

@ -1,7 +1,6 @@
import asyncio import asyncio
import hashlib import hashlib
import logging import logging
import os
import tempfile import tempfile
from pathlib import Path from pathlib import Path
@ -33,13 +32,14 @@ class Segments:
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
tmpDir = tempfile.gettempdir() tmpFile = Path(tempfile.gettempdir()).joinpath(
tmpFile = os.path.join(tmpDir, f"ytptube_stream.{hashlib.sha256(str(file).encode()).hexdigest()}") f"ytptube_stream.{hashlib.sha256(str(file).encode()).hexdigest()}"
)
if not os.path.exists(tmpFile): if not tmpFile.exists():
os.symlink(file, tmpFile) tmpFile.symlink_to(file, target_is_directory=False)
startTime = f"{0:.6f}" if self.index == 0 else f"{self.duration * self.index:.6f}" startTime: str = f"{0:.6f}" if self.index == 0 else f"{self.duration * self.index:.6f}"
fargs = [ fargs = [
"-xerror", "-xerror",
@ -78,7 +78,7 @@ class Segments:
return fargs return fargs
async def stream(self, file: Path, resp: web.StreamResponse): async def stream(self, file: Path, resp: web.StreamResponse):
ffmpeg_args = await self.build_ffmpeg_args(file) ffmpeg_args: list[str] = await self.build_ffmpeg_args(file)
proc = await asyncio.create_subprocess_exec( proc = await asyncio.create_subprocess_exec(
"ffmpeg", "ffmpeg",
@ -94,7 +94,7 @@ class Segments:
try: try:
while True: while True:
chunk = await proc.stdout.read(1024 * 64) chunk: bytes = await proc.stdout.read(1024 * 64)
if not chunk: if not chunk:
break break
try: try:

View file

@ -22,9 +22,6 @@ SubstationFormat.ms_to_timestamp = ms_to_timestamp
class Subtitle: class Subtitle:
def __init__(self, download_path: str):
self.download_path = download_path
async def make(self, file: Path) -> str: async def make(self, file: Path) -> str:
if file.suffix not in ALLOWED_SUBS_EXTENSIONS: if file.suffix not in ALLOWED_SUBS_EXTENSIONS:
msg = f"File '{file}' subtitle type is not supported." msg = f"File '{file}' subtitle type is not supported."
@ -34,7 +31,7 @@ class Subtitle:
async with await anyio.open_file(file) as f: async with await anyio.open_file(file) as f:
return await f.read() return await f.read()
subs = pysubs2.load(path=str(file)) subs: pysubs2.SSAFile = pysubs2.load(path=str(file))
if len(subs.events) < 1: if len(subs.events) < 1:
msg = f"No subtitle events were found in '{file}'." msg = f"No subtitle events were found in '{file}'."
@ -43,7 +40,10 @@ class Subtitle:
if len(subs.events) < 2: if len(subs.events) < 2:
return subs.to_string("vtt") return subs.to_string("vtt")
if subs.events[0].end == subs.events[len(subs.events) - 1].end: try:
subs.events.pop(0) if subs.events[0].end == subs.events[len(subs.events) - 1].end:
subs.events.pop(0)
except Exception:
pass
return subs.to_string("vtt") return subs.to_string("vtt")

View file

@ -1,10 +1,10 @@
import asyncio import asyncio
import json import json
import logging import logging
import os
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from datetime import UTC, datetime from datetime import UTC, datetime
from pathlib import Path
from typing import Any from typing import Any
import httpx import httpx
@ -15,7 +15,7 @@ from .encoder import Encoder
from .Events import EventBus, Events, error, info, success from .Events import EventBus, Events, error, info, success
from .Scheduler import Scheduler from .Scheduler import Scheduler
from .Singleton import Singleton from .Singleton import Singleton
from .Utils import clean_item from .Utils import init_class
LOG = logging.getLogger("tasks") LOG = logging.getLogger("tasks")
@ -65,18 +65,18 @@ class Tasks(metaclass=Singleton):
config = config or Config.get_instance() config = config or Config.get_instance()
self._debug = config.debug self._debug: bool = config.debug
self._default_preset = config.default_preset self._default_preset: str = config.default_preset
self._file = file or os.path.join(config.config_path, "tasks.json") self._file: Path = Path(file) if file else Path(config.config_path).joinpath("tasks.json")
self._client = client or httpx.AsyncClient() self._client: httpx.AsyncClient = client or httpx.AsyncClient()
self._encoder = encoder or Encoder() self._encoder: Encoder = encoder or Encoder()
self._loop = loop or asyncio.get_event_loop() self._loop: asyncio.AbstractEventLoop = loop or asyncio.get_event_loop()
self._scheduler = scheduler or Scheduler.get_instance() self._scheduler: Scheduler = scheduler or Scheduler.get_instance()
self._notify = EventBus.get_instance() self._notify: EventBus = EventBus.get_instance()
if os.path.exists(self._file) and "600" != oct(os.stat(self._file).st_mode)[-3:]: if self._file.exists() and "600" != self._file.stat().st_mode:
try: try:
os.chmod(self._file, 0o600) self._file.chmod(0o600)
except Exception: except Exception:
pass pass
@ -126,29 +126,23 @@ class Tasks(metaclass=Singleton):
""" """
self.clear() self.clear()
if not os.path.exists(self._file) or os.path.getsize(self._file) < 10: if not self._file.exists() or self._file.stat().st_size < 1:
return self return self
LOG.info(f"Loading tasks from '{self._file}'.")
try: try:
with open(self._file) as f: LOG.info(f"Loading '{self._file}'.")
tasks = json.load(f) tasks = json.loads(self._file.read_text())
except Exception as e: except Exception as e:
LOG.error(f"Failed to parse tasks from '{self._file}'. '{e!s}'.") LOG.error(f"Error loading '{self._file}'. '{e!s}'.")
return self return self
if not tasks or len(tasks) < 1: if not tasks or len(tasks) < 1:
LOG.info(f"No tasks were defined in '{self._file}'.")
return self return self
need_save = False
for i, task in enumerate(tasks): for i, task in enumerate(tasks):
try: try:
task, task_status = clean_item(task, keys=("cookies", "config")) Tasks.validate(task)
self.validate(task) task: Task = init_class(Task, task)
task = Task(**task)
if task_status:
need_save = True
except Exception as e: except Exception as e:
LOG.error(f"Failed to parse task at list position '{i}'. '{e!s}'.") LOG.error(f"Failed to parse task at list position '{i}'. '{e!s}'.")
continue continue
@ -172,11 +166,7 @@ class Tasks(metaclass=Singleton):
LOG.info(f"Task '{i}: {task.name}' queued to be executed '{schedule_time}'.") LOG.info(f"Task '{i}: {task.name}' queued to be executed '{schedule_time}'.")
except Exception as e: except Exception as e:
LOG.exception(e) LOG.exception(e)
LOG.error(f"Failed to queue task '{i}: {task.name}'. '{e!s}'.") LOG.error(f"Failed to queue '{i}: {task.name}'. '{e!s}'.")
if need_save:
LOG.info("Updating tasks file to remove old keys.")
self.save(self.get_all())
return self return self
@ -193,18 +183,19 @@ class Tasks(metaclass=Singleton):
for task in self._tasks: for task in self._tasks:
try: try:
LOG.info(f"Stopping task '{task.id}: {task.name}'.") LOG.info(f"Stopping '{task.id}: {task.name}'.")
self._scheduler.remove(task.id) self._scheduler.remove(task.id)
except Exception as e: except Exception as e:
if not shutdown: if not shutdown:
LOG.exception(e) LOG.exception(e)
LOG.error(f"Failed to stop task '{task.id}: {task.name}'. '{e!s}'.") LOG.error(f"Failed to stop '{task.id}: {task.name}'. '{e!s}'.")
self._tasks.clear() self._tasks.clear()
return self return self
def validate(self, task: Task | dict) -> bool: @staticmethod
def validate(task: Task | dict) -> bool:
""" """
Validate the task. Validate the task.
@ -262,31 +253,28 @@ class Tasks(metaclass=Singleton):
""" """
for i, task in enumerate(tasks): for i, task in enumerate(tasks):
try:
if not isinstance(task, Task):
task = Task(**task)
tasks[i] = task
except Exception as e:
LOG.error(f"Failed to save task '{i}' unable to parse task. '{e!s}'.")
continue
try: try:
self.validate(task) self.validate(task)
if not isinstance(task, Task):
task: Task = init_class(Task, task)
tasks[i] = task
except ValueError as e: except ValueError as e:
LOG.error(f"Failed to add task '{i}: {task.name}'. '{e}'.") LOG.error(f"Failed to validate item '{i}: {task.name}'. '{e}'.")
continue
except Exception as e:
LOG.error(f"Failed to save task '{i}'. '{e!s}'.")
continue continue
try: try:
with open(self._file, "w") as f: self._file.write_text(json.dumps([i.serialize() for i in tasks], indent=4))
json.dump(obj=[task.serialize() for task in tasks], fp=f, indent=4) LOG.info(f"Updated '{self._file}'.")
LOG.info(f"Tasks saved to '{self._file}'.")
except Exception as e: except Exception as e:
LOG.error(f"Failed to save tasks to '{self._file}'. '{e!s}'.") LOG.error(f"Error saving '{self._file}'. '{e!s}'.")
return self return self
async def _runner(self, task: Task): async def _runner(self, task: Task) -> None:
""" """
Run the task. Run the task.
@ -297,11 +285,11 @@ class Tasks(metaclass=Singleton):
None None
""" """
timeNow: str = datetime.now(UTC).isoformat()
try: try:
timeNow = datetime.now(UTC).isoformat() started: float = 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.id}: {task.name}'. No URL found.")
return return
preset: str = str(task.preset or self._default_preset) preset: str = str(task.preset or self._default_preset)
@ -309,13 +297,10 @@ class Tasks(metaclass=Singleton):
template: str = task.template if task.template else "" template: str = task.template if task.template else ""
cli: str = task.cli if task.cli else "" cli: str = task.cli if task.cli else ""
LOG.info(f"Task '{task.id}: {task.name}' dispatched at '{timeNow}'.") LOG.info(f"Dispatched '{task.id}: {task.name}' at '{timeNow}'.")
tasks = [] tasks: list = [
tasks.append( self._notify.emit(Events.LOG_INFO, data=info(f"Dispatched '{task.name}' at '{timeNow}'.")),
self._notify.emit(Events.LOG_INFO, data=info(f"Task '{task.name}' dispatched at '{timeNow}'."))
)
tasks.append(
self._notify.emit( self._notify.emit(
Events.ADD_URL, Events.ADD_URL,
data={ data={
@ -327,22 +312,21 @@ class Tasks(metaclass=Singleton):
}, },
id=task.id, id=task.id,
), ),
) ]
await asyncio.wait_for(asyncio.gather(*tasks), timeout=None) await asyncio.wait_for(asyncio.gather(*tasks), timeout=None)
timeNow = datetime.now(UTC).isoformat() timeNow = datetime.now(UTC).isoformat()
ended = time.time() ended: float = time.time()
LOG.info(f"Task '{task.id}: {task.name}' completed at '{timeNow}' took '{ended - started:.2f}' seconds.") LOG.info(f"Completed '{task.id}: {task.name}' at '{timeNow}' took '{ended - started:.2f}' seconds.")
await self._notify.emit( await self._notify.emit(
Events.LOG_SUCCESS, Events.LOG_SUCCESS,
data=success(f"Task '{task.name}' completed in '{ended - started:.2f}' seconds."), data=success(f"Completed '{task.name}' in '{ended - started:.2f}' seconds."),
) )
except Exception as e: except Exception as e:
timeNow = datetime.now(UTC).isoformat() LOG.error(f"Failed to execute '{task.id}: {task.name}' at '{timeNow}'. '{e!s}'.")
LOG.error(f"Task '{task.id}: {task.name}' has failed to execute at '{timeNow}'. '{e!s}'.")
await self._notify.emit( await self._notify.emit(
Events.ERROR, data=error(f"Task '{task.name}' failed to execute at '{timeNow}'. '{e!s}'.") Events.ERROR, data=error(f"Failed to execute '{task.name}' at '{timeNow}'. '{e!s}'.")
) )

View file

@ -1016,7 +1016,7 @@ async def tail_log(file: Path, emitter: callable, sleep_time: float = 0.5):
return return
def load_cookies(file: str) -> tuple[bool, MozillaCookieJar]: def load_cookies(file: str | Path) -> tuple[bool, MozillaCookieJar]:
""" """
Validate and load a cookie file. Validate and load a cookie file.
@ -1030,7 +1030,7 @@ def load_cookies(file: str) -> tuple[bool, MozillaCookieJar]:
try: try:
from http.cookiejar import MozillaCookieJar from http.cookiejar import MozillaCookieJar
cookies = MozillaCookieJar(file, None, None) cookies = MozillaCookieJar(str(file), None, None)
cookies.load() cookies.load()
return (True, cookies) return (True, cookies)

View file

@ -12,31 +12,18 @@ LOG = logging.getLogger("upgrader")
class Upgrader: class Upgrader:
def __init__(self): def __init__(self):
import argparse config_path: Path = Path(__file__).parent.parent / "var" / "config"
if env_path := os.environ.get("YTP_CONFIG_PATH", None):
config_path = Path(env_path)
parser = argparse.ArgumentParser( if config_path.exists():
prog="upgrader.py", envFile: Path = config_path / ".env"
formatter_class=argparse.ArgumentDefaultsHelpFormatter, if envFile.exists():
description="Upgrade packages and run the application.", LOG.debug(f"loading environment variables from '{envFile}'.")
epilog="Example: upgrader.py --run", load_dotenv(str(envFile))
) else:
parser.add_argument(
"-r", "--run", action="store_true", help="Run the application after upgrading the packages."
)
args, _ = parser.parse_known_args()
rootPath = str(Path(__file__).parent.parent.absolute())
config_path = os.environ.get("YTP_CONFIG_PATH", None) or os.path.join(rootPath, "var", "config")
if not Path(config_path).exists():
LOG.error(f"config path '{config_path}' doesn't exists.") LOG.error(f"config path '{config_path}' doesn't exists.")
envFile = Path(config_path, ".env")
if envFile.exists():
LOG.debug(f"loading environment variables from '{envFile}'.")
load_dotenv(str(envFile))
pkg_installer = PackageInstaller() pkg_installer = PackageInstaller()
ytdlp_auto_update: bool = os.environ.get("YTP_YTDLP_AUTO_UPDATE", "true").strip().lower() == "true" ytdlp_auto_update: bool = os.environ.get("YTP_YTDLP_AUTO_UPDATE", "true").strip().lower() == "true"
@ -65,11 +52,6 @@ class Upgrader:
LOG.exception(e) LOG.exception(e)
LOG.error(f"Failed to check for packages. '{e!s}'.") LOG.error(f"Failed to check for packages. '{e!s}'.")
if args.run:
from main import Main
Main().start()
if __name__ == "__main__": if __name__ == "__main__":
try: try: