Convert json config in tasks to use cli arguments.
This commit is contained in:
parent
8f313d7f58
commit
1bda1893e4
6 changed files with 154 additions and 236 deletions
|
|
@ -872,15 +872,12 @@ class HttpAPI(Common):
|
||||||
if not item.get("timer", None) or str(item.get("timer")).strip() == "":
|
if not item.get("timer", None) or str(item.get("timer")).strip() == "":
|
||||||
item["timer"] = f"{random.randint(1,59)} */1 * * *" # noqa: S311
|
item["timer"] = f"{random.randint(1,59)} */1 * * *" # noqa: S311
|
||||||
|
|
||||||
if not item.get("cookies", None):
|
|
||||||
item["cookies"] = ""
|
|
||||||
|
|
||||||
if not item.get("config", None) or str(item.get("config")).strip() == "":
|
|
||||||
item["config"] = {}
|
|
||||||
|
|
||||||
if not item.get("template", None):
|
if not item.get("template", None):
|
||||||
item["template"] = ""
|
item["template"] = ""
|
||||||
|
|
||||||
|
if not item.get("cli", None):
|
||||||
|
item["cli"] = ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ins.validate(item)
|
ins.validate(item)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,15 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
|
from app.library.Utils import arg_converter
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .encoder import Encoder
|
from .encoder import Encoder
|
||||||
from .Events import EventBus, Events, error, info, success
|
from .Events import EventBus, Events, error, info, success
|
||||||
|
|
@ -28,8 +30,7 @@ class Task:
|
||||||
preset: str = ""
|
preset: str = ""
|
||||||
timer: str = ""
|
timer: str = ""
|
||||||
template: str = ""
|
template: str = ""
|
||||||
cookies: str = ""
|
cli: str = ""
|
||||||
config: dict[str, str] = field(default_factory=dict)
|
|
||||||
|
|
||||||
def serialize(self) -> dict:
|
def serialize(self) -> dict:
|
||||||
return self.__dict__
|
return self.__dict__
|
||||||
|
|
@ -134,16 +135,20 @@ class Tasks(metaclass=Singleton):
|
||||||
with open(self._file) as f:
|
with open(self._file) as f:
|
||||||
tasks = json.load(f)
|
tasks = json.load(f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Failed to parse tasks from '{self._file}'. '{e}'.")
|
LOG.error(f"Failed to parse tasks from '{self._file}'. '{e!s}'.")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
if not tasks or len(tasks) < 1:
|
if not tasks or len(tasks) < 1:
|
||||||
LOG.info(f"No tasks were defined in '{self._file}'.")
|
LOG.info(f"No tasks were defined in '{self._file}'.")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
need_update = False
|
||||||
for i, task in enumerate(tasks):
|
for i, task in enumerate(tasks):
|
||||||
try:
|
try:
|
||||||
|
task, task_status = self.clean_task(task)
|
||||||
task = Task(**task)
|
task = Task(**task)
|
||||||
|
if task_status:
|
||||||
|
need_update = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Failed to parse task at list position '{i}'. '{e!s}'.")
|
LOG.error(f"Failed to parse task at list position '{i}'. '{e!s}'.")
|
||||||
continue
|
continue
|
||||||
|
|
@ -157,6 +162,10 @@ class Tasks(metaclass=Singleton):
|
||||||
LOG.exception(e)
|
LOG.exception(e)
|
||||||
LOG.error(f"Failed to queue task '{i}: {task.name}'. '{e!s}'.")
|
LOG.error(f"Failed to queue task '{i}: {task.name}'. '{e!s}'.")
|
||||||
|
|
||||||
|
if need_update:
|
||||||
|
LOG.info("Updating tasks file to remove old keys.")
|
||||||
|
self.save(self.get_all())
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def clear(self, shutdown: bool = False) -> "Tasks":
|
def clear(self, shutdown: bool = False) -> "Tasks":
|
||||||
|
|
@ -213,17 +222,12 @@ class Tasks(metaclass=Singleton):
|
||||||
msg = "No URL found."
|
msg = "No URL found."
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
if not isinstance(task.get("cookies"), str):
|
if task.get("cli"):
|
||||||
msg = "Invalid cookies type."
|
try:
|
||||||
raise ValueError(msg) # noqa: TRY004
|
arg_converter(args=task.get("cli"))
|
||||||
|
except Exception as e:
|
||||||
if not isinstance(task.get("config"), dict):
|
msg = f"Invalid cli options. '{e!s}'."
|
||||||
msg = "Invalid config type."
|
raise ValueError(msg) from e
|
||||||
raise ValueError(msg) # noqa: TRY004
|
|
||||||
|
|
||||||
if not isinstance(task.get("template"), str):
|
|
||||||
msg = "Invalid template type."
|
|
||||||
raise ValueError(msg) # noqa: TRY004
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -263,6 +267,27 @@ class Tasks(metaclass=Singleton):
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def clean_task(self, task: dict) -> tuple[dict, bool]:
|
||||||
|
"""
|
||||||
|
Clean the task from old keys.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task (dict): The task to clean.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[dict, bool]: The cleaned task and a status if the task was cleaned.
|
||||||
|
|
||||||
|
"""
|
||||||
|
status = False
|
||||||
|
removedKeys = ["cookies", "config"]
|
||||||
|
|
||||||
|
for key in removedKeys:
|
||||||
|
if key in task:
|
||||||
|
status = True
|
||||||
|
task.pop(key)
|
||||||
|
|
||||||
|
return task, status
|
||||||
|
|
||||||
async def _runner(self, task: Task):
|
async def _runner(self, task: Task):
|
||||||
"""
|
"""
|
||||||
Run the task.
|
Run the task.
|
||||||
|
|
@ -283,16 +308,8 @@ class Tasks(metaclass=Singleton):
|
||||||
|
|
||||||
preset: str = str(task.preset or self._default_preset)
|
preset: str = str(task.preset or self._default_preset)
|
||||||
folder: str = task.folder if task.folder else ""
|
folder: str = task.folder if task.folder else ""
|
||||||
cookies: str = str(task.cookies) if task.cookies else ""
|
|
||||||
template: str = task.template if task.template else ""
|
template: str = task.template if task.template else ""
|
||||||
|
cli: str = task.cli if task.cli else ""
|
||||||
config = task.config if task.config else {}
|
|
||||||
if isinstance(config, str) and config:
|
|
||||||
try:
|
|
||||||
config = json.loads(config)
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error(f"Failed to parse json yt-dlp config for '{task.name}'. {e!s}")
|
|
||||||
return
|
|
||||||
|
|
||||||
LOG.info(f"Task '{task.id}: {task.name}' dispatched at '{timeNow}'.")
|
LOG.info(f"Task '{task.id}: {task.name}' dispatched at '{timeNow}'.")
|
||||||
|
|
||||||
|
|
@ -307,9 +324,8 @@ class Tasks(metaclass=Singleton):
|
||||||
"url": task.url,
|
"url": task.url,
|
||||||
"preset": preset,
|
"preset": preset,
|
||||||
"folder": folder,
|
"folder": folder,
|
||||||
"cookies": cookies,
|
|
||||||
"config": config,
|
|
||||||
"template": template,
|
"template": template,
|
||||||
|
"cli": cli,
|
||||||
},
|
},
|
||||||
id=task.id,
|
id=task.id,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -434,12 +434,13 @@ def validate_url(url: str) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def arg_converter(args: str) -> dict:
|
def arg_converter(args: str, remove_options: bool = False) -> dict:
|
||||||
"""
|
"""
|
||||||
Convert yt-dlp options to a dictionary.
|
Convert yt-dlp options to a dictionary.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
args (str): yt-dlp options string.
|
args (str): yt-dlp options string.
|
||||||
|
remove_options (bool): Remove default options.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: yt-dlp options dictionary.
|
dict: yt-dlp options dictionary.
|
||||||
|
|
@ -472,6 +473,14 @@ def arg_converter(args: str) -> dict:
|
||||||
if isinstance(matchFilter, set):
|
if isinstance(matchFilter, set):
|
||||||
diff["match_filter"] = {"filters": list(matchFilter)}
|
diff["match_filter"] = {"filters": list(matchFilter)}
|
||||||
|
|
||||||
|
if remove_options:
|
||||||
|
for key in diff.copy():
|
||||||
|
if key in IGNORED_KEYS:
|
||||||
|
diff.pop(key, None)
|
||||||
|
|
||||||
|
if "_warnings" in diff:
|
||||||
|
diff.pop("_warnings", None)
|
||||||
|
|
||||||
from .encoder import Encoder
|
from .encoder import Encoder
|
||||||
|
|
||||||
return json.loads(json.dumps(diff, cls=Encoder))
|
return json.loads(json.dumps(diff, cls=Encoder))
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import logging
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .DownloadQueue import DownloadQueue
|
from .DownloadQueue import DownloadQueue
|
||||||
from .encoder import Encoder
|
from .encoder import Encoder
|
||||||
|
from .Utils import arg_converter
|
||||||
|
|
||||||
LOG = logging.getLogger("common")
|
LOG = logging.getLogger("common")
|
||||||
|
|
||||||
|
|
@ -94,6 +95,14 @@ class Common:
|
||||||
msg = f"Failed to parse json yt-dlp config for '{url}'. {e!s}"
|
msg = f"Failed to parse json yt-dlp config for '{url}'. {e!s}"
|
||||||
raise ValueError(msg) from e
|
raise ValueError(msg) from e
|
||||||
|
|
||||||
|
cli = item.get("cli")
|
||||||
|
if cli and len(cli) > 1:
|
||||||
|
try:
|
||||||
|
config = arg_converter(args=cli, remove_options=True)
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"Failed to parse yt-dlp cli options. {e!s}"
|
||||||
|
raise ValueError(msg) from e
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"url": url,
|
"url": url,
|
||||||
"preset": preset,
|
"preset": preset,
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="column is-6-tablet is-12-mobile">
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline" for="name" v-text="'Name'" />
|
<label class="label is-inline" for="name" v-text="'Name'" />
|
||||||
|
|
@ -81,14 +80,11 @@
|
||||||
|
|
||||||
<div class="column is-6-tablet is-12-mobile">
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline" for="folder">
|
<label class="label is-inline" for="preset">Preset</label>
|
||||||
Preset
|
|
||||||
</label>
|
|
||||||
<div class="control has-icons-left">
|
<div class="control has-icons-left">
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
<select id="preset" class="is-fullwidth" v-model="form.preset"
|
<select id="preset" class="is-fullwidth" v-model="form.preset"
|
||||||
:disabled="addInProgress || hasFormatInConfig"
|
:disabled="addInProgress || hasFormatInConfig">
|
||||||
v-tooltip.bottom="hasFormatInConfig ? 'Presets are disabled. Format key is present in the config.' : ''">
|
|
||||||
<option v-for="item in config.presets" :key="item.name" :value="item.name">
|
<option v-for="item in config.presets" :key="item.name" :value="item.name">
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</option>
|
</option>
|
||||||
|
|
@ -98,8 +94,10 @@
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
<span>Select the preset to use for this URL. The preset will be ignored if format key is present in
|
<span>Select the preset to use for this URL. <span class="text-has-danger">If the
|
||||||
config.</span>
|
<code>-f, --format</code> argument is present in the command line options, the preset and all
|
||||||
|
it's options will be ignored.</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -126,7 +124,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline" for="folder">
|
<label class="label is-inline" for="folder">
|
||||||
Download path
|
Download path
|
||||||
|
|
@ -138,24 +136,25 @@
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
<span>Downloads are relative to download path, defaults to root path if not set.</span>
|
<span>Paths are relative to global download path, defaults to preset download path if set otherwise,
|
||||||
|
fallback root path if not set.</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline" for="output_template">
|
<label class="label is-inline" for="output_template">
|
||||||
Output template
|
Output template
|
||||||
</label>
|
</label>
|
||||||
<div class="control has-icons-left">
|
<div class="control has-icons-left">
|
||||||
<input type="text" class="input" id="output_template" placeholder="The output template to use"
|
<input type="text" class="input" id="output_template" :disabled="addInProgress"
|
||||||
v-model="form.template" :disabled="addInProgress">
|
placeholder="Leave empty to use default template." v-model="form.template">
|
||||||
<span class="icon is-small is-left"><i class="fa-solid fa-file" /></span>
|
<span class="icon is-small is-left"><i class="fa-solid fa-file" /></span>
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
<span>The output template to use, if not set, it will defaults to
|
<span>Use this output template if non are given with URL. if not set, it will defaults to
|
||||||
<code>{{ config.app.output_template }}</code>.
|
<code>{{ config.app.output_template }}</code>.
|
||||||
For more information <NuxtLink href="https://github.com/yt-dlp/yt-dlp#output-template"
|
For more information <NuxtLink href="https://github.com/yt-dlp/yt-dlp#output-template"
|
||||||
target="_blank">visit this url</NuxtLink>.
|
target="_blank">visit this url</NuxtLink>.
|
||||||
|
|
@ -164,41 +163,24 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
|
<div class="column is-12">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline" for="config"
|
<label class="label is-inline" for="cli_options">
|
||||||
v-tooltip="'Extends current global yt-dlp config. (JSON)'">
|
Command arguments for yt-dlp
|
||||||
JSON yt-dlp config or CLI options. <NuxtLink
|
|
||||||
v-if="form.config && (typeof form.config === 'string') && !form.config.trim().startsWith('{')"
|
|
||||||
@click="convertOptions()">Convert to JSON</NuxtLink>
|
|
||||||
</label>
|
</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea class="textarea" id="config" v-model="form.config" :disabled="addInProgress"
|
<input type="text" class="input" v-model="form.cli" id="cli_options" :disabled="addInProgress"
|
||||||
placeholder="--no-embed-metadata --no-embed-thumbnail"></textarea>
|
placeholder="command options to use, e.g. --no-embed-metadata --no-embed-thumbnail">
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
<span> Extends current global yt-dlp config with given options. Some fields are ignored like
|
<span>yt-dlp cli arguments. Check <NuxtLink target="_blank"
|
||||||
<code>cookiefile</code>, <code>paths</code>, and <code>outtmpl</code> etc. Warning: Use with
|
to="https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#general-options">this page</NuxtLink>.
|
||||||
caution
|
For more info. Some arguments are ignored by default. Warning: Use with
|
||||||
some of those options can break yt-dlp or the frontend.</span>
|
caution some of those options can break yt-dlp or the frontend. <span class="has-text-danger">If
|
||||||
</span>
|
<code>-f, --format</code> argument is present, the preset and all it's options will be
|
||||||
</div>
|
ignored</span>.
|
||||||
</div>
|
</span>
|
||||||
|
|
||||||
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label is-inline" for="cookies"
|
|
||||||
v-tooltip="'Netscape HTTP Cookie format.'">Cookies</label>
|
|
||||||
<div class="control">
|
|
||||||
<textarea class="textarea is-pre" id="cookies" v-model="form.cookies" :disabled="addInProgress" />
|
|
||||||
</div>
|
|
||||||
<span class="help">
|
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
|
||||||
<span>Use the <NuxtLink target="_blank"
|
|
||||||
to="https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp">
|
|
||||||
Recommended addon</NuxtLink> by yt-dlp to export cookies. The cookies MUST be in Netscape HTTP
|
|
||||||
Cookie format.</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -221,12 +203,6 @@
|
||||||
<span>Cancel</span>
|
<span>Cancel</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<p class="card-footer-item">
|
|
||||||
<button class="button is-fullwidth is-info" type="button" @click="showAdvanced = !showAdvanced">
|
|
||||||
<span class="icon"><i class="fa-solid fa-cog" /></span>
|
|
||||||
<span>Advanced</span>
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -244,8 +220,6 @@ import { CronExpressionParser } from 'cron-parser'
|
||||||
const emitter = defineEmits(['cancel', 'submit']);
|
const emitter = defineEmits(['cancel', 'submit']);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const config = useConfigStore();
|
const config = useConfigStore();
|
||||||
const convertInProgress = ref(false);
|
|
||||||
const showAdvanced = useStorage('task.showAdvanced', false);
|
|
||||||
const showImport = useStorage('showImport', false);
|
const showImport = useStorage('showImport', false);
|
||||||
const import_string = ref('');
|
const import_string = ref('');
|
||||||
|
|
||||||
|
|
@ -269,10 +243,6 @@ const props = defineProps({
|
||||||
const form = reactive(props.task);
|
const form = reactive(props.task);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.task?.config && (typeof props.task.config === 'object')) {
|
|
||||||
form.config = JSON.stringify(props.task.config, null, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!props.task?.preset || '' === props.task.preset) {
|
if (!props.task?.preset || '' === props.task.preset) {
|
||||||
form.preset = toRaw(config.app.default_preset);
|
form.preset = toRaw(config.app.default_preset);
|
||||||
}
|
}
|
||||||
|
|
@ -304,60 +274,17 @@ const checkInfo = async () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof form.config === 'object') {
|
if (form?.cli && '' !== form.cli) {
|
||||||
form.config = JSON.stringify(form.config, null, 4);
|
const options = await convertOptions(form.cli);
|
||||||
}
|
if (null === options) {
|
||||||
|
return
|
||||||
if (form.config && form.config && !form.config.trim().startsWith('{')) {
|
|
||||||
await convertOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (form.config) {
|
|
||||||
try {
|
|
||||||
form.config = JSON.parse(form.config)
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(`Invalid JSON yt-dlp config. ${e.message}`)
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
form.cli = form.cli.trim(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
emitter('submit', { reference: toRaw(props.reference), task: toRaw(form) });
|
emitter('submit', { reference: toRaw(props.reference), task: toRaw(form) });
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertOptions = async () => {
|
|
||||||
if (convertInProgress.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
convertInProgress.value = true
|
|
||||||
const response = await convertCliOptions(form.config)
|
|
||||||
form.config = JSON.stringify(response.opts, null, 2)
|
|
||||||
if (response.output_template) {
|
|
||||||
form.template = response.output_template
|
|
||||||
}
|
|
||||||
if (response.download_path) {
|
|
||||||
form.folder = response.download_path
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(e.message)
|
|
||||||
} finally {
|
|
||||||
convertInProgress.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasFormatInConfig = computed(() => {
|
|
||||||
if (!form.config) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const config = JSON.parse(form.config)
|
|
||||||
return "format" in config
|
|
||||||
} catch (e) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const importItem = async () => {
|
const importItem = async () => {
|
||||||
let val = import_string.value.trim()
|
let val = import_string.value.trim()
|
||||||
if (!val) {
|
if (!val) {
|
||||||
|
|
@ -384,7 +311,7 @@ const importItem = async () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.config || form.url || form.timer) {
|
if (form.url || form.timer) {
|
||||||
if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
|
if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -398,8 +325,8 @@ const importItem = async () => {
|
||||||
form.url = item.url
|
form.url = item.url
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.preset) {
|
if (item.template) {
|
||||||
form.preset = item.preset
|
form.template = item.template
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.timer) {
|
if (item.timer) {
|
||||||
|
|
@ -410,12 +337,19 @@ const importItem = async () => {
|
||||||
form.folder = item.folder
|
form.folder = item.folder
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.template) {
|
if (item.cli) {
|
||||||
form.template = item.template
|
form.cli = item.cli
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.config) {
|
if (item.preset) {
|
||||||
form.config = JSON.stringify(item.config, null, 2)
|
// -- check if the preset exists in config.presets
|
||||||
|
const preset = config.presets.find(p => p.name === item.preset)
|
||||||
|
if (!preset) {
|
||||||
|
toast.warning(`Preset '${item.preset}' not found. Preset will be set to default.`)
|
||||||
|
form.preset = 'default'
|
||||||
|
} else {
|
||||||
|
form.preset = item.preset
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import_string.value = ''
|
import_string.value = ''
|
||||||
|
|
@ -424,4 +358,33 @@ const importItem = async () => {
|
||||||
toast.error(`Failed to import string. ${e.message}`)
|
toast.error(`Failed to import string. ${e.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const convertOptions = async args => {
|
||||||
|
try {
|
||||||
|
const response = await convertCliOptions(args)
|
||||||
|
|
||||||
|
if (response.output_template) {
|
||||||
|
form.template = response.output_template
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.download_path) {
|
||||||
|
form.folder = response.download_path
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.opts
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFormatInConfig = computed(() => {
|
||||||
|
if (!form?.cli) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (form.cli.includes('-f') || form.cli.includes('--format')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,7 @@ div.is-centered {
|
||||||
</div>
|
</div>
|
||||||
<div class="is-hidden-mobile">
|
<div class="is-hidden-mobile">
|
||||||
<span class="subtitle">
|
<span class="subtitle">
|
||||||
The task runner is simple queue system that allows you to schedule downloads. The tasks are run at the
|
The task runner is simple queue system that allows you to schedule downloads to run at the specific time.
|
||||||
scheduled time.
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -62,10 +61,6 @@ div.is-centered {
|
||||||
<a class="has-text-primary" v-tooltip="'Export task.'" @click.prevent="exportItem(item)">
|
<a class="has-text-primary" v-tooltip="'Export task.'" @click.prevent="exportItem(item)">
|
||||||
<span class="icon"><i class="fa-solid fa-file-export" /></span>
|
<span class="icon"><i class="fa-solid fa-file-export" /></span>
|
||||||
</a>
|
</a>
|
||||||
<button @click="item.raw = !item.raw">
|
|
||||||
<span class="icon"><i class="fa-solid"
|
|
||||||
:class="{ 'fa-arrow-down': !item?.raw, 'fa-arrow-up': item?.raw }" /></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
|
|
@ -86,11 +81,10 @@ div.is-centered {
|
||||||
<span class="icon"><i class="fa-solid fa-tv" /></span>
|
<span class="icon"><i class="fa-solid fa-tv" /></span>
|
||||||
<span>{{ item.preset ?? config.app.default_preset }}</span>
|
<span>{{ item.preset ?? config.app.default_preset }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<p class="is-text-overflow">
|
||||||
</div>
|
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||||
<div class="card-content" v-if="item?.raw">
|
<span>{{ item.cli }}</span>
|
||||||
<div class="content">
|
</p>
|
||||||
<pre><code>{{ filterItem(item) }}</code></pre>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
|
|
@ -120,17 +114,6 @@ div.is-centered {
|
||||||
icon="fas fa-exclamation-circle" v-if="!tasks || tasks.length < 1" />
|
icon="fas fa-exclamation-circle" v-if="!tasks || tasks.length < 1" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-12" v-if="tasks && tasks.length > 0 && !toggleForm">
|
|
||||||
<Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle">
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
When you export task, the preset settings is automatically merged into the task and the preset set to
|
|
||||||
<code>default</code> to be more portable. The exporter doesn't include <code>Cookies</code> field for
|
|
||||||
security reasons.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Message>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -202,6 +185,8 @@ const resetForm = (closeForm = false) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateTasks = async items => {
|
const updateTasks = async items => {
|
||||||
|
let data = {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
addInProgress.value = true
|
addInProgress.value = true
|
||||||
|
|
||||||
|
|
@ -213,7 +198,7 @@ const updateTasks = async items => {
|
||||||
body: JSON.stringify(items),
|
body: JSON.stringify(items),
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await response.json()
|
data = await response.json()
|
||||||
|
|
||||||
if (200 !== response.status) {
|
if (200 !== response.status) {
|
||||||
toast.error(`Failed to update task. ${data.error}`);
|
toast.error(`Failed to update task. ${data.error}`);
|
||||||
|
|
@ -224,7 +209,7 @@ const updateTasks = async items => {
|
||||||
resetForm(true)
|
resetForm(true)
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(`Failed to update task. ${data.error}`);
|
toast.error(`Failed to update task. ${data?.error ?? e.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
addInProgress.value = false
|
addInProgress.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -272,11 +257,6 @@ const updateItem = async ({ reference, task }) => {
|
||||||
resetForm(true)
|
resetForm(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterItem = item => {
|
|
||||||
const { raw, ...rest } = item
|
|
||||||
return JSON.stringify(rest, null, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const editItem = item => {
|
const editItem = item => {
|
||||||
task.value = item
|
task.value = item
|
||||||
taskRef.value = item.id
|
taskRef.value = item.id
|
||||||
|
|
@ -327,21 +307,8 @@ const runNow = item => {
|
||||||
data.template = item.template
|
data.template = item.template
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.cookies) {
|
if (item.cli) {
|
||||||
data.cookies = item.cookies
|
data.cli = item.cli
|
||||||
}
|
|
||||||
|
|
||||||
if (item.config) {
|
|
||||||
if (typeof item.config === 'object') {
|
|
||||||
data.config = JSON.stringify(item.config)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
JSON.parse(data.config)
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(`Invalid JSON yt-dlp config. ${e.message}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.emit('add_url', data)
|
socket.emit('add_url', data)
|
||||||
|
|
@ -359,69 +326,26 @@ const statusHandler = async stream => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportItem = async item => {
|
const exportItem = async item => {
|
||||||
let preset = config.presets.find(p => p.name === item.preset)
|
|
||||||
if (!preset) {
|
|
||||||
toast.error('Preset not found.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const info = JSON.parse(JSON.stringify(item))
|
const info = JSON.parse(JSON.stringify(item))
|
||||||
preset = JSON.parse(JSON.stringify(preset))
|
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
name: info.name,
|
name: info.name,
|
||||||
url: info.url,
|
url: info.url,
|
||||||
preset: 'default',
|
preset: info.preset,
|
||||||
timer: info.timer,
|
timer: info.timer,
|
||||||
folder: info.folder,
|
folder: info.folder,
|
||||||
template: info.template,
|
|
||||||
config: info.config,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- merge preset options with task args.
|
if (info.template) {
|
||||||
let args = {}
|
data.template = info.template
|
||||||
|
|
||||||
if (preset.args && Object.keys(preset.args).length > 0) {
|
|
||||||
for (const key of Object.keys(preset.args)) {
|
|
||||||
args[key] = preset.args[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const defaults = ['default', 'not_set']
|
|
||||||
if (preset.format && !defaults.includes(preset.format)) {
|
|
||||||
args.format = preset.format
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preset.postprocessors && preset.postprocessors.length > 0) {
|
if (info.cli) {
|
||||||
args.postprocessors = preset.postprocessors
|
data.cli = info.cli
|
||||||
}
|
|
||||||
|
|
||||||
if (preset.folder && !info.folder) {
|
|
||||||
data.folder = preset.folder
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preset.template && !info.template) {
|
|
||||||
data.template = preset.template
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.config || Object.keys(data.config).length < 1) {
|
|
||||||
data.config = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key of Object.keys(args)) {
|
|
||||||
if (key in data.config && Array.isArray(args[key]) && Array.isArray(data.config[key])) {
|
|
||||||
data.config[key] = data.config[key].concat(args[key])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data?.config[key]) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
data.config[key] = args[key]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data['_type'] = 'task'
|
data['_type'] = 'task'
|
||||||
data['_version'] = '1.1'
|
data['_version'] = '2.0'
|
||||||
|
|
||||||
return copyText(base64UrlEncode(JSON.stringify(data)));
|
return copyText(base64UrlEncode(JSON.stringify(data)));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue