Implement a way to mark items from task as already downloaded.
This commit is contained in:
parent
59508b5332
commit
1589e73063
4 changed files with 197 additions and 21 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -178,6 +178,10 @@ hr {
|
|||
color: #fff;
|
||||
}
|
||||
|
||||
.has-text-purple {
|
||||
color: #5f00d1;
|
||||
}
|
||||
|
||||
.progress,
|
||||
.progress-percentage {
|
||||
border-radius: unset !important;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue