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
)
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):
item["id"] = str(uuid.uuid4())
@ -936,7 +931,7 @@ class HttpAPI(Common):
presets.append(Preset(**item))
try:
presets = cls.save(presets=presets).load().get_all()
presets = cls.save(items=presets).load().get_all()
except Exception as e:
LOG.exception(e)
return web.json_response(
@ -1728,7 +1723,7 @@ class HttpAPI(Common):
LOG.error(str(e))
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:
opts = {
@ -1745,8 +1740,8 @@ class HttpAPI(Common):
LOG.debug(f"Checking '{url}' redirection.")
response = await client.request(method="GET", url=url, follow_redirects=False)
return web.json_response(
data={"message": "Authenticated." if response.status_code == 200 else "Not authenticated."},
status=200 if response.status_code == 200 else 401,
data={"message": "Authenticated." if 200 == response.status_code else "Not authenticated."},
status=200 if 200 == response.status_code else 401,
)
except Exception as e:
LOG.error(f"Failed to request '{url}'. '{e}'.")

View file

@ -24,9 +24,6 @@ class Preset:
name: str
"""The name of the preset."""
format: str
"""The format of the preset."""
folder: str = ""
"""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.
"""
_presets: list[Preset] = []
_items: list[Preset] = []
"""The list of presets."""
_instance = None
"""The instance of the class."""
_default_presets: list[Preset] = []
_default: list[Preset] = []
def __init__(self, file: str | None = None, config: Config | None = None):
Presets._instance = self
@ -77,13 +74,14 @@ class Presets(metaclass=Singleton):
except Exception:
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)):
try:
self.validate(preset)
self._default_presets.append(Preset(**preset))
self._default.append(Preset(**preset))
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
def event_handler(_, __):
@ -111,7 +109,7 @@ class Presets(metaclass=Singleton):
def attach(self, _: web.Application):
"""
Attach the work to the aiohttp application.
Attach the class to the aiohttp application.
Args:
_ (web.Application): The aiohttp application.
@ -123,12 +121,12 @@ class Presets(metaclass=Singleton):
self.load()
def get_all(self) -> list[Preset]:
"""Return the presets."""
return self._default_presets + self._presets
"""Return the items."""
return self._default + self._items
def load(self) -> "Presets":
"""
Load the Presets.
Load the items.
Returns:
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:
return self
LOG.info(f"Loading presets from '{self._file}'.")
LOG.info(f"Loading '{self._file}'.")
try:
with open(self._file) as f:
presets = json.load(f)
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
if not presets or len(presets) < 1:
LOG.info(f"No presets were defined in '{self._file}'.")
return self
need_save = False
@ -160,134 +157,144 @@ class Presets(metaclass=Singleton):
need_save = True
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)
if preset_status:
need_save = True
self._presets.append(preset)
self._items.append(preset)
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
if need_save:
LOG.info("Saving presets due to format, or id change.")
self.save(self._presets)
LOG.info(f"Saving '{self._file}' due to changes.")
self.save(self._items)
return self
def clear(self) -> "Presets":
"""
Clear all presets
Clear all items.
Returns:
Presets: The current instance.
"""
if len(self._presets) < 1:
if len(self._items) < 1:
return self
self._presets.clear()
self._items.clear()
return self
def validate(self, preset: Preset | dict) -> bool:
def validate(self, item: Preset | dict) -> bool:
"""
Validate the preset.
Validate the item.
Args:
preset (Preset|dict): The preset to validate.
item (Preset|dict): The item to validate.
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(preset, Preset):
msg = f"Invalid preset type. Was expecting a (Preset|dict), but got '{type(preset).__name__}'."
if not isinstance(item, dict):
if not isinstance(item, Preset):
msg = f"Unexpected '{type(item).__name__}' type was given."
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."
raise ValueError(msg)
if not preset.get("name"):
if not item.get("name"):
msg = "No name found."
raise ValueError(msg)
if not preset.get("format"):
msg = "No format found."
raise ValueError(msg)
if preset.get("cli"):
if item.get("cli"):
try:
arg_converter(args=preset.get("cli"))
arg_converter(args=item.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":
def save(self, items: list[Preset | dict]) -> "Presets":
"""
Save the presets.
Save the items.
Args:
presets (list[Preset]): The presets to save.
items (list[Preset]): The items to save.
Returns:
Presets: The current instance.
"""
for i, preset in enumerate(presets):
for i, preset in enumerate(items):
try:
if not isinstance(preset, Preset):
preset = Preset(**preset)
presets[i] = preset
items[i] = preset
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
try:
self.validate(preset)
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
try:
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:
LOG.error(f"Failed to save presets to '{self._file}'. '{e!s}'.")
LOG.error(f"Failed to save '{self._file}'. '{e!s}'.")
return self
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:
id_or_name (str): The id or name of the preset.
id_or_name (str): The id or name of the item.
Returns:
bool: True if the preset exists, False otherwise.
bool: True if exists, False otherwise.
"""
return self.get(id_or_name) is not 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:
id_or_name (str): The id or name of the preset.
id_or_name (str): The id or name of the item.
Returns:
Preset|None: The preset if found, None otherwise.
Preset|None: The item if found, None otherwise.
"""
if not id_or_name:

View file

@ -135,9 +135,6 @@ class YTDLPOpts(metaclass=Singleton):
self._preset_opts["cookiefile"] = str(file)
if preset.format:
self._preset_opts["format"] = preset.format
if preset.template:
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",
"name": "default",
"format": "default",
"folder": "",
"template": "",
"cookies": "",
@ -12,41 +11,37 @@
{
"id": "5bf9c42b-8852-468a-99f5-915622dfba25",
"name": "Best video and audio",
"format": "bv+ba/b",
"folder": "",
"template": "",
"cookies": "",
"cli": "",
"cli": "--format 'bv+ba/b'",
"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]",
"folder": "",
"template": "",
"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
},
{
"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]",
"folder": "",
"template": "",
"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
},
{
"id": "a6fd4b25-2b3e-458d-bb57-b75e41cc4330",
"name": "Audio only",
"format": "bestaudio/best",
"folder": "",
"template": "",
"cookies": "",
"cli": "--extract-audio --add-chapters --embed-metadata --embed-thumbnail",
"cli": "--extract-audio --add-chapters --embed-metadata --embed-thumbnail --format 'bestaudio/best'",
"default": true
}
]

View file

@ -49,7 +49,7 @@
</span>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="column is-12">
<div class="field">
<label class="label is-inline" for="name">
<span class="icon"><i class="fa-solid fa-tag" /></span>
@ -65,26 +65,7 @@
</div>
</div>
<div class="column is-6-tablet is-12-mobile">
<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="column is-12">
<div class="field">
<label class="label is-inline" for="folder">
<span class="icon"><i class="fa-solid fa-folder" /></span>
@ -102,7 +83,7 @@
</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="output_template">
<span class="icon"><i class="fa-solid fa-file" /></span>
@ -127,7 +108,7 @@
<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
Command options for yt-dlp
</label>
<div class="control">
<textarea class="textarea is-pre" v-model="form.cli" id="cli_options" :disabled="addInProgress"
@ -241,8 +222,7 @@ onMounted(() => {
})
const checkInfo = async () => {
const required = ['name', 'format'];
for (const key of required) {
for (const key of ['name']) {
if (!form[key]) {
toast.error(`The ${key} field is required.`);
return
@ -299,10 +279,6 @@ const convertOptions = async args => {
form.folder = response.download_path
}
if (response.format) {
form.format = response.format
}
return response.opts
} catch (e) {
toast.error(e.message)
@ -336,7 +312,7 @@ const importItem = async () => {
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
}
@ -344,14 +320,20 @@ const importItem = async () => {
form.name = item.name
}
if (item.format) {
form.format = item.format
}
if (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) {
form.template = item.template
}
@ -364,7 +346,7 @@ const importItem = async () => {
showImport.value = false
} catch (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>
<div class="card-content is-flex-grow-1">
<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">
<span class="icon"><i class="fa-solid fa-folder" /></span>
<span>{{ calcPath(item.folder) }}</span>
@ -322,7 +317,7 @@ const exportItem = item => {
}
userData['_type'] = 'preset'
userData['_version'] = '2.0'
userData['_version'] = '2.5'
return copyText(base64UrlEncode(JSON.stringify(userData)))
}