Fold preset --format into the cli args

This commit is contained in:
arabcoders 2025-05-10 17:54:04 +03:00
parent f7abca8d10
commit 5a76f1d68a
6 changed files with 87 additions and 116 deletions

View file

@ -918,11 +918,6 @@ class HttpAPI(Common):
{"error": "name is required.", "data": item}, status=web.HTTPBadRequest.status_code {"error": "name is required.", "data": item}, status=web.HTTPBadRequest.status_code
) )
if not item.get("format"):
return web.json_response(
{"error": "format is required.", "data": item}, status=web.HTTPBadRequest.status_code
)
if not item.get("id", None) or not validate_uuid(item.get("id"), version=4): if not item.get("id", None) or not validate_uuid(item.get("id"), version=4):
item["id"] = str(uuid.uuid4()) item["id"] = str(uuid.uuid4())
@ -936,7 +931,7 @@ class HttpAPI(Common):
presets.append(Preset(**item)) presets.append(Preset(**item))
try: try:
presets = cls.save(presets=presets).load().get_all() presets = cls.save(items=presets).load().get_all()
except Exception as e: except Exception as e:
LOG.exception(e) LOG.exception(e)
return web.json_response( return web.json_response(
@ -1728,7 +1723,7 @@ class HttpAPI(Common):
LOG.error(str(e)) LOG.error(str(e))
return web.json_response(data={"message": str(e)}, status=web.HTTPInternalServerError.status_code) return web.json_response(data={"message": str(e)}, status=web.HTTPInternalServerError.status_code)
url = "https://www.youtube.com/account" url = "https://www.youtube.com/paid_memberships"
try: try:
opts = { opts = {
@ -1745,8 +1740,8 @@ class HttpAPI(Common):
LOG.debug(f"Checking '{url}' redirection.") LOG.debug(f"Checking '{url}' redirection.")
response = await client.request(method="GET", url=url, follow_redirects=False) response = await client.request(method="GET", url=url, follow_redirects=False)
return web.json_response( return web.json_response(
data={"message": "Authenticated." if response.status_code == 200 else "Not authenticated."}, data={"message": "Authenticated." if 200 == response.status_code else "Not authenticated."},
status=200 if response.status_code == 200 else 401, status=200 if 200 == response.status_code else 401,
) )
except Exception as e: except Exception as e:
LOG.error(f"Failed to request '{url}'. '{e}'.") LOG.error(f"Failed to request '{url}'. '{e}'.")

View file

@ -24,9 +24,6 @@ class Preset:
name: str name: str
"""The name of the preset.""" """The name of the preset."""
format: str
"""The format of the preset."""
folder: str = "" folder: str = ""
"""The default download folder to use if non is given.""" """The default download folder to use if non is given."""
@ -56,13 +53,13 @@ class Presets(metaclass=Singleton):
This class is used to manage the presets. This class is used to manage the presets.
""" """
_presets: list[Preset] = [] _items: list[Preset] = []
"""The list of presets.""" """The list of presets."""
_instance = None _instance = None
"""The instance of the class.""" """The instance of the class."""
_default_presets: list[Preset] = [] _default: list[Preset] = []
def __init__(self, file: str | None = None, config: Config | None = None): def __init__(self, file: str | None = None, config: Config | None = None):
Presets._instance = self Presets._instance = self
@ -77,13 +74,14 @@ class Presets(metaclass=Singleton):
except Exception: except Exception:
pass pass
with open(os.path.join(os.path.dirname(__file__), "presets.json")) as f: default_file = os.path.join(os.path.dirname(__file__), "presets.json")
with open(default_file) as f:
for i, preset in enumerate(json.load(f)): for i, preset in enumerate(json.load(f)):
try: try:
self.validate(preset) self.validate(preset)
self._default_presets.append(Preset(**preset)) self._default.append(Preset(**preset))
except Exception as e: except Exception as e:
LOG.error(f"Failed to parse default preset '{i}'. '{e!s}'.") LOG.error(f"Failed to parse '{default_file}:{i}'. '{e!s}'.")
continue continue
def event_handler(_, __): def event_handler(_, __):
@ -111,7 +109,7 @@ class Presets(metaclass=Singleton):
def attach(self, _: web.Application): def attach(self, _: web.Application):
""" """
Attach the work to the aiohttp application. Attach the class to the aiohttp application.
Args: Args:
_ (web.Application): The aiohttp application. _ (web.Application): The aiohttp application.
@ -123,12 +121,12 @@ class Presets(metaclass=Singleton):
self.load() self.load()
def get_all(self) -> list[Preset]: def get_all(self) -> list[Preset]:
"""Return the presets.""" """Return the items."""
return self._default_presets + self._presets return self._default + self._items
def load(self) -> "Presets": def load(self) -> "Presets":
""" """
Load the Presets. Load the items.
Returns: Returns:
Presets: The current instance. Presets: The current instance.
@ -139,16 +137,15 @@ class Presets(metaclass=Singleton):
if not os.path.exists(self._file) or os.path.getsize(self._file) < 10: if not os.path.exists(self._file) or os.path.getsize(self._file) < 10:
return self return self
LOG.info(f"Loading presets from '{self._file}'.") LOG.info(f"Loading '{self._file}'.")
try: try:
with open(self._file) as f: with open(self._file) as f:
presets = json.load(f) presets = json.load(f)
except Exception as e: except Exception as e:
LOG.error(f"Failed to parse presets from '{self._file}'. '{e}'.") LOG.error(f"Failed to parse '{self._file}'. '{e}'.")
return self return self
if not presets or len(presets) < 1: if not presets or len(presets) < 1:
LOG.info(f"No presets were defined in '{self._file}'.")
return self return self
need_save = False need_save = False
@ -160,134 +157,144 @@ class Presets(metaclass=Singleton):
need_save = True need_save = True
preset, preset_status = clean_item(preset, keys=("args", "postprocessors")) preset, preset_status = clean_item(preset, keys=("args", "postprocessors"))
if preset.get("format"):
if not preset.get("cli"):
preset.update({"cli": f"--format {preset['format']}"})
else:
preset["cli"] = f"--format '{preset['format']}'\n" + preset["cli"]
preset["cli"] = str(preset["cli"]).strip()
preset.pop("format")
need_save = True
preset = Preset(**preset) preset = Preset(**preset)
if preset_status: if preset_status:
need_save = True need_save = True
self._presets.append(preset) self._items.append(preset)
except Exception as e: except Exception as e:
LOG.error(f"Failed to parse preset at list position '{i}'. '{e!s}'.") LOG.error(f"Failed to parse '{self._file}:{i}'. '{e!s}'.")
continue continue
if need_save: if need_save:
LOG.info("Saving presets due to format, or id change.") LOG.info(f"Saving '{self._file}' due to changes.")
self.save(self._presets) self.save(self._items)
return self return self
def clear(self) -> "Presets": def clear(self) -> "Presets":
""" """
Clear all presets Clear all items.
Returns: Returns:
Presets: The current instance. Presets: The current instance.
""" """
if len(self._presets) < 1: if len(self._items) < 1:
return self return self
self._presets.clear() self._items.clear()
return self return self
def validate(self, preset: Preset | dict) -> bool: def validate(self, item: Preset | dict) -> bool:
""" """
Validate the preset. Validate the item.
Args: Args:
preset (Preset|dict): The preset to validate. item (Preset|dict): The item to validate.
Returns: Returns:
bool: True if the preset is valid, False otherwise. bool: True if valid
Raises:
ValueError: If the item is not valid.
""" """
if not isinstance(preset, dict): if not isinstance(item, dict):
if not isinstance(preset, Preset): if not isinstance(item, Preset):
msg = f"Invalid preset type. Was expecting a (Preset|dict), but got '{type(preset).__name__}'." msg = f"Unexpected '{type(item).__name__}' type was given."
raise ValueError(msg) # noqa: TRY004 raise ValueError(msg) # noqa: TRY004
preset = preset.serialize() item = item.serialize()
if not preset.get("id"): if not item.get("id"):
msg = "No id found." msg = "No id found."
raise ValueError(msg) raise ValueError(msg)
if not preset.get("name"): if not item.get("name"):
msg = "No name found." msg = "No name found."
raise ValueError(msg) raise ValueError(msg)
if not preset.get("format"): if item.get("cli"):
msg = "No format found."
raise ValueError(msg)
if preset.get("cli"):
try: try:
arg_converter(args=preset.get("cli")) arg_converter(args=item.get("cli"))
except Exception as e: except Exception as e:
msg = f"Invalid cli options. '{e!s}'." msg = f"Invalid cli options. '{e!s}'."
raise ValueError(msg) from e raise ValueError(msg) from e
return True return True
def save(self, presets: list[Preset | dict]) -> "Presets": def save(self, items: list[Preset | dict]) -> "Presets":
""" """
Save the presets. Save the items.
Args: Args:
presets (list[Preset]): The presets to save. items (list[Preset]): The items to save.
Returns: Returns:
Presets: The current instance. Presets: The current instance.
""" """
for i, preset in enumerate(presets): for i, preset in enumerate(items):
try: try:
if not isinstance(preset, Preset): if not isinstance(preset, Preset):
preset = Preset(**preset) preset = Preset(**preset)
presets[i] = preset items[i] = preset
except Exception as e: except Exception as e:
LOG.error(f"Failed to save preset '{i}' due to parsing error. '{e!s}'.") LOG.error(f"Failed to save item '{i}' due to parsing error. '{e!s}'.")
continue continue
try: try:
self.validate(preset) self.validate(preset)
except ValueError as e: except ValueError as e:
LOG.error(f"Failed to validate preset '{i}: {preset.name}'. '{e}'.") LOG.error(f"Failed to validate item '{i}: {preset.name}'. '{e}'.")
continue continue
try: try:
with open(self._file, "w") as f: with open(self._file, "w") as f:
json.dump(obj=[preset.serialize() for preset in presets if preset.default is False], fp=f, indent=4) json.dump(obj=[preset.serialize() for preset in items if preset.default is False], fp=f, indent=4)
LOG.info(f"Presets saved to '{self._file}'.") LOG.info(f"Saved '{self._file}'.")
except Exception as e: except Exception as e:
LOG.error(f"Failed to save presets to '{self._file}'. '{e!s}'.") LOG.error(f"Failed to save '{self._file}'. '{e!s}'.")
return self return self
def has(self, id_or_name: str) -> bool: def has(self, id_or_name: str) -> bool:
""" """
Check if the preset exists by id or name. Check if the item exists by id or name.
Args: Args:
id_or_name (str): The id or name of the preset. id_or_name (str): The id or name of the item.
Returns: Returns:
bool: True if the preset exists, False otherwise. bool: True if exists, False otherwise.
""" """
return self.get(id_or_name) is not None return self.get(id_or_name) is not None
def get(self, id_or_name: str) -> Preset | None: def get(self, id_or_name: str) -> Preset | None:
""" """
Get the preset by id or name. Get the item by id or name.
Args: Args:
id_or_name (str): The id or name of the preset. id_or_name (str): The id or name of the item.
Returns: Returns:
Preset|None: The preset if found, None otherwise. Preset|None: The item if found, None otherwise.
""" """
if not id_or_name: if not id_or_name:

View file

@ -135,9 +135,6 @@ class YTDLPOpts(metaclass=Singleton):
self._preset_opts["cookiefile"] = str(file) self._preset_opts["cookiefile"] = str(file)
if preset.format:
self._preset_opts["format"] = preset.format
if preset.template: if preset.template:
self._preset_opts["outtmpl"] = {"default": preset.template, "chapter": self._config.output_template_chapter} self._preset_opts["outtmpl"] = {"default": preset.template, "chapter": self._config.output_template_chapter}

View file

@ -2,7 +2,6 @@
{ {
"id": "3e163c6c-64eb-4448-924f-814b629b3810", "id": "3e163c6c-64eb-4448-924f-814b629b3810",
"name": "default", "name": "default",
"format": "default",
"folder": "", "folder": "",
"template": "", "template": "",
"cookies": "", "cookies": "",
@ -12,41 +11,37 @@
{ {
"id": "5bf9c42b-8852-468a-99f5-915622dfba25", "id": "5bf9c42b-8852-468a-99f5-915622dfba25",
"name": "Best video and audio", "name": "Best video and audio",
"format": "bv+ba/b",
"folder": "", "folder": "",
"template": "", "template": "",
"cookies": "", "cookies": "",
"cli": "", "cli": "--format 'bv+ba/b'",
"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]",
"folder": "", "folder": "",
"template": "", "template": "",
"cookies": "", "cookies": "",
"cli": "-S vcodec:h264", "cli": "-S vcodec:h264 --format 'bv[height<=1080][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]'",
"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]",
"folder": "", "folder": "",
"template": "", "template": "",
"cookies": "", "cookies": "",
"cli": "-S vcodec:h264", "cli": "-S vcodec:h264 --format 'bv[height<=720][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]'",
"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",
"folder": "", "folder": "",
"template": "", "template": "",
"cookies": "", "cookies": "",
"cli": "--extract-audio --add-chapters --embed-metadata --embed-thumbnail", "cli": "--extract-audio --add-chapters --embed-metadata --embed-thumbnail --format 'bestaudio/best'",
"default": true "default": true
} }
] ]

View file

@ -49,7 +49,7 @@
</span> </span>
</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="name"> <label class="label is-inline" for="name">
<span class="icon"><i class="fa-solid fa-tag" /></span> <span class="icon"><i class="fa-solid fa-tag" /></span>
@ -65,26 +65,7 @@
</div> </div>
</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="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">
</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
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>
<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">
<span class="icon"><i class="fa-solid fa-folder" /></span> <span class="icon"><i class="fa-solid fa-folder" /></span>
@ -102,7 +83,7 @@
</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="output_template"> <label class="label is-inline" for="output_template">
<span class="icon"><i class="fa-solid fa-file" /></span> <span class="icon"><i class="fa-solid fa-file" /></span>
@ -127,7 +108,7 @@
<div class="field"> <div class="field">
<label class="label is-inline" for="cli_options"> <label class="label is-inline" for="cli_options">
<span class="icon"><i class="fa-solid fa-terminal" /></span> <span class="icon"><i class="fa-solid fa-terminal" /></span>
Command arguments for yt-dlp Command options for yt-dlp
</label> </label>
<div class="control"> <div class="control">
<textarea class="textarea is-pre" v-model="form.cli" id="cli_options" :disabled="addInProgress" <textarea class="textarea is-pre" v-model="form.cli" id="cli_options" :disabled="addInProgress"
@ -241,8 +222,7 @@ onMounted(() => {
}) })
const checkInfo = async () => { const checkInfo = async () => {
const required = ['name', 'format']; for (const key of ['name']) {
for (const key of required) {
if (!form[key]) { if (!form[key]) {
toast.error(`The ${key} field is required.`); toast.error(`The ${key} field is required.`);
return return
@ -299,10 +279,6 @@ const convertOptions = async args => {
form.folder = response.download_path form.folder = response.download_path
} }
if (response.format) {
form.format = response.format
}
return response.opts return response.opts
} catch (e) { } catch (e) {
toast.error(e.message) toast.error(e.message)
@ -336,7 +312,7 @@ const importItem = async () => {
return return
} }
if ((form.format || form.cli) && false === confirm('This will overwrite the current data. Are you sure?')) { if (form.cli && false === confirm('This will overwrite the current data. Are you sure?')) {
return return
} }
@ -344,14 +320,20 @@ const importItem = async () => {
form.name = item.name form.name = item.name
} }
if (item.format) {
form.format = item.format
}
if (item.cli) { if (item.cli) {
form.cli = item.cli form.cli = item.cli
} }
// -- backwards compatibility for old presets.
if (item.format) {
if (!item?.cli) {
form.cli = `--format '${item.format}'`
} else {
form.cli = `--format '${item.format}'\n${form.cli}`
}
form.cli = form.cli.trim()
}
if (item.template) { if (item.template) {
form.template = item.template form.template = item.template
} }
@ -364,7 +346,7 @@ const importItem = async () => {
showImport.value = false showImport.value = false
} catch (e) { } catch (e) {
console.error(e) console.error(e)
toast.error(`Failed to string. ${e.message}`) toast.error(`Failed to parse string. ${e.message}`)
} }
} }

View file

@ -71,11 +71,6 @@ div.is-centered {
</header> </header>
<div class="card-content is-flex-grow-1"> <div class="card-content is-flex-grow-1">
<div class="content"> <div class="content">
<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>
<span v-text="item.format" />
</p>
<p class="is-text-overflow" v-if="item.folder"> <p class="is-text-overflow" v-if="item.folder">
<span class="icon"><i class="fa-solid fa-folder" /></span> <span class="icon"><i class="fa-solid fa-folder" /></span>
<span>{{ calcPath(item.folder) }}</span> <span>{{ calcPath(item.folder) }}</span>
@ -322,7 +317,7 @@ const exportItem = item => {
} }
userData['_type'] = 'preset' userData['_type'] = 'preset'
userData['_version'] = '2.0' userData['_version'] = '2.5'
return copyText(base64UrlEncode(JSON.stringify(userData))) return copyText(base64UrlEncode(JSON.stringify(userData)))
} }