Implement a way to mark items from task as already downloaded.

This commit is contained in:
arabcoders 2025-08-05 17:34:07 +03:00
parent 59508b5332
commit 1589e73063
4 changed files with 197 additions and 21 deletions

View file

@ -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.

View file

@ -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)

View file

@ -178,6 +178,10 @@ hr {
color: #fff;
}
.has-text-purple {
color: #5f00d1;
}
.progress,
.progress-percentage {
border-radius: unset !important;

View file

@ -97,7 +97,7 @@
<div class="columns is-multiline" v-if="!isLoading && !toggleForm && filteredTasks && filteredTasks.length > 0">
<template v-if="'list' === display_style">
<div class="column is-12">
<div class="table-container">
<div :class="{ 'table-container': table_container }">
<table class="table is-striped is-hoverable is-fullwidth is-bordered"
style="min-width: 850px; table-layout: fixed;">
<thead>
@ -171,20 +171,6 @@
</td>
<td class="is-vcentered is-items-center">
<div class="field is-grouped is-grouped-centered">
<div class="control">
<button class="button is-purple is-small is-fullwidth" v-tooltip="'Run now'"
@click="runNow(item)" :class="{ 'is-loading': item?.in_progress }">
<span class="icon"><i class="fa-solid fa-up-right-from-square" /></span>
</button>
</div>
<div class="control">
<button class="button is-info is-small is-fullwidth" v-tooltip="'Export'"
@click="exportItem(item)">
<span class="icon"><i class="fa-solid fa-file-export" /></span>
</button>
</div>
<div class="control">
<button class="button is-warning is-small is-fullwidth" v-tooltip="'Edit'"
@click="editItem(item)">
@ -197,6 +183,31 @@
<span class="icon"><i class="fa-solid fa-trash" /></span>
</button>
</div>
<div class="control is-expanded">
<Dropdown icons="fa-solid fa-cogs" label="Actions" button_classes="is-small"
@open_state="s => table_container = !s">
<NuxtLink class="dropdown-item has-text-purple" @click="runNow(item)">
<span class="icon"><i class="fa-solid fa-up-right-from-square" /></span>
<span>Run now</span>
</NuxtLink>
<hr class="dropdown-divider" />
<NuxtLink class="dropdown-item has-text-danger" @click="archiveItems(item)">
<span class="icon"><i class="fa-solid fa-box-archive" /></span>
<span>Archive all</span>
</NuxtLink>
<hr class="dropdown-divider" />
<NuxtLink class="dropdown-item has-text-info" @click="exportItem(item)">
<span class="icon"><i class="fa-solid fa-file-export" /></span>
<span>Export Task</span>
</NuxtLink>
</Dropdown>
</div>
</div>
</td>
</tr>
@ -292,12 +303,21 @@
<span>Delete</span>
</button>
</div>
<div class="card-footer-item">
<button class="button is-purple is-fullwidth" @click="runNow(item)"
:class="{ 'is-loading': item?.in_progress }">
<span class="icon"><i class="fa-solid fa-up-right-from-square" /></span>
<span>Run now</span>
</button>
<Dropdown icons="fa-solid fa-cogs" label="Actions">
<NuxtLink class="dropdown-item has-text-purple" @click="runNow(item)">
<span class="icon"><i class="fa-solid fa-up-right-from-square" /></span>
<span>Run now</span>
</NuxtLink>
<hr class="dropdown-divider" />
<NuxtLink class="dropdown-item has-text-danger" @click="archiveItems(item)">
<span class="icon"><i class="fa-solid fa-box-archive" /></span>
<span>Archive all</span>
</NuxtLink>
</Dropdown>
</div>
</div>
</div>
@ -348,6 +368,7 @@ const selectedElms = ref<Array<string>>([])
const masterSelectAll = ref(false)
const massRun = ref<boolean>(false)
const massDelete = ref<boolean>(false)
const table_container = ref(false)
const reset_dialog = () => ({
visible: false,
@ -727,4 +748,32 @@ const get_tags = (name: string): Array<string> => {
}
const remove_tags = (name: string): string => name.replace(/\[(.*?)\]/g, '').trim();
const archiveItems = async (item: task_item) => {
dialog_confirm.value.visible = true
dialog_confirm.value.title = 'Archive All videos'
dialog_confirm.value.message = `Archive all items for '${item.name}' task? This will mark all items as downloaded and update the archive file.`
dialog_confirm.value.confirm = async () => await archiveAll(item)
}
const archiveAll = async (item: task_item) => {
try {
dialog_confirm.value.visible = false
item.in_progress = true
const response = await request(`/api/tasks/${item.id}/mark`, { method: 'POST' })
const data = await response.json()
if (data?.error) {
toast.error(data.error)
return
}
toast.success(data.message)
} catch (e: any) {
toast.error(`Failed to archive items. ${e.message || 'Unknown error.'}`)
return
} finally {
item.in_progress = false
}
}
</script>