diff --git a/API.md b/API.md index 68717ba8..ba5d5a29 100644 --- a/API.md +++ b/API.md @@ -25,8 +25,8 @@ This document describes the available endpoints and their usage. All endpoints r - [POST /api/history/{id}](#post-apihistoryid) - [GET /api/history/{id}](#get-apihistoryid) - [GET /api/history](#get-apihistory) - - [DELETE /api/archive/{id}](#delete-apiarchiveid) - - [POST /api/archive/{id}](#post-apiarchiveid) + - [DELETE /api/history/{id}/archive](#delete-apihistoryidarchive) + - [POST /api/history/{id}/archive](#post-apihistoryidarchive) - [GET /api/tasks](#get-apitasks) - [PUT /api/tasks](#put-apitasks) - [POST /api/tasks/{id}/mark](#post-apitasksidmark) @@ -361,17 +361,11 @@ or an error: --- -### DELETE /api/archive/{id} -**Purpose**: Remove an item's URL from the yt-dlp archive file, allowing it to be re-downloaded. +### DELETE /api/history/{id}/archive +**Purpose**: Remove an item from archive file, allowing it to be re-downloaded. **Path Parameter**: -- `id`: Item ID from the history (if body `url` is not provided). - -**Body (optional)**: -```json -{ "url": "https://..." } -``` -If `url` is provided, it is used directly; otherwise the route resolves the item by `id` and uses its URL. +- `id`: Item ID from the history. **Response**: ```json @@ -387,8 +381,8 @@ or an error: --- -### POST /api/archive/{id} -**Purpose**: Manually mark an item as archived by writing its archive ID to the archive file. +### POST /api/history/{id}/archive +**Purpose**: Add item to the archive file preventing it from being downloaded. **Path Parameter**: - `id`: Item ID from the history. diff --git a/app/routes/api/archive.py b/app/routes/api/archive.py deleted file mode 100644 index dabb2044..00000000 --- a/app/routes/api/archive.py +++ /dev/null @@ -1,148 +0,0 @@ -import logging -from pathlib import Path -from typing import TYPE_CHECKING - -from aiohttp import web -from aiohttp.web import Request, Response - -from app.library.Download import Download -from app.library.DownloadQueue import DownloadQueue -from app.library.router import route -from app.library.Utils import archive_add, archive_delete, archive_read, get_archive_id -from app.library.YTDLPOpts import YTDLPOpts - -if TYPE_CHECKING: - from library.Download import Download - -LOG: logging.Logger = logging.getLogger(__name__) - - -@route("POST", r"api/archive/{id}", "archive.item") -async def archive_item(request: Request, queue: DownloadQueue): - """ - Manually mark an item as archived. - - Args: - request (Request): The request object. - queue (DownloadQueue): The download queue instance. - config (Config): The configuration instance. - - Returns: - Response: The response object. - - """ - if not (id := request.match_info.get("id")): - return web.json_response(data={"error": "id is required."}, status=web.HTTPBadRequest.status_code) - - try: - item: Download | None = queue.done.get_by_id(id) - if not item: - return web.json_response(data={"error": f"item '{id}' not found."}, status=web.HTTPNotFound.status_code) - except KeyError: - return web.json_response(data={"error": f"item '{id}' not found."}, status=web.HTTPNotFound.status_code) - - params: dict = item.get_ytdlp_opts().get_all() - - if not (archive_file := params.get("download_archive")): - return web.json_response( - data={"error": f"item '{item.info.title}' does not have an archive file."}, - status=web.HTTPBadRequest.status_code, - ) - - idDict = get_archive_id(url=item.info.url) - if not (archive_id := idDict.get("archive_id")): - return web.json_response( - data={"error": f"item '{item.info.title}' does not have an archive ID."}, - status=web.HTTPBadRequest.status_code, - ) - - if len(archive_read(archive_file, [archive_id])) > 0: - return web.json_response( - data={"error": f"item '{item.info.title}' already archived."}, - status=web.HTTPConflict.status_code, - ) - - archive_add(archive_file, [archive_id]) - - return web.json_response( - data={"message": f"item '{item.info.title}' archived."}, - status=web.HTTPOk.status_code, - ) - - -@route("DELETE", r"api/archive/{id}", "archive.remove") -async def archive_remove(request: Request, queue: DownloadQueue) -> Response: - """ - Remove an item from the archive. - - Args: - request (Request): The request object. - queue (DownloadQueue): The download queue instance. - - Returns: - Response: The response object. - - """ - item = None - try: - data: dict | None = await request.json() - except Exception: - data = {} - - title: str = "" - url: str | None = data.get("url") - id: str | None = request.match_info.get("id") - preset: str | None = data.get("preset",) - - if not id and not url: - return web.json_response(data={"error": "id or url is required."}, status=web.HTTPBadRequest.status_code) - - if id: - try: - item: Download | None = queue.done.get_by_id(id) - if not item: - return web.json_response(data={"error": f"item '{id}' not found."}, status=web.HTTPNotFound.status_code) - - url = item.info.url - title = f" '{item.info.title}'" - params = item.get_ytdlp_opts().get_all() - except KeyError: - return web.json_response(data={"error": f"item '{id}' not found."}, status=web.HTTPNotFound.status_code) - else: - params: YTDLPOpts = YTDLPOpts.get_instance() - if preset := data.get("preset"): - params = params.preset(name=preset) - - params = params.get_all() - - if not (archive_file := params.get("download_archive", None)): - return web.json_response( - data={"error": "Archive file is not configured."}, - status=web.HTTPBadRequest.status_code, - ) - - archive_file = Path(archive_file) - - if not archive_file.exists(): - return web.json_response( - data={"error": f"Archive file '{archive_file}' does not exist."}, - status=web.HTTPNotFound.status_code, - ) - - idDict = get_archive_id(url=url) - if not (archive_id := idDict.get("archive_id")): - return web.json_response( - data={"error": "item does not have an archive ID."}, - status=web.HTTPBadRequest.status_code, - ) - - if not archive_delete(archive_file, [archive_id]): - return web.json_response( - data={"error": f"item{title} not found in '{archive_file}' archive."}, - status=web.HTTPNotFound.status_code, - ) - - return web.json_response( - data={"message": f"item{title} removed from '{archive_file}' archive."}, - status=web.HTTPOk.status_code, - ) diff --git a/app/routes/api/history.py b/app/routes/api/history.py index aba91bad..57788f2c 100644 --- a/app/routes/api/history.py +++ b/app/routes/api/history.py @@ -7,13 +7,14 @@ from aiohttp import web from aiohttp.web import Request, Response from app.library.config import Config +from app.library.Download import Download from app.library.DownloadQueue import DownloadQueue from app.library.encoder import Encoder from app.library.Events import EventBus, Events from app.library.ItemDTO import Item from app.library.Presets import Preset, Presets from app.library.router import route -from app.library.Utils import archive_read +from app.library.Utils import archive_add, archive_delete, archive_read, get_archive_id if TYPE_CHECKING: from library.Download import Download @@ -263,3 +264,116 @@ async def items_add(request: Request, queue: DownloadQueue, encoder: Encoder) -> response.append({"item": item, "status": "ok" == status[i].get("status"), "msg": status[i].get("msg")}) return web.json_response(data=response, status=web.HTTPOk.status_code, dumps=encoder.encode) + + +@route("POST", r"api/history/{id}/archive", "history.item.archive.add") +async def item_archive_add(request: Request, queue: DownloadQueue) -> Response: + """ + Manually mark an item as archived. + + Args: + request (Request): The request object. + queue (DownloadQueue): The download queue instance. + config (Config): The configuration instance. + + Returns: + Response: The response object. + + """ + if not (id := request.match_info.get("id")): + return web.json_response(data={"error": "id is required."}, status=web.HTTPBadRequest.status_code) + + try: + item: Download | None = queue.done.get_by_id(id) + if not item: + return web.json_response(data={"error": f"item '{id}' not found."}, status=web.HTTPNotFound.status_code) + except KeyError: + return web.json_response(data={"error": f"item '{id}' not found."}, status=web.HTTPNotFound.status_code) + + params: dict = item.get_ytdlp_opts().get_all() + + if not (archive_file := params.get("download_archive")): + return web.json_response( + data={"error": f"item '{item.info.title}' does not have an archive file."}, + status=web.HTTPBadRequest.status_code, + ) + + idDict = get_archive_id(url=item.info.url) + if not (archive_id := idDict.get("archive_id")): + return web.json_response( + data={"error": f"item '{item.info.title}' does not have an archive ID."}, + status=web.HTTPBadRequest.status_code, + ) + + if len(archive_read(archive_file, [archive_id])) > 0: + return web.json_response( + data={"error": f"item '{item.info.title}' already archived."}, + status=web.HTTPConflict.status_code, + ) + + archive_add(archive_file, [archive_id]) + + return web.json_response( + data={"message": f"item '{item.info.title}' archived."}, + status=web.HTTPOk.status_code, + ) + + +@route("DELETE", r"api/history/{id}/archive", "history.item.archive.delete") +async def item_archive_delete(request: Request, queue: DownloadQueue) -> Response: + """ + Remove an item from the archive. + + Args: + request (Request): The request object. + queue (DownloadQueue): The download queue instance. + + Returns: + Response: The response object. + + """ + if not (id := request.match_info.get("id")): + return web.json_response(data={"error": "id is required."}, status=web.HTTPBadRequest.status_code) + + try: + item: Download | None = queue.done.get_by_id(id) + if not item: + return web.json_response(data={"error": f"item '{id}' not found."}, status=web.HTTPNotFound.status_code) + except KeyError: + return web.json_response(data={"error": f"item '{id}' not found."}, status=web.HTTPNotFound.status_code) + + url: str = item.info.url + title: str = f" '{item.info.title}'" + params: dict = item.get_ytdlp_opts().get_all() + + if not (archive_file := params.get("download_archive")): + return web.json_response( + data={"error": "Archive file is not configured."}, + status=web.HTTPBadRequest.status_code, + ) + + archive_file = Path(archive_file) + + if not archive_file.exists(): + return web.json_response( + data={"error": f"Archive file '{archive_file}' does not exist."}, + status=web.HTTPNotFound.status_code, + ) + + idDict = get_archive_id(url=url) + if not (archive_id := idDict.get("archive_id")): + return web.json_response( + data={"error": "item does not have an archive ID."}, + status=web.HTTPBadRequest.status_code, + ) + + if not archive_delete(archive_file, [archive_id]): + return web.json_response( + data={"error": f"item{title} not found in '{archive_file}' archive."}, + status=web.HTTPNotFound.status_code, + ) + + return web.json_response( + data={"message": f"item{title} removed from '{archive_file}' archive."}, + status=web.HTTPOk.status_code, + ) diff --git a/ui/app/components/GetInfo.vue b/ui/app/components/GetInfo.vue index 9b16433b..cc28e18b 100644 --- a/ui/app/components/GetInfo.vue +++ b/ui/app/components/GetInfo.vue @@ -47,6 +47,7 @@ const emitter = defineEmits<{ (e: 'closeModel'): void }>() const props = defineProps<{ link?: string preset?: string + cli?: string useUrl?: boolean externalModel?: boolean }>() @@ -71,6 +72,9 @@ onMounted(async (): Promise => { if (props.preset) { params.append('preset', props.preset) } + if (props.cli) { + params.append('args', props.cli) + } params.append('url', props.link || '') url += '?' + params.toString() } diff --git a/ui/app/components/History.vue b/ui/app/components/History.vue index 1fe47119..4c94077d 100644 --- a/ui/app/components/History.vue +++ b/ui/app/components/History.vue @@ -193,7 +193,7 @@ - + yt-dlp Information @@ -368,7 +368,7 @@ - yt-dlp Information @@ -466,7 +466,7 @@ import { useStorage } from '@vueuse/core' import type { StoreItem } from '~/types/store' const emitter = defineEmits<{ - (e: 'getInfo', url: string, preset: string): void + (e: 'getInfo', url: string, preset: string, cli: string): void (e: 'add_new', item: Partial): void (e: 'getItemInfo', id: string): void (e: 'clear_search'): void @@ -737,7 +737,7 @@ const addArchiveDialog = (item: StoreItem) => { const archiveItem = async (item: StoreItem, opts = {}) => { try { - const req = await request(`/api/archive/${item._id}`, { + const req = await request(`/api/history/${item._id}/archive`, { credentials: 'include', method: 'POST', }) @@ -871,10 +871,7 @@ const removeFromArchiveDialog = (item: StoreItem) => { const removeFromArchive = async (item: StoreItem, opts?: { re_add?: boolean, remove_history?: boolean }) => { console.log('Removing from archive:', item, opts) try { - const req = await request(`/api/archive/${item._id}`, { - credentials: 'include', - method: 'DELETE', - }) + const req = await request(`/api/history/${item._id}/archive`, { credentials: 'include', method: 'DELETE' }) const data = await req.json() if (!req.ok) { toast.error(data.error) diff --git a/ui/app/components/NewDownload.vue b/ui/app/components/NewDownload.vue index 13962837..a59a0069 100644 --- a/ui/app/components/NewDownload.vue +++ b/ui/app/components/NewDownload.vue @@ -167,16 +167,11 @@ - + yt-dlp Information - - - Remove from archive - - @@ -194,7 +189,8 @@
-
-
- -
-
- - + + - + @@ -128,8 +129,9 @@ const show_thumbnail = useStorage('show_thumbnail', true) const info_view = ref({ url: '', preset: '', + cli: '', useUrl: false, -}) as Ref<{ url: string, preset: string, useUrl: boolean }> +}) as Ref<{ url: string, preset: string, cli: string, useUrl: boolean }> const item_form = ref({}) const query = ref() const toggleFilter = ref(false) @@ -198,10 +200,11 @@ const close_info = () => { info_view.value.useUrl = false } -const view_info = (url: string, useUrl: boolean = false, preset: string = '') => { +const view_info = (url: string, useUrl: boolean = false, preset: string = '', cli: string = '') => { info_view.value.url = url info_view.value.useUrl = useUrl info_view.value.preset = preset + info_view.value.cli = cli } watch(() => info_view.value.url, v => {