diff --git a/API.md b/API.md index 0f1fff6f..302013ec 100644 --- a/API.md +++ b/API.md @@ -18,6 +18,7 @@ This document describes the available endpoints and their usage. All endpoints r - [Endpoints](#endpoints) - [GET /api/ping](#get-apiping) - [POST /api/yt-dlp/convert](#post-apiyt-dlpconvert) + - [POST /api/yt-dlp/command/](#post-apiyt-dlpcommand) - [GET /api/yt-dlp/url/info](#get-apiyt-dlpurlinfo) - [GET /api/history/add](#get-apihistoryadd) - [POST /api/history](#post-apihistory) @@ -64,7 +65,6 @@ This document describes the available endpoints and their usage. All endpoints r - [GET /api/notifications](#get-apinotifications) - [PUT /api/notifications](#put-apinotifications) - [POST /api/yt-dlp/archive\_id/](#post-apiyt-dlparchive_id) - - [POST /api/yt-dlp/save\_cookies/](#post-apiyt-dlpsave_cookies) - [POST /api/notifications/test](#post-apinotificationstest) - [GET /api/yt-dlp/options](#get-apiyt-dlpoptions) - [POST /api/system/pause](#post-apisystempause) @@ -210,6 +210,45 @@ or an error: --- +### POST /api/yt-dlp/command/ +**Purpose**: Build a complete yt-dlp CLI command string with priority-based argument merging from user input, presets, and defaults. + +**Requires**: Console must be enabled (`YTP_CONSOLE_ENABLED=true` env). + +**Body**: JSON object: +```json +{ + "url": "https://example.com/video",// required - item url + "preset": "preset_name", // optional - preset name to apply + "folder": "subfolder", // optional - output folder (relative to download_path) + "template": "%(title)s.%(ext)s", // optional - output filename template + "cli": "--write-sub --embed-subs", // optional - additional yt-dlp CLI arguments + "cookies": "cookie_string" // optional - authentication cookies as string +} +``` + +If cookies are given, they will be stored in a temporary file and the appropriate `--cookies ` argument +will be added to the command. + +**Priority System** (User > Preset > Default): +1. **User fields** take highest priority (from request body) +2. **Preset fields** used only if user didn't provide them +3. **Default fields** used as final fallback (from configuration) + +**Response**: +```json +{ + "command": "--output-path /downloads/subfolder --output %(title)s.%(ext)s --write-sub --embed-subs https://example.com/video" +} +``` + +**Error Responses**: +- `403 Forbidden` if console is disabled +- `400 Bad Request` if body is invalid JSON or Item format validation fails +- `400 Bad Request` if CLI command building fails + +--- + ### GET /api/yt-dlp/url/info **Purpose**: Retrieves metadata (info) for a provided URL without adding it to the download queue. @@ -1450,45 +1489,6 @@ or an error: --- -### POST /api/yt-dlp/save_cookies/ -**Purpose**: Save cookies to a file for use with yt-dlp CLI operations. Requires console to be enabled (`console_enabled` in configuration). -**Body**: JSON object with `cookies` field containing the cookie string. -```json -{ - "cookies": "cookie_string_or_netscape_format" -} -``` - -**Response on Success**: -```json -{ - "status": true, - "cookie_file": "/path/to/temp/c_uuid.txt" -} -``` - -**Response on Error**: -```json -{ - "error": "error_message" -} -``` - -**Status Codes**: -- `200 OK` if cookies were successfully saved. -- `400 Bad Request` if the request body is invalid or missing the `cookies` field. -- `403 Forbidden` if console is disabled. -- `413 Payload Too Large` if cookies exceed 1MB. -- `500 Internal Server Error` if cookie file creation fails. - -**Notes**: -- Console must be enabled in configuration (`console_enabled: true`). -- Cookies must be a valid string (≤ 1MB). -- Cookies are stored in the temporary directory with a UUID-based filename. -- Cookie files can be used with subsequent yt-dlp operations. - ---- - ### POST /api/notifications/test **Purpose**: Triggers a test notification event to all configured targets. diff --git a/app/routes/api/yt_dlp.py b/app/routes/api/yt_dlp.py index fcd222bf..3da5b60b 100644 --- a/app/routes/api/yt_dlp.py +++ b/app/routes/api/yt_dlp.py @@ -2,7 +2,6 @@ import json import logging import time from collections import OrderedDict -from pathlib import Path from typing import Any from aiohttp import web @@ -10,6 +9,7 @@ from aiohttp.web import Request, Response from app.library.cache import Cache from app.library.config import Config +from app.library.ItemDTO import Item from app.library.Presets import Presets from app.library.router import route from app.library.Utils import ( @@ -20,7 +20,7 @@ from app.library.Utils import ( get_archive_id, validate_url, ) -from app.library.YTDLPOpts import YTDLPOpts +from app.library.YTDLPOpts import YTDLPCli, YTDLPOpts LOG: logging.Logger = logging.getLogger(__name__) @@ -275,58 +275,41 @@ async def get_archive_ids(request: Request, config: Config) -> Response: return web.json_response(data=response, status=web.HTTPOk.status_code) -@route("POST", "api/yt-dlp/save_cookies/", "save_cookies") -async def save_cookies(request: Request, config: Config) -> Response: +@route("POST", "api/yt-dlp/command/", "make_command") +async def make_command(request: Request, config: Config) -> Response: """ - Save cookies for use with CLI. + Build yt-dlp CLI command. + + Args: + request (Request): The request object. + config (Config): The config instance. Returns: - Response: The response object with the yt-dlp CLI options. + Response: The response object with the merged fields and final yt-dlp CLI command string. """ if not config.console_enabled: - return web.json_response( - data={"error": "Console is disabled."}, - status=web.HTTPForbidden.status_code, - ) + return web.json_response(data={"error": "Console is disabled."}, status=web.HTTPForbidden.status_code) data = (await request.json()) if request.body_exists else None if not data or not isinstance(data, dict): return web.json_response( - data={"error": "Invalid request. expecting dict with 'cookies' field."}, + data={"error": "Invalid request. expecting JSON body."}, status=web.HTTPBadRequest.status_code, ) - cookies = data.get("cookies") - if not cookies or not isinstance(cookies, str): - return web.json_response( - data={"error": "Invalid request. 'cookies' field is required and must be a string."}, - status=web.HTTPBadRequest.status_code, - ) - - if len(cookies) > 1_000_000: - return web.json_response( - data={"error": "Cookie size exceeds the maximum limit of 1MB."}, - status=web.HTTPBadRequest.status_code, - ) - - import uuid - - from app.library.Utils import load_cookies - - cookie_file: Path = Path(config.temp_path) / f"c_{uuid.uuid4()!s}.txt" - try: - cookie_file.write_text(cookies) - file_path = str(cookie_file) - load_cookies(cookie_file) + it = Item.format(data) + except ValueError as e: + return web.json_response(data={"error": str(e), "data": data}, status=web.HTTPBadRequest.status_code) + + try: + command, _ = YTDLPCli(item=it, config=config).build() except Exception as e: + LOG.exception(e) return web.json_response( - data={"error": f"Failed to create cookie file. '{e!s}'."}, - status=web.HTTPInternalServerError.status_code, + data={"error": "Failed to build CLI command"}, + status=web.HTTPBadRequest.status_code, ) - return web.json_response( - data={"status": True, "cookie_file": file_path}, - status=web.HTTPOk.status_code, - ) + return web.json_response(data={"command": command}, status=web.HTTPOk.status_code)