remove presets v1.0

This commit is contained in:
arabcoders 2025-04-01 20:05:35 +03:00
parent ff2139261b
commit fa9c084397
8 changed files with 112 additions and 237 deletions

View file

@ -392,7 +392,7 @@ class DownloadQueue(metaclass=Singleton):
{ "status": "text" }
"""
_preset = Presets.get_instance().get(name=preset)
_preset = Presets.get_instance().get(preset)
config = config if config else {}
if cli:

View file

@ -501,7 +501,7 @@ class HttpAPI(Common):
preset = request.query.get("preset")
if preset:
exists = Presets.get_instance().get(name=preset)
exists = Presets.get_instance().get(preset)
if not exists:
return web.json_response(
data={"status": False, "message": f"Preset '{preset}' does not exist."},
@ -674,7 +674,7 @@ class HttpAPI(Common):
preset = request.query.get("preset")
if preset:
exists = Presets.get_instance().get(name=preset)
exists = Presets.get_instance().get(preset)
if not exists:
return web.json_response(
data={"status": False, "message": f"Preset '{preset}' does not exist."},
@ -790,18 +790,6 @@ class HttpAPI(Common):
if not item.get("id", None) or not validate_uuid(item.get("id"), version=4):
item["id"] = str(uuid.uuid4())
if not item.get("args", None) or str(item.get("args")).strip() == "":
item["args"] = {}
if item.get("args", None) and isinstance(item.get("args"), str):
item["args"] = json.loads(item.get("args"))
if not item.get("postprocessors", None) or str(item.get("postprocessors")).strip() == "":
item["postprocessors"] = []
if item.get("postprocessors", None) and isinstance(item.get("postprocessors"), str):
item["postprocessors"] = json.loads(item.get("postprocessors"))
try:
cls.validate(item)
except ValueError as e:

View file

@ -11,6 +11,7 @@ from .config import Config
from .encoder import Encoder
from .Events import EventBus, Events
from .Singleton import Singleton
from .Utils import arg_converter, clean_item
LOG = logging.getLogger("presets")
@ -26,12 +27,6 @@ class Preset:
format: str
"""The format of the preset."""
args: dict[str, list[str] | bool] = field(default_factory=dict)
"""The arguments of the preset."""
postprocessors: list = field(default_factory=list)
"""The postprocessors of the preset."""
folder: str = ""
"""The default download folder to use if non is given."""
@ -83,13 +78,19 @@ class Presets(metaclass=Singleton):
pass
with open(os.path.join(os.path.dirname(__file__), "presets.json")) as f:
self._default_presets = [Preset(**preset) for preset in json.load(f)]
for i, preset in enumerate(json.load(f)):
try:
self.validate(preset)
self._default_presets.append(Preset(**preset))
except Exception as e:
LOG.error(f"Failed to parse default preset '{i}'. '{e!s}'.")
continue
EventBus.get_instance().subscribe(
Events.PRESETS_ADD,
lambda data, _, **kwargs: self.add(**data.data), # noqa: ARG005
f"{__class__.__name__}.save",
)
def event_handler(_, __):
msg = "Not implemented"
raise Exception(msg)
EventBus.get_instance().subscribe(Events.PRESETS_ADD, event_handler, f"{__class__.__name__}.save")
@staticmethod
def get_instance() -> "Presets":
@ -150,22 +151,27 @@ class Presets(metaclass=Singleton):
LOG.info(f"No presets were defined in '{self._file}'.")
return self
needSaving = False
need_save = False
for i, preset in enumerate(presets):
try:
if "id" not in preset:
preset["id"] = str(uuid.uuid4())
needSaving = True
need_save = True
preset, preset_status = clean_item(preset, keys=("args", "postprocessors"))
preset = Preset(**preset)
if preset_status:
need_save = True
self._presets.append(preset)
except Exception as e:
LOG.error(f"Failed to parse preset at list position '{i}'. '{e!s}'.")
continue
if needSaving:
LOG.info("Saving presets due to missing ids.")
if need_save:
LOG.info("Saving presets due to format, or id change.")
self.save(self._presets)
return self
@ -198,7 +204,7 @@ class Presets(metaclass=Singleton):
"""
if not isinstance(preset, dict):
if not isinstance(preset, Preset):
msg = "Invalid preset type."
msg = f"Invalid preset type. Was expecting a (Preset|dict), but got '{type(preset).__name__}'."
raise ValueError(msg) # noqa: TRY004
preset = preset.serialize()
@ -215,18 +221,8 @@ class Presets(metaclass=Singleton):
msg = "No format found."
raise ValueError(msg)
if preset.get("args") and not isinstance(preset.get("args"), dict):
msg = "Invalid args type. expected dict."
raise ValueError(msg)
if preset.get("postprocessors") and not isinstance(preset.get("postprocessors"), list):
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}'."
@ -270,26 +266,24 @@ class Presets(metaclass=Singleton):
return self
def get(self, id: str | None = None, name: str | None = None) -> Preset | None:
def get(self, id_or_name: str) -> Preset | None:
"""
Get the preset by id or name.
Args:
id (str|None): The id of the preset.
name (str|None): The name of the preset.
id_or_name (str): The id or name of the preset.
Returns:
Preset|None: The preset if found, None otherwise.
"""
if not id and not name:
if not id_or_name:
return None
for preset in self.get_all():
if preset.id == id:
return preset
if id_or_name not in (preset.id, preset.name):
continue
if preset.name == name:
return preset
return preset
return None

View file

@ -15,6 +15,7 @@ from .encoder import Encoder
from .Events import EventBus, Events, error, info, success
from .Scheduler import Scheduler
from .Singleton import Singleton
from .Utils import clean_item
LOG = logging.getLogger("tasks")
@ -140,13 +141,13 @@ class Tasks(metaclass=Singleton):
LOG.info(f"No tasks were defined in '{self._file}'.")
return self
need_update = False
need_save = False
for i, task in enumerate(tasks):
try:
task, task_status = self.clean_task(task)
task, task_status = clean_item(task, keys=("cookies", "config"))
task = Task(**task)
if task_status:
need_update = True
need_save = True
except Exception as e:
LOG.error(f"Failed to parse task at list position '{i}'. '{e!s}'.")
continue
@ -160,7 +161,7 @@ class Tasks(metaclass=Singleton):
LOG.exception(e)
LOG.error(f"Failed to queue task '{i}: {task.name}'. '{e!s}'.")
if need_update:
if need_save:
LOG.info("Updating tasks file to remove old keys.")
self.save(self.get_all())
@ -223,6 +224,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}'."
@ -266,27 +268,6 @@ class Tasks(metaclass=Singleton):
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):
"""
Run the task.

View file

@ -522,8 +522,6 @@ def arg_converter(
bad_options.update(item.items())
LOG.debug("Removed %i the following options: '%s'.", level, ", ".join(bad_options.values()))
for key in diff.copy():
if key not in bad_options:
continue
@ -889,3 +887,41 @@ def get_files(base_path: str, dir: str | None = None):
)
return contents
def clean_item(item: dict, keys: list | tuple) -> tuple[dict, bool]:
"""
Remove given keys from a dictionary.
This function modifies the dictionary in place and returns a tuple
containing the modified dictionary and a boolean indicating
whether any keys were removed.
Args:
item (dict): The item to clean.
keys (list|tuple): The keys to remove.
Returns:
tuple[dict, bool]: The cleaned item and a the status of cleaning operation.
Raises:
TypeError: If item is not a dictionary or keys is not a list or tuple.
"""
status = False
if not isinstance(item, dict):
msg = "Item must be a dictionary."
raise TypeError(msg)
if not isinstance(keys, list | tuple):
msg = "Keys must be a list or tuple."
raise TypeError(msg)
for key in keys:
if key not in item:
continue
status = True
item.pop(key)
return item, status

View file

@ -78,7 +78,7 @@ class YTDLPOpts(metaclass=Singleton):
YTDLPOpts: The instance of the class
"""
preset = Presets.get_instance().get(name=name)
preset = Presets.get_instance().get(name)
if not preset or "default" == name:
return self
@ -118,25 +118,6 @@ class YTDLPOpts(metaclass=Singleton):
"temp": self._config.temp_path,
}
# @Deprecated - To be removed in future versions.
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:
bad_options = {k: v for d in REMOVE_KEYS for k, v in d.items()}
removed_options = []
for key, value in preset.args.items():
if key in bad_options:
removed_options.append(bad_options[key])
continue
self._preset_opts[key] = value
if len(removed_options) > 0:
LOG.warning(
"Removed the following options '%s' from '%s' args.", ", ".join(removed_options), preset.name
)
return self
def get_all(self, keep: bool = False) -> dict:

View file

@ -27,13 +27,13 @@
<div class="column is-12" v-if="showImport || !reference">
<label class="label is-inline" for="import_string">
<span class="icon"><i class="fa-solid fa-file-import" /></span>
Import string
</label>
<div class="field has-addons">
<div class="control has-icons-left is-expanded">
<div class="control is-expanded">
<input type="text" class="input" id="import_string" v-model="import_string" autocomplete="off">
<span class="icon is-small is-left"><i class="fa-solid fa-t" /></span>
</div>
<div class="control">
@ -51,31 +51,35 @@
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="name" v-text="'Name'" />
<div class="control has-icons-left">
<label class="label is-inline" for="name">
<span class="icon"><i class="fa-solid fa-tag" /></span>
Name
</label>
<div class="control">
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress">
<span class="icon is-small is-left"><i class="fa-solid fa-n" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The name to refers to this custom settings.</span>
<span>The name to refers to this preset of settings.</span>
</span>
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="format" v-text="'Format'" />
<div class="control has-icons-left">
<label class="label is-inline" for="format">
<span class="icon"><i class="fa-solid fa-f" /></span>
Format
</label>
<div class="control">
<input type="text" class="input" id="format" v-model="form.format" :disabled="addInProgress">
<span class="icon is-small is-left"><i class="fa-solid fa-f" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The yt-dlp <code>[--format, -f]</code> video format code. see <NuxtLink
href="https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#format-selection" target="blank">this
url</NuxtLink> for more info.</span>. Note, as this key is required, you can set the value to
<code>default</code> to let <code>yt-dlp</code> choose the best format.
page</NuxtLink> for more info. Note, as this key is required, you can set the value to
<code>default</code> to let <code>yt-dlp</code> choose the best format.</span>
</span>
</div>
</div>
@ -83,12 +87,12 @@
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="folder">
<span class="icon"><i class="fa-solid fa-folder" /></span>
Default Download path
</label>
<div class="control has-icons-left">
<div class="control">
<input type="text" class="input" id="folder" placeholder="Leave empty to use default download path"
v-model="form.folder" :disabled="addInProgress" list="folders">
<span class="icon is-small is-left"><i class="fa-solid fa-folder" /></span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
@ -101,19 +105,19 @@
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="output_template">
<span class="icon"><i class="fa-solid fa-file" /></span>
Default Output template
</label>
<div class="control has-icons-left">
<div class="control">
<input type="text" class="input" id="output_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>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Use this output template if non are given with URL. if not set, it will defaults to
<code>{{ config.app.output_template }}</code>.
For more information <NuxtLink href="https://github.com/yt-dlp/yt-dlp#output-template"
target="_blank">visit this url</NuxtLink>.
For more information visit <NuxtLink href="https://github.com/yt-dlp/yt-dlp#output-template"
target="_blank">this page</NuxtLink>.
</span>
</span>
</div>
@ -122,11 +126,12 @@
<div class="column is-12">
<div class="field">
<label class="label is-inline" for="cli_options">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
Command arguments for yt-dlp
</label>
<div class="control">
<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">
<textarea class="textarea is-pre" 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>
@ -141,8 +146,10 @@
<div class="column is-12">
<div class="field">
<label class="label is-inline" for="cookies"
v-tooltip="'Netscape HTTP Cookie format.'">Cookies</label>
<label class="label is-inline" for="cookies" v-tooltip="'Netscape HTTP Cookie format.'">
<span class="icon"><i class="fa-solid fa-cookie" /></span>
Cookies
</label>
<div class="control">
<textarea class="textarea is-pre" id="cookies" v-model="form.cookies" :disabled="addInProgress"
placeholder="Leave empty to use default cookies" />
@ -157,56 +164,7 @@
</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>
@ -319,55 +277,11 @@ const checkInfo = async () => {
return;
}
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 (copy?.args) {
delete copy.args
}
if (copy?.postprocessors) {
delete copy.postprocessors
}
}
else {
if (typeof copy.args === 'object') {
copy.args = JSON.stringify(copy.args, null, 2);
}
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;
}
}
}
// trim all fields in copy only if they are strings
for (const key in copy) {
if (typeof copy[key] === 'string') {
copy[key] = copy[key].trim()
if (typeof copy[key] !== 'string') {
continue
}
copy[key] = copy[key].trim()
}
emitter('submit', { reference: toRaw(props.reference), preset: toRaw(copy) });
@ -417,18 +331,13 @@ 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 = ''
if (item?._type || 'preset' !== item._type) {
toast.error(`Invalid import string. Expected type 'preset', got '${item._type ?? 'unknown'}'.`)
return
}
if (form.format || form.cli) {
if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
return
}
if ((form.format || form.cli) && false === confirm('This will overwrite the current data. Are you sure?')) {
return
}
if (item.name) {

View file

@ -71,15 +71,6 @@ 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>
</p>
<hr>
</template>
<p class="is-text-overflow"
v-if="item?.format && false === ['default', 'not_set'].includes(item.format)">
<span class="icon"><i class="fa-solid fa-f" /></span>
@ -307,11 +298,6 @@ const exportItem = item => {
}
})
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 = {}
for (const key of Object.keys(data)) {