[FEAT] add option to see the compiled yt-dlp options
This commit is contained in:
parent
b939aad1db
commit
058bf567d1
5 changed files with 225 additions and 32 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
85
ui/app/components/ModalText.vue
Normal file
85
ui/app/components/ModalText.vue
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<style scoped>
|
||||
code {
|
||||
color: var(--bulma-code) !important
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="modal is-active" v-if="false === externalModel">
|
||||
<div class="modal-background" @click="emitter('closeModel')"></div>
|
||||
<div class="modal-content modal-content-max">
|
||||
<div style="font-size:30vh; width: 99%" class="has-text-centered" v-if="isLoading">
|
||||
<i class="fas fa-circle-notch fa-spin" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="content p-0 m-0" style="position: relative">
|
||||
<pre :class="[code_classes, custom_classes]"><code class="p-4 is-block" v-text="data" />
|
||||
<div class="m-4 is-flex" style="position: absolute; top:0; right:0;">
|
||||
<button class="button is-small is-purple mr-3" @click="() => toggleClass('is-pre-wrap-force')">
|
||||
<span class="icon"><i class="fas fa-text-width" /></span>
|
||||
</button>
|
||||
<button class="button is-info is-small" @click="() => copyText(JSON.stringify(data, null, 2))" >
|
||||
<span class="icon"><i class="fas fa-copy" /></span>
|
||||
</button>
|
||||
</div>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="emitter('closeModel')"></button>
|
||||
</div>
|
||||
<div class="modal-content-max" style="height: 80vh;" v-else>
|
||||
<div class="content p-0 m-0" style="position: relative">
|
||||
<pre :class="[code_classes, custom_classes]"><code class="p-4 is-block" v-text="data" /></pre>
|
||||
<div class="m-4 is-flex" style="position: absolute; top:0; right:0;">
|
||||
<button class="button is-small is-purple mr-3" @click="() => toggleClass('is-pre-wrap-force')">
|
||||
<span class="icon"><i class="fas fa-text-width" /></span>
|
||||
</button>
|
||||
<button class="button is-info is-small" @click="() => copyText(JSON.stringify(data, null, 2))">
|
||||
<span class="icon"><i class="fas fa-copy" /></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { disableOpacity, enableOpacity } from '~/utils';
|
||||
|
||||
const emitter = defineEmits<{ (e: 'closeModel'): void }>()
|
||||
|
||||
withDefaults(defineProps<{ externalModel?: boolean, data: any, code_classes?: string, isLoading?: boolean }>(), {
|
||||
code_classes: '',
|
||||
isLoading: false,
|
||||
externalModel: false,
|
||||
})
|
||||
|
||||
const custom_classes = useStorage<string>('modal_text_classes', '')
|
||||
|
||||
const handle_event = (e: KeyboardEvent): void => {
|
||||
if (e.key === 'Escape') {
|
||||
emitter('closeModel')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async (): Promise<void> => {
|
||||
disableOpacity()
|
||||
document.addEventListener('keydown', handle_event)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
enableOpacity()
|
||||
document.removeEventListener('keydown', handle_event)
|
||||
})
|
||||
|
||||
const toggleClass = (className: string) => {
|
||||
if (custom_classes.value.includes(className)) {
|
||||
custom_classes.value = custom_classes.value.replace(className, '').trim()
|
||||
} else {
|
||||
custom_classes.value += ` ${className}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -15,8 +15,7 @@
|
|||
:disabled="!socket.isConnected || addInProgress" v-model="form.url">
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary"
|
||||
:class="{ 'is-loading': !socket.isConnected || addInProgress }"
|
||||
<button type="submit" class="button is-primary" :class="{ 'is-loading': addInProgress }"
|
||||
:disabled="!socket.isConnected || addInProgress || !form?.url">
|
||||
<span class="icon"><i class="fa-solid fa-plus" /></span>
|
||||
<span>Add</span>
|
||||
|
|
@ -72,7 +71,7 @@
|
|||
|
||||
<div class="column">
|
||||
<button type="button" class="button is-info" @click="showAdvanced = !showAdvanced"
|
||||
:class="{ 'is-loading': !socket.isConnected }" :disabled="!socket.isConnected">
|
||||
:disabled="!socket.isConnected">
|
||||
<span class="icon"><i class="fa-solid fa-cog" /></span>
|
||||
<span>Advanced Options</span>
|
||||
</button>
|
||||
|
|
@ -178,6 +177,11 @@
|
|||
<span>Run CLI</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink class="dropdown-item" @click="testDownloadOptions" v-if="config.app.console_enabled">
|
||||
<span class="icon has-text-success"><i class="fa-solid fa-flask" /></span>
|
||||
<span>Show compiled yt-dlp options</span>
|
||||
</NuxtLink>
|
||||
|
||||
<hr class="dropdown-divider" />
|
||||
<NuxtLink class="dropdown-item" @click="resetConfig">
|
||||
<span class="icon has-text-danger"><i class="fa-solid fa-rotate-left" /></span>
|
||||
|
|
@ -189,26 +193,31 @@
|
|||
<div class="field is-grouped is-justify-self-end is-hidden-mobile">
|
||||
<div class="control">
|
||||
<button type="button" class="button is-purple" @click="() => showFields = true"
|
||||
:class="{ 'is-loading': !socket.isConnected }" :disabled="!socket.isConnected">
|
||||
:disabled="!socket.isConnected" v-tooltip="'Manage custom fields'">
|
||||
<span class="icon"><i class="fa-solid fa-plus" /></span>
|
||||
<span>Custom Fields</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control" v-if="config.app.console_enabled" v-tooltip="'Run directly in console'">
|
||||
<button type="button" class="button is-warning" @click="runCliCommand"
|
||||
:disabled="!socket.isConnected || !form?.url">
|
||||
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<button type="button" class="button is-info"
|
||||
v-tooltip="'Get yt-dlp information for the provided URL.'"
|
||||
@click="emitter('getInfo', form.url, form.preset, form.cli)"
|
||||
:class="{ 'is-loading': !socket.isConnected }"
|
||||
:disabled="!socket.isConnected || addInProgress || !form?.url">
|
||||
:disabled="!socket.isConnected || addInProgress || !form?.url || multiURLs">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Information</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control" v-if="config.app.console_enabled">
|
||||
<button type="button" class="button is-warning" @click="runCliCommand"
|
||||
:disabled="!socket.isConnected || !form?.url">
|
||||
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||
<span>Run CLI</span>
|
||||
<button type="button" class="button is-success" @click="testDownloadOptions"
|
||||
:disabled="!socket.isConnected || !form?.url" v-tooltip="'Show compiled yt-dlp options.'">
|
||||
<span class="icon"><i class="fa-solid fa-flask" /></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -216,7 +225,6 @@
|
|||
<button type="button" class="button is-danger" @click="resetConfig"
|
||||
:disabled="!!(!socket.isConnected || form?.id)" v-tooltip="'Reset local settings'">
|
||||
<span class="icon"><i class="fa-solid fa-rotate-left" /></span>
|
||||
<span>Reset</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -234,6 +242,8 @@
|
|||
<Modal v-if="showOptions" @close="showOptions = false" :contentClass="'modal-content-max'">
|
||||
<YTDLPOptions />
|
||||
</Modal>
|
||||
|
||||
<ModalText v-if="showTestResults" @closeModel="CloseTestResults" :data="testResultsData" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
|
|
@ -266,6 +276,8 @@ const storedCommand = useStorage<string>('console_command', '')
|
|||
const addInProgress = ref<boolean>(false)
|
||||
const showFields = ref<boolean>(false)
|
||||
const showOptions = ref<boolean>(false)
|
||||
const showTestResults = ref<boolean>(false)
|
||||
const testResultsData = ref<any>(null)
|
||||
const dlFieldsExtra = ['--no-download-archive']
|
||||
const ytDlpOpt = ref<AutoCompleteOptions>([])
|
||||
|
||||
|
|
@ -496,10 +508,9 @@ const runCliCommand = async (): Promise<void> => {
|
|||
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<void> => {
|
|||
}
|
||||
}
|
||||
|
||||
const testDownloadOptions = async (): Promise<void> => {
|
||||
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(/(?<!\S)(-f|--format)(=|\s)(\S+)/))
|
||||
|
||||
const filter_presets = (flag: boolean = true) => 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
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue