Fold preset --format into the cli args
This commit is contained in:
parent
f7abca8d10
commit
5a76f1d68a
6 changed files with 87 additions and 116 deletions
|
|
@ -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}'.")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue