ytptube/app/features/tasks/service.py
2026-06-14 18:35:03 +03:00

237 lines
8.4 KiB
Python

from __future__ import annotations
from typing import TYPE_CHECKING
from app.features.core.schemas import CEAction, CEFeature, ConfigEvent
from app.features.tasks.models import TaskModel
from app.features.tasks.utils import cron_time
from app.library.Events import Event, EventBus, Events
from app.library.log import get_logger
from app.library.Scheduler import Scheduler
from app.library.Services import Services
from app.library.Singleton import Singleton
if TYPE_CHECKING:
from aiohttp import web
LOG = get_logger()
class Tasks(metaclass=Singleton):
def __init__(self):
from app.features.tasks.deps import get_tasks_repo
self._repo = get_tasks_repo()
self._loaded: bool = False
self._handlers_service = None
self._scheduler = Scheduler.get_instance()
@staticmethod
def get_instance() -> Tasks:
"""Get the singleton instance of Tasks."""
return Tasks()
def attach(self, _: web.Application) -> None:
async def handle_started(_, __):
await self._repo.run_migrations()
await self._load_tasks()
await self._init_handlers_service(self._scheduler)
Services.get_instance().add("tasks_service", self).add("tasks_repository", self._repo)
async def handle_config_update(e: Event, _):
if isinstance(e.data, ConfigEvent) and CEFeature.TASKS == e.data.feature:
await self._handle_task_change(e.data)
EventBus.get_instance().subscribe(
Events.CONFIG_UPDATE, handle_config_update, "Tasks.config_update_scheduler"
).subscribe(Events.STARTED, handle_started, "TasksRepository.run_migrations")
async def on_shutdown(self, _: web.Application) -> None:
pass
async def _load_tasks(self) -> None:
tasks = await self._repo.all()
for task in tasks:
if not task.timer or not task.enabled:
continue
try:
self._scheduler.add(timer=task.timer, func=self._runner, args=(task,), id=f"task-cronjob-{task.id}")
LOG.info(
"Queued task '%s' to run at '%s'.",
task.name,
cron_time(task.timer),
extra={"task_id": task.id, "task_name": task.name, "timer": task.timer},
)
except Exception as e:
LOG.exception(
"Failed to queue task '%s'.",
task.name,
extra={
"task_id": task.id,
"task_name": task.name,
"timer": task.timer,
"exception_type": type(e).__name__,
},
)
async def _init_handlers_service(self, scheduler) -> None:
"""Initialize the handlers service after migrations."""
if self._handlers_service is not None:
return
from app.features.tasks.definitions.service import TaskHandle
from app.library.config import Config
config = Config.get_instance()
self._handlers_service = TaskHandle(scheduler, self._repo, config)
self._handlers_service.load()
LOG.debug("Task handlers service initialized.")
Services.get_instance().add("task_handle_service", self._handlers_service)
async def _handle_task_change(self, event_data) -> None:
task_data: dict = event_data.data
task_id: str = f"task-cronjob-{task_data['id']}"
if CEAction.DELETE == event_data.action:
if self._scheduler.has(task_id):
self._scheduler.remove(task_id)
elif event_data.action in (CEAction.CREATE, CEAction.UPDATE):
if not (task := await self._repo.get(int(task_data["id"]))):
return
if self._scheduler.has(task_id):
self._scheduler.remove(task_id)
if task.timer and task.enabled:
self._scheduler.add(timer=task.timer, func=self._runner, args=(task,), id=task_id)
LOG.info(
"Queued task '%s' to run at '%s'.",
task.name,
cron_time(task.timer),
extra={"task_id": task.id, "task_name": task.name, "timer": task.timer},
)
async def _runner(self, task: TaskModel) -> None:
"""
Execute a scheduled task.
Args:
task: The TaskModel to execute.
"""
import time
from datetime import UTC, datetime
from app.library.config import Config
from app.library.downloads import DownloadQueue
from app.library.ItemDTO import Item
timeNow: str = datetime.now(UTC).isoformat()
task_id = task.id
task_name = task.name
try:
current_task = await self._repo.get(task_id)
if not current_task:
LOG.info("Task '%s' no longer exists.", task_name, extra={"task_id": task_id, "task_name": task_name})
return
task = current_task
if not task.enabled:
LOG.debug(
"Task '%s' is disabled. Skipping execution.",
task.name,
extra={"task_id": task.id, "task_name": task.name},
)
return
if not task.url:
LOG.error(
"Failed to dispatch task '%s' because it has no URL.",
task.name,
extra={"task_id": task.id, "task_name": task.name},
)
return
started: float = time.time()
config = Config.get_instance()
preset: str = task.preset or config.default_preset
folder: str = task.folder or ""
template: str = task.template or ""
cli: str = task.cli or ""
notify: EventBus = EventBus.get_instance()
status = await DownloadQueue.get_instance().add(
item=Item.format(
{
"url": task.url,
"preset": preset,
"folder": folder,
"template": template,
"cli": cli,
"auto_start": task.auto_start,
"extras": {
"source_name": task.name,
"source_id": str(task.id),
"source_handler": "Tasks",
},
}
)
)
timeNow = datetime.now(UTC).isoformat()
ended: float = time.time()
LOG.info(
"Task '%s' completed in %.2f seconds.",
task.name,
ended - started,
extra={
"task_id": task.id,
"task_name": task.name,
"url": task.url,
"preset": preset,
"elapsed_s": round(ended - started, 2),
"status": status.get("status") if isinstance(status, dict) else None,
},
)
notify.emit(
Events.TASK_DISPATCHED,
data={**status, "preset": task.preset} if status else {"preset": task.preset},
title=f"Task '{task.name}' dispatched",
message=f"Task '{task.name}' dispatched at '{timeNow}'.",
)
notify.emit(
Events.LOG_SUCCESS,
data={"preset": task.preset, "lowPriority": True},
title="Task completed",
message=f"Task '{task.name}' completed in '{ended - started:.2f}'.",
)
except Exception as e:
LOG.exception(
"Failed to execute scheduled task '%s'.",
task.name,
extra={
"task_id": task.id,
"task_name": task.name,
"url": task.url,
"time": timeNow,
"exception_type": type(e).__name__,
},
)
EventBus.get_instance().emit(
Events.LOG_ERROR,
data={"preset": task.preset},
title="Task failed",
message=f"Failed to execute '{task.name}'. '{e!s}'",
)
@property
def handlers(self):
"""Get the handlers service instance."""
return self._handlers_service