Merge pull request #243 from arabcoders/dev
deprecate JSON yt-dlp config, and encourage cli options.
This commit is contained in:
commit
6cf0b9120e
24 changed files with 727 additions and 572 deletions
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
|
|
@ -19,11 +19,16 @@
|
|||
"attl",
|
||||
"autonumber",
|
||||
"changeslog",
|
||||
"consoletitle",
|
||||
"cookiesfrombrowser",
|
||||
"copyts",
|
||||
"daterange",
|
||||
"dotenv",
|
||||
"finaldir",
|
||||
"flac",
|
||||
"forcejson",
|
||||
"forceprint",
|
||||
"fribidi",
|
||||
"getpid",
|
||||
"gpac",
|
||||
"httpx",
|
||||
|
|
@ -41,6 +46,7 @@
|
|||
"postprocessor",
|
||||
"preferredcodec",
|
||||
"preferredquality",
|
||||
"printtraffic",
|
||||
"quicktime",
|
||||
"tmpfilename",
|
||||
"urandom",
|
||||
|
|
@ -56,7 +62,6 @@
|
|||
"en"
|
||||
],
|
||||
"spellright.documentTypes": [
|
||||
"latex",
|
||||
"plaintext"
|
||||
"latex"
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ ENV XDG_CONFIG_HOME=/config
|
|||
ENV XDG_CACHE_HOME=/tmp
|
||||
|
||||
RUN mkdir /config /downloads && ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone && \
|
||||
apk add --update --no-cache bash mkvtoolnix patch aria2 coreutils curl shadow sqlite tzdata libmagic ffmpeg rtmpdump && \
|
||||
apk add --update --no-cache bash mkvtoolnix patch aria2 coreutils curl shadow sqlite tzdata libmagic ffmpeg rtmpdump fribidi && \
|
||||
useradd -u ${USER_ID:-1000} -U -d /app -s /bin/bash app && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
|
|
|
|||
77
README.md
77
README.md
|
|
@ -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**.
|
||||
|
|
@ -25,22 +25,47 @@ Web GUI for [yt-dlp](https://github.com/yt-dlp/yt-dlp) with playlist & channel s
|
|||
* Support for curl_cffi, see [yt-dlp documentation](https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#impersonation)
|
||||
* Support for both advanced and basic mode for WebUI.
|
||||
* Bundled tools in container: curl-cffi, ffmpeg, ffprobe, aria2, rtmpdump, mkvtoolsnix, mp4box.
|
||||
|
||||
# Recommended basic `ytdlp.json` file settings
|
||||
|
||||
Your `/config/ytdlp.json` config should include the following basic options for optimal working conditions.
|
||||
# Breaking changes
|
||||
|
||||
```json
|
||||
{
|
||||
"windowsfilenames": true,
|
||||
"continue_dl": true,
|
||||
"live_from_start": true,
|
||||
"format_sort": [ "codec:avc:m4a" ],
|
||||
}
|
||||
```
|
||||
Starting with versions tagged `*-20250330-*` I have deprecated the JSON yt-dlp config and all of it's related settings.
|
||||
|
||||
> [!NOTE]
|
||||
> Note, the `format_sort`, forces YouTube to use x264 instead of vp9 codec, you can ignore it if you want. i prefer the media in x264.
|
||||
To be frank, it was pain to manage and hard for users to understand how to map cli options to json options. So, I have
|
||||
decided to just use the cli options directly. This means that you can now use the same options you would use in the command line
|
||||
directly in the WebUI, presets, and tasks.
|
||||
|
||||
## `ytdlp.json` vs `ytdlp.cli`
|
||||
|
||||
I have also added a new `ytdlp.cli` file that will be used to store the global options for yt-dlp. This file is located in the `/config` directory
|
||||
and will be used to store the global options for yt-dlp. This file is not required and presets can fill the gap for most of the
|
||||
use cases. But, if you want to use the same options for all your downloads, you can use this file to store the global options.
|
||||
|
||||
So, if you have `ytdlp.cli` file it will take priority over the `ytdlp.json` file if both exists. As mitigation, I have implemented
|
||||
fallback to `ytdlp.json` file if `ytdlp.cli` file is not found.
|
||||
|
||||
## Presets
|
||||
|
||||
I have deprecated the `JSON yt-dlp config` and `JSON yt-dlp Post-Processors` fields, and added new `Command arguments for yt-dlp`
|
||||
field to the presets. This field will be used to store the command line options for yt-dlp. I have also have added fallback,
|
||||
if you have the `JSON yt-dlp config/Post-Processors` field set, the logic is, if `Command arguments for yt-dlp` is set
|
||||
it will take priority over the `JSON yt-dlp config/Post-Processors` field. If not set, it will fallback to the `JSON yt-dlp config/Post-Processors` field.
|
||||
|
||||
If you are adding new presets, the deprecated fields will not show up anymore, they will only show up if actually have content in them
|
||||
and editing old preset. So, Please migrate your presets to the new format.
|
||||
|
||||
## Tasks
|
||||
|
||||
I have also removed the `JSON yt-dlp config` and replaced it with `Command arguments for yt-dlp` field. Sadly, there is
|
||||
no fallback, and once you upgrade to any version after `*-20250330-*` your `tasks.json` file will be updated to remove the
|
||||
`config` key. so, please make sure to backup your `tasks.json` file before upgrading.
|
||||
|
||||
## closing statement
|
||||
|
||||
I know it's a painful breaking change, but for the sake of maintainability and ease of use, I have decided to
|
||||
to make it happen sooner than later, the `JSON yt-dlp config`, was hard to manage and some features weren't really working
|
||||
as expected, for example `--match-filter` and `--date` arguments etc.
|
||||
|
||||
So, Starting with `*-2025040*-*` tagged versions, the all the fallbacks and backwards compatibility will be removed.
|
||||
|
||||
# Run using docker command
|
||||
|
||||
|
|
@ -240,25 +265,19 @@ A Docker image can be built locally (it will build the UI too):
|
|||
docker build . -t ytptube
|
||||
```
|
||||
|
||||
# ytdlp.json file
|
||||
# ytdlp.cli file
|
||||
|
||||
The `config/ytdlp.json`, is a json file which can be used to alter the default `yt-dlp` config settings globally.
|
||||
The `config/ytdlp.cli`, is a command line options file for `yt-dlp` it will be globally applied to all downloads.
|
||||
|
||||
We recommend not use this file for options that aren't **truly global**, everything that can be done via the `ytdlp.json` file
|
||||
can be done via a preset, which only effects the download that uses it. Example of good basic `ytdlp.json` file.
|
||||
We strongly recommend not use this file for options that aren't **truly global**, everything that can be done via the file
|
||||
can also be done via the presets which is dynamic can be altered per download. Example of good global options are to be
|
||||
used for all downloads are:
|
||||
|
||||
```json
|
||||
{
|
||||
"windowsfilenames": true,
|
||||
"continue_dl": true,
|
||||
"live_from_start": true,
|
||||
"format_sort": [ "codec:avc:m4a" ],
|
||||
}
|
||||
```bash
|
||||
--continue --windows-filenames --live-from-start
|
||||
```
|
||||
|
||||
Everything else can be done via the presets, and it's more flexible and easier to manage. You can convert your
|
||||
own yt-dlp command arguments into a preset using the box found in the presets add page. For reference, The options can be found
|
||||
at [yt-dlp YoutubeDL.py](https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/YoutubeDL.py#L214) file. And for the postprocessors at [yt-dlp postprocessor](https://github.com/yt-dlp/yt-dlp/tree/master/yt_dlp/postprocessor).
|
||||
Everything else can be done via the presets, and it's more flexible and easier to manage.
|
||||
|
||||
# Authentication
|
||||
|
||||
|
|
@ -275,7 +294,7 @@ What does the basic mode do? it hides the the following features from the WebUI.
|
|||
|
||||
### Header
|
||||
|
||||
It disables everything except the `theme switcher` and `reload` button.
|
||||
It disables everything except the `settings button` and `reload` button.
|
||||
|
||||
### Add form
|
||||
|
||||
|
|
|
|||
|
|
@ -135,6 +135,12 @@ class Download:
|
|||
|
||||
self.status_queue.put({"id": self.id, "status": "finished", "filename": filename})
|
||||
|
||||
def post_hooks(self, filename: str | None = None):
|
||||
if not filename:
|
||||
return
|
||||
|
||||
self.status_queue.put({"id": self.id, "filename": filename})
|
||||
|
||||
def _download(self):
|
||||
try:
|
||||
params = (
|
||||
|
|
@ -178,6 +184,7 @@ class Download:
|
|||
{
|
||||
"progress_hooks": [self._progress_hook],
|
||||
"postprocessor_hooks": [self._postprocessor_hook],
|
||||
"post_hooks": [self.post_hooks],
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -215,7 +222,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)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ from .Events import EventBus, Events
|
|||
from .ItemDTO import ItemDTO
|
||||
from .Presets import Presets
|
||||
from .Singleton import Singleton
|
||||
from .Utils import calc_download_path, extract_info, is_downloaded
|
||||
from .Utils import arg_converter, calc_download_path, extract_info, is_downloaded
|
||||
from .YTDLPOpts import YTDLPOpts
|
||||
|
||||
LOG = logging.getLogger("DownloadQueue")
|
||||
|
|
@ -177,6 +177,7 @@ class DownloadQueue(metaclass=Singleton):
|
|||
cookies: str = "",
|
||||
template: str = "",
|
||||
extras: dict | None = None,
|
||||
cli: str = "",
|
||||
already=None,
|
||||
):
|
||||
"""
|
||||
|
|
@ -190,9 +191,9 @@ class DownloadQueue(metaclass=Singleton):
|
|||
cookies (str): The cookies to use for the download.
|
||||
template (str): The output template to use for the download.
|
||||
extras (dict): The extra information to add to the download.
|
||||
cli (str): The yt-dlp command line options to use for the download.
|
||||
already (set): The set of already downloaded items.
|
||||
|
||||
|
||||
Returns:
|
||||
dict: The status of the operation.
|
||||
|
||||
|
|
@ -244,6 +245,7 @@ class DownloadQueue(metaclass=Singleton):
|
|||
cookies=cookies,
|
||||
template=template,
|
||||
extras=extras,
|
||||
cli=cli,
|
||||
already=already,
|
||||
)
|
||||
)
|
||||
|
|
@ -320,6 +322,7 @@ class DownloadQueue(metaclass=Singleton):
|
|||
is_live=is_live,
|
||||
live_in=live_in,
|
||||
options=options,
|
||||
cli=cli,
|
||||
extras=extras,
|
||||
)
|
||||
|
||||
|
|
@ -352,6 +355,7 @@ class DownloadQueue(metaclass=Singleton):
|
|||
cookies=cookies,
|
||||
template=template,
|
||||
extras=extras,
|
||||
cli=cli,
|
||||
already=already,
|
||||
)
|
||||
|
||||
|
|
@ -365,12 +369,39 @@ class DownloadQueue(metaclass=Singleton):
|
|||
config: dict | None = None,
|
||||
cookies: str = "",
|
||||
template: str = "",
|
||||
cli: str = "",
|
||||
extras: dict | None = None,
|
||||
already=None,
|
||||
):
|
||||
"""
|
||||
Add an item to the download queue.
|
||||
|
||||
Args:
|
||||
url (str): The url to be added to the queue.
|
||||
preset (str): The preset to be used for the download.
|
||||
folder (str): The folder to save the download to.
|
||||
config (dict): The yt-dlp config to be used for the download.
|
||||
cookies (str): The cookies to be used for the download.
|
||||
template (str): The template to be used for the download.
|
||||
cli (str): The yt-dlp cli options to be used for the download.
|
||||
extras (dict): Extra data to be added to the download
|
||||
already (set): Set of already downloaded items.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: The status of the download.
|
||||
{ "status": "text" }
|
||||
|
||||
"""
|
||||
_preset = Presets.get_instance().get(name=preset)
|
||||
|
||||
config = config if config else {}
|
||||
if cli:
|
||||
try:
|
||||
config = arg_converter(args=cli, level=True)
|
||||
except Exception as e:
|
||||
LOG.error(f"Invalid cli options '{cli}'. {e!s}")
|
||||
return {"status": "error", "msg": f"Invalid cli options '{cli}'. {e!s}"}
|
||||
|
||||
folder = str(folder) if folder else ""
|
||||
if not extras:
|
||||
extras = {}
|
||||
|
|
@ -390,7 +421,7 @@ class DownloadQueue(metaclass=Singleton):
|
|||
cookie_file = os.path.join(self.config.temp_path, f"c_{uuid.uuid4().hex}.txt")
|
||||
|
||||
LOG.info(
|
||||
f"Adding 'URL: {url}' to 'Folder: {filePath}' with 'Preset: {preset}' 'Naming: {template}', 'Cookies: {len(cookies)}/chars' 'YTConfig: {config}' 'Extras: {extras}'."
|
||||
f"Adding 'URL: {url}' to 'Folder: {filePath}' with 'Preset: {preset}' 'Naming: {template}', 'Cookies: {len(cookies)}/chars' 'CLI: {cli}' 'Extras: {extras}'."
|
||||
)
|
||||
|
||||
if isinstance(config, str):
|
||||
|
|
@ -486,6 +517,7 @@ class DownloadQueue(metaclass=Singleton):
|
|||
template=template,
|
||||
already=already,
|
||||
extras=extras,
|
||||
cli=cli,
|
||||
)
|
||||
|
||||
async def cancel(self, ids: list[str]) -> dict[str, str]:
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ class Events:
|
|||
LOG_SUCCESS = "log_success"
|
||||
|
||||
INITIAL_DATA = "initial_data"
|
||||
YTDLP_CONVERT = "ytdlp_convert"
|
||||
ITEM_DELETE = "item_delete"
|
||||
ITEM_CANCEL = "item_cancel"
|
||||
STATUS = "status"
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ from .Segments import Segments
|
|||
from .Subtitle import Subtitle
|
||||
from .Tasks import Task, Tasks
|
||||
from .Utils import (
|
||||
IGNORED_KEYS,
|
||||
REMOVE_KEYS,
|
||||
StreamingError,
|
||||
arg_converter,
|
||||
decrypt_data,
|
||||
|
|
@ -445,7 +445,7 @@ class HttpAPI(Common):
|
|||
try:
|
||||
response = {"opts": {}, "output_template": None, "download_path": None}
|
||||
|
||||
data = arg_converter(args)
|
||||
data = arg_converter(args, dumps=True)
|
||||
|
||||
if "outtmpl" in data and "default" in data["outtmpl"]:
|
||||
response["output_template"] = data["outtmpl"]["default"]
|
||||
|
|
@ -453,20 +453,29 @@ 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"]
|
||||
|
||||
bad_options = {k: v for d in REMOVE_KEYS for k, v in d.items()}
|
||||
removed_options = []
|
||||
|
||||
for key in data:
|
||||
if key in IGNORED_KEYS:
|
||||
if key in bad_options.items():
|
||||
removed_options.append(bad_options[key])
|
||||
continue
|
||||
if not key.startswith("_"):
|
||||
response["opts"][key] = data[key]
|
||||
|
||||
if len(removed_options) > 0:
|
||||
response["opts"]["removed"] = ", ".join(removed_options)
|
||||
|
||||
return web.json_response(data=response, status=web.HTTPOk.status_code)
|
||||
except Exception as e:
|
||||
err = str(e).strip()
|
||||
err = err.split("\n")[-1] if "\n" in err else err
|
||||
LOG.error(f"Failed to convert args. '{err}'.")
|
||||
LOG.exception(e)
|
||||
err = err.replace("main.py: error: ", "").strip().capitalize()
|
||||
return web.json_response(
|
||||
data={"error": f"Failed to convert args. '{err}'."}, status=web.HTTPBadRequest.status_code
|
||||
data={"error": f"Failed to command line arguments for yt-dlp. '{err}'."}, status=web.HTTPBadRequest.status_code
|
||||
)
|
||||
|
||||
@route("GET", "api/yt-dlp/url/info")
|
||||
|
|
@ -872,15 +881,12 @@ class HttpAPI(Common):
|
|||
if not item.get("timer", None) or str(item.get("timer")).strip() == "":
|
||||
item["timer"] = f"{random.randint(1,59)} */1 * * *" # noqa: S311
|
||||
|
||||
if not item.get("cookies", None):
|
||||
item["cookies"] = ""
|
||||
|
||||
if not item.get("config", None) or str(item.get("config")).strip() == "":
|
||||
item["config"] = {}
|
||||
|
||||
if not item.get("template", None):
|
||||
item["template"] = ""
|
||||
|
||||
if not item.get("cli", None):
|
||||
item["cli"] = ""
|
||||
|
||||
try:
|
||||
ins.validate(item)
|
||||
except ValueError as e:
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from .DownloadQueue import DownloadQueue
|
|||
from .encoder import Encoder
|
||||
from .Events import Event, EventBus, Events, error
|
||||
from .Presets import Presets
|
||||
from .Utils import arg_converter, is_downloaded
|
||||
from .Utils import is_downloaded
|
||||
|
||||
LOG = logging.getLogger("socket_api")
|
||||
|
||||
|
|
@ -285,25 +285,3 @@ class HttpSocket(Common):
|
|||
async def resume(self, *_, **__):
|
||||
self.queue.resume()
|
||||
await self._notify.emit(Events.PAUSED, data={"paused": False, "at": time.time()})
|
||||
|
||||
@ws_event
|
||||
async def ytdlp_convert(self, sid: str, data: dict):
|
||||
if not isinstance(data, dict) or "args" not in data:
|
||||
await self._notify.emit(Events.ERROR, data=error("Invalid request or no options were given."), to=sid)
|
||||
return
|
||||
|
||||
args: str | None = data.get("args")
|
||||
|
||||
if not args:
|
||||
await self._notify.emit(Events.ERROR, data=error("no options were given."), to=sid)
|
||||
return
|
||||
|
||||
try:
|
||||
await self._notify.emit(Events.YTDLP_CONVERT, data=arg_converter(args), to=sid)
|
||||
except Exception as e:
|
||||
err = str(e).strip()
|
||||
err = err.split("\n")[-1] if "\n" in err else err
|
||||
LOG.error(f"Failed to convert args. '{err}'.")
|
||||
await self._notify.emit(Events.ERROR, data=error(f"Failed to convert options. '{err}'."), to=sid)
|
||||
|
||||
return
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ class ItemDTO:
|
|||
temp_dir: str | None = None
|
||||
status: str | None = None
|
||||
cookies: str | None = None
|
||||
config: dict = field(default_factory=dict)
|
||||
template: str | None = None
|
||||
template_chapter: str | None = None
|
||||
timestamp: float = field(default_factory=lambda: time.time_ns())
|
||||
|
|
@ -37,6 +36,7 @@ class ItemDTO:
|
|||
file_size: int | None = None
|
||||
options: dict = field(default_factory=dict)
|
||||
extras: dict = field(default_factory=dict)
|
||||
cli: str = ""
|
||||
|
||||
# yt-dlp injected fields.
|
||||
tmpfilename: str | None = None
|
||||
|
|
@ -49,12 +49,13 @@ class ItemDTO:
|
|||
speed: str | None = None
|
||||
eta: str | None = None
|
||||
|
||||
# DEPRECATED: These fields are deprecated and will be removed in the future.
|
||||
# @Deprecated: These fields are deprecated and will be removed in the future.
|
||||
thumbnail: str | None = None
|
||||
ytdlp_cookies: str | None = None
|
||||
ytdlp_config: dict = field(default_factory=dict)
|
||||
output_template: str | None = None
|
||||
output_template_chapter: str | None = None
|
||||
config: dict = field(default_factory=dict)
|
||||
|
||||
def serialize(self) -> dict:
|
||||
deprecated: tuple = (
|
||||
|
|
@ -65,6 +66,7 @@ class ItemDTO:
|
|||
"ytdlp_config",
|
||||
"output_template",
|
||||
"output_template_chapter",
|
||||
"config",
|
||||
)
|
||||
|
||||
if self.thumbnail and "thumbnail" not in self.extras:
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import json
|
|||
import logging
|
||||
import os
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -28,8 +28,7 @@ class Task:
|
|||
preset: str = ""
|
||||
timer: str = ""
|
||||
template: str = ""
|
||||
cookies: str = ""
|
||||
config: dict[str, str] = field(default_factory=dict)
|
||||
cli: str = ""
|
||||
|
||||
def serialize(self) -> dict:
|
||||
return self.__dict__
|
||||
|
|
@ -134,16 +133,20 @@ class Tasks(metaclass=Singleton):
|
|||
with open(self._file) as f:
|
||||
tasks = json.load(f)
|
||||
except Exception as e:
|
||||
LOG.error(f"Failed to parse tasks from '{self._file}'. '{e}'.")
|
||||
LOG.error(f"Failed to parse tasks from '{self._file}'. '{e!s}'.")
|
||||
return self
|
||||
|
||||
if not tasks or len(tasks) < 1:
|
||||
LOG.info(f"No tasks were defined in '{self._file}'.")
|
||||
return self
|
||||
|
||||
need_update = False
|
||||
for i, task in enumerate(tasks):
|
||||
try:
|
||||
task, task_status = self.clean_task(task)
|
||||
task = Task(**task)
|
||||
if task_status:
|
||||
need_update = True
|
||||
except Exception as e:
|
||||
LOG.error(f"Failed to parse task at list position '{i}'. '{e!s}'.")
|
||||
continue
|
||||
|
|
@ -157,6 +160,10 @@ class Tasks(metaclass=Singleton):
|
|||
LOG.exception(e)
|
||||
LOG.error(f"Failed to queue task '{i}: {task.name}'. '{e!s}'.")
|
||||
|
||||
if need_update:
|
||||
LOG.info("Updating tasks file to remove old keys.")
|
||||
self.save(self.get_all())
|
||||
|
||||
return self
|
||||
|
||||
def clear(self, shutdown: bool = False) -> "Tasks":
|
||||
|
|
@ -213,17 +220,13 @@ class Tasks(metaclass=Singleton):
|
|||
msg = "No URL found."
|
||||
raise ValueError(msg)
|
||||
|
||||
if not isinstance(task.get("cookies"), str):
|
||||
msg = "Invalid cookies type."
|
||||
raise ValueError(msg) # noqa: TRY004
|
||||
|
||||
if not isinstance(task.get("config"), dict):
|
||||
msg = "Invalid config type."
|
||||
raise ValueError(msg) # noqa: TRY004
|
||||
|
||||
if not isinstance(task.get("template"), str):
|
||||
msg = "Invalid template type."
|
||||
raise ValueError(msg) # noqa: TRY004
|
||||
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}'."
|
||||
raise ValueError(msg) from e
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -263,6 +266,27 @@ 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.
|
||||
|
|
@ -283,16 +307,8 @@ class Tasks(metaclass=Singleton):
|
|||
|
||||
preset: str = str(task.preset or self._default_preset)
|
||||
folder: str = task.folder if task.folder else ""
|
||||
cookies: str = str(task.cookies) if task.cookies else ""
|
||||
template: str = task.template if task.template else ""
|
||||
|
||||
config = task.config if task.config else {}
|
||||
if isinstance(config, str) and config:
|
||||
try:
|
||||
config = json.loads(config)
|
||||
except Exception as e:
|
||||
LOG.error(f"Failed to parse json yt-dlp config for '{task.name}'. {e!s}")
|
||||
return
|
||||
cli: str = task.cli if task.cli else ""
|
||||
|
||||
LOG.info(f"Task '{task.id}: {task.name}' dispatched at '{timeNow}'.")
|
||||
|
||||
|
|
@ -307,9 +323,8 @@ class Tasks(metaclass=Singleton):
|
|||
"url": task.url,
|
||||
"preset": preset,
|
||||
"folder": folder,
|
||||
"cookies": cookies,
|
||||
"config": config,
|
||||
"template": template,
|
||||
"cli": cli,
|
||||
},
|
||||
id=task.id,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -21,7 +21,42 @@ from .LogWrapper import LogWrapper
|
|||
|
||||
LOG = logging.getLogger("Utils")
|
||||
|
||||
IGNORED_KEYS: tuple[str] = ("paths", "outtmpl", "progress_hooks", "postprocessor_hooks", "download_archive")
|
||||
REMOVE_KEYS: list = [
|
||||
{
|
||||
"paths": "-P, --paths",
|
||||
"outtmpl": "-o, --output",
|
||||
"progress_hooks": "--progress_hooks",
|
||||
"postprocessor_hooks": "--postprocessor_hooks",
|
||||
"post_hooks": "--post_hooks",
|
||||
"download_archive": "--download_archive",
|
||||
},
|
||||
{
|
||||
"quiet": "-q, --quiet",
|
||||
"no_warnings": "--no-warnings",
|
||||
"skip_download": "--skip-download",
|
||||
"forceprint": "-O, --print",
|
||||
"simulate": "--simulate",
|
||||
"noprogress": "--no-progress",
|
||||
"wait_for_video": "--wait-for-video",
|
||||
"mark_watched": "--mark-watched",
|
||||
"color": "--color",
|
||||
"verbose": "-v, --verbose",
|
||||
"debug_printtraffic": "--print-traffic",
|
||||
"write_pages": "--write-pages",
|
||||
"dump_intermediate_pages": "--dump-pages",
|
||||
"progress_delta": " --progress-delta",
|
||||
"progress_template": "--progress-template",
|
||||
"consoletitle": "--console-title",
|
||||
"progress_with_newline": "--newline",
|
||||
"forcejson": "-j, --dump-json",
|
||||
"print_to_file": "--print-to-file",
|
||||
"cookiesfrombrowser": "--cookies-from-browser",
|
||||
},
|
||||
{
|
||||
"cookiefile": "--cookies",
|
||||
},
|
||||
]
|
||||
|
||||
YTDLP_INFO_CLS: yt_dlp.YoutubeDL = None
|
||||
|
||||
ALLOWED_SUBS_EXTENSIONS: tuple[str] = (".srt", ".vtt", ".ass")
|
||||
|
|
@ -434,12 +469,20 @@ def validate_url(url: str) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
def arg_converter(args: str) -> dict:
|
||||
def arg_converter(
|
||||
args: str,
|
||||
level: int | bool | None = None,
|
||||
dumps: bool = False,
|
||||
removed_options: list | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Convert yt-dlp options to a dictionary.
|
||||
|
||||
Args:
|
||||
args (str): yt-dlp options string.
|
||||
level (int|bool|None): Level of options to remove, True for all.
|
||||
dumps (bool): Dump options as JSON.
|
||||
removed_options (list|None): List of removed options.
|
||||
|
||||
Returns:
|
||||
dict: yt-dlp options dictionary.
|
||||
|
|
@ -465,16 +508,44 @@ def arg_converter(args: str) -> dict:
|
|||
if "postprocessors" in diff:
|
||||
diff["postprocessors"] = [pp for pp in diff["postprocessors"] if pp not in default_opts["postprocessors"]]
|
||||
|
||||
if "match_filter" in diff:
|
||||
import inspect
|
||||
if "_warnings" in diff:
|
||||
diff.pop("_warnings", None)
|
||||
|
||||
matchFilter = inspect.getclosurevars(diff["match_filter"].func).nonlocals["filters"]
|
||||
if isinstance(matchFilter, set):
|
||||
diff["match_filter"] = {"filters": list(matchFilter)}
|
||||
if level is True or isinstance(level, int):
|
||||
bad_options = {}
|
||||
if isinstance(level, bool) or not isinstance(level, int):
|
||||
level = len(REMOVE_KEYS)
|
||||
|
||||
from .encoder import Encoder
|
||||
for i, item in enumerate(REMOVE_KEYS):
|
||||
if i > level:
|
||||
break
|
||||
|
||||
return json.loads(json.dumps(diff, cls=Encoder))
|
||||
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
|
||||
|
||||
if isinstance(removed_options, list):
|
||||
removed_options.append(bad_options[key])
|
||||
|
||||
diff.pop(key, None)
|
||||
|
||||
if dumps is True:
|
||||
from .encoder import Encoder
|
||||
|
||||
if "match_filter" in diff:
|
||||
import inspect
|
||||
|
||||
matchFilter = inspect.getclosurevars(diff["match_filter"].func).nonlocals["filters"]
|
||||
if isinstance(matchFilter, set):
|
||||
diff["match_filter"] = {"filters": list(matchFilter)}
|
||||
|
||||
return json.loads(json.dumps(diff, cls=Encoder))
|
||||
|
||||
return diff
|
||||
|
||||
|
||||
def validate_uuid(uuid_str: str, version: int = 4) -> bool:
|
||||
|
|
|
|||
|
|
@ -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 REMOVE_KEYS, arg_converter, calc_download_path, merge_dict
|
||||
|
||||
LOG = logging.getLogger("YTDLPOpts")
|
||||
|
||||
|
|
@ -48,11 +48,22 @@ class YTDLPOpts(metaclass=Singleton):
|
|||
YTDLPOpts: The instance of the class
|
||||
|
||||
"""
|
||||
bad_options = {}
|
||||
if from_user:
|
||||
bad_options = {k: v for d in REMOVE_KEYS for k, v in d.items()}
|
||||
|
||||
removed_options = []
|
||||
|
||||
for key, value in config.items():
|
||||
if key in IGNORED_KEYS and from_user:
|
||||
if from_user and key in bad_options:
|
||||
removed_options.append(bad_options[key])
|
||||
continue
|
||||
|
||||
self._item_opts[key] = value
|
||||
|
||||
if len(removed_options) > 0:
|
||||
LOG.warning("Removed the following options: '%s'.", ", ".join(removed_options))
|
||||
|
||||
return self
|
||||
|
||||
def preset(self, name: str, with_cookies: bool = False) -> "YTDLPOpts":
|
||||
|
|
@ -71,6 +82,19 @@ class YTDLPOpts(metaclass=Singleton):
|
|||
if not preset or "default" == name:
|
||||
return self
|
||||
|
||||
if preset.cli:
|
||||
try:
|
||||
removed_options = []
|
||||
self._preset_opts = arg_converter(args=preset.cli, level=True, removed_options=removed_options)
|
||||
if len(removed_options) > 0:
|
||||
LOG.warning(
|
||||
"Removed the following options '%s' from preset '%s'.", ", ".join(removed_options), preset.name
|
||||
)
|
||||
|
||||
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 +118,24 @@ 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
|
||||
# @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:
|
||||
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:
|
||||
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
|
||||
|
||||
|
|
@ -132,6 +166,7 @@ class YTDLPOpts(metaclass=Singleton):
|
|||
if "format" in data and data["format"] in ["not_set", "default"]:
|
||||
data["format"] = None
|
||||
|
||||
# @Deprecated - To be removed in future versions, All the checks below this line are deprecated.
|
||||
if "daterange" in data and isinstance(data["daterange"], dict):
|
||||
from yt_dlp.utils import DateRange
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import logging
|
|||
from .config import Config
|
||||
from .DownloadQueue import DownloadQueue
|
||||
from .encoder import Encoder
|
||||
from .Utils import arg_converter
|
||||
|
||||
LOG = logging.getLogger("common")
|
||||
|
||||
|
|
@ -27,7 +28,16 @@ class Common:
|
|||
self.default_preset = config.default_preset
|
||||
|
||||
async def add(
|
||||
self, url: str, preset: str, folder: str, cookies: str, config: dict, template: str, extras: dict | None = None
|
||||
self,
|
||||
url: str,
|
||||
preset: str,
|
||||
folder: str,
|
||||
cookies: str,
|
||||
# @deprecated: config: dict,
|
||||
config: dict,
|
||||
template: str,
|
||||
extras: dict | None = None,
|
||||
cli: str = "",
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Add an item to the download queue.
|
||||
|
|
@ -40,6 +50,7 @@ class Common:
|
|||
config (dict): The yt-dlp config to be used for the download.
|
||||
template (str): The template to be used for the download.
|
||||
extras (dict): Extra data to be added to the download
|
||||
cli (str): The yt-dlp cli options to be used for the download.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: The status of the download.
|
||||
|
|
@ -56,6 +67,7 @@ class Common:
|
|||
cookies=cookies,
|
||||
config=config if isinstance(config, dict) else {},
|
||||
template=template,
|
||||
cli=cli,
|
||||
extras=extras,
|
||||
)
|
||||
|
||||
|
|
@ -94,6 +106,19 @@ class Common:
|
|||
msg = f"Failed to parse json yt-dlp config for '{url}'. {e!s}"
|
||||
raise ValueError(msg) from e
|
||||
|
||||
cli = item.get("cli")
|
||||
if cli and len(cli) > 2:
|
||||
try:
|
||||
removed_options = []
|
||||
arg_converter(args=cli, level=True, removed_options=removed_options)
|
||||
if len(removed_options) > 0:
|
||||
LOG.warning("Removed the following options '%s'.", ", ".join(removed_options))
|
||||
|
||||
config = {}
|
||||
except Exception as e:
|
||||
msg = f"Failed to parse yt-dlp cli options. {e!s}"
|
||||
raise ValueError(msg) from e
|
||||
|
||||
return {
|
||||
"url": url,
|
||||
"preset": preset,
|
||||
|
|
@ -101,5 +126,6 @@ class Common:
|
|||
"cookies": cookies,
|
||||
"config": config if isinstance(config, dict) else {},
|
||||
"template": template,
|
||||
"cli": cli if isinstance(cli, str) else "",
|
||||
"extras": extras if isinstance(extras, dict) else {},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import coloredlogs
|
|||
from dotenv import load_dotenv
|
||||
from yt_dlp.version import __version__ as YTDLP_VERSION
|
||||
|
||||
from .Utils import IGNORED_KEYS, load_file, merge_dict
|
||||
from .Utils import REMOVE_KEYS, arg_converter, load_file, merge_dict
|
||||
from .version import APP_VERSION
|
||||
|
||||
|
||||
|
|
@ -218,7 +218,7 @@ class Config:
|
|||
"instance_title",
|
||||
"sentry_dsn",
|
||||
"console_enabled",
|
||||
"browser_enabled"
|
||||
"browser_enabled",
|
||||
)
|
||||
"The variables that are relevant to the frontend."
|
||||
|
||||
|
|
@ -321,8 +321,38 @@ class Config:
|
|||
except Exception as e:
|
||||
LOG.error(f"Error starting debugpy server at '0.0.0.0:{self.debugpy_port}'. {e}")
|
||||
|
||||
ytdlp_cli: str = os.path.join(self.config_path, "ytdlp.cli")
|
||||
optsFile: str = os.path.join(self.config_path, "ytdlp.json")
|
||||
if os.path.exists(optsFile) and os.path.getsize(optsFile) > 5:
|
||||
if os.path.exists(ytdlp_cli) and os.path.getsize(ytdlp_cli) > 2:
|
||||
LOG.info(f"Loading yt-dlp custom options from '{ytdlp_cli}'.")
|
||||
with open(ytdlp_cli) as f:
|
||||
ytdlp_cli_opts = f.read().strip()
|
||||
if ytdlp_cli_opts:
|
||||
try:
|
||||
removed_options = []
|
||||
self.ytdl_options = arg_converter(
|
||||
args=ytdlp_cli_opts,
|
||||
level=1,
|
||||
removed_options=removed_options,
|
||||
)
|
||||
|
||||
try:
|
||||
LOG.debug("Parsed yt-dlp cli options '%s'.", self.ytdl_options)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(removed_options) > 0:
|
||||
LOG.warning(
|
||||
"Removed the following options: '%s' from '%s'", ", ".join(removed_options), ytdlp_cli
|
||||
)
|
||||
except Exception as e:
|
||||
msg = f"Failed to parse yt-dlp cli options from '{ytdlp_cli}'. '{e!s}'."
|
||||
raise ValueError(msg) from e
|
||||
else:
|
||||
LOG.warning(f"Empty yt-dlp custom options file '{ytdlp_cli}'.")
|
||||
# @Deprecated - To be removed in future versions.
|
||||
elif os.path.exists(optsFile) and os.path.getsize(optsFile) > 5:
|
||||
LOG.warning("The JSON ytdlp.json options file is deprecated, please use 'ytdlp.cli' file instead.")
|
||||
LOG.info(f"Loading yt-dlp custom options from '{optsFile}'.")
|
||||
|
||||
(opts, status, error) = load_file(optsFile, dict)
|
||||
|
|
@ -330,16 +360,24 @@ class Config:
|
|||
LOG.error(f"Could not load yt-dlp custom options from '{optsFile}'. {error}")
|
||||
sys.exit(1)
|
||||
if isinstance(opts, dict):
|
||||
for key in IGNORED_KEYS:
|
||||
if key in opts:
|
||||
LOG.error(f"Key '{key}' is not allowed to be loaded via 'ytdlp.json' file.")
|
||||
del opts[key]
|
||||
bad_options = {k: v for d in REMOVE_KEYS for k, v in d.items()}
|
||||
removed_options = []
|
||||
for key in opts.copy():
|
||||
if key not in bad_options:
|
||||
continue
|
||||
|
||||
removed_options.append(bad_options[key])
|
||||
opts.pop(key, None)
|
||||
|
||||
if len(removed_options) > 0:
|
||||
LOG.warning(
|
||||
"Removed the following options: '%s' from 'ytdlp.json' file.", ", ".join(removed_options)
|
||||
)
|
||||
self.ytdl_options = merge_dict(self.ytdl_options, opts)
|
||||
else:
|
||||
LOG.error(f"Invalid yt-dlp custom options file '{optsFile}'.")
|
||||
else:
|
||||
LOG.info(f"No yt-dlp custom options found at '{optsFile}'.")
|
||||
LOG.info(f"No yt-dlp custom options found at '{ytdlp_cli}'.")
|
||||
|
||||
self.ytdl_options["socket_timeout"] = self.socket_timeout
|
||||
|
||||
|
|
@ -373,7 +411,6 @@ class Config:
|
|||
|
||||
key_file: str = os.path.join(self.config_path, "secret.key")
|
||||
|
||||
# save key as bytes.
|
||||
if os.path.exists(key_file) and os.path.getsize(key_file) > 5:
|
||||
with open(key_file, "rb") as f:
|
||||
self.secret_key = f.read().strip()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -141,17 +141,17 @@
|
|||
</td>
|
||||
<td class="is-vcentered is-items-center">
|
||||
<div class="field is-grouped is-grouped-centered">
|
||||
<div class="control" v-if="'finished' === item.status || isEmbedable(item.url)">
|
||||
<button v-if="'finished' === item.status" @click="playVideo(item)" v-tooltip="'Play video'"
|
||||
class="button is-danger is-light is-small">
|
||||
<div class="control" v-if="('finished' === item.status && item.filename) || isEmbedable(item.url)">
|
||||
<button v-if="'finished' === item.status && item.filename" @click="playVideo(item)"
|
||||
v-tooltip="'Play video'" class="button is-danger is-light is-small">
|
||||
<span class="icon"><i class="fa-solid fa-play" /></span>
|
||||
</button>
|
||||
<button v-else @click="embed_url = getEmbedable(item.url)" v-tooltip="'Play video'"
|
||||
class="button is-danger is-light is-small">
|
||||
class="button is-danger is-small">
|
||||
<span class="icon"><i class="fa-solid fa-play" /></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control" v-if="item.status != 'finished'">
|
||||
<div class="control" v-if="item.status != 'finished' || !item.filename">
|
||||
<button class="button is-warning is-fullwidth is-small" v-tooltip="'Re-queue video'"
|
||||
@click="reQueueItem(item)">
|
||||
<span class="icon"><i class="fa-solid fa-rotate-right" /></span>
|
||||
|
|
@ -203,7 +203,7 @@
|
|||
|
||||
<div class="card-header-icon">
|
||||
<span v-if="hideThumbnail">
|
||||
<a v-if="'finished' === item.status" href="#" @click.prevent="playVideo(item)"
|
||||
<a v-if="'finished' === item.status && item.filename" href="#" @click.prevent="playVideo(item)"
|
||||
v-tooltip="'Play video.'">
|
||||
<span class="icon"><i class="fa-solid fa-play" /></span>
|
||||
</a>
|
||||
|
|
@ -223,7 +223,7 @@
|
|||
</header>
|
||||
<div v-if="false === hideThumbnail" class="card-image">
|
||||
<figure class="image is-3by1">
|
||||
<span v-if="'finished' === item.status" @click="playVideo(item)" class="play-overlay">
|
||||
<span v-if="'finished' === item.status && item.filename" @click="playVideo(item)" class="play-overlay">
|
||||
<div class="play-icon"></div>
|
||||
<img @load="e => pImg(e)" :src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)"
|
||||
v-if="item.extras?.thumbnail" />
|
||||
|
|
@ -282,7 +282,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="columns is-mobile is-multiline">
|
||||
<div class="column is-half-mobile" v-if="item.status != 'finished'">
|
||||
<div class="column is-half-mobile" v-if="item.status != 'finished' || !item.filename">
|
||||
<a class="button is-warning is-fullwidth" @click="reQueueItem(item)">
|
||||
<span class="icon-text is-block">
|
||||
<span class="icon"><i class="fa-solid fa-rotate-right" /></span>
|
||||
|
|
@ -512,6 +512,9 @@ const clearIncomplete = () => {
|
|||
|
||||
const setIcon = item => {
|
||||
if ('finished' === item.status) {
|
||||
if (!item.filename) {
|
||||
return 'fa-solid fa-exclamation'
|
||||
}
|
||||
return item.is_live ? 'fa-solid fa-globe' : 'fa-solid fa-circle-check'
|
||||
}
|
||||
|
||||
|
|
@ -524,7 +527,7 @@ const setIcon = item => {
|
|||
}
|
||||
|
||||
if ('not_live' === item.status) {
|
||||
return 'fa-solid fa-hourglass-half fa-spin'
|
||||
return 'fa-solid fa-headset'
|
||||
}
|
||||
|
||||
return 'fa-solid fa-circle'
|
||||
|
|
@ -532,6 +535,9 @@ const setIcon = item => {
|
|||
|
||||
const setIconColor = item => {
|
||||
if ('finished' === item.status) {
|
||||
if (!item.filename) {
|
||||
return 'has-text-warning'
|
||||
}
|
||||
return 'has-text-success'
|
||||
}
|
||||
|
||||
|
|
@ -548,6 +554,9 @@ const setIconColor = item => {
|
|||
|
||||
const setStatus = item => {
|
||||
if ('finished' === item.status) {
|
||||
if (!item.filename) {
|
||||
return 'Skipped?'
|
||||
}
|
||||
return item.is_live ? 'Live Ended' : 'Completed'
|
||||
}
|
||||
|
||||
|
|
@ -617,9 +626,9 @@ const reQueueItem = item => {
|
|||
url: item.url,
|
||||
preset: item.preset,
|
||||
folder: item.folder,
|
||||
config: item.config,
|
||||
cookies: item.cookies,
|
||||
template: item.template,
|
||||
cli: item?.cli,
|
||||
extras: extras
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,17 @@
|
|||
<div class="select is-fullwidth">
|
||||
<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 config.' : ''">
|
||||
<option v-for="item in config.presets" :key="item.name" :value="item.name">
|
||||
{{ item.name }}
|
||||
</option>
|
||||
v-tooltip.bottom="hasFormatInConfig ? 'Presets are disabled. Format key is present in the Command arguments for yt-dlp.' : ''">
|
||||
<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>
|
||||
|
|
@ -59,16 +66,18 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="columns is-multiline is-mobile" v-if="showAdvanced && !config.app.basic_mode">
|
||||
<div class="column is-12">
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="output_format"
|
||||
v-tooltip="'Default Format: ' + config.app.output_template">
|
||||
Output Template
|
||||
</label>
|
||||
<div class="control">
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" v-model="output_template" id="output_format"
|
||||
:disabled="!socket.isConnected || addInProgress"
|
||||
placeholder="Uses default output template naming if empty.">
|
||||
<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>
|
||||
|
|
@ -77,32 +86,31 @@
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="ytdlpConfig"
|
||||
v-tooltip="'Extends current global yt-dlp config. (JSON)'">
|
||||
JSON yt-dlp config or CLI options.
|
||||
<NuxtLink v-if="ytdlpConfig && ytdlpConfig.trim() && !ytdlpConfig.trim().startsWith('{')"
|
||||
@click="convertOptions()">
|
||||
Convert to JSON
|
||||
</NuxtLink>
|
||||
<label class="label is-inline" for="cli_options">
|
||||
Command arguments for yt-dlp
|
||||
</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" id="ytdlpConfig" v-model="ytdlpConfig"
|
||||
:disabled="!socket.isConnected || addInProgress || convertInProgress"
|
||||
placeholder="--no-embed-metadata --no-embed-thumbnail"></textarea>
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" v-model="ytdlp_cli" id="cli_options"
|
||||
:disabled="!socket.isConnected || addInProgress"
|
||||
placeholder="command options to use, e.g. --no-embed-metadata --no-embed-thumbnail">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-terminal" /></span>
|
||||
</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. If <code>Format</code> key is present
|
||||
in the config, <span class="has-text-danger">the preset and all it's options will be
|
||||
ignored</span>.</span>
|
||||
<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>
|
||||
</div>
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="ytdlpCookies" v-tooltip="'Netscape HTTP Cookie format'">
|
||||
Cookies
|
||||
|
|
@ -152,7 +160,6 @@
|
|||
|
||||
<script setup>
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { request } from '~/utils/index'
|
||||
|
||||
const emitter = defineEmits(['getInfo'])
|
||||
const config = useConfigStore()
|
||||
|
|
@ -160,31 +167,25 @@ const socket = useSocketStore()
|
|||
const toast = useToast()
|
||||
|
||||
const selectedPreset = useStorage('selectedPreset', config.app.default_preset)
|
||||
const ytdlpConfig = useStorage('ytdlp_config', '')
|
||||
const ytdlpCookies = useStorage('ytdlp_cookies', '')
|
||||
const ytdlp_cli = useStorage('ytdlp_cli', '')
|
||||
const output_template = useStorage('output_template', null)
|
||||
const downloadPath = useStorage('downloadPath', null)
|
||||
const url = useStorage('downloadUrl', null)
|
||||
const showAdvanced = useStorage('show_advanced', false)
|
||||
const addInProgress = ref(false)
|
||||
const convertInProgress = ref(false)
|
||||
|
||||
const addDownload = async () => {
|
||||
// -- send request to convert cli options to JSON
|
||||
if (ytdlpConfig.value && !ytdlpConfig.value.trim().startsWith('{')) {
|
||||
await convertOptions()
|
||||
}
|
||||
|
||||
if (ytdlpConfig.value) {
|
||||
try {
|
||||
JSON.parse(ytdlpConfig.value)
|
||||
} catch (e) {
|
||||
toast.error(`Invalid JSON yt-dlp config. ${e.message}`)
|
||||
if (ytdlp_cli.value && '' !== ytdlp_cli.value) {
|
||||
const options = await convertOptions(ytdlp_cli.value)
|
||||
if (null === options) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
addInProgress.value = true
|
||||
|
||||
url.value.split(',').forEach(url => {
|
||||
if (!url.trim()) {
|
||||
return
|
||||
|
|
@ -193,9 +194,9 @@ const addDownload = async () => {
|
|||
url: url,
|
||||
preset: config.app.basic_mode ? config.app.default_preset : selectedPreset.value,
|
||||
folder: config.app.basic_mode ? null : downloadPath.value,
|
||||
config: config.app.basic_mode ? '' : ytdlpConfig.value,
|
||||
cookies: config.app.basic_mode ? '' : ytdlpCookies.value,
|
||||
template: config.app.basic_mode ? null : output_template.value,
|
||||
cli: config.app.basic_mode ? null : ytdlp_cli.value,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -206,7 +207,6 @@ const resetConfig = () => {
|
|||
}
|
||||
|
||||
selectedPreset.value = config.app.default_preset.value
|
||||
ytdlpConfig.value = ''
|
||||
ytdlpCookies.value = ''
|
||||
output_template.value = null
|
||||
url.value = null
|
||||
|
|
@ -239,27 +239,24 @@ const unlockDownload = async stream => {
|
|||
}
|
||||
}
|
||||
|
||||
const convertOptions = async () => {
|
||||
if (convertInProgress.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const convertOptions = async args => {
|
||||
try {
|
||||
convertInProgress.value = true
|
||||
const response = await convertCliOptions(ytdlpConfig.value)
|
||||
ytdlpConfig.value = JSON.stringify(response.opts, null, 2)
|
||||
const response = await convertCliOptions(args)
|
||||
|
||||
if (response.output_template) {
|
||||
output_template.value = response.output_template
|
||||
}
|
||||
|
||||
if (response.download_path) {
|
||||
downloadPath.value = response.download_path
|
||||
}
|
||||
|
||||
return response.opts
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
} finally {
|
||||
convertInProgress.value = false
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
|
@ -276,14 +273,13 @@ onUnmounted(() => {
|
|||
})
|
||||
|
||||
const hasFormatInConfig = computed(() => {
|
||||
if (!ytdlpConfig.value) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const config = JSON.parse(ytdlpConfig.value)
|
||||
return "format" in config
|
||||
} catch (e) {
|
||||
if (!ytdlp_cli.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
return /(?<!\w)(-f|--format)(=|:)?(?!\w)/.test(ytdlp_cli.value)
|
||||
})
|
||||
|
||||
const filter_presets = (flag = true) => config.presets.filter(item => item.default === flag)
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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,16 +439,12 @@ 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) {
|
||||
form.template = item.output_template
|
||||
if (item.template) {
|
||||
form.template = item.template
|
||||
}
|
||||
|
||||
if (item.folder) {
|
||||
|
|
@ -465,4 +458,5 @@ const importItem = async () => {
|
|||
toast.error(`Failed to string. ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="name" v-text="'Name'" />
|
||||
|
|
@ -81,25 +80,32 @@
|
|||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="folder">
|
||||
Preset
|
||||
</label>
|
||||
<label class="label is-inline" for="preset">Preset</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<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 config.' : ''">
|
||||
<option v-for="item in config.presets" :key="item.name" :value="item.name">
|
||||
{{ item.name }}
|
||||
</option>
|
||||
v-tooltip.bottom="hasFormatInConfig ? 'Presets are disabled. Format key is present in the command arguments for yt-dlp.' : ''">
|
||||
<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>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Select the preset to use for this URL. The preset will be ignored if format key is present in
|
||||
config.</span>
|
||||
<span>Select the preset to use for this URL. <span class="text-has-danger">If the
|
||||
<code>-f, --format</code> argument is present in the command line options, the preset and all
|
||||
it's options will be ignored.</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -126,7 +132,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="folder">
|
||||
Download path
|
||||
|
|
@ -138,24 +144,25 @@
|
|||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Downloads are relative to download path, defaults to root path if not set.</span>
|
||||
<span>Paths are relative to global download path, defaults to preset download path if set otherwise,
|
||||
fallback root path if not set.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="output_template">
|
||||
Output template
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" id="output_template" placeholder="The output template to use"
|
||||
v-model="form.template" :disabled="addInProgress">
|
||||
<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>The output template to use, if not set, it will defaults to
|
||||
<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>.
|
||||
|
|
@ -164,45 +171,25 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="config"
|
||||
v-tooltip="'Extends current global yt-dlp config. (JSON)'">
|
||||
JSON yt-dlp config or CLI options. <NuxtLink
|
||||
v-if="form.config && (typeof form.config === 'string') && !form.config.trim().startsWith('{')"
|
||||
@click="convertOptions()">Convert to JSON</NuxtLink>
|
||||
<label class="label is-inline" for="cli_options">
|
||||
Command arguments for yt-dlp
|
||||
</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" id="config" v-model="form.config" :disabled="addInProgress"
|
||||
placeholder="--no-embed-metadata --no-embed-thumbnail"></textarea>
|
||||
<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>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>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="cookies"
|
||||
v-tooltip="'Netscape HTTP Cookie format.'">Cookies</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea is-pre" id="cookies" v-model="form.cookies" :disabled="addInProgress" />
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Use the <NuxtLink target="_blank"
|
||||
to="https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp">
|
||||
Recommended addon</NuxtLink> by yt-dlp to export cookies. The cookies MUST be in Netscape HTTP
|
||||
Cookie format.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -221,12 +208,6 @@
|
|||
<span>Cancel</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="card-footer-item">
|
||||
<button class="button is-fullwidth is-info" type="button" @click="showAdvanced = !showAdvanced">
|
||||
<span class="icon"><i class="fa-solid fa-cog" /></span>
|
||||
<span>Advanced</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -244,8 +225,6 @@ import { CronExpressionParser } from 'cron-parser'
|
|||
const emitter = defineEmits(['cancel', 'submit']);
|
||||
const toast = useToast();
|
||||
const config = useConfigStore();
|
||||
const convertInProgress = ref(false);
|
||||
const showAdvanced = useStorage('task.showAdvanced', false);
|
||||
const showImport = useStorage('showImport', false);
|
||||
const import_string = ref('');
|
||||
|
||||
|
|
@ -269,10 +248,6 @@ const props = defineProps({
|
|||
const form = reactive(props.task);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.task?.config && (typeof props.task.config === 'object')) {
|
||||
form.config = JSON.stringify(props.task.config, null, 4);
|
||||
}
|
||||
|
||||
if (!props.task?.preset || '' === props.task.preset) {
|
||||
form.preset = toRaw(config.app.default_preset);
|
||||
}
|
||||
|
|
@ -304,60 +279,17 @@ const checkInfo = async () => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (typeof form.config === 'object') {
|
||||
form.config = JSON.stringify(form.config, null, 4);
|
||||
}
|
||||
|
||||
if (form.config && form.config && !form.config.trim().startsWith('{')) {
|
||||
await convertOptions();
|
||||
}
|
||||
|
||||
if (form.config) {
|
||||
try {
|
||||
form.config = JSON.parse(form.config)
|
||||
} catch (e) {
|
||||
toast.error(`Invalid JSON yt-dlp config. ${e.message}`)
|
||||
return;
|
||||
if (form?.cli && '' !== form.cli) {
|
||||
const options = await convertOptions(form.cli);
|
||||
if (null === options) {
|
||||
return
|
||||
}
|
||||
form.cli = form.cli.trim(" ")
|
||||
}
|
||||
|
||||
emitter('submit', { reference: toRaw(props.reference), task: toRaw(form) });
|
||||
}
|
||||
|
||||
const convertOptions = async () => {
|
||||
if (convertInProgress.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
convertInProgress.value = true
|
||||
const response = await convertCliOptions(form.config)
|
||||
form.config = JSON.stringify(response.opts, null, 2)
|
||||
if (response.output_template) {
|
||||
form.template = response.output_template
|
||||
}
|
||||
if (response.download_path) {
|
||||
form.folder = response.download_path
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
} finally {
|
||||
convertInProgress.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const hasFormatInConfig = computed(() => {
|
||||
if (!form.config) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const config = JSON.parse(form.config)
|
||||
return "format" in config
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const importItem = async () => {
|
||||
let val = import_string.value.trim()
|
||||
if (!val) {
|
||||
|
|
@ -384,7 +316,7 @@ const importItem = async () => {
|
|||
return
|
||||
}
|
||||
|
||||
if (form.config || form.url || form.timer) {
|
||||
if (form.url || form.timer) {
|
||||
if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
|
||||
return
|
||||
}
|
||||
|
|
@ -398,8 +330,8 @@ const importItem = async () => {
|
|||
form.url = item.url
|
||||
}
|
||||
|
||||
if (item.preset) {
|
||||
form.preset = item.preset
|
||||
if (item.template) {
|
||||
form.template = item.template
|
||||
}
|
||||
|
||||
if (item.timer) {
|
||||
|
|
@ -410,12 +342,19 @@ const importItem = async () => {
|
|||
form.folder = item.folder
|
||||
}
|
||||
|
||||
if (item.template) {
|
||||
form.template = item.template
|
||||
if (item.cli) {
|
||||
form.cli = item.cli
|
||||
}
|
||||
|
||||
if (item.config) {
|
||||
form.config = JSON.stringify(item.config, null, 2)
|
||||
if (item.preset) {
|
||||
// -- check if the preset exists in config.presets
|
||||
const preset = config.presets.find(p => p.name === item.preset)
|
||||
if (!preset) {
|
||||
toast.warning(`Preset '${item.preset}' not found. Preset will be set to default.`)
|
||||
form.preset = 'default'
|
||||
} else {
|
||||
form.preset = item.preset
|
||||
}
|
||||
}
|
||||
|
||||
import_string.value = ''
|
||||
|
|
@ -424,4 +363,35 @@ const importItem = async () => {
|
|||
toast.error(`Failed to import string. ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const convertOptions = async args => {
|
||||
try {
|
||||
const response = await convertCliOptions(args)
|
||||
|
||||
if (response.output_template) {
|
||||
form.template = response.output_template
|
||||
}
|
||||
|
||||
if (response.download_path) {
|
||||
form.folder = response.download_path
|
||||
}
|
||||
|
||||
return response.opts
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasFormatInConfig = computed(() => {
|
||||
if (!form?.cli) {
|
||||
return false
|
||||
}
|
||||
|
||||
return /(?<!\w)(-f|--format)(=|:)?(?!\w)/.test(form.cli)
|
||||
})
|
||||
|
||||
const filter_presets = (flag = true) => config.presets.filter(item => item.default === flag)
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -71,7 +71,17 @@ div.is-centered {
|
|||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
<p class="is-text-overflow">
|
||||
<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>
|
||||
<span v-text="item.format" />
|
||||
</p>
|
||||
|
|
@ -83,6 +93,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 +307,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 +322,7 @@ const exportItem = item => {
|
|||
}
|
||||
|
||||
userData['_type'] = 'preset'
|
||||
userData['_version'] = '1.0'
|
||||
userData['_version'] = '2.0'
|
||||
|
||||
return copyText(base64UrlEncode(JSON.stringify(userData)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,8 +39,7 @@ div.is-centered {
|
|||
</div>
|
||||
<div class="is-hidden-mobile">
|
||||
<span class="subtitle">
|
||||
The task runner is simple queue system that allows you to schedule downloads. The tasks are run at the
|
||||
scheduled time.
|
||||
The task runner is simple queue system that allows you to schedule downloads to run at the specific time.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -62,10 +61,6 @@ div.is-centered {
|
|||
<a class="has-text-primary" v-tooltip="'Export task.'" @click.prevent="exportItem(item)">
|
||||
<span class="icon"><i class="fa-solid fa-file-export" /></span>
|
||||
</a>
|
||||
<button @click="item.raw = !item.raw">
|
||||
<span class="icon"><i class="fa-solid"
|
||||
:class="{ 'fa-arrow-down': !item?.raw, 'fa-arrow-up': item?.raw }" /></span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
|
|
@ -86,11 +81,10 @@ div.is-centered {
|
|||
<span class="icon"><i class="fa-solid fa-tv" /></span>
|
||||
<span>{{ item.preset ?? config.app.default_preset }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content" v-if="item?.raw">
|
||||
<div class="content">
|
||||
<pre><code>{{ filterItem(item) }}</code></pre>
|
||||
<p class="is-text-overflow">
|
||||
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||
<span>{{ item.cli }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
|
|
@ -120,17 +114,6 @@ div.is-centered {
|
|||
icon="fas fa-exclamation-circle" v-if="!tasks || tasks.length < 1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12" v-if="tasks && tasks.length > 0 && !toggleForm">
|
||||
<Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle">
|
||||
<ul>
|
||||
<li>
|
||||
When you export task, the preset settings is automatically merged into the task and the preset set to
|
||||
<code>default</code> to be more portable. The exporter doesn't include <code>Cookies</code> field for
|
||||
security reasons.
|
||||
</li>
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -202,6 +185,8 @@ const resetForm = (closeForm = false) => {
|
|||
}
|
||||
|
||||
const updateTasks = async items => {
|
||||
let data = {}
|
||||
|
||||
try {
|
||||
addInProgress.value = true
|
||||
|
||||
|
|
@ -213,7 +198,7 @@ const updateTasks = async items => {
|
|||
body: JSON.stringify(items),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
data = await response.json()
|
||||
|
||||
if (200 !== response.status) {
|
||||
toast.error(`Failed to update task. ${data.error}`);
|
||||
|
|
@ -224,7 +209,7 @@ const updateTasks = async items => {
|
|||
resetForm(true)
|
||||
return true
|
||||
} catch (e) {
|
||||
toast.error(`Failed to update task. ${data.error}`);
|
||||
toast.error(`Failed to update task. ${data?.error ?? e.message}`);
|
||||
} finally {
|
||||
addInProgress.value = false
|
||||
}
|
||||
|
|
@ -272,11 +257,6 @@ const updateItem = async ({ reference, task }) => {
|
|||
resetForm(true)
|
||||
}
|
||||
|
||||
const filterItem = item => {
|
||||
const { raw, ...rest } = item
|
||||
return JSON.stringify(rest, null, 2)
|
||||
}
|
||||
|
||||
const editItem = item => {
|
||||
task.value = item
|
||||
taskRef.value = item.id
|
||||
|
|
@ -327,21 +307,8 @@ const runNow = item => {
|
|||
data.template = item.template
|
||||
}
|
||||
|
||||
if (item.cookies) {
|
||||
data.cookies = item.cookies
|
||||
}
|
||||
|
||||
if (item.config) {
|
||||
if (typeof item.config === 'object') {
|
||||
data.config = JSON.stringify(item.config)
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(data.config)
|
||||
} catch (e) {
|
||||
toast.error(`Invalid JSON yt-dlp config. ${e.message}`)
|
||||
return
|
||||
}
|
||||
if (item.cli) {
|
||||
data.cli = item.cli
|
||||
}
|
||||
|
||||
socket.emit('add_url', data)
|
||||
|
|
@ -359,69 +326,26 @@ const statusHandler = async stream => {
|
|||
}
|
||||
|
||||
const exportItem = async item => {
|
||||
let preset = config.presets.find(p => p.name === item.preset)
|
||||
if (!preset) {
|
||||
toast.error('Preset not found.')
|
||||
return
|
||||
}
|
||||
|
||||
const info = JSON.parse(JSON.stringify(item))
|
||||
preset = JSON.parse(JSON.stringify(preset))
|
||||
|
||||
let data = {
|
||||
name: info.name,
|
||||
url: info.url,
|
||||
preset: 'default',
|
||||
preset: info.preset,
|
||||
timer: info.timer,
|
||||
folder: info.folder,
|
||||
template: info.template,
|
||||
config: info.config,
|
||||
}
|
||||
|
||||
// -- merge preset options with task args.
|
||||
let args = {}
|
||||
|
||||
if (preset.args && Object.keys(preset.args).length > 0) {
|
||||
for (const key of Object.keys(preset.args)) {
|
||||
args[key] = preset.args[key]
|
||||
}
|
||||
}
|
||||
const defaults = ['default', 'not_set']
|
||||
if (preset.format && !defaults.includes(preset.format)) {
|
||||
args.format = preset.format
|
||||
if (info.template) {
|
||||
data.template = info.template
|
||||
}
|
||||
|
||||
if (preset.postprocessors && preset.postprocessors.length > 0) {
|
||||
args.postprocessors = preset.postprocessors
|
||||
}
|
||||
|
||||
if (preset.folder && !info.folder) {
|
||||
data.folder = preset.folder
|
||||
}
|
||||
|
||||
if (preset.template && !info.template) {
|
||||
data.template = preset.template
|
||||
}
|
||||
|
||||
if (!data.config || Object.keys(data.config).length < 1) {
|
||||
data.config = {}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(args)) {
|
||||
if (key in data.config && Array.isArray(args[key]) && Array.isArray(data.config[key])) {
|
||||
data.config[key] = data.config[key].concat(args[key])
|
||||
continue
|
||||
}
|
||||
|
||||
if (data?.config[key]) {
|
||||
continue
|
||||
}
|
||||
|
||||
data.config[key] = args[key]
|
||||
if (info.cli) {
|
||||
data.cli = info.cli
|
||||
}
|
||||
|
||||
data['_type'] = 'task'
|
||||
data['_version'] = '1.1'
|
||||
data['_version'] = '2.0'
|
||||
|
||||
return copyText(base64UrlEncode(JSON.stringify(data)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,12 @@ const CONFIG_KEYS = {
|
|||
presets: [
|
||||
{
|
||||
'name': 'default',
|
||||
'format': 'default'
|
||||
'format': 'default',
|
||||
'folder': '',
|
||||
'template': '',
|
||||
'cookies': '',
|
||||
'cli': '',
|
||||
'default': true
|
||||
}
|
||||
],
|
||||
folders: [],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue