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.
* Scheduler to queue channels or playlists to be downloaded automatically at a specified time.
* 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.
* 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**.

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.'
)
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)

View file

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

View file

@ -41,6 +41,9 @@ class Preset:
cookies: str = ""
"""The default cookies to use if non is given."""
cli: str = ""
"""yt-dlp cli command line arguments."""
default: bool = False
def serialize(self) -> dict:
@ -220,6 +223,15 @@ class Presets(metaclass=Singleton):
msg = "Invalid postprocessors type. expected list."
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
def save(self, presets: list[Preset | dict]) -> "Presets":

View file

@ -10,8 +10,6 @@ from typing import Any
import httpx
from aiohttp import web
from app.library.Utils import arg_converter
from .config import Config
from .encoder import Encoder
from .Events import EventBus, Events, error, info, success
@ -224,6 +222,7 @@ class Tasks(metaclass=Singleton):
if task.get("cli"):
try:
from .Utils import arg_converter
arg_converter(args=task.get("cli"))
except Exception as e:
msg = f"Invalid cli options. '{e!s}'."

View file

@ -4,7 +4,7 @@ from pathlib import Path
from .config import Config
from .Presets import Presets
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")
@ -71,6 +71,13 @@ class YTDLPOpts(metaclass=Singleton):
if not preset or "default" == name:
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:
file = Path(self._config.config_path, "cookies", f"{preset.id}.txt")
@ -94,14 +101,15 @@ class YTDLPOpts(metaclass=Singleton):
"temp": self._config.temp_path,
}
if preset.postprocessors and isinstance(preset.postprocessors, list) and len(preset.postprocessors) > 0:
self._preset_opts["postprocessors"] = preset.postprocessors
if not preset.cli:
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:
for key, value in preset.args.items():
if key in IGNORED_KEYS:
continue
self._preset_opts[key] = value
if preset.args and isinstance(preset.args, dict) and len(preset.args) > 0:
for key, value in preset.args.items():
if key in IGNORED_KEYS:
continue
self._preset_opts[key] = value
return self

View file

@ -6,6 +6,7 @@
"folder": "",
"template": "",
"cookies": "",
"cli": "",
"default": true
},
{
@ -15,69 +16,37 @@
"folder": "",
"template": "",
"cookies": "",
"cli": "",
"default": true
},
{
"id": "441675ed-b739-40f0-a0b0-1ecfcb9dc48b",
"name": "1080p H264/m4a or best available",
"format": "bv[height<=1080][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]",
"args": {
"format_sort": [
"vcodec:h264"
]
},
"folder": "",
"template": "",
"cookies": "",
"cli": "-S vcodec:h264",
"default": true
},
{
"id": "9719fcc3-4cf2-4d88-b1e4-74dff3dba00e",
"name": "720p h264/m4a or best available",
"format": "bv[height<=720][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]",
"args": {
"format_sort": [
"vcodec:h264"
]
},
"folder": "",
"template": "",
"cookies": "",
"cli": "-S vcodec:h264",
"default": true
},
{
"id": "a6fd4b25-2b3e-458d-bb57-b75e41cc4330",
"name": "Audio only",
"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": "",
"template": "",
"cookies": "",
"cli": "--extract-audio --add-chapters --embed-metadata --embed-thumbnail",
"default": true
}
]

View file

@ -23,9 +23,16 @@
<select id="preset" class="is-fullwidth"
: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.' : ''">
<option v-for="item in config.presets" :key="item.name" :value="item.name">
{{ item.name }}
</option>
<optgroup label="Default presets">
<option v-for="item in filter_presets(true)" :key="item.name" :value="item.name">
{{ 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>
</div>
</div>
@ -273,4 +280,7 @@ const hasFormatInConfig = computed(() => {
return true
}
})
const filter_presets = (flag = true) => config.presets.filter(item => item.default === flag)
</script>

View file

@ -1,43 +1,5 @@
<template>
<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">
<form autocomplete="off" id="presetForm" @submit.prevent="checkInfo()">
<div class="card">
@ -157,43 +119,21 @@
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="column is-12">
<div class="field">
<label class="label is-inline" for="args" v-tooltip="'Extends current global yt-dlp config. (JSON)'">
JSON yt-dlp config
<label class="label is-inline" for="cli_options">
Command arguments for yt-dlp
</label>
<div class="control">
<textarea class="textarea" id="args" v-model="form.args" :disabled="addInProgress"
placeholder="{}" />
<input type="text" class="input" v-model="form.cli" id="cli_options" :disabled="addInProgress"
placeholder="command options to use, e.g. --no-embed-metadata --no-embed-thumbnail">
</div>
<span class="help">
<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
<code>cookiefile</code>, <code>paths</code>, and <code>outtmpl</code> etc. Warning: Use with
caution
some of those options 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>yt-dlp cli arguments. Check <NuxtLink target="_blank"
to="https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#general-options">this page</NuxtLink>.
For more info. <span class="has-text-danger">Not all options are supported some are ignored. Use
with caution those arguments can break yt-dlp or the frontend.</span>
</span>
</span>
</div>
@ -217,6 +157,56 @@
</span>
</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>
@ -239,6 +229,7 @@
</div>
</form>
</div>
<datalist id="folders" v-if="config?.folders">
<option v-for="dir in config.folders" :key="dir" :value="dir" />
</datalist>
@ -273,14 +264,15 @@ const props = defineProps({
const config = useConfigStore()
const toast = useToast()
const convertInProgress = ref(false)
const form = reactive(JSON.parse(JSON.stringify(props.preset)))
const opts = ref('')
const import_string = ref('')
const showImport = useStorage('showImport', false);
const convertExpanded = ref(false)
onMounted(() => {
if (props.preset?.cli && '' !== props.preset?.cli) {
return
}
if (props.preset?.args && (typeof props.preset.args === 'object')) {
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 usedName = false;
@ -319,29 +319,47 @@ const checkInfo = async () => {
return;
}
if (typeof copy.args === 'object') {
copy.args = JSON.stringify(copy.args, null, 2);
}
if (form?.cli && '' !== form.cli) {
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') {
copy.postprocessors = JSON.stringify(copy.postprocessors, null, 2);
}
if (copy?.args) {
delete copy.args
}
if (copy.args) {
try {
copy.args = JSON.parse(copy.args)
} catch (e) {
toast.error(`Invalid JSON yt-dlp config. ${e.message}`)
return;
if (copy?.postprocessors) {
delete copy.postprocessors
}
}
else {
if (typeof copy.args === 'object') {
copy.args = JSON.stringify(copy.args, null, 2);
}
if (copy.postprocessors) {
try {
copy.postprocessors = JSON.parse(copy.postprocessors)
} catch (e) {
toast.error(`Invalid JSON yt-dlp Post-Processors. ${e.message}`)
return;
if (typeof copy.postprocessors === 'object') {
copy.postprocessors = JSON.stringify(copy.postprocessors, null, 2);
}
if (copy?.args) {
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) });
}
const convertOptions = async () => {
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
}
}
const convertOptions = async args => {
try {
convertInProgress.value = true
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
}
const response = await convertCliOptions(args)
if (response.output_template) {
form.template = response.output_template
@ -388,18 +385,16 @@ const convertOptions = async () => {
form.folder = response.download_path
}
if (response.opts.postprocessors) {
form.postprocessors = JSON.stringify(response.opts.postprocessors, null, 2)
delete response.opts.postprocessors
if (response.format) {
form.format = response.format
}
form.args = JSON.stringify(response.opts, null, 2)
opts.value = ''
return response.opts
} catch (e) {
toast.error(e.message)
} finally {
convertInProgress.value = false
}
return null;
}
const importItem = async () => {
@ -422,13 +417,15 @@ const importItem = async () => {
try {
const item = JSON.parse(val)
console.log(item)
if ('preset' !== item._type) {
toast.error(`Invalid import string. Expected type 'preset', got '${item._type}'.`)
import_string.value = ''
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?')) {
return
}
@ -442,15 +439,11 @@ const importItem = async () => {
form.format = item.format
}
if (item.args) {
form.args = JSON.stringify(item.args, null, 2)
if (item.cli) {
form.cli = item.cli
}
if (item.postprocessors) {
form.postprocessors = JSON.stringify(item.postprocessors, null, 2)
}
if (item.output_template) {
if (item.template) {
form.template = item.output_template
}
@ -465,4 +458,5 @@ const importItem = async () => {
toast.error(`Failed to string. ${e.message}`)
}
}
</script>

View file

@ -86,9 +86,16 @@
<select id="preset" class="is-fullwidth" v-model="form.preset"
:disabled="addInProgress || hasFormatInConfig"
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">
{{ item.name }}
</option>
<optgroup label="Default presets">
<option v-for="item in filter_presets(true)" :key="item.name" :value="item.name">
{{ 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>
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
@ -385,4 +392,7 @@ const hasFormatInConfig = computed(() => {
return true
}
})
const filter_presets = (flag = true) => config.presets.filter(item => item.default === flag)
</script>

View file

@ -71,6 +71,15 @@ div.is-centered {
</header>
<div class="card-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">
<span class="icon"><i class="fa-solid fa-f" /></span>
<span v-text="item.format" />
@ -83,6 +92,10 @@ div.is-centered {
<span class="icon"><i class="fa-solid fa-file" /></span>
<span>{{ item.template }}</span>
</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">
<span class="icon"><i class="fa-solid fa-cookie" /></span>
<span>Has cookies</span>
@ -293,12 +306,9 @@ const exportItem = item => {
}
})
if (data.args && typeof data.args === "string") {
data.args = JSON.parse(data.args)
}
if (data.postprocessors && typeof data.postprocessors === "string") {
data.postprocessors = JSON.parse(data.postprocessors)
if (has_data(data?.args) || has_data(data?.postprocessors)) {
toast.error("v1.0 presets are no longer supported target for export. Please update your preset.")
return
}
let userData = {}
@ -311,7 +321,7 @@ const exportItem = item => {
}
userData['_type'] = 'preset'
userData['_version'] = '1.0'
userData['_version'] = '2.0'
return copyText(base64UrlEncode(JSON.stringify(userData)))
}

View file

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

View file

@ -428,6 +428,38 @@ const base64UrlDecode = input => {
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 {
ag_set,
ag,
@ -452,4 +484,5 @@ export {
convertCliOptions,
base64UrlEncode,
base64UrlDecode,
has_data,
}