deprecate JSON yt-dlp config & Post-Processors in presets and recommand cli options.

This commit is contained in:
arabcoders 2025-03-31 02:13:33 +03:00
parent ac119db644
commit 23b1b409f4
13 changed files with 243 additions and 190 deletions

View file

@ -13,7 +13,7 @@ Web GUI for [yt-dlp](https://github.com/yt-dlp/yt-dlp) with playlist & channel s
* Can Handle live streams. * Can Handle live streams.
* Scheduler to queue channels or playlists to be downloaded automatically at a specified time. * Scheduler to queue channels or playlists to be downloaded automatically at a specified time.
* Send notification to targets based on selected events. * Send notification to targets based on selected events.
* Support per link `yt-dlp JSON config or cli options`, `cookies` & `output format`. * Support per link `cli options` & `cookies`
* Queue multiple URLs separated by comma. * Queue multiple URLs separated by comma.
* Simple file browser. `Disabled by default` * Simple file browser. `Disabled by default`
* A built in video player that can play any video file regardless of the format. **With support for sidecar external subtitles**. * A built in video player that can play any video file regardless of the format. **With support for sidecar external subtitles**.

View file

@ -215,7 +215,7 @@ class Download:
f'Task id="{self.info.id}" PID="{os.getpid()}" title="{self.info.title}" preset="{self.preset}" started.' f'Task id="{self.info.id}" PID="{os.getpid()}" title="{self.info.title}" preset="{self.preset}" started.'
) )
self.logger.debug("Params before passing to yt-dlp.", extra=params) self.logger.debug(f"Params before passing to yt-dlp. {params}")
params["logger"] = NestedLogger(self.logger) params["logger"] = NestedLogger(self.logger)

View file

@ -453,6 +453,9 @@ class HttpAPI(Common):
if "paths" in data and "home" in data["paths"]: if "paths" in data and "home" in data["paths"]:
response["download_path"] = data["paths"]["home"] response["download_path"] = data["paths"]["home"]
if "format" in data:
response["format"] = data["format"]
for key in data: for key in data:
if key in IGNORED_KEYS: if key in IGNORED_KEYS:
continue continue

View file

@ -41,6 +41,9 @@ class Preset:
cookies: str = "" cookies: str = ""
"""The default cookies to use if non is given.""" """The default cookies to use if non is given."""
cli: str = ""
"""yt-dlp cli command line arguments."""
default: bool = False default: bool = False
def serialize(self) -> dict: def serialize(self) -> dict:
@ -220,6 +223,15 @@ class Presets(metaclass=Singleton):
msg = "Invalid postprocessors type. expected list." msg = "Invalid postprocessors type. expected list."
raise ValueError(msg) raise ValueError(msg)
if preset.get("cli"):
try:
from .Utils import arg_converter
arg_converter(args=preset.get("cli"))
except Exception as e:
msg = f"Invalid cli options. '{e!s}'."
raise ValueError(msg) from e
return True return True
def save(self, presets: list[Preset | dict]) -> "Presets": def save(self, presets: list[Preset | dict]) -> "Presets":

View file

@ -10,8 +10,6 @@ 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
@ -224,6 +222,7 @@ class Tasks(metaclass=Singleton):
if task.get("cli"): if task.get("cli"):
try: try:
from .Utils import arg_converter
arg_converter(args=task.get("cli")) arg_converter(args=task.get("cli"))
except Exception as e: except Exception as e:
msg = f"Invalid cli options. '{e!s}'." msg = f"Invalid cli options. '{e!s}'."

View file

@ -4,7 +4,7 @@ from pathlib import Path
from .config import Config from .config import Config
from .Presets import Presets from .Presets import Presets
from .Singleton import Singleton from .Singleton import Singleton
from .Utils import IGNORED_KEYS, calc_download_path, merge_dict from .Utils import IGNORED_KEYS, arg_converter, calc_download_path, merge_dict
LOG = logging.getLogger("YTDLPOpts") LOG = logging.getLogger("YTDLPOpts")
@ -71,6 +71,13 @@ class YTDLPOpts(metaclass=Singleton):
if not preset or "default" == name: if not preset or "default" == name:
return self return self
if preset.cli:
try:
self._preset_opts = arg_converter(args=preset.cli, remove_options=True)
except Exception as e:
msg = f"Invalid cli options for preset '{preset.name}'. '{e!s}'."
raise ValueError(msg) from e
if preset.cookies and with_cookies: if preset.cookies and with_cookies:
file = Path(self._config.config_path, "cookies", f"{preset.id}.txt") file = Path(self._config.config_path, "cookies", f"{preset.id}.txt")
@ -94,14 +101,15 @@ class YTDLPOpts(metaclass=Singleton):
"temp": self._config.temp_path, "temp": self._config.temp_path,
} }
if preset.postprocessors and isinstance(preset.postprocessors, list) and len(preset.postprocessors) > 0: if not preset.cli:
self._preset_opts["postprocessors"] = preset.postprocessors if preset.postprocessors and isinstance(preset.postprocessors, list) and len(preset.postprocessors) > 0:
self._preset_opts["postprocessors"] = preset.postprocessors
if preset.args and isinstance(preset.args, dict) and len(preset.args) > 0: if preset.args and isinstance(preset.args, dict) and len(preset.args) > 0:
for key, value in preset.args.items(): for key, value in preset.args.items():
if key in IGNORED_KEYS: if key in IGNORED_KEYS:
continue continue
self._preset_opts[key] = value self._preset_opts[key] = value
return self return self

View file

@ -6,6 +6,7 @@
"folder": "", "folder": "",
"template": "", "template": "",
"cookies": "", "cookies": "",
"cli": "",
"default": true "default": true
}, },
{ {
@ -15,69 +16,37 @@
"folder": "", "folder": "",
"template": "", "template": "",
"cookies": "", "cookies": "",
"cli": "",
"default": true "default": true
}, },
{ {
"id": "441675ed-b739-40f0-a0b0-1ecfcb9dc48b", "id": "441675ed-b739-40f0-a0b0-1ecfcb9dc48b",
"name": "1080p H264/m4a or best available", "name": "1080p H264/m4a or best available",
"format": "bv[height<=1080][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]", "format": "bv[height<=1080][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]",
"args": {
"format_sort": [
"vcodec:h264"
]
},
"folder": "", "folder": "",
"template": "", "template": "",
"cookies": "", "cookies": "",
"cli": "-S vcodec:h264",
"default": true "default": true
}, },
{ {
"id": "9719fcc3-4cf2-4d88-b1e4-74dff3dba00e", "id": "9719fcc3-4cf2-4d88-b1e4-74dff3dba00e",
"name": "720p h264/m4a or best available", "name": "720p h264/m4a or best available",
"format": "bv[height<=720][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]", "format": "bv[height<=720][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]",
"args": {
"format_sort": [
"vcodec:h264"
]
},
"folder": "", "folder": "",
"template": "", "template": "",
"cookies": "", "cookies": "",
"cli": "-S vcodec:h264",
"default": true "default": true
}, },
{ {
"id": "a6fd4b25-2b3e-458d-bb57-b75e41cc4330", "id": "a6fd4b25-2b3e-458d-bb57-b75e41cc4330",
"name": "Audio only", "name": "Audio only",
"format": "bestaudio/best", "format": "bestaudio/best",
"args": {
"writethumbnail": true
},
"postprocessors": [
{
"key": "FFmpegExtractAudio",
"preferredcodec": "best",
"preferredquality": "5",
"nopostoverwrites": false
},
{
"key": "FFmpegMetadata",
"add_chapters": true,
"add_metadata": true,
"add_infojson": "if_exists"
},
{
"key": "EmbedThumbnail",
"already_have_thumbnail": false
},
{
"key": "FFmpegConcat",
"only_multi_video": true,
"when": "playlist"
}
],
"folder": "", "folder": "",
"template": "", "template": "",
"cookies": "", "cookies": "",
"cli": "--extract-audio --add-chapters --embed-metadata --embed-thumbnail",
"default": true "default": true
} }
] ]

View file

@ -23,9 +23,16 @@
<select id="preset" class="is-fullwidth" <select id="preset" class="is-fullwidth"
:disabled="!socket.isConnected || addInProgress || hasFormatInConfig" v-model="selectedPreset" :disabled="!socket.isConnected || addInProgress || hasFormatInConfig" v-model="selectedPreset"
v-tooltip.bottom="hasFormatInConfig ? 'Presets are disabled. Format key is present in the Command arguments for yt-dlp.' : ''"> v-tooltip.bottom="hasFormatInConfig ? 'Presets are disabled. Format key is present in the Command arguments for yt-dlp.' : ''">
<option v-for="item in config.presets" :key="item.name" :value="item.name"> <optgroup label="Default presets">
{{ item.name }} <option v-for="item in filter_presets(true)" :key="item.name" :value="item.name">
</option> {{ item.name }}
</option>
</optgroup>
<optgroup label="Custom presets" v-if="config?.presets.filter(p => !p?.default).length > 0">
<option v-for="item in filter_presets(false)" :key="item.name" :value="item.name">
{{ item.name }}
</option>
</optgroup>
</select> </select>
</div> </div>
</div> </div>
@ -273,4 +280,7 @@ const hasFormatInConfig = computed(() => {
return true return true
} }
}) })
const filter_presets = (flag = true) => config.presets.filter(item => item.default === flag)
</script> </script>

View file

@ -1,43 +1,5 @@
<template> <template>
<main class="columns mt-2 is-multiline"> <main class="columns mt-2 is-multiline">
<div class="column is-12">
<h1 class="is-unselectable is-pointer title is-5" @click="convertExpanded = !convertExpanded">
<span class="icon-text">
<span class="icon"><i class="fa-solid" :class="convertExpanded ? 'fa-arrow-up' : 'fa-arrow-down'" /></span>
<span>Convert yt-dlp cli options.</span>
</span>
</h1>
<form autocomplete="off" id="convertOpts" @submit.prevent="convertOptions()" v-if="convertExpanded">
<div class="box">
<label class="label" for="opts">
yt-dlp CLI options
</label>
<div class="field has-addons">
<div class="control has-icons-left is-expanded">
<input type="text" class="input" id="opts" v-model="opts"
placeholder="-x --audio-format mp3 -f bestaudio">
<span class="icon is-small is-left"><i class="fa-solid fa-n" /></span>
</div>
<div class="control">
<button form="convertOpts" class="button is-primary" :disabled="convertInProgress || !opts" type="submit"
:class="{ 'is-loading': convertInProgress }">
<span class="icon"><i class="fa-solid fa-cog" /></span>
<span>Convert</span>
</button>
</div>
</div>
<p class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Convert yt-dlp CLI options to JSON format. This will overwrite the current form fields.</span>
</p>
</div>
</form>
</div>
<div class="column is-12"> <div class="column is-12">
<form autocomplete="off" id="presetForm" @submit.prevent="checkInfo()"> <form autocomplete="off" id="presetForm" @submit.prevent="checkInfo()">
<div class="card"> <div class="card">
@ -157,43 +119,21 @@
</div> </div>
</div> </div>
<div class="column is-6-tablet is-12-mobile"> <div class="column is-12">
<div class="field"> <div class="field">
<label class="label is-inline" for="args" v-tooltip="'Extends current global yt-dlp config. (JSON)'"> <label class="label is-inline" for="cli_options">
JSON yt-dlp config Command arguments for yt-dlp
</label> </label>
<div class="control"> <div class="control">
<textarea class="textarea" id="args" v-model="form.args" :disabled="addInProgress" <input type="text" class="input" v-model="form.cli" id="cli_options" :disabled="addInProgress"
placeholder="{}" /> 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. <span class="has-text-danger">Not all options are supported some are ignored. Use
some of those options can break yt-dlp or the frontend.</span> with caution those arguments can break yt-dlp or the frontend.</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="postprocessors"
v-tooltip="'Things to do after download is done.'">
JSON yt-dlp Post-Processors
</label>
<div class="control">
<textarea class="textarea" id="postprocessors" v-model="form.postprocessors"
:disabled="addInProgress" placeholder="[]" />
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
Post-processing operations, refer to <NuxtLink
href="https://github.com/yt-dlp/yt-dlp/tree/master/yt_dlp/postprocessor" target="blank">this url
</NuxtLink> for more info. It's easier for you to use the <b>Convert CLI options</b> to get what
you
want and it will auto-populate the fields if necessary.
</span> </span>
</span> </span>
</div> </div>
@ -217,6 +157,56 @@
</span> </span>
</div> </div>
</div> </div>
<div class="column is-12" v-if="has_data(form?.args) || has_data(form?.postprocessors)">
<Message title="Deprecation Warning" class="is-background-warning-80 has-text-dark"
icon="fas fa-exclamation-circle">
<ul>
<li>
The <code>JSON yt-dlp config</code> and <code>JSON yt-dlp Post-Processors</code> fields are
deprecated and will be removed in the future. Please use the <b>Command arguments for yt-dlp</b>
field instead. The deprecated fields will still be working for now but they will stop, we suggest
that you migrate to the new field. as soon as possible to avoid any issues. No support will be
given for the deprecated fields.
</li>
<li>
If both fields are set, the <b>Command arguments for yt-dlp</b> field will take precedence over
the deprecated fields. and when you click save it will remove the deprecated fields.
</li>
</ul>
</Message>
</div>
<div class="column is-6-tablet is-12-mobile" v-if="has_data(form?.args)">
<div class="field">
<label class="label is-inline" for="args" v-tooltip="'Extends current global yt-dlp config. (JSON)'">
JSON yt-dlp config <span class="has-text-danger">(DEPRECATED)</span>
</label>
<div class="control">
<textarea class="textarea" id="args" v-model="form.args" :disabled="addInProgress"
placeholder="{}" />
</div>
<span class="help has-text-danger">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Deprecated, use <b>Command arguments for yt-dlp</b> field instead. </span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile" v-if="has_data(form?.postprocessors)">
<div class="field">
<label class="label is-inline" for="postprocessors"
v-tooltip="'Things to do after download is done.'">
JSON yt-dlp Post-Processors <span class="has-text-danger">(DEPRECATED)</span>
</label>
<div class="control">
<textarea class="textarea" id="postprocessors" v-model="form.postprocessors"
:disabled="addInProgress" placeholder="[]" />
</div>
<span class="help has-text-danger">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Deprecated, use <b>Command arguments for yt-dlp</b> field instead. </span>
</span>
</div>
</div>
</div> </div>
</div> </div>
@ -239,6 +229,7 @@
</div> </div>
</form> </form>
</div> </div>
<datalist id="folders" v-if="config?.folders"> <datalist id="folders" v-if="config?.folders">
<option v-for="dir in config.folders" :key="dir" :value="dir" /> <option v-for="dir in config.folders" :key="dir" :value="dir" />
</datalist> </datalist>
@ -273,14 +264,15 @@ const props = defineProps({
const config = useConfigStore() const config = useConfigStore()
const toast = useToast() const toast = useToast()
const convertInProgress = ref(false)
const form = reactive(JSON.parse(JSON.stringify(props.preset))) const form = reactive(JSON.parse(JSON.stringify(props.preset)))
const opts = ref('')
const import_string = ref('') const import_string = ref('')
const showImport = useStorage('showImport', false); const showImport = useStorage('showImport', false);
const convertExpanded = ref(false)
onMounted(() => { onMounted(() => {
if (props.preset?.cli && '' !== props.preset?.cli) {
return
}
if (props.preset?.args && (typeof props.preset.args === 'object')) { if (props.preset?.args && (typeof props.preset.args === 'object')) {
form.args = JSON.stringify(props.preset.args, null, 2) form.args = JSON.stringify(props.preset.args, null, 2)
} }
@ -299,6 +291,14 @@ const checkInfo = async () => {
} }
} }
if (form?.cli && '' !== form.cli) {
const options = await convertOptions(form.cli);
if (null === options) {
return
}
form.cli = form.cli.trim()
}
let copy = JSON.parse(JSON.stringify(form)); let copy = JSON.parse(JSON.stringify(form));
let usedName = false; let usedName = false;
@ -319,29 +319,47 @@ const checkInfo = async () => {
return; return;
} }
if (typeof copy.args === 'object') { if (form?.cli && '' !== form.cli) {
copy.args = JSON.stringify(copy.args, null, 2); if ((has_data(copy?.args) || has_data(copy?.postprocessors))) {
} if (false === confirm('cli options are set, this will remove the JSON yt-dlp config and Post-Processors. Are you sure?')) {
toast.warning('User cancelled the operation.')
return
}
}
if (typeof copy.postprocessors === 'object') { if (copy?.args) {
copy.postprocessors = JSON.stringify(copy.postprocessors, null, 2); delete copy.args
} }
if (copy.args) { if (copy?.postprocessors) {
try { delete copy.postprocessors
copy.args = JSON.parse(copy.args)
} catch (e) {
toast.error(`Invalid JSON yt-dlp config. ${e.message}`)
return;
} }
} }
else {
if (typeof copy.args === 'object') {
copy.args = JSON.stringify(copy.args, null, 2);
}
if (copy.postprocessors) { if (typeof copy.postprocessors === 'object') {
try { copy.postprocessors = JSON.stringify(copy.postprocessors, null, 2);
copy.postprocessors = JSON.parse(copy.postprocessors) }
} catch (e) {
toast.error(`Invalid JSON yt-dlp Post-Processors. ${e.message}`) if (copy?.args) {
return; try {
copy.args = JSON.parse(copy.args)
} catch (e) {
toast.error(`Invalid JSON yt-dlp config. ${e.message}`)
return;
}
}
if (copy?.postprocessors) {
try {
copy.postprocessors = JSON.parse(copy.postprocessors)
} catch (e) {
toast.error(`Invalid JSON yt-dlp Post-Processors. ${e.message}`)
return;
}
} }
} }
@ -355,30 +373,9 @@ const checkInfo = async () => {
emitter('submit', { reference: toRaw(props.reference), preset: toRaw(copy) }); emitter('submit', { reference: toRaw(props.reference), preset: toRaw(copy) });
} }
const convertOptions = async () => { const convertOptions = async args => {
if (convertInProgress.value) {
return
}
if (form.format || form.args || form.postprocessors || form.template || form.folder) {
if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
return
}
}
try { try {
convertInProgress.value = true const response = await convertCliOptions(args)
const response = await convertCliOptions(opts.value)
if (!response.opts) {
toast.error('Failed to convert options.')
return
}
if (response.opts.format) {
form.format = response.opts.format
delete response.opts.format
}
if (response.output_template) { if (response.output_template) {
form.template = response.output_template form.template = response.output_template
@ -388,18 +385,16 @@ const convertOptions = async () => {
form.folder = response.download_path form.folder = response.download_path
} }
if (response.opts.postprocessors) { if (response.format) {
form.postprocessors = JSON.stringify(response.opts.postprocessors, null, 2) form.format = response.format
delete response.opts.postprocessors
} }
form.args = JSON.stringify(response.opts, null, 2) return response.opts
opts.value = ''
} catch (e) { } catch (e) {
toast.error(e.message) toast.error(e.message)
} finally {
convertInProgress.value = false
} }
return null;
} }
const importItem = async () => { const importItem = async () => {
@ -422,13 +417,15 @@ const importItem = async () => {
try { try {
const item = JSON.parse(val) const item = JSON.parse(val)
console.log(item)
if ('preset' !== item._type) { if ('preset' !== item._type) {
toast.error(`Invalid import string. Expected type 'preset', got '${item._type}'.`) toast.error(`Invalid import string. Expected type 'preset', got '${item._type}'.`)
import_string.value = '' import_string.value = ''
return return
} }
if (form.format || form.args || form.postprocessors) { if (form.format || form.cli) {
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
} }
@ -442,15 +439,11 @@ const importItem = async () => {
form.format = item.format form.format = item.format
} }
if (item.args) { if (item.cli) {
form.args = JSON.stringify(item.args, null, 2) form.cli = item.cli
} }
if (item.postprocessors) { if (item.template) {
form.postprocessors = JSON.stringify(item.postprocessors, null, 2)
}
if (item.output_template) {
form.template = item.output_template form.template = item.output_template
} }
@ -465,4 +458,5 @@ const importItem = async () => {
toast.error(`Failed to string. ${e.message}`) toast.error(`Failed to string. ${e.message}`)
} }
} }
</script> </script>

View file

@ -86,9 +86,16 @@
<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 command arguments for yt-dlp.' : ''"> v-tooltip.bottom="hasFormatInConfig ? 'Presets are disabled. Format key is present in the command arguments for yt-dlp.' : ''">
<option v-for="item in config.presets" :key="item.name" :value="item.name"> <optgroup label="Default presets">
{{ item.name }} <option v-for="item in filter_presets(true)" :key="item.name" :value="item.name">
</option> {{ item.name }}
</option>
</optgroup>
<optgroup label="Custom presets" v-if="config?.presets.filter(p => !p?.default).length > 0">
<option v-for="item in filter_presets(false)" :key="item.name" :value="item.name">
{{ item.name }}
</option>
</optgroup>
</select> </select>
</div> </div>
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span> <span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
@ -385,4 +392,7 @@ const hasFormatInConfig = computed(() => {
return true return true
} }
}) })
const filter_presets = (flag = true) => config.presets.filter(item => item.default === flag)
</script> </script>

View file

@ -71,6 +71,15 @@ div.is-centered {
</header> </header>
<div class="card-content"> <div class="card-content">
<div class="content"> <div class="content">
<template v-if="has_data(item?.args) || has_data(item.postprocessors)">
<p class="has-text-danger is-5 has-text-bold">
<span class="icon"><i class="fa-solid fa-triangle-exclamation" /></span>
<span>The preset is using deprecated options. It is recommended to update the preset to use the
new options. It will cease to work in future versions.</span>
<hr>
</p>
</template>
<p class="is-text-overflow"> <p class="is-text-overflow">
<span class="icon"><i class="fa-solid fa-f" /></span> <span class="icon"><i class="fa-solid fa-f" /></span>
<span v-text="item.format" /> <span v-text="item.format" />
@ -83,6 +92,10 @@ div.is-centered {
<span class="icon"><i class="fa-solid fa-file" /></span> <span class="icon"><i class="fa-solid fa-file" /></span>
<span>{{ item.template }}</span> <span>{{ item.template }}</span>
</p> </p>
<p class="is-text-overflow" v-if="item.cli">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<span>{{ item.cli }}</span>
</p>
<p class="is-text-overflow" v-if="item.cookies"> <p class="is-text-overflow" v-if="item.cookies">
<span class="icon"><i class="fa-solid fa-cookie" /></span> <span class="icon"><i class="fa-solid fa-cookie" /></span>
<span>Has cookies</span> <span>Has cookies</span>
@ -293,12 +306,9 @@ const exportItem = item => {
} }
}) })
if (data.args && typeof data.args === "string") { if (has_data(data?.args) || has_data(data?.postprocessors)) {
data.args = JSON.parse(data.args) toast.error("v1.0 presets are no longer supported target for export. Please update your preset.")
} return
if (data.postprocessors && typeof data.postprocessors === "string") {
data.postprocessors = JSON.parse(data.postprocessors)
} }
let userData = {} let userData = {}
@ -311,7 +321,7 @@ const exportItem = item => {
} }
userData['_type'] = 'preset' userData['_type'] = 'preset'
userData['_version'] = '1.0' userData['_version'] = '2.0'
return copyText(base64UrlEncode(JSON.stringify(userData))) return copyText(base64UrlEncode(JSON.stringify(userData)))
} }

View file

@ -22,7 +22,12 @@ const CONFIG_KEYS = {
presets: [ presets: [
{ {
'name': 'default', 'name': 'default',
'format': 'default' 'format': 'default',
'folder': '',
'template': '',
'cookies': '',
'cli': '',
'default': true
} }
], ],
folders: [], folders: [],

View file

@ -428,6 +428,38 @@ const base64UrlDecode = input => {
return atob(base64); return atob(base64);
} }
/**
* Check if array or object has data.
*
* @param {string} item - The item to check
* @returns {boolean} - True if the item has data, false otherwise.
*/
const has_data = item => {
if (!item) {
return false
}
if (typeof item === 'string') {
try {
item = JSON.parse(item)
} catch (e) {
return true
}
}
try {
if (typeof item === 'object') {
return Object.keys(item).length > 0
}
return item.length > 0
} catch (e) {
console.error(e)
}
return false
}
export { export {
ag_set, ag_set,
ag, ag,
@ -452,4 +484,5 @@ export {
convertCliOptions, convertCliOptions,
base64UrlEncode, base64UrlEncode,
base64UrlDecode, base64UrlDecode,
has_data,
} }