diff --git a/app/library/Tasks.py b/app/library/Tasks.py index 256283a0..00bdd1e8 100644 --- a/app/library/Tasks.py +++ b/app/library/Tasks.py @@ -19,7 +19,8 @@ from .ItemDTO import Item from .Scheduler import Scheduler from .Services import Services from .Singleton import Singleton -from .Utils import init_class, validate_url +from .Utils import extract_info, init_class, validate_url +from .YTDLPOpts import YTDLPOpts LOG: logging.Logger = logging.getLogger("tasks") @@ -46,6 +47,80 @@ class Task: def get(self, key: str, default: Any = None) -> Any: return self.serialize().get(key, default) + def mark(self) -> tuple[bool, str]: + if not self.url: + return False, "No URL found in task parameters." + + params: YTDLPOpts = YTDLPOpts.get_instance().preset(name=self.preset) + if self.cli: + params.add_cli(self.cli, from_user=True) + + params = params.get_all() + if not (_archive := params.get("download_archive", None)): + return False, "No archive file found in task parameters." + + _archive: Path = Path(_archive) + if not _archive.parent.exists(): + _archive.parent.mkdir(parents=True, exist_ok=True) + + if not _archive.exists(): + _archive.touch() + + _info = extract_info(params, self.url, no_archive=True, follow_redirect=True) + if not _info or not isinstance(_info, dict): + return False, "Failed to extract information from URL." + + if "playlist" != _info.get("_type"): + return False, "Expected a playlist type from extract_info." + + archived_items: list[str] = [] + with _archive.open() as f: + for line in f: + line = line.strip() + if not line or not isinstance(line, str) or line.startswith("#"): + continue + + archived_items.append(line) + + def _process(item: dict) -> bool: + status = False + for entry in item.get("entries", []): + if not isinstance(entry, dict): + continue + + if "playlist" == entry.get("_type"): + _status = _process(entry) + if status is False: + status = _status + continue + + if entry.get("_type") not in ("video", "url"): + continue + + if not entry.get("id") or not entry.get("ie_key"): + continue + + archive_id = f'{entry.get("ie_key","").lower()} {entry.get("id")}' + + if archive_id in archived_items: + continue + + archived_items.append(archive_id) + status = True + + return status + + updated = _process(_info) + + if not updated: + return True, "No new items to mark as downloaded." + + with _archive.open("a") as f: + for item in archived_items: + f.write(f"{item}\n") + + return True, f"Task '{self.name}' marked as downloaded. Updated archive file '{_archive}'." + class Tasks(metaclass=Singleton): """ @@ -124,6 +199,19 @@ class Tasks(metaclass=Singleton): """Return the tasks.""" return self._tasks + def get(self, task_id: str) -> Task | None: + """ + Get a task by its ID. + + Args: + task_id (str): The ID of the task. + + Returns: + Task | None: The task if found, otherwise None. + + """ + return next((task for task in self._tasks if task.id == task_id), None) + def load(self) -> "Tasks": """ Load the tasks. diff --git a/app/routes/api/tasks.py b/app/routes/api/tasks.py index 92ecdea4..ac7b7886 100644 --- a/app/routes/api/tasks.py +++ b/app/routes/api/tasks.py @@ -91,3 +91,38 @@ async def tasks_add(request: Request, encoder: Encoder) -> Response: ) return web.json_response(data=tasks, status=web.HTTPOk.status_code, dumps=encoder.encode) + + +@route("POST", "api/tasks/{id}/mark", "tasks_mark") +async def mark_task(request: Request, encoder: Encoder) -> Response: + """ + Mark all items from task as downloaded. + + Args: + request (Request): The request object. + encoder (Encoder): The encoder instance. + + Returns: + Response: The response object + + """ + task_id: str = request.match_info.get("id", None) + + if not task_id: + return web.json_response(data={"error": "No task id."}, status=web.HTTPBadRequest.status_code) + + tasks = Tasks.get_instance() + try: + task = tasks.get(task_id) + if not task: + return web.json_response( + data={"error": f"Task '{task_id}' does not exist."}, status=web.HTTPNotFound.status_code + ) + + _status, _message = task.mark() + if not _status: + return web.json_response(data={"error": _message}, status=web.HTTPBadRequest.status_code) + + return web.json_response(data={"message": _message}, status=web.HTTPOk.status_code, dumps=encoder.encode) + except ValueError as e: + return web.json_response(data={"error": str(e)}, status=web.HTTPBadRequest.status_code) diff --git a/ui/app/assets/css/style.css b/ui/app/assets/css/style.css index 99262327..e2b67b87 100644 --- a/ui/app/assets/css/style.css +++ b/ui/app/assets/css/style.css @@ -178,6 +178,10 @@ hr { color: #fff; } +.has-text-purple { + color: #5f00d1; +} + .progress, .progress-percentage { border-radius: unset !important; diff --git a/ui/app/pages/tasks.vue b/ui/app/pages/tasks.vue index 74b9e910..f851f59e 100644 --- a/ui/app/pages/tasks.vue +++ b/ui/app/pages/tasks.vue @@ -97,7 +97,7 @@