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",
|
"attl",
|
||||||
"autonumber",
|
"autonumber",
|
||||||
"changeslog",
|
"changeslog",
|
||||||
|
"consoletitle",
|
||||||
|
"cookiesfrombrowser",
|
||||||
"copyts",
|
"copyts",
|
||||||
"daterange",
|
"daterange",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"finaldir",
|
"finaldir",
|
||||||
"flac",
|
"flac",
|
||||||
|
"forcejson",
|
||||||
|
"forceprint",
|
||||||
|
"fribidi",
|
||||||
"getpid",
|
"getpid",
|
||||||
"gpac",
|
"gpac",
|
||||||
"httpx",
|
"httpx",
|
||||||
|
|
@ -41,6 +46,7 @@
|
||||||
"postprocessor",
|
"postprocessor",
|
||||||
"preferredcodec",
|
"preferredcodec",
|
||||||
"preferredquality",
|
"preferredquality",
|
||||||
|
"printtraffic",
|
||||||
"quicktime",
|
"quicktime",
|
||||||
"tmpfilename",
|
"tmpfilename",
|
||||||
"urandom",
|
"urandom",
|
||||||
|
|
@ -56,7 +62,6 @@
|
||||||
"en"
|
"en"
|
||||||
],
|
],
|
||||||
"spellright.documentTypes": [
|
"spellright.documentTypes": [
|
||||||
"latex",
|
"latex"
|
||||||
"plaintext"
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ ENV XDG_CONFIG_HOME=/config
|
||||||
ENV XDG_CACHE_HOME=/tmp
|
ENV XDG_CACHE_HOME=/tmp
|
||||||
|
|
||||||
RUN mkdir /config /downloads && ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone && \
|
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 && \
|
useradd -u ${USER_ID:-1000} -U -d /app -s /bin/bash app && \
|
||||||
rm -rf /var/cache/apk/*
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
|
|
||||||
75
README.md
75
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.
|
* Can Handle live streams.
|
||||||
* Scheduler to queue channels or playlists to be downloaded automatically at a specified time.
|
* Scheduler to queue channels or playlists to be downloaded automatically at a specified time.
|
||||||
* Send notification to targets based on selected events.
|
* 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.
|
* Queue multiple URLs separated by comma.
|
||||||
* Simple file browser. `Disabled by default`
|
* 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**.
|
* A built in video player that can play any video file regardless of the format. **With support for sidecar external subtitles**.
|
||||||
|
|
@ -26,21 +26,46 @@ Web GUI for [yt-dlp](https://github.com/yt-dlp/yt-dlp) with playlist & channel s
|
||||||
* Support for both advanced and basic mode for WebUI.
|
* Support for both advanced and basic mode for WebUI.
|
||||||
* Bundled tools in container: curl-cffi, ffmpeg, ffprobe, aria2, rtmpdump, mkvtoolsnix, mp4box.
|
* Bundled tools in container: curl-cffi, ffmpeg, ffprobe, aria2, rtmpdump, mkvtoolsnix, mp4box.
|
||||||
|
|
||||||
# Recommended basic `ytdlp.json` file settings
|
# Breaking changes
|
||||||
|
|
||||||
Your `/config/ytdlp.json` config should include the following basic options for optimal working conditions.
|
Starting with versions tagged `*-20250330-*` I have deprecated the JSON yt-dlp config and all of it's related settings.
|
||||||
|
|
||||||
```json
|
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
|
||||||
"windowsfilenames": true,
|
directly in the WebUI, presets, and tasks.
|
||||||
"continue_dl": true,
|
|
||||||
"live_from_start": true,
|
|
||||||
"format_sort": [ "codec:avc:m4a" ],
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!NOTE]
|
## `ytdlp.json` vs `ytdlp.cli`
|
||||||
> 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.
|
|
||||||
|
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
|
# 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
|
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
|
We strongly recommend not use this file for options that aren't **truly global**, everything that can be done via the file
|
||||||
can be done via a preset, which only effects the download that uses it. Example of good basic `ytdlp.json` 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
|
```bash
|
||||||
{
|
--continue --windows-filenames --live-from-start
|
||||||
"windowsfilenames": true,
|
|
||||||
"continue_dl": true,
|
|
||||||
"live_from_start": true,
|
|
||||||
"format_sort": [ "codec:avc:m4a" ],
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Everything else can be done via the presets, and it's more flexible and easier to manage. You can convert your
|
Everything else can be done via the presets, and it's more flexible and easier to manage.
|
||||||
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).
|
|
||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
|
|
||||||
|
|
@ -275,7 +294,7 @@ What does the basic mode do? it hides the the following features from the WebUI.
|
||||||
|
|
||||||
### Header
|
### Header
|
||||||
|
|
||||||
It disables everything except the `theme switcher` and `reload` button.
|
It disables everything except the `settings button` and `reload` button.
|
||||||
|
|
||||||
### Add form
|
### Add form
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,12 @@ class Download:
|
||||||
|
|
||||||
self.status_queue.put({"id": self.id, "status": "finished", "filename": filename})
|
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):
|
def _download(self):
|
||||||
try:
|
try:
|
||||||
params = (
|
params = (
|
||||||
|
|
@ -178,6 +184,7 @@ class Download:
|
||||||
{
|
{
|
||||||
"progress_hooks": [self._progress_hook],
|
"progress_hooks": [self._progress_hook],
|
||||||
"postprocessor_hooks": [self._postprocessor_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.'
|
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)
|
params["logger"] = NestedLogger(self.logger)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ from .Events import EventBus, Events
|
||||||
from .ItemDTO import ItemDTO
|
from .ItemDTO import ItemDTO
|
||||||
from .Presets import Presets
|
from .Presets import Presets
|
||||||
from .Singleton import Singleton
|
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
|
from .YTDLPOpts import YTDLPOpts
|
||||||
|
|
||||||
LOG = logging.getLogger("DownloadQueue")
|
LOG = logging.getLogger("DownloadQueue")
|
||||||
|
|
@ -177,6 +177,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
cookies: str = "",
|
cookies: str = "",
|
||||||
template: str = "",
|
template: str = "",
|
||||||
extras: dict | None = None,
|
extras: dict | None = None,
|
||||||
|
cli: str = "",
|
||||||
already=None,
|
already=None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
|
@ -190,9 +191,9 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
cookies (str): The cookies to use for the download.
|
cookies (str): The cookies to use for the download.
|
||||||
template (str): The output template 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.
|
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.
|
already (set): The set of already downloaded items.
|
||||||
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: The status of the operation.
|
dict: The status of the operation.
|
||||||
|
|
||||||
|
|
@ -244,6 +245,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
cookies=cookies,
|
cookies=cookies,
|
||||||
template=template,
|
template=template,
|
||||||
extras=extras,
|
extras=extras,
|
||||||
|
cli=cli,
|
||||||
already=already,
|
already=already,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -320,6 +322,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
is_live=is_live,
|
is_live=is_live,
|
||||||
live_in=live_in,
|
live_in=live_in,
|
||||||
options=options,
|
options=options,
|
||||||
|
cli=cli,
|
||||||
extras=extras,
|
extras=extras,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -352,6 +355,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
cookies=cookies,
|
cookies=cookies,
|
||||||
template=template,
|
template=template,
|
||||||
extras=extras,
|
extras=extras,
|
||||||
|
cli=cli,
|
||||||
already=already,
|
already=already,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -365,12 +369,39 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
config: dict | None = None,
|
config: dict | None = None,
|
||||||
cookies: str = "",
|
cookies: str = "",
|
||||||
template: str = "",
|
template: str = "",
|
||||||
|
cli: str = "",
|
||||||
extras: dict | None = None,
|
extras: dict | None = None,
|
||||||
already=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)
|
_preset = Presets.get_instance().get(name=preset)
|
||||||
|
|
||||||
config = config if config else {}
|
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 ""
|
folder = str(folder) if folder else ""
|
||||||
if not extras:
|
if not extras:
|
||||||
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")
|
cookie_file = os.path.join(self.config.temp_path, f"c_{uuid.uuid4().hex}.txt")
|
||||||
|
|
||||||
LOG.info(
|
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):
|
if isinstance(config, str):
|
||||||
|
|
@ -486,6 +517,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
template=template,
|
template=template,
|
||||||
already=already,
|
already=already,
|
||||||
extras=extras,
|
extras=extras,
|
||||||
|
cli=cli,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def cancel(self, ids: list[str]) -> dict[str, str]:
|
async def cancel(self, ids: list[str]) -> dict[str, str]:
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,6 @@ class Events:
|
||||||
LOG_SUCCESS = "log_success"
|
LOG_SUCCESS = "log_success"
|
||||||
|
|
||||||
INITIAL_DATA = "initial_data"
|
INITIAL_DATA = "initial_data"
|
||||||
YTDLP_CONVERT = "ytdlp_convert"
|
|
||||||
ITEM_DELETE = "item_delete"
|
ITEM_DELETE = "item_delete"
|
||||||
ITEM_CANCEL = "item_cancel"
|
ITEM_CANCEL = "item_cancel"
|
||||||
STATUS = "status"
|
STATUS = "status"
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ from .Segments import Segments
|
||||||
from .Subtitle import Subtitle
|
from .Subtitle import Subtitle
|
||||||
from .Tasks import Task, Tasks
|
from .Tasks import Task, Tasks
|
||||||
from .Utils import (
|
from .Utils import (
|
||||||
IGNORED_KEYS,
|
REMOVE_KEYS,
|
||||||
StreamingError,
|
StreamingError,
|
||||||
arg_converter,
|
arg_converter,
|
||||||
decrypt_data,
|
decrypt_data,
|
||||||
|
|
@ -445,7 +445,7 @@ class HttpAPI(Common):
|
||||||
try:
|
try:
|
||||||
response = {"opts": {}, "output_template": None, "download_path": None}
|
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"]:
|
if "outtmpl" in data and "default" in data["outtmpl"]:
|
||||||
response["output_template"] = data["outtmpl"]["default"]
|
response["output_template"] = data["outtmpl"]["default"]
|
||||||
|
|
@ -453,20 +453,29 @@ class HttpAPI(Common):
|
||||||
if "paths" in data and "home" in data["paths"]:
|
if "paths" in data and "home" in data["paths"]:
|
||||||
response["download_path"] = data["paths"]["home"]
|
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:
|
for key in data:
|
||||||
if key in IGNORED_KEYS:
|
if key in bad_options.items():
|
||||||
|
removed_options.append(bad_options[key])
|
||||||
continue
|
continue
|
||||||
if not key.startswith("_"):
|
if not key.startswith("_"):
|
||||||
response["opts"][key] = data[key]
|
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)
|
return web.json_response(data=response, status=web.HTTPOk.status_code)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err = str(e).strip()
|
err = str(e).strip()
|
||||||
err = err.split("\n")[-1] if "\n" in err else err
|
err = err.split("\n")[-1] if "\n" in err else err
|
||||||
LOG.error(f"Failed to convert args. '{err}'.")
|
err = err.replace("main.py: error: ", "").strip().capitalize()
|
||||||
LOG.exception(e)
|
|
||||||
return web.json_response(
|
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")
|
@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() == "":
|
if not item.get("timer", None) or str(item.get("timer")).strip() == "":
|
||||||
item["timer"] = f"{random.randint(1,59)} */1 * * *" # noqa: S311
|
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):
|
if not item.get("template", None):
|
||||||
item["template"] = ""
|
item["template"] = ""
|
||||||
|
|
||||||
|
if not item.get("cli", None):
|
||||||
|
item["cli"] = ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ins.validate(item)
|
ins.validate(item)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ from .DownloadQueue import DownloadQueue
|
||||||
from .encoder import Encoder
|
from .encoder import Encoder
|
||||||
from .Events import Event, EventBus, Events, error
|
from .Events import Event, EventBus, Events, error
|
||||||
from .Presets import Presets
|
from .Presets import Presets
|
||||||
from .Utils import arg_converter, is_downloaded
|
from .Utils import is_downloaded
|
||||||
|
|
||||||
LOG = logging.getLogger("socket_api")
|
LOG = logging.getLogger("socket_api")
|
||||||
|
|
||||||
|
|
@ -285,25 +285,3 @@ class HttpSocket(Common):
|
||||||
async def resume(self, *_, **__):
|
async def resume(self, *_, **__):
|
||||||
self.queue.resume()
|
self.queue.resume()
|
||||||
await self._notify.emit(Events.PAUSED, data={"paused": False, "at": time.time()})
|
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
|
temp_dir: str | None = None
|
||||||
status: str | None = None
|
status: str | None = None
|
||||||
cookies: str | None = None
|
cookies: str | None = None
|
||||||
config: dict = field(default_factory=dict)
|
|
||||||
template: str | None = None
|
template: str | None = None
|
||||||
template_chapter: str | None = None
|
template_chapter: str | None = None
|
||||||
timestamp: float = field(default_factory=lambda: time.time_ns())
|
timestamp: float = field(default_factory=lambda: time.time_ns())
|
||||||
|
|
@ -37,6 +36,7 @@ class ItemDTO:
|
||||||
file_size: int | None = None
|
file_size: int | None = None
|
||||||
options: dict = field(default_factory=dict)
|
options: dict = field(default_factory=dict)
|
||||||
extras: dict = field(default_factory=dict)
|
extras: dict = field(default_factory=dict)
|
||||||
|
cli: str = ""
|
||||||
|
|
||||||
# yt-dlp injected fields.
|
# yt-dlp injected fields.
|
||||||
tmpfilename: str | None = None
|
tmpfilename: str | None = None
|
||||||
|
|
@ -49,12 +49,13 @@ class ItemDTO:
|
||||||
speed: str | None = None
|
speed: str | None = None
|
||||||
eta: 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
|
thumbnail: str | None = None
|
||||||
ytdlp_cookies: str | None = None
|
ytdlp_cookies: str | None = None
|
||||||
ytdlp_config: dict = field(default_factory=dict)
|
ytdlp_config: dict = field(default_factory=dict)
|
||||||
output_template: str | None = None
|
output_template: str | None = None
|
||||||
output_template_chapter: str | None = None
|
output_template_chapter: str | None = None
|
||||||
|
config: dict = field(default_factory=dict)
|
||||||
|
|
||||||
def serialize(self) -> dict:
|
def serialize(self) -> dict:
|
||||||
deprecated: tuple = (
|
deprecated: tuple = (
|
||||||
|
|
@ -65,6 +66,7 @@ class ItemDTO:
|
||||||
"ytdlp_config",
|
"ytdlp_config",
|
||||||
"output_template",
|
"output_template",
|
||||||
"output_template_chapter",
|
"output_template_chapter",
|
||||||
|
"config",
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.thumbnail and "thumbnail" not in self.extras:
|
if self.thumbnail and "thumbnail" not in self.extras:
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,9 @@ class Preset:
|
||||||
cookies: str = ""
|
cookies: str = ""
|
||||||
"""The default cookies to use if non is given."""
|
"""The default cookies to use if non is given."""
|
||||||
|
|
||||||
|
cli: str = ""
|
||||||
|
"""yt-dlp cli command line arguments."""
|
||||||
|
|
||||||
default: bool = False
|
default: bool = False
|
||||||
|
|
||||||
def serialize(self) -> dict:
|
def serialize(self) -> dict:
|
||||||
|
|
@ -220,6 +223,15 @@ class Presets(metaclass=Singleton):
|
||||||
msg = "Invalid postprocessors type. expected list."
|
msg = "Invalid postprocessors type. expected list."
|
||||||
raise ValueError(msg)
|
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
|
return True
|
||||||
|
|
||||||
def save(self, presets: list[Preset | dict]) -> "Presets":
|
def save(self, presets: list[Preset | dict]) -> "Presets":
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
@ -28,8 +28,7 @@ class Task:
|
||||||
preset: str = ""
|
preset: str = ""
|
||||||
timer: str = ""
|
timer: str = ""
|
||||||
template: str = ""
|
template: str = ""
|
||||||
cookies: str = ""
|
cli: str = ""
|
||||||
config: dict[str, str] = field(default_factory=dict)
|
|
||||||
|
|
||||||
def serialize(self) -> dict:
|
def serialize(self) -> dict:
|
||||||
return self.__dict__
|
return self.__dict__
|
||||||
|
|
@ -134,16 +133,20 @@ class Tasks(metaclass=Singleton):
|
||||||
with open(self._file) as f:
|
with open(self._file) as f:
|
||||||
tasks = json.load(f)
|
tasks = json.load(f)
|
||||||
except Exception as e:
|
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
|
return self
|
||||||
|
|
||||||
if not tasks or len(tasks) < 1:
|
if not tasks or len(tasks) < 1:
|
||||||
LOG.info(f"No tasks were defined in '{self._file}'.")
|
LOG.info(f"No tasks were defined in '{self._file}'.")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
need_update = False
|
||||||
for i, task in enumerate(tasks):
|
for i, task in enumerate(tasks):
|
||||||
try:
|
try:
|
||||||
|
task, task_status = self.clean_task(task)
|
||||||
task = Task(**task)
|
task = Task(**task)
|
||||||
|
if task_status:
|
||||||
|
need_update = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Failed to parse task at list position '{i}'. '{e!s}'.")
|
LOG.error(f"Failed to parse task at list position '{i}'. '{e!s}'.")
|
||||||
continue
|
continue
|
||||||
|
|
@ -157,6 +160,10 @@ class Tasks(metaclass=Singleton):
|
||||||
LOG.exception(e)
|
LOG.exception(e)
|
||||||
LOG.error(f"Failed to queue task '{i}: {task.name}'. '{e!s}'.")
|
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
|
return self
|
||||||
|
|
||||||
def clear(self, shutdown: bool = False) -> "Tasks":
|
def clear(self, shutdown: bool = False) -> "Tasks":
|
||||||
|
|
@ -213,17 +220,13 @@ class Tasks(metaclass=Singleton):
|
||||||
msg = "No URL found."
|
msg = "No URL found."
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
if not isinstance(task.get("cookies"), str):
|
if task.get("cli"):
|
||||||
msg = "Invalid cookies type."
|
try:
|
||||||
raise ValueError(msg) # noqa: TRY004
|
from .Utils import arg_converter
|
||||||
|
arg_converter(args=task.get("cli"))
|
||||||
if not isinstance(task.get("config"), dict):
|
except Exception as e:
|
||||||
msg = "Invalid config type."
|
msg = f"Invalid cli options. '{e!s}'."
|
||||||
raise ValueError(msg) # noqa: TRY004
|
raise ValueError(msg) from e
|
||||||
|
|
||||||
if not isinstance(task.get("template"), str):
|
|
||||||
msg = "Invalid template type."
|
|
||||||
raise ValueError(msg) # noqa: TRY004
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -263,6 +266,27 @@ class Tasks(metaclass=Singleton):
|
||||||
|
|
||||||
return self
|
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):
|
async def _runner(self, task: Task):
|
||||||
"""
|
"""
|
||||||
Run the task.
|
Run the task.
|
||||||
|
|
@ -283,16 +307,8 @@ class Tasks(metaclass=Singleton):
|
||||||
|
|
||||||
preset: str = str(task.preset or self._default_preset)
|
preset: str = str(task.preset or self._default_preset)
|
||||||
folder: str = task.folder if task.folder else ""
|
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 ""
|
template: str = task.template if task.template else ""
|
||||||
|
cli: str = task.cli if task.cli 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
|
|
||||||
|
|
||||||
LOG.info(f"Task '{task.id}: {task.name}' dispatched at '{timeNow}'.")
|
LOG.info(f"Task '{task.id}: {task.name}' dispatched at '{timeNow}'.")
|
||||||
|
|
||||||
|
|
@ -307,9 +323,8 @@ class Tasks(metaclass=Singleton):
|
||||||
"url": task.url,
|
"url": task.url,
|
||||||
"preset": preset,
|
"preset": preset,
|
||||||
"folder": folder,
|
"folder": folder,
|
||||||
"cookies": cookies,
|
|
||||||
"config": config,
|
|
||||||
"template": template,
|
"template": template,
|
||||||
|
"cli": cli,
|
||||||
},
|
},
|
||||||
id=task.id,
|
id=task.id,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,42 @@ from .LogWrapper import LogWrapper
|
||||||
|
|
||||||
LOG = logging.getLogger("Utils")
|
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
|
YTDLP_INFO_CLS: yt_dlp.YoutubeDL = None
|
||||||
|
|
||||||
ALLOWED_SUBS_EXTENSIONS: tuple[str] = (".srt", ".vtt", ".ass")
|
ALLOWED_SUBS_EXTENSIONS: tuple[str] = (".srt", ".vtt", ".ass")
|
||||||
|
|
@ -434,12 +469,20 @@ def validate_url(url: str) -> bool:
|
||||||
return True
|
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.
|
Convert yt-dlp options to a dictionary.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
args (str): yt-dlp options string.
|
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:
|
Returns:
|
||||||
dict: yt-dlp options dictionary.
|
dict: yt-dlp options dictionary.
|
||||||
|
|
@ -465,16 +508,44 @@ def arg_converter(args: str) -> dict:
|
||||||
if "postprocessors" in diff:
|
if "postprocessors" in diff:
|
||||||
diff["postprocessors"] = [pp for pp in diff["postprocessors"] if pp not in default_opts["postprocessors"]]
|
diff["postprocessors"] = [pp for pp in diff["postprocessors"] if pp not in default_opts["postprocessors"]]
|
||||||
|
|
||||||
if "match_filter" in diff:
|
if "_warnings" in diff:
|
||||||
import inspect
|
diff.pop("_warnings", None)
|
||||||
|
|
||||||
matchFilter = inspect.getclosurevars(diff["match_filter"].func).nonlocals["filters"]
|
if level is True or isinstance(level, int):
|
||||||
if isinstance(matchFilter, set):
|
bad_options = {}
|
||||||
diff["match_filter"] = {"filters": list(matchFilter)}
|
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:
|
def validate_uuid(uuid_str: str, version: int = 4) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from pathlib import Path
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .Presets import Presets
|
from .Presets import Presets
|
||||||
from .Singleton import Singleton
|
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")
|
LOG = logging.getLogger("YTDLPOpts")
|
||||||
|
|
||||||
|
|
@ -48,11 +48,22 @@ class YTDLPOpts(metaclass=Singleton):
|
||||||
YTDLPOpts: The instance of the class
|
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():
|
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
|
continue
|
||||||
|
|
||||||
self._item_opts[key] = value
|
self._item_opts[key] = value
|
||||||
|
|
||||||
|
if len(removed_options) > 0:
|
||||||
|
LOG.warning("Removed the following options: '%s'.", ", ".join(removed_options))
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def preset(self, name: str, with_cookies: bool = False) -> "YTDLPOpts":
|
def preset(self, name: str, with_cookies: bool = False) -> "YTDLPOpts":
|
||||||
|
|
@ -71,6 +82,19 @@ class YTDLPOpts(metaclass=Singleton):
|
||||||
if not preset or "default" == name:
|
if not preset or "default" == name:
|
||||||
return self
|
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:
|
if preset.cookies and with_cookies:
|
||||||
file = Path(self._config.config_path, "cookies", f"{preset.id}.txt")
|
file = Path(self._config.config_path, "cookies", f"{preset.id}.txt")
|
||||||
|
|
||||||
|
|
@ -94,14 +118,24 @@ class YTDLPOpts(metaclass=Singleton):
|
||||||
"temp": self._config.temp_path,
|
"temp": self._config.temp_path,
|
||||||
}
|
}
|
||||||
|
|
||||||
if preset.postprocessors and isinstance(preset.postprocessors, list) and len(preset.postprocessors) > 0:
|
# @Deprecated - To be removed in future versions.
|
||||||
self._preset_opts["postprocessors"] = preset.postprocessors
|
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:
|
if preset.args and isinstance(preset.args, dict) and len(preset.args) > 0:
|
||||||
for key, value in preset.args.items():
|
bad_options = {k: v for d in REMOVE_KEYS for k, v in d.items()}
|
||||||
if key in IGNORED_KEYS:
|
removed_options = []
|
||||||
continue
|
for key, value in preset.args.items():
|
||||||
self._preset_opts[key] = value
|
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
|
return self
|
||||||
|
|
||||||
|
|
@ -132,6 +166,7 @@ class YTDLPOpts(metaclass=Singleton):
|
||||||
if "format" in data and data["format"] in ["not_set", "default"]:
|
if "format" in data and data["format"] in ["not_set", "default"]:
|
||||||
data["format"] = None
|
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):
|
if "daterange" in data and isinstance(data["daterange"], dict):
|
||||||
from yt_dlp.utils import DateRange
|
from yt_dlp.utils import DateRange
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import logging
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .DownloadQueue import DownloadQueue
|
from .DownloadQueue import DownloadQueue
|
||||||
from .encoder import Encoder
|
from .encoder import Encoder
|
||||||
|
from .Utils import arg_converter
|
||||||
|
|
||||||
LOG = logging.getLogger("common")
|
LOG = logging.getLogger("common")
|
||||||
|
|
||||||
|
|
@ -27,7 +28,16 @@ class Common:
|
||||||
self.default_preset = config.default_preset
|
self.default_preset = config.default_preset
|
||||||
|
|
||||||
async def add(
|
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]:
|
) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Add an item to the download queue.
|
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.
|
config (dict): The yt-dlp config to be used for the download.
|
||||||
template (str): The template 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
|
extras (dict): Extra data to be added to the download
|
||||||
|
cli (str): The yt-dlp cli options to be used for the download.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict[str, str]: The status of the download.
|
dict[str, str]: The status of the download.
|
||||||
|
|
@ -56,6 +67,7 @@ class Common:
|
||||||
cookies=cookies,
|
cookies=cookies,
|
||||||
config=config if isinstance(config, dict) else {},
|
config=config if isinstance(config, dict) else {},
|
||||||
template=template,
|
template=template,
|
||||||
|
cli=cli,
|
||||||
extras=extras,
|
extras=extras,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -94,6 +106,19 @@ class Common:
|
||||||
msg = f"Failed to parse json yt-dlp config for '{url}'. {e!s}"
|
msg = f"Failed to parse json yt-dlp config for '{url}'. {e!s}"
|
||||||
raise ValueError(msg) from e
|
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 {
|
return {
|
||||||
"url": url,
|
"url": url,
|
||||||
"preset": preset,
|
"preset": preset,
|
||||||
|
|
@ -101,5 +126,6 @@ class Common:
|
||||||
"cookies": cookies,
|
"cookies": cookies,
|
||||||
"config": config if isinstance(config, dict) else {},
|
"config": config if isinstance(config, dict) else {},
|
||||||
"template": template,
|
"template": template,
|
||||||
|
"cli": cli if isinstance(cli, str) else "",
|
||||||
"extras": extras if isinstance(extras, dict) else {},
|
"extras": extras if isinstance(extras, dict) else {},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import coloredlogs
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from yt_dlp.version import __version__ as YTDLP_VERSION
|
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
|
from .version import APP_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -218,7 +218,7 @@ class Config:
|
||||||
"instance_title",
|
"instance_title",
|
||||||
"sentry_dsn",
|
"sentry_dsn",
|
||||||
"console_enabled",
|
"console_enabled",
|
||||||
"browser_enabled"
|
"browser_enabled",
|
||||||
)
|
)
|
||||||
"The variables that are relevant to the frontend."
|
"The variables that are relevant to the frontend."
|
||||||
|
|
||||||
|
|
@ -321,8 +321,38 @@ class Config:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Error starting debugpy server at '0.0.0.0:{self.debugpy_port}'. {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")
|
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}'.")
|
LOG.info(f"Loading yt-dlp custom options from '{optsFile}'.")
|
||||||
|
|
||||||
(opts, status, error) = load_file(optsFile, dict)
|
(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}")
|
LOG.error(f"Could not load yt-dlp custom options from '{optsFile}'. {error}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if isinstance(opts, dict):
|
if isinstance(opts, dict):
|
||||||
for key in IGNORED_KEYS:
|
bad_options = {k: v for d in REMOVE_KEYS for k, v in d.items()}
|
||||||
if key in opts:
|
removed_options = []
|
||||||
LOG.error(f"Key '{key}' is not allowed to be loaded via 'ytdlp.json' file.")
|
for key in opts.copy():
|
||||||
del opts[key]
|
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)
|
self.ytdl_options = merge_dict(self.ytdl_options, opts)
|
||||||
else:
|
else:
|
||||||
LOG.error(f"Invalid yt-dlp custom options file '{optsFile}'.")
|
LOG.error(f"Invalid yt-dlp custom options file '{optsFile}'.")
|
||||||
else:
|
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
|
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")
|
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:
|
if os.path.exists(key_file) and os.path.getsize(key_file) > 5:
|
||||||
with open(key_file, "rb") as f:
|
with open(key_file, "rb") as f:
|
||||||
self.secret_key = f.read().strip()
|
self.secret_key = f.read().strip()
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
"folder": "",
|
"folder": "",
|
||||||
"template": "",
|
"template": "",
|
||||||
"cookies": "",
|
"cookies": "",
|
||||||
|
"cli": "",
|
||||||
"default": true
|
"default": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -15,69 +16,37 @@
|
||||||
"folder": "",
|
"folder": "",
|
||||||
"template": "",
|
"template": "",
|
||||||
"cookies": "",
|
"cookies": "",
|
||||||
|
"cli": "",
|
||||||
"default": true
|
"default": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "441675ed-b739-40f0-a0b0-1ecfcb9dc48b",
|
"id": "441675ed-b739-40f0-a0b0-1ecfcb9dc48b",
|
||||||
"name": "1080p H264/m4a or best available",
|
"name": "1080p H264/m4a or best available",
|
||||||
"format": "bv[height<=1080][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]",
|
"format": "bv[height<=1080][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]",
|
||||||
"args": {
|
|
||||||
"format_sort": [
|
|
||||||
"vcodec:h264"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"folder": "",
|
"folder": "",
|
||||||
"template": "",
|
"template": "",
|
||||||
"cookies": "",
|
"cookies": "",
|
||||||
|
"cli": "-S vcodec:h264",
|
||||||
"default": true
|
"default": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "9719fcc3-4cf2-4d88-b1e4-74dff3dba00e",
|
"id": "9719fcc3-4cf2-4d88-b1e4-74dff3dba00e",
|
||||||
"name": "720p h264/m4a or best available",
|
"name": "720p h264/m4a or best available",
|
||||||
"format": "bv[height<=720][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]",
|
"format": "bv[height<=720][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]",
|
||||||
"args": {
|
|
||||||
"format_sort": [
|
|
||||||
"vcodec:h264"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"folder": "",
|
"folder": "",
|
||||||
"template": "",
|
"template": "",
|
||||||
"cookies": "",
|
"cookies": "",
|
||||||
|
"cli": "-S vcodec:h264",
|
||||||
"default": true
|
"default": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a6fd4b25-2b3e-458d-bb57-b75e41cc4330",
|
"id": "a6fd4b25-2b3e-458d-bb57-b75e41cc4330",
|
||||||
"name": "Audio only",
|
"name": "Audio only",
|
||||||
"format": "bestaudio/best",
|
"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": "",
|
"folder": "",
|
||||||
"template": "",
|
"template": "",
|
||||||
"cookies": "",
|
"cookies": "",
|
||||||
|
"cli": "--extract-audio --add-chapters --embed-metadata --embed-thumbnail",
|
||||||
"default": true
|
"default": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -141,17 +141,17 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="is-vcentered is-items-center">
|
<td class="is-vcentered is-items-center">
|
||||||
<div class="field is-grouped is-grouped-centered">
|
<div class="field is-grouped is-grouped-centered">
|
||||||
<div class="control" v-if="'finished' === item.status || isEmbedable(item.url)">
|
<div class="control" v-if="('finished' === item.status && item.filename) || isEmbedable(item.url)">
|
||||||
<button v-if="'finished' === item.status" @click="playVideo(item)" v-tooltip="'Play video'"
|
<button v-if="'finished' === item.status && item.filename" @click="playVideo(item)"
|
||||||
class="button is-danger is-light is-small">
|
v-tooltip="'Play video'" class="button is-danger is-light is-small">
|
||||||
<span class="icon"><i class="fa-solid fa-play" /></span>
|
<span class="icon"><i class="fa-solid fa-play" /></span>
|
||||||
</button>
|
</button>
|
||||||
<button v-else @click="embed_url = getEmbedable(item.url)" v-tooltip="'Play video'"
|
<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>
|
<span class="icon"><i class="fa-solid fa-play" /></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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'"
|
<button class="button is-warning is-fullwidth is-small" v-tooltip="'Re-queue video'"
|
||||||
@click="reQueueItem(item)">
|
@click="reQueueItem(item)">
|
||||||
<span class="icon"><i class="fa-solid fa-rotate-right" /></span>
|
<span class="icon"><i class="fa-solid fa-rotate-right" /></span>
|
||||||
|
|
@ -203,7 +203,7 @@
|
||||||
|
|
||||||
<div class="card-header-icon">
|
<div class="card-header-icon">
|
||||||
<span v-if="hideThumbnail">
|
<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.'">
|
v-tooltip="'Play video.'">
|
||||||
<span class="icon"><i class="fa-solid fa-play" /></span>
|
<span class="icon"><i class="fa-solid fa-play" /></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -223,7 +223,7 @@
|
||||||
</header>
|
</header>
|
||||||
<div v-if="false === hideThumbnail" class="card-image">
|
<div v-if="false === hideThumbnail" class="card-image">
|
||||||
<figure class="image is-3by1">
|
<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>
|
<div class="play-icon"></div>
|
||||||
<img @load="e => pImg(e)" :src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)"
|
<img @load="e => pImg(e)" :src="'/api/thumbnail?url=' + encodePath(item.extras.thumbnail)"
|
||||||
v-if="item.extras?.thumbnail" />
|
v-if="item.extras?.thumbnail" />
|
||||||
|
|
@ -282,7 +282,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="columns is-mobile is-multiline">
|
<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)">
|
<a class="button is-warning is-fullwidth" @click="reQueueItem(item)">
|
||||||
<span class="icon-text is-block">
|
<span class="icon-text is-block">
|
||||||
<span class="icon"><i class="fa-solid fa-rotate-right" /></span>
|
<span class="icon"><i class="fa-solid fa-rotate-right" /></span>
|
||||||
|
|
@ -512,6 +512,9 @@ const clearIncomplete = () => {
|
||||||
|
|
||||||
const setIcon = item => {
|
const setIcon = item => {
|
||||||
if ('finished' === item.status) {
|
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'
|
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) {
|
if ('not_live' === item.status) {
|
||||||
return 'fa-solid fa-hourglass-half fa-spin'
|
return 'fa-solid fa-headset'
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'fa-solid fa-circle'
|
return 'fa-solid fa-circle'
|
||||||
|
|
@ -532,6 +535,9 @@ const setIcon = item => {
|
||||||
|
|
||||||
const setIconColor = item => {
|
const setIconColor = item => {
|
||||||
if ('finished' === item.status) {
|
if ('finished' === item.status) {
|
||||||
|
if (!item.filename) {
|
||||||
|
return 'has-text-warning'
|
||||||
|
}
|
||||||
return 'has-text-success'
|
return 'has-text-success'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -548,6 +554,9 @@ const setIconColor = item => {
|
||||||
|
|
||||||
const setStatus = item => {
|
const setStatus = item => {
|
||||||
if ('finished' === item.status) {
|
if ('finished' === item.status) {
|
||||||
|
if (!item.filename) {
|
||||||
|
return 'Skipped?'
|
||||||
|
}
|
||||||
return item.is_live ? 'Live Ended' : 'Completed'
|
return item.is_live ? 'Live Ended' : 'Completed'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -617,9 +626,9 @@ const reQueueItem = item => {
|
||||||
url: item.url,
|
url: item.url,
|
||||||
preset: item.preset,
|
preset: item.preset,
|
||||||
folder: item.folder,
|
folder: item.folder,
|
||||||
config: item.config,
|
|
||||||
cookies: item.cookies,
|
cookies: item.cookies,
|
||||||
template: item.template,
|
template: item.template,
|
||||||
|
cli: item?.cli,
|
||||||
extras: extras
|
extras: extras
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,17 @@
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
<select id="preset" class="is-fullwidth"
|
<select id="preset" class="is-fullwidth"
|
||||||
:disabled="!socket.isConnected || addInProgress || hasFormatInConfig" v-model="selectedPreset"
|
:disabled="!socket.isConnected || addInProgress || hasFormatInConfig" v-model="selectedPreset"
|
||||||
v-tooltip.bottom="hasFormatInConfig ? 'Presets are disabled. Format key is present in the config.' : ''">
|
v-tooltip.bottom="hasFormatInConfig ? 'Presets are disabled. Format key is present in the Command arguments for yt-dlp.' : ''">
|
||||||
<option v-for="item in config.presets" :key="item.name" :value="item.name">
|
<optgroup label="Default presets">
|
||||||
{{ item.name }}
|
<option v-for="item in filter_presets(true)" :key="item.name" :value="item.name">
|
||||||
</option>
|
{{ 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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -59,16 +66,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="columns is-multiline is-mobile" v-if="showAdvanced && !config.app.basic_mode">
|
<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">
|
<div class="field">
|
||||||
<label class="label is-inline" for="output_format"
|
<label class="label is-inline" for="output_format"
|
||||||
v-tooltip="'Default Format: ' + config.app.output_template">
|
v-tooltip="'Default Format: ' + config.app.output_template">
|
||||||
Output Template
|
Output Template
|
||||||
</label>
|
</label>
|
||||||
<div class="control">
|
<div class="control has-icons-left">
|
||||||
<input type="text" class="input" v-model="output_template" id="output_format"
|
<input type="text" class="input" v-model="output_template" id="output_format"
|
||||||
:disabled="!socket.isConnected || addInProgress"
|
:disabled="!socket.isConnected || addInProgress"
|
||||||
placeholder="Uses default output template naming if empty.">
|
placeholder="Uses default output template naming if empty.">
|
||||||
|
<span class="icon is-small is-left"><i class="fa-solid fa-file" /></span>
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
|
|
@ -77,32 +86,31 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-6-tablet is-12-mobile">
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline" for="ytdlpConfig"
|
<label class="label is-inline" for="cli_options">
|
||||||
v-tooltip="'Extends current global yt-dlp config. (JSON)'">
|
Command arguments for yt-dlp
|
||||||
JSON yt-dlp config or CLI options.
|
|
||||||
<NuxtLink v-if="ytdlpConfig && ytdlpConfig.trim() && !ytdlpConfig.trim().startsWith('{')"
|
|
||||||
@click="convertOptions()">
|
|
||||||
Convert to JSON
|
|
||||||
</NuxtLink>
|
|
||||||
</label>
|
</label>
|
||||||
<div class="control">
|
<div class="control has-icons-left">
|
||||||
<textarea class="textarea" id="ytdlpConfig" v-model="ytdlpConfig"
|
<input type="text" class="input" v-model="ytdlp_cli" id="cli_options"
|
||||||
:disabled="!socket.isConnected || addInProgress || convertInProgress"
|
:disabled="!socket.isConnected || addInProgress"
|
||||||
placeholder="--no-embed-metadata --no-embed-thumbnail"></textarea>
|
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>
|
</div>
|
||||||
|
|
||||||
<span class="help">
|
<span class="help">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<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
|
<span>yt-dlp cli arguments. Check <NuxtLink target="_blank"
|
||||||
<code>cookiefile</code>, <code>paths</code>, and <code>outtmpl</code> etc. Warning: Use with caution
|
to="https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#general-options">this page</NuxtLink>.
|
||||||
some of those options can break yt-dlp or the frontend. If <code>Format</code> key is present
|
For more info. <span class="has-text-danger">Not all options are supported some are ignored. Use
|
||||||
in the config, <span class="has-text-danger">the preset and all it's options will be
|
with caution those arguments can break yt-dlp or the frontend.</span>
|
||||||
ignored</span>.</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-6-tablet is-12-mobile">
|
|
||||||
|
<div class="column is-12">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline" for="ytdlpCookies" v-tooltip="'Netscape HTTP Cookie format'">
|
<label class="label is-inline" for="ytdlpCookies" v-tooltip="'Netscape HTTP Cookie format'">
|
||||||
Cookies
|
Cookies
|
||||||
|
|
@ -152,7 +160,6 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { request } from '~/utils/index'
|
|
||||||
|
|
||||||
const emitter = defineEmits(['getInfo'])
|
const emitter = defineEmits(['getInfo'])
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
|
|
@ -160,31 +167,25 @@ const socket = useSocketStore()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const selectedPreset = useStorage('selectedPreset', config.app.default_preset)
|
const selectedPreset = useStorage('selectedPreset', config.app.default_preset)
|
||||||
const ytdlpConfig = useStorage('ytdlp_config', '')
|
|
||||||
const ytdlpCookies = useStorage('ytdlp_cookies', '')
|
const ytdlpCookies = useStorage('ytdlp_cookies', '')
|
||||||
|
const ytdlp_cli = useStorage('ytdlp_cli', '')
|
||||||
const output_template = useStorage('output_template', null)
|
const output_template = useStorage('output_template', null)
|
||||||
const downloadPath = useStorage('downloadPath', null)
|
const downloadPath = useStorage('downloadPath', null)
|
||||||
const url = useStorage('downloadUrl', null)
|
const url = useStorage('downloadUrl', null)
|
||||||
const showAdvanced = useStorage('show_advanced', false)
|
const showAdvanced = useStorage('show_advanced', false)
|
||||||
const addInProgress = ref(false)
|
const addInProgress = ref(false)
|
||||||
const convertInProgress = ref(false)
|
|
||||||
|
|
||||||
const addDownload = async () => {
|
const addDownload = async () => {
|
||||||
// -- send request to convert cli options to JSON
|
// -- send request to convert cli options to JSON
|
||||||
if (ytdlpConfig.value && !ytdlpConfig.value.trim().startsWith('{')) {
|
if (ytdlp_cli.value && '' !== ytdlp_cli.value) {
|
||||||
await convertOptions()
|
const options = await convertOptions(ytdlp_cli.value)
|
||||||
}
|
if (null === options) {
|
||||||
|
|
||||||
if (ytdlpConfig.value) {
|
|
||||||
try {
|
|
||||||
JSON.parse(ytdlpConfig.value)
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(`Invalid JSON yt-dlp config. ${e.message}`)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addInProgress.value = true
|
addInProgress.value = true
|
||||||
|
|
||||||
url.value.split(',').forEach(url => {
|
url.value.split(',').forEach(url => {
|
||||||
if (!url.trim()) {
|
if (!url.trim()) {
|
||||||
return
|
return
|
||||||
|
|
@ -193,9 +194,9 @@ const addDownload = async () => {
|
||||||
url: url,
|
url: url,
|
||||||
preset: config.app.basic_mode ? config.app.default_preset : selectedPreset.value,
|
preset: config.app.basic_mode ? config.app.default_preset : selectedPreset.value,
|
||||||
folder: config.app.basic_mode ? null : downloadPath.value,
|
folder: config.app.basic_mode ? null : downloadPath.value,
|
||||||
config: config.app.basic_mode ? '' : ytdlpConfig.value,
|
|
||||||
cookies: config.app.basic_mode ? '' : ytdlpCookies.value,
|
cookies: config.app.basic_mode ? '' : ytdlpCookies.value,
|
||||||
template: config.app.basic_mode ? null : output_template.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
|
selectedPreset.value = config.app.default_preset.value
|
||||||
ytdlpConfig.value = ''
|
|
||||||
ytdlpCookies.value = ''
|
ytdlpCookies.value = ''
|
||||||
output_template.value = null
|
output_template.value = null
|
||||||
url.value = null
|
url.value = null
|
||||||
|
|
@ -239,27 +239,24 @@ const unlockDownload = async stream => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertOptions = async () => {
|
const convertOptions = async args => {
|
||||||
if (convertInProgress.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
convertInProgress.value = true
|
const response = await convertCliOptions(args)
|
||||||
const response = await convertCliOptions(ytdlpConfig.value)
|
|
||||||
ytdlpConfig.value = JSON.stringify(response.opts, null, 2)
|
|
||||||
if (response.output_template) {
|
if (response.output_template) {
|
||||||
output_template.value = response.output_template
|
output_template.value = response.output_template
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.download_path) {
|
if (response.download_path) {
|
||||||
downloadPath.value = response.download_path
|
downloadPath.value = response.download_path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return response.opts
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e.message)
|
toast.error(e.message)
|
||||||
} finally {
|
|
||||||
convertInProgress.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
@ -276,14 +273,13 @@ onUnmounted(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasFormatInConfig = computed(() => {
|
const hasFormatInConfig = computed(() => {
|
||||||
if (!ytdlpConfig.value) {
|
if (!ytdlp_cli.value) {
|
||||||
return false
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const config = JSON.parse(ytdlpConfig.value)
|
|
||||||
return "format" in config
|
|
||||||
} catch (e) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return /(?<!\w)(-f|--format)(=|:)?(?!\w)/.test(ytdlp_cli.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const filter_presets = (flag = true) => config.presets.filter(item => item.default === flag)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<main class="columns mt-2 is-multiline">
|
<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">
|
<div class="column is-12">
|
||||||
<form autocomplete="off" id="presetForm" @submit.prevent="checkInfo()">
|
<form autocomplete="off" id="presetForm" @submit.prevent="checkInfo()">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
@ -157,43 +119,21 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-6-tablet is-12-mobile">
|
<div class="column is-12">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline" for="args" v-tooltip="'Extends current global yt-dlp config. (JSON)'">
|
<label class="label is-inline" for="cli_options">
|
||||||
JSON yt-dlp config
|
Command arguments for yt-dlp
|
||||||
</label>
|
</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea class="textarea" id="args" v-model="form.args" :disabled="addInProgress"
|
<input type="text" class="input" v-model="form.cli" id="cli_options" :disabled="addInProgress"
|
||||||
placeholder="{}" />
|
placeholder="command options to use, e.g. --no-embed-metadata --no-embed-thumbnail">
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<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
|
<span>yt-dlp cli arguments. Check <NuxtLink target="_blank"
|
||||||
<code>cookiefile</code>, <code>paths</code>, and <code>outtmpl</code> etc. Warning: Use with
|
to="https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#general-options">this page</NuxtLink>.
|
||||||
caution
|
For more info. <span class="has-text-danger">Not all options are supported some are ignored. Use
|
||||||
some of those options can break yt-dlp or the frontend.</span>
|
with caution those arguments 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>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -217,6 +157,56 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -239,6 +229,7 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<datalist id="folders" v-if="config?.folders">
|
<datalist id="folders" v-if="config?.folders">
|
||||||
<option v-for="dir in config.folders" :key="dir" :value="dir" />
|
<option v-for="dir in config.folders" :key="dir" :value="dir" />
|
||||||
</datalist>
|
</datalist>
|
||||||
|
|
@ -273,14 +264,15 @@ const props = defineProps({
|
||||||
|
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const convertInProgress = ref(false)
|
|
||||||
const form = reactive(JSON.parse(JSON.stringify(props.preset)))
|
const form = reactive(JSON.parse(JSON.stringify(props.preset)))
|
||||||
const opts = ref('')
|
|
||||||
const import_string = ref('')
|
const import_string = ref('')
|
||||||
const showImport = useStorage('showImport', false);
|
const showImport = useStorage('showImport', false);
|
||||||
const convertExpanded = ref(false)
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (props.preset?.cli && '' !== props.preset?.cli) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (props.preset?.args && (typeof props.preset.args === 'object')) {
|
if (props.preset?.args && (typeof props.preset.args === 'object')) {
|
||||||
form.args = JSON.stringify(props.preset.args, null, 2)
|
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 copy = JSON.parse(JSON.stringify(form));
|
||||||
|
|
||||||
let usedName = false;
|
let usedName = false;
|
||||||
|
|
@ -319,29 +319,47 @@ const checkInfo = async () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof copy.args === 'object') {
|
if (form?.cli && '' !== form.cli) {
|
||||||
copy.args = JSON.stringify(copy.args, null, 2);
|
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') {
|
if (copy?.args) {
|
||||||
copy.postprocessors = JSON.stringify(copy.postprocessors, null, 2);
|
delete copy.args
|
||||||
}
|
}
|
||||||
|
|
||||||
if (copy.args) {
|
if (copy?.postprocessors) {
|
||||||
try {
|
delete copy.postprocessors
|
||||||
copy.args = JSON.parse(copy.args)
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(`Invalid JSON yt-dlp config. ${e.message}`)
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
if (typeof copy.args === 'object') {
|
||||||
|
copy.args = JSON.stringify(copy.args, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
if (copy.postprocessors) {
|
if (typeof copy.postprocessors === 'object') {
|
||||||
try {
|
copy.postprocessors = JSON.stringify(copy.postprocessors, null, 2);
|
||||||
copy.postprocessors = JSON.parse(copy.postprocessors)
|
}
|
||||||
} catch (e) {
|
|
||||||
toast.error(`Invalid JSON yt-dlp Post-Processors. ${e.message}`)
|
if (copy?.args) {
|
||||||
return;
|
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) });
|
emitter('submit', { reference: toRaw(props.reference), preset: toRaw(copy) });
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertOptions = async () => {
|
const convertOptions = async args => {
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
convertInProgress.value = true
|
const response = await convertCliOptions(args)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.output_template) {
|
if (response.output_template) {
|
||||||
form.template = response.output_template
|
form.template = response.output_template
|
||||||
|
|
@ -388,18 +385,16 @@ const convertOptions = async () => {
|
||||||
form.folder = response.download_path
|
form.folder = response.download_path
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.opts.postprocessors) {
|
if (response.format) {
|
||||||
form.postprocessors = JSON.stringify(response.opts.postprocessors, null, 2)
|
form.format = response.format
|
||||||
delete response.opts.postprocessors
|
|
||||||
}
|
}
|
||||||
|
|
||||||
form.args = JSON.stringify(response.opts, null, 2)
|
return response.opts
|
||||||
opts.value = ''
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e.message)
|
toast.error(e.message)
|
||||||
} finally {
|
|
||||||
convertInProgress.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const importItem = async () => {
|
const importItem = async () => {
|
||||||
|
|
@ -422,13 +417,15 @@ const importItem = async () => {
|
||||||
try {
|
try {
|
||||||
const item = JSON.parse(val)
|
const item = JSON.parse(val)
|
||||||
|
|
||||||
|
console.log(item)
|
||||||
|
|
||||||
if ('preset' !== item._type) {
|
if ('preset' !== item._type) {
|
||||||
toast.error(`Invalid import string. Expected type 'preset', got '${item._type}'.`)
|
toast.error(`Invalid import string. Expected type 'preset', got '${item._type}'.`)
|
||||||
import_string.value = ''
|
import_string.value = ''
|
||||||
return
|
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?')) {
|
if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -442,16 +439,12 @@ const importItem = async () => {
|
||||||
form.format = item.format
|
form.format = item.format
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.args) {
|
if (item.cli) {
|
||||||
form.args = JSON.stringify(item.args, null, 2)
|
form.cli = item.cli
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.postprocessors) {
|
if (item.template) {
|
||||||
form.postprocessors = JSON.stringify(item.postprocessors, null, 2)
|
form.template = item.template
|
||||||
}
|
|
||||||
|
|
||||||
if (item.output_template) {
|
|
||||||
form.template = item.output_template
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.folder) {
|
if (item.folder) {
|
||||||
|
|
@ -465,4 +458,5 @@ const importItem = async () => {
|
||||||
toast.error(`Failed to string. ${e.message}`)
|
toast.error(`Failed to string. ${e.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="column is-6-tablet is-12-mobile">
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline" for="name" v-text="'Name'" />
|
<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="column is-6-tablet is-12-mobile">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline" for="folder">
|
<label class="label is-inline" for="preset">Preset</label>
|
||||||
Preset
|
|
||||||
</label>
|
|
||||||
<div class="control has-icons-left">
|
<div class="control has-icons-left">
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
<select id="preset" class="is-fullwidth" v-model="form.preset"
|
<select id="preset" class="is-fullwidth" v-model="form.preset"
|
||||||
:disabled="addInProgress || hasFormatInConfig"
|
:disabled="addInProgress || hasFormatInConfig"
|
||||||
v-tooltip.bottom="hasFormatInConfig ? 'Presets are disabled. Format key is present in the config.' : ''">
|
v-tooltip.bottom="hasFormatInConfig ? 'Presets are disabled. Format key is present in the command arguments for yt-dlp.' : ''">
|
||||||
<option v-for="item in config.presets" :key="item.name" :value="item.name">
|
<optgroup label="Default presets">
|
||||||
{{ item.name }}
|
<option v-for="item in filter_presets(true)" :key="item.name" :value="item.name">
|
||||||
</option>
|
{{ 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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
|
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<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
|
<span>Select the preset to use for this URL. <span class="text-has-danger">If the
|
||||||
config.</span>
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -126,7 +132,7 @@
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="field">
|
||||||
<label class="label is-inline" for="folder">
|
<label class="label is-inline" for="folder">
|
||||||
Download path
|
Download path
|
||||||
|
|
@ -138,24 +144,25 @@
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="field">
|
||||||
<label class="label is-inline" for="output_template">
|
<label class="label is-inline" for="output_template">
|
||||||
Output template
|
Output template
|
||||||
</label>
|
</label>
|
||||||
<div class="control has-icons-left">
|
<div class="control has-icons-left">
|
||||||
<input type="text" class="input" id="output_template" placeholder="The output template to use"
|
<input type="text" class="input" id="output_template" :disabled="addInProgress"
|
||||||
v-model="form.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>
|
<span class="icon is-small is-left"><i class="fa-solid fa-file" /></span>
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<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>.
|
<code>{{ config.app.output_template }}</code>.
|
||||||
For more information <NuxtLink href="https://github.com/yt-dlp/yt-dlp#output-template"
|
For more information <NuxtLink href="https://github.com/yt-dlp/yt-dlp#output-template"
|
||||||
target="_blank">visit this url</NuxtLink>.
|
target="_blank">visit this url</NuxtLink>.
|
||||||
|
|
@ -164,45 +171,25 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
|
<div class="column is-12">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline" for="config"
|
<label class="label is-inline" for="cli_options">
|
||||||
v-tooltip="'Extends current global yt-dlp config. (JSON)'">
|
Command arguments for yt-dlp
|
||||||
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>
|
</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea class="textarea" id="config" v-model="form.config" :disabled="addInProgress"
|
<input type="text" class="input" v-model="form.cli" id="cli_options" :disabled="addInProgress"
|
||||||
placeholder="--no-embed-metadata --no-embed-thumbnail"></textarea>
|
placeholder="command options to use, e.g. --no-embed-metadata --no-embed-thumbnail">
|
||||||
</div>
|
</div>
|
||||||
<span class="help">
|
<span class="help">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<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
|
<span>yt-dlp cli arguments. Check <NuxtLink target="_blank"
|
||||||
<code>cookiefile</code>, <code>paths</code>, and <code>outtmpl</code> etc. Warning: Use with
|
to="https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#general-options">this page</NuxtLink>.
|
||||||
caution
|
For more info. <span class="has-text-danger">Not all options are supported some are ignored. Use
|
||||||
some of those options can break yt-dlp or the frontend.</span>
|
with caution those arguments can break yt-dlp or the frontend.</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -221,12 +208,6 @@
|
||||||
<span>Cancel</span>
|
<span>Cancel</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -244,8 +225,6 @@ import { CronExpressionParser } from 'cron-parser'
|
||||||
const emitter = defineEmits(['cancel', 'submit']);
|
const emitter = defineEmits(['cancel', 'submit']);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const config = useConfigStore();
|
const config = useConfigStore();
|
||||||
const convertInProgress = ref(false);
|
|
||||||
const showAdvanced = useStorage('task.showAdvanced', false);
|
|
||||||
const showImport = useStorage('showImport', false);
|
const showImport = useStorage('showImport', false);
|
||||||
const import_string = ref('');
|
const import_string = ref('');
|
||||||
|
|
||||||
|
|
@ -269,10 +248,6 @@ const props = defineProps({
|
||||||
const form = reactive(props.task);
|
const form = reactive(props.task);
|
||||||
|
|
||||||
onMounted(() => {
|
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) {
|
if (!props.task?.preset || '' === props.task.preset) {
|
||||||
form.preset = toRaw(config.app.default_preset);
|
form.preset = toRaw(config.app.default_preset);
|
||||||
}
|
}
|
||||||
|
|
@ -304,60 +279,17 @@ const checkInfo = async () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof form.config === 'object') {
|
if (form?.cli && '' !== form.cli) {
|
||||||
form.config = JSON.stringify(form.config, null, 4);
|
const options = await convertOptions(form.cli);
|
||||||
}
|
if (null === options) {
|
||||||
|
return
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
form.cli = form.cli.trim(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
emitter('submit', { reference: toRaw(props.reference), task: toRaw(form) });
|
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 () => {
|
const importItem = async () => {
|
||||||
let val = import_string.value.trim()
|
let val = import_string.value.trim()
|
||||||
if (!val) {
|
if (!val) {
|
||||||
|
|
@ -384,7 +316,7 @@ const importItem = async () => {
|
||||||
return
|
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?')) {
|
if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -398,8 +330,8 @@ const importItem = async () => {
|
||||||
form.url = item.url
|
form.url = item.url
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.preset) {
|
if (item.template) {
|
||||||
form.preset = item.preset
|
form.template = item.template
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.timer) {
|
if (item.timer) {
|
||||||
|
|
@ -410,12 +342,19 @@ const importItem = async () => {
|
||||||
form.folder = item.folder
|
form.folder = item.folder
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.template) {
|
if (item.cli) {
|
||||||
form.template = item.template
|
form.cli = item.cli
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.config) {
|
if (item.preset) {
|
||||||
form.config = JSON.stringify(item.config, null, 2)
|
// -- 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 = ''
|
import_string.value = ''
|
||||||
|
|
@ -424,4 +363,35 @@ const importItem = async () => {
|
||||||
toast.error(`Failed to import string. ${e.message}`)
|
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>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,17 @@ div.is-centered {
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="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 class="icon"><i class="fa-solid fa-f" /></span>
|
||||||
<span v-text="item.format" />
|
<span v-text="item.format" />
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -83,6 +93,10 @@ div.is-centered {
|
||||||
<span class="icon"><i class="fa-solid fa-file" /></span>
|
<span class="icon"><i class="fa-solid fa-file" /></span>
|
||||||
<span>{{ item.template }}</span>
|
<span>{{ item.template }}</span>
|
||||||
</p>
|
</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">
|
<p class="is-text-overflow" v-if="item.cookies">
|
||||||
<span class="icon"><i class="fa-solid fa-cookie" /></span>
|
<span class="icon"><i class="fa-solid fa-cookie" /></span>
|
||||||
<span>Has cookies</span>
|
<span>Has cookies</span>
|
||||||
|
|
@ -293,12 +307,9 @@ const exportItem = item => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (data.args && typeof data.args === "string") {
|
if (has_data(data?.args) || has_data(data?.postprocessors)) {
|
||||||
data.args = JSON.parse(data.args)
|
toast.error("v1.0 presets are no longer supported target for export. Please update your preset.")
|
||||||
}
|
return
|
||||||
|
|
||||||
if (data.postprocessors && typeof data.postprocessors === "string") {
|
|
||||||
data.postprocessors = JSON.parse(data.postprocessors)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let userData = {}
|
let userData = {}
|
||||||
|
|
@ -311,7 +322,7 @@ const exportItem = item => {
|
||||||
}
|
}
|
||||||
|
|
||||||
userData['_type'] = 'preset'
|
userData['_type'] = 'preset'
|
||||||
userData['_version'] = '1.0'
|
userData['_version'] = '2.0'
|
||||||
|
|
||||||
return copyText(base64UrlEncode(JSON.stringify(userData)))
|
return copyText(base64UrlEncode(JSON.stringify(userData)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,7 @@ div.is-centered {
|
||||||
</div>
|
</div>
|
||||||
<div class="is-hidden-mobile">
|
<div class="is-hidden-mobile">
|
||||||
<span class="subtitle">
|
<span class="subtitle">
|
||||||
The task runner is simple queue system that allows you to schedule downloads. The tasks are run at the
|
The task runner is simple queue system that allows you to schedule downloads to run at the specific time.
|
||||||
scheduled time.
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -62,10 +61,6 @@ div.is-centered {
|
||||||
<a class="has-text-primary" v-tooltip="'Export task.'" @click.prevent="exportItem(item)">
|
<a class="has-text-primary" v-tooltip="'Export task.'" @click.prevent="exportItem(item)">
|
||||||
<span class="icon"><i class="fa-solid fa-file-export" /></span>
|
<span class="icon"><i class="fa-solid fa-file-export" /></span>
|
||||||
</a>
|
</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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
|
|
@ -86,11 +81,10 @@ div.is-centered {
|
||||||
<span class="icon"><i class="fa-solid fa-tv" /></span>
|
<span class="icon"><i class="fa-solid fa-tv" /></span>
|
||||||
<span>{{ item.preset ?? config.app.default_preset }}</span>
|
<span>{{ item.preset ?? config.app.default_preset }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<p class="is-text-overflow">
|
||||||
</div>
|
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||||
<div class="card-content" v-if="item?.raw">
|
<span>{{ item.cli }}</span>
|
||||||
<div class="content">
|
</p>
|
||||||
<pre><code>{{ filterItem(item) }}</code></pre>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
|
|
@ -120,17 +114,6 @@ div.is-centered {
|
||||||
icon="fas fa-exclamation-circle" v-if="!tasks || tasks.length < 1" />
|
icon="fas fa-exclamation-circle" v-if="!tasks || tasks.length < 1" />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -202,6 +185,8 @@ const resetForm = (closeForm = false) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateTasks = async items => {
|
const updateTasks = async items => {
|
||||||
|
let data = {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
addInProgress.value = true
|
addInProgress.value = true
|
||||||
|
|
||||||
|
|
@ -213,7 +198,7 @@ const updateTasks = async items => {
|
||||||
body: JSON.stringify(items),
|
body: JSON.stringify(items),
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await response.json()
|
data = await response.json()
|
||||||
|
|
||||||
if (200 !== response.status) {
|
if (200 !== response.status) {
|
||||||
toast.error(`Failed to update task. ${data.error}`);
|
toast.error(`Failed to update task. ${data.error}`);
|
||||||
|
|
@ -224,7 +209,7 @@ const updateTasks = async items => {
|
||||||
resetForm(true)
|
resetForm(true)
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(`Failed to update task. ${data.error}`);
|
toast.error(`Failed to update task. ${data?.error ?? e.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
addInProgress.value = false
|
addInProgress.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -272,11 +257,6 @@ const updateItem = async ({ reference, task }) => {
|
||||||
resetForm(true)
|
resetForm(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterItem = item => {
|
|
||||||
const { raw, ...rest } = item
|
|
||||||
return JSON.stringify(rest, null, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const editItem = item => {
|
const editItem = item => {
|
||||||
task.value = item
|
task.value = item
|
||||||
taskRef.value = item.id
|
taskRef.value = item.id
|
||||||
|
|
@ -327,21 +307,8 @@ const runNow = item => {
|
||||||
data.template = item.template
|
data.template = item.template
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.cookies) {
|
if (item.cli) {
|
||||||
data.cookies = item.cookies
|
data.cli = item.cli
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.emit('add_url', data)
|
socket.emit('add_url', data)
|
||||||
|
|
@ -359,69 +326,26 @@ const statusHandler = async stream => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportItem = async item => {
|
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))
|
const info = JSON.parse(JSON.stringify(item))
|
||||||
preset = JSON.parse(JSON.stringify(preset))
|
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
name: info.name,
|
name: info.name,
|
||||||
url: info.url,
|
url: info.url,
|
||||||
preset: 'default',
|
preset: info.preset,
|
||||||
timer: info.timer,
|
timer: info.timer,
|
||||||
folder: info.folder,
|
folder: info.folder,
|
||||||
template: info.template,
|
|
||||||
config: info.config,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- merge preset options with task args.
|
if (info.template) {
|
||||||
let args = {}
|
data.template = info.template
|
||||||
|
|
||||||
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 (preset.postprocessors && preset.postprocessors.length > 0) {
|
if (info.cli) {
|
||||||
args.postprocessors = preset.postprocessors
|
data.cli = info.cli
|
||||||
}
|
|
||||||
|
|
||||||
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]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data['_type'] = 'task'
|
data['_type'] = 'task'
|
||||||
data['_version'] = '1.1'
|
data['_version'] = '2.0'
|
||||||
|
|
||||||
return copyText(base64UrlEncode(JSON.stringify(data)));
|
return copyText(base64UrlEncode(JSON.stringify(data)));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,12 @@ const CONFIG_KEYS = {
|
||||||
presets: [
|
presets: [
|
||||||
{
|
{
|
||||||
'name': 'default',
|
'name': 'default',
|
||||||
'format': 'default'
|
'format': 'default',
|
||||||
|
'folder': '',
|
||||||
|
'template': '',
|
||||||
|
'cookies': '',
|
||||||
|
'cli': '',
|
||||||
|
'default': true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
folders: [],
|
folders: [],
|
||||||
|
|
|
||||||
|
|
@ -428,6 +428,38 @@ const base64UrlDecode = input => {
|
||||||
return atob(base64);
|
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 {
|
export {
|
||||||
ag_set,
|
ag_set,
|
||||||
ag,
|
ag,
|
||||||
|
|
@ -452,4 +484,5 @@ export {
|
||||||
convertCliOptions,
|
convertCliOptions,
|
||||||
base64UrlEncode,
|
base64UrlEncode,
|
||||||
base64UrlDecode,
|
base64UrlDecode,
|
||||||
|
has_data,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue