diff --git a/app/library/YTDLPOpts.py b/app/library/YTDLPOpts.py index 80ba5551..f5ccab41 100644 --- a/app/library/YTDLPOpts.py +++ b/app/library/YTDLPOpts.py @@ -1,6 +1,7 @@ import logging import shlex from pathlib import Path +from typing import Any from .config import Config from .Presets import Preset, Presets @@ -28,7 +29,7 @@ class ARGSMerger: if not args or not isinstance(args, str) or len(args) < 2: return self - _args = shlex.split(args) + _args: list[str] = shlex.split(args) if len(_args) > 0: self.args.extend(_args) @@ -124,8 +125,8 @@ class YTDLPCli: raise ValueError(msg) self.item = item - self.preset = item.get_preset() - self._config = config or Config.get_instance() + self.preset: Preset = item.get_preset() + self._config: Config = config or Config.get_instance() def build(self) -> tuple[str, dict]: """ @@ -138,7 +139,7 @@ class YTDLPCli: template: str | None = None save_path: str | None = None cookie_file: Path | None = None - cli_args = ARGSMerger.get_instance() + cli_args: ARGSMerger = ARGSMerger.get_instance() if self.item.cookies: cookie_file = create_cookies_file(self.item.cookies) @@ -160,10 +161,10 @@ class YTDLPCli: template = self.preset.template if self.preset.cli: - cli_args.add(self.preset.cli) + cli_args.add(self._replace_vars(self.preset.cli)) if self.item.cli: - cli_args.add(self.item.cli) + cli_args.add(self._replace_vars(self.item.cli)) if not save_path: save_path = self._config.download_path @@ -172,24 +173,22 @@ class YTDLPCli: template = self._config.output_template if cookie_file: - cli_args.add(f'--cookies "{cookie_file!s}"') + cli_args.add(self._replace_vars(f'--cookies "{cookie_file!s}"')) if template: - cli_args.add(f'--output "{template}"') + cli_args.add(self._replace_vars(f'--output "{template}"')) if save_path: - cli_args.add(f'--paths "home:{save_path}"') + cli_args.add(self._replace_vars(f'--paths "home:{save_path}"')) - cli_args.add(f'--paths "temp:{self._config.temp_path}"') + cli_args.add(self._replace_vars(f'--paths "temp:{self._config.temp_path}"')) if self.item.url: cli_args.add(self.item.url) - command = str(cli_args) - for k, v in self._config.get_replacers().items(): - command = command.replace(f"%({k})s", v if isinstance(v, str) else str(v)) + command: str = self._replace_vars(str(cli_args)) - info = { + info: dict[str, Any] = { "command": command, "dict": cli_args.as_dict(), "ytdlp": cli_args.as_ytdlp(), @@ -202,6 +201,22 @@ class YTDLPCli: return command, info + def _replace_vars(self, text: str) -> str: + """ + Replace variables in the given text. + + Args: + text (str): The text to replace variables in + + Returns: + str: The text with variables replaced + + """ + for k, v in self._config.get_replacers().items(): + text: str = text.replace(f"%({k})s", v if isinstance(v, str) else str(v)) + + return text + class YTDLPOpts: def __init__(self): diff --git a/app/routes/api/yt_dlp.py b/app/routes/api/yt_dlp.py index 3da5b60b..e11ec531 100644 --- a/app/routes/api/yt_dlp.py +++ b/app/routes/api/yt_dlp.py @@ -9,6 +9,7 @@ from aiohttp.web import Request, Response from app.library.cache import Cache from app.library.config import Config +from app.library.encoder import Encoder from app.library.ItemDTO import Item from app.library.Presets import Presets from app.library.router import route @@ -276,13 +277,14 @@ async def get_archive_ids(request: Request, config: Config) -> Response: @route("POST", "api/yt-dlp/command/", "make_command") -async def make_command(request: Request, config: Config) -> Response: +async def make_command(request: Request, config: Config, encoder: Encoder) -> Response: """ Build yt-dlp CLI command. Args: request (Request): The request object. config (Config): The config instance. + encoder (Encoder): The encoder instance. Returns: Response: The response object with the merged fields and final yt-dlp CLI command string. @@ -304,7 +306,7 @@ async def make_command(request: Request, config: Config) -> Response: return web.json_response(data={"error": str(e), "data": data}, status=web.HTTPBadRequest.status_code) try: - command, _ = YTDLPCli(item=it, config=config).build() + command, info = YTDLPCli(item=it, config=config).build() except Exception as e: LOG.exception(e) return web.json_response( @@ -312,4 +314,7 @@ async def make_command(request: Request, config: Config) -> Response: status=web.HTTPBadRequest.status_code, ) + if request.query.get("full", False): + return web.json_response(data=info, status=web.HTTPOk.status_code, dumps=encoder.encode) + return web.json_response(data={"command": command}, status=web.HTTPOk.status_code) diff --git a/ui/app/assets/css/style.css b/ui/app/assets/css/style.css index fb18446a..c6bd7d8a 100644 --- a/ui/app/assets/css/style.css +++ b/ui/app/assets/css/style.css @@ -289,6 +289,10 @@ hr { white-space: pre-wrap; } +.is-pre-wrap-force { + white-space: pre-wrap !important; +} + .has-text-bold { font-weight: bold; } @@ -370,4 +374,3 @@ div.is-centered { padding: 0; margin: 0 auto; } - diff --git a/ui/app/components/ModalText.vue b/ui/app/components/ModalText.vue new file mode 100644 index 00000000..008981da --- /dev/null +++ b/ui/app/components/ModalText.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/ui/app/components/NewDownload.vue b/ui/app/components/NewDownload.vue index a2955c1f..dffddd99 100644 --- a/ui/app/components/NewDownload.vue +++ b/ui/app/components/NewDownload.vue @@ -15,8 +15,7 @@ :disabled="!socket.isConnected || addInProgress" v-model="form.url">
- @@ -178,6 +177,11 @@ Run CLI + + + Show compiled yt-dlp options + + @@ -189,26 +193,31 @@
+ +
+ +
+
-
@@ -216,7 +225,6 @@
@@ -234,6 +242,8 @@ + + @@ -266,6 +276,8 @@ const storedCommand = useStorage('console_command', '') const addInProgress = ref(false) const showFields = ref(false) const showOptions = ref(false) +const showTestResults = ref(false) +const testResultsData = ref(null) const dlFieldsExtra = ['--no-download-archive'] const ytDlpOpt = ref([]) @@ -496,10 +508,9 @@ const runCliCommand = async (): Promise => { return } - const {status} = await dialog.confirmDialog({ + const { status } = await dialog.confirmDialog({ title: 'Run CLI Command', message: `This will generate a yt-dlp command and run it in the console. Continue?`, - confirmColor: 'is-warning', }) if (!status) { @@ -568,6 +579,73 @@ const runCliCommand = async (): Promise => { } } +const testDownloadOptions = async (): Promise => { + if (!form.value.url) { + toast.warning('Please enter a URL first') + return + } + + let form_cli = (form.value?.cli || '').trim() + + if (dlFields.value && Object.keys(dlFields.value).length > 0) { + const joined = [] + for (const [key, value] of Object.entries(dlFields.value)) { + if (false === is_valid_dl_field(key)) { + continue + } + + if ([undefined, null, '', false].includes(value as any)) { + continue + } + + const keyRegex = new RegExp(`(^|\\s)${key}(\\s|$)`); + if (form_cli && keyRegex.test(form_cli)) { + continue; + } + + joined.push(true === value ? `${key}` : `${key} ${value}`) + } + + if (joined.length > 0) { + form_cli = form_cli ? `${form_cli} ${joined.join(' ')}` : joined.join(' ') + } + } + + try { + const resp = await request('/api/yt-dlp/command?full=true', { + method: 'POST', + body: JSON.stringify({ + url: form.value.url, + preset: form.value.preset, + folder: form.value.folder, + cookies: form.value.cookies, + template: form.value.template, + cli: form_cli, + }) + }) + + const json = await resp.json() + + if (!resp.ok) { + toast.error(`Error: ${json.error || 'Failed to generate command.'}`) + return + } + + testResultsData.value = { + command: json.command, + yt_dlp: json.ytdlp, + } + showTestResults.value = true + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Failed to test download options') + } +} + +const CloseTestResults = () => { + showTestResults.value = false + testResultsData.value = null +} + const hasFormatInConfig = computed((): boolean => !!form.value.cli?.match(/(? config.presets.filter(item => item.default === flag) @@ -606,4 +684,11 @@ const getDefault = (type: 'cookies' | 'cli' | 'template' | 'folder', ret: string // eslint-disable-next-line vue/no-side-effects-in-computed-properties const sortedDLFields = computed(() => config.dl_fields.sort((a, b) => (a.order || 0) - (b.order || 0))) + +const multiURLs = computed(() => { + if (!form.value.url) { + return false + } + return form.value.url.split(separator.value).filter((u: string) => u.trim()).length > 1 +})