Make presets more powerful
This commit is contained in:
parent
61fd6d45f6
commit
cc21a93cb5
12 changed files with 439 additions and 235 deletions
|
|
@ -14,7 +14,7 @@ from .config import Config
|
||||||
from .Emitter import Emitter
|
from .Emitter import Emitter
|
||||||
from .ffprobe import ffprobe
|
from .ffprobe import ffprobe
|
||||||
from .ItemDTO import ItemDTO
|
from .ItemDTO import ItemDTO
|
||||||
from .Utils import get_opts, merge_config
|
from .YTDLPOpts import YTDLPOpts
|
||||||
|
|
||||||
LOG = logging.getLogger("download")
|
LOG = logging.getLogger("download")
|
||||||
|
|
||||||
|
|
@ -114,22 +114,30 @@ class Download:
|
||||||
|
|
||||||
def _download(self):
|
def _download(self):
|
||||||
try:
|
try:
|
||||||
params: dict = get_opts(self.preset, merge_config(self.default_ytdl_opts, self.ytdl_opts))
|
params = (
|
||||||
params.update(
|
YTDLPOpts.get_instance()
|
||||||
{
|
.preset(self.preset)
|
||||||
"color": "no_color",
|
.add(self.ytdl_opts, from_user=True)
|
||||||
"paths": {"home": self.download_dir, "temp": self.temp_path},
|
.add(
|
||||||
"outtmpl": {"default": self.template, "chapter": self.template_chapter},
|
{
|
||||||
"noprogress": True,
|
"color": "no_color",
|
||||||
"break_on_existing": True,
|
"paths": {"home": self.download_dir, "temp": self.temp_path},
|
||||||
"progress_hooks": [self._progress_hook],
|
"outtmpl": {"default": self.template, "chapter": self.template_chapter},
|
||||||
"postprocessor_hooks": [self._postprocessor_hook],
|
"noprogress": True,
|
||||||
"ignoreerrors": False,
|
"break_on_existing": True,
|
||||||
}
|
"ignoreerrors": False,
|
||||||
|
},
|
||||||
|
from_user=False,
|
||||||
|
)
|
||||||
|
.get_all()
|
||||||
)
|
)
|
||||||
|
|
||||||
if "format" not in params and self.default_ytdl_opts.get("format", None):
|
params.update(
|
||||||
params["format"] = "best"
|
{
|
||||||
|
"progress_hooks": [self._progress_hook],
|
||||||
|
"postprocessor_hooks": [self._postprocessor_hook],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if self.debug:
|
if self.debug:
|
||||||
params["verbose"] = True
|
params["verbose"] = True
|
||||||
|
|
@ -137,7 +145,9 @@ class Download:
|
||||||
|
|
||||||
if self.info.cookies:
|
if self.info.cookies:
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(self.temp_path, f"cookie_{self.info._id}.txt"), "w") as f:
|
cookie_file = os.path.join(self.temp_path, f"cookie_{self.info._id}.txt")
|
||||||
|
LOG.debug(f"Creating cookie file for '{self.info.id}: {self.info.title}' - '{cookie_file}'.")
|
||||||
|
with open(cookie_file, "w") as f:
|
||||||
f.write(self.info.cookies)
|
f.write(self.info.cookies)
|
||||||
params["cookiefile"] = f.name
|
params["cookiefile"] = f.name
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|
@ -175,6 +185,7 @@ class Download:
|
||||||
|
|
||||||
self.status_queue.put({"id": self.id, "status": "finished" if ret == 0 else "error"})
|
self.status_queue.put({"id": self.id, "status": "finished" if ret == 0 else "error"})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
LOG.exception(exc)
|
||||||
self.status_queue.put({"id": self.id, "status": "error", "msg": str(exc), "error": str(exc)})
|
self.status_queue.put({"id": self.id, "status": "error", "msg": str(exc), "error": str(exc)})
|
||||||
|
|
||||||
LOG.info(f'Task id="{self.info.id}" PID="{os.getpid()}" title="{self.info.title}" completed.')
|
LOG.info(f'Task id="{self.info.id}" PID="{os.getpid()}" title="{self.info.title}" completed.')
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ from .Download import Download
|
||||||
from .Emitter import Emitter
|
from .Emitter import Emitter
|
||||||
from .EventsSubscriber import Events
|
from .EventsSubscriber import Events
|
||||||
from .ItemDTO import ItemDTO
|
from .ItemDTO import ItemDTO
|
||||||
|
from .Presets import Presets
|
||||||
from .Singleton import Singleton
|
from .Singleton import Singleton
|
||||||
from .Utils import calc_download_path, extract_info, get_opts, is_downloaded, merge_config
|
from .Utils import calc_download_path, extract_info, is_downloaded
|
||||||
|
from .YTDLPOpts import YTDLPOpts
|
||||||
|
|
||||||
LOG = logging.getLogger("DownloadQueue")
|
LOG = logging.getLogger("DownloadQueue")
|
||||||
|
|
||||||
|
|
@ -352,9 +354,21 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
template: str = "",
|
template: str = "",
|
||||||
already=None,
|
already=None,
|
||||||
):
|
):
|
||||||
|
_preset = Presets.get_instance().get(name=preset)
|
||||||
|
|
||||||
config = config if config else {}
|
config = config if config else {}
|
||||||
folder = str(folder) if folder else ""
|
folder = str(folder) if folder else ""
|
||||||
|
|
||||||
|
if _preset:
|
||||||
|
if _preset.folder and not folder:
|
||||||
|
folder = _preset.folder
|
||||||
|
|
||||||
|
if _preset.template and not template:
|
||||||
|
template = _preset.template
|
||||||
|
|
||||||
|
if _preset.cookies and not cookies:
|
||||||
|
cookies = _preset.cookies
|
||||||
|
|
||||||
filePath = calc_download_path(base_path=self.config.download_path, folder=folder)
|
filePath = calc_download_path(base_path=self.config.download_path, folder=folder)
|
||||||
yt_conf = {}
|
yt_conf = {}
|
||||||
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")
|
||||||
|
|
@ -395,7 +409,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
"func": lambda _, msg: logs.append(msg),
|
"func": lambda _, msg: logs.append(msg),
|
||||||
"level": logging.WARNING,
|
"level": logging.WARNING,
|
||||||
},
|
},
|
||||||
**get_opts(preset, merge_config(self.config.ytdl_options, config)),
|
**YTDLPOpts.get_instance().preset(name=preset).add(config=config, from_user=True).get_all(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if cookies:
|
if cookies:
|
||||||
|
|
|
||||||
|
|
@ -708,7 +708,7 @@ class HttpAPI(Common):
|
||||||
item["id"] = str(uuid.uuid4())
|
item["id"] = str(uuid.uuid4())
|
||||||
|
|
||||||
if not item.get("args", None) or str(item.get("args")).strip() == "":
|
if not item.get("args", None) or str(item.get("args")).strip() == "":
|
||||||
item["config"] = {}
|
item["args"] = {}
|
||||||
|
|
||||||
if item.get("args", None) and isinstance(item.get("args"), str):
|
if item.get("args", None) and isinstance(item.get("args"), str):
|
||||||
item["args"] = json.loads(item.get("args"))
|
item["args"] = json.loads(item.get("args"))
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,21 @@ class Preset:
|
||||||
format: str
|
format: str
|
||||||
"""The format of the preset."""
|
"""The format of the preset."""
|
||||||
|
|
||||||
args: dict[str, list[str] | bool] | None = field(default_factory=dict)
|
args: dict[str, list[str] | bool] = field(default_factory=dict)
|
||||||
"""The arguments of the preset."""
|
"""The arguments of the preset."""
|
||||||
|
|
||||||
postprocessors: list | None = field(default_factory=list)
|
postprocessors: list = field(default_factory=list)
|
||||||
"""The postprocessors of the preset."""
|
"""The postprocessors of the preset."""
|
||||||
|
|
||||||
|
folder: str = ""
|
||||||
|
"""The default download folder to use if non is given."""
|
||||||
|
|
||||||
|
template: str = ""
|
||||||
|
"""The default template to use if non is given."""
|
||||||
|
|
||||||
|
cookies: str = ""
|
||||||
|
"""The default cookies to use if non is given."""
|
||||||
|
|
||||||
default: bool = False
|
default: bool = False
|
||||||
|
|
||||||
def serialize(self) -> dict:
|
def serialize(self) -> dict:
|
||||||
|
|
|
||||||
|
|
@ -31,52 +31,7 @@ class StreamingError(Exception):
|
||||||
"""Raised when an error occurs during streaming."""
|
"""Raised when an error occurs during streaming."""
|
||||||
|
|
||||||
|
|
||||||
def get_opts(preset: str, ytdl_opts: dict) -> dict:
|
def get_video_info(url: str, ytdlp_opts: dict | None = None, no_archive: bool = True) -> Any | dict[str, Any] | None:
|
||||||
"""
|
|
||||||
Returns ytdlp options download options
|
|
||||||
|
|
||||||
Args:
|
|
||||||
preset (str): the name of the preset selected.
|
|
||||||
ytdl_opts (dict): current options selected
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ytdl extra options
|
|
||||||
|
|
||||||
"""
|
|
||||||
if "format" in ytdl_opts and len(ytdl_opts["format"]) > 2:
|
|
||||||
format = ytdl_opts["format"]
|
|
||||||
LOG.info(f"Format '{format}' was given via yt-dlp options. Therefore, the preset will be ignored.")
|
|
||||||
return ytdl_opts
|
|
||||||
|
|
||||||
opts = copy.deepcopy(ytdl_opts)
|
|
||||||
|
|
||||||
if "default" == preset:
|
|
||||||
LOG.debug("Using default preset.")
|
|
||||||
return opts
|
|
||||||
|
|
||||||
from .Presets import Presets
|
|
||||||
|
|
||||||
p = Presets.get_instance().get(name=preset)
|
|
||||||
if not p:
|
|
||||||
LOG.error(f"Preset '{preset}' is not defined as preset.")
|
|
||||||
return opts
|
|
||||||
|
|
||||||
opts["format"] = p.get("format")
|
|
||||||
|
|
||||||
postprocessors = p.get("postprocessors", [])
|
|
||||||
if isinstance(postprocessors, list) and len(postprocessors) > 0:
|
|
||||||
opts["postprocessors"] = postprocessors
|
|
||||||
|
|
||||||
args = p.get("args", {})
|
|
||||||
if isinstance(args, dict) and len(args) > 0:
|
|
||||||
for key, value in args.items():
|
|
||||||
opts[key] = value
|
|
||||||
|
|
||||||
LOG.debug(f"Using preset '{preset}', altered options: {opts}")
|
|
||||||
return opts
|
|
||||||
|
|
||||||
|
|
||||||
def get_video_info(url: str, ytdlp_opts: dict = None, no_archive: bool = True) -> Any | dict[str, Any] | None:
|
|
||||||
"""
|
"""
|
||||||
Extracts video information from the given URL.
|
Extracts video information from the given URL.
|
||||||
|
|
||||||
|
|
@ -111,6 +66,11 @@ def calc_download_path(base_path: str, folder: str | None = None, create_path: b
|
||||||
"""
|
"""
|
||||||
Calculates download path and prevents folder traversal.
|
Calculates download path and prevents folder traversal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_path (str): Base download path.
|
||||||
|
folder (str): Folder to add to the base path.
|
||||||
|
create_path (bool): Create the path if it does not exist.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Download path with base folder factored in.
|
Download path with base folder factored in.
|
||||||
|
|
||||||
|
|
@ -135,29 +95,33 @@ def calc_download_path(base_path: str, folder: str | None = None, create_path: b
|
||||||
|
|
||||||
|
|
||||||
def extract_info(config: dict, url: str, debug: bool = False) -> dict:
|
def extract_info(config: dict, url: str, debug: bool = False) -> dict:
|
||||||
|
"""
|
||||||
|
Extracts video information from the given URL.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config (dict): Configuration options.
|
||||||
|
url (str): URL to extract information from.
|
||||||
|
debug (bool): Enable debug logging.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Video information.
|
||||||
|
|
||||||
|
"""
|
||||||
log_wrapper = LogWrapper()
|
log_wrapper = LogWrapper()
|
||||||
|
|
||||||
params: dict = {
|
params: dict = {
|
||||||
|
**config,
|
||||||
"color": "no_color",
|
"color": "no_color",
|
||||||
"extract_flat": True,
|
"extract_flat": True,
|
||||||
"skip_download": True,
|
"skip_download": True,
|
||||||
"ignoreerrors": True,
|
"ignoreerrors": True,
|
||||||
"ignore_no_formats_error": True,
|
"ignore_no_formats_error": True,
|
||||||
**config,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Remove keys that are not needed for info extraction.
|
# Remove keys that are not needed for info extraction.
|
||||||
keys: list = [
|
keys_to_remove = [key for key in params if str(key).startswith("write") or key in ["postprocessors"]]
|
||||||
"writeinfojson",
|
for key in keys_to_remove:
|
||||||
"writethumbnail",
|
params.pop(key, None)
|
||||||
"writedescription",
|
|
||||||
"writeautomaticsub",
|
|
||||||
"postprocessors",
|
|
||||||
]
|
|
||||||
|
|
||||||
for key in keys:
|
|
||||||
if key in params:
|
|
||||||
params.pop(key)
|
|
||||||
|
|
||||||
log_wrapper.add_target(target=logging.getLogger("yt-dlp"), level=logging.DEBUG if debug else logging.WARNING)
|
log_wrapper.add_target(target=logging.getLogger("yt-dlp"), level=logging.DEBUG if debug else logging.WARNING)
|
||||||
if debug:
|
if debug:
|
||||||
|
|
@ -166,7 +130,6 @@ def extract_info(config: dict, url: str, debug: bool = False) -> dict:
|
||||||
params["quiet"] = True
|
params["quiet"] = True
|
||||||
|
|
||||||
if "callback" in params:
|
if "callback" in params:
|
||||||
# callback can be a function or dict with {level: level, func: target}
|
|
||||||
if isinstance(params["callback"], dict):
|
if isinstance(params["callback"], dict):
|
||||||
log_wrapper.add_target(
|
log_wrapper.add_target(
|
||||||
target=params["callback"]["func"],
|
target=params["callback"]["func"],
|
||||||
|
|
@ -186,47 +149,56 @@ def extract_info(config: dict, url: str, debug: bool = False) -> dict:
|
||||||
|
|
||||||
|
|
||||||
def merge_dict(source: dict, destination: dict) -> dict:
|
def merge_dict(source: dict, destination: dict) -> dict:
|
||||||
"""Merge data from source into destination"""
|
"""
|
||||||
|
Merge data from source into destination safely.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source (dict): Source data
|
||||||
|
destination (dict): Destination data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: The merged dictionary
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not isinstance(source, dict) or not isinstance(destination, dict):
|
||||||
|
msg = "Both source and destination must be dictionaries."
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
destination_copy = copy.deepcopy(destination)
|
destination_copy = copy.deepcopy(destination)
|
||||||
|
|
||||||
for key, value in source.items():
|
for key, value in source.items():
|
||||||
destination_key_value = destination_copy.get(key)
|
if key in {"__class__", "__dict__", "__globals__", "__builtins__"}:
|
||||||
if isinstance(value, dict) and isinstance(destination_key_value, dict):
|
continue
|
||||||
destination_copy[key] = merge_dict(source=value, destination=destination_copy.setdefault(key, {}))
|
|
||||||
elif isinstance(value, list) and isinstance(destination_key_value, list):
|
destination_value = destination_copy.get(key)
|
||||||
destination_copy[key] = destination_key_value + value
|
|
||||||
|
# Recursively merge dictionaries
|
||||||
|
if isinstance(value, dict) and isinstance(destination_value, dict):
|
||||||
|
destination_copy[key] = merge_dict(value, destination_value)
|
||||||
|
|
||||||
|
# Safely extend lists without reference issues
|
||||||
|
elif isinstance(value, list) and isinstance(destination_value, list):
|
||||||
|
destination_copy[key] = copy.deepcopy(destination_value) + copy.deepcopy(value)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
destination_copy[key] = value
|
destination_copy[key] = copy.deepcopy(value)
|
||||||
|
|
||||||
return destination_copy
|
return destination_copy
|
||||||
|
|
||||||
|
|
||||||
def merge_config(config: dict, new_config: dict) -> dict:
|
def is_downloaded(archive_file: str, url: str) -> tuple[bool, dict[str | None, str | None, str | None]]:
|
||||||
"""
|
"""
|
||||||
Merge user provided config into default config
|
Check if the video is already downloaded.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config (dict): Default config
|
archive_file (str): Archive file path.
|
||||||
new_config (dict): User provided config
|
url (str): URL to check.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Merged config
|
bool: True if the video is already downloaded.
|
||||||
|
dict: Video information.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
for key in IGNORED_KEYS:
|
|
||||||
if key in new_config:
|
|
||||||
LOG.error(f"Key '{key}' is not allowed to be manually set via config.")
|
|
||||||
del new_config[key]
|
|
||||||
|
|
||||||
conf = merge_dict(new_config, config)
|
|
||||||
|
|
||||||
if "impersonate" in conf:
|
|
||||||
conf["impersonate"] = ImpersonateTarget.from_str(conf["impersonate"])
|
|
||||||
|
|
||||||
return conf
|
|
||||||
|
|
||||||
|
|
||||||
def is_downloaded(archive_file: str, url: str) -> tuple[bool, dict[str | None, str | None, str | None]]:
|
|
||||||
global YTDLP_INFO_CLS # noqa: PLW0603
|
global YTDLP_INFO_CLS # noqa: PLW0603
|
||||||
|
|
||||||
idDict = {
|
idDict = {
|
||||||
|
|
@ -301,11 +273,7 @@ def load_file(file: str, check_type=None) -> tuple[dict | list, bool, str]:
|
||||||
if check_type:
|
if check_type:
|
||||||
assert isinstance(opts, check_type) # noqa: S101
|
assert isinstance(opts, check_type) # noqa: S101
|
||||||
|
|
||||||
return (
|
return (opts, True, "")
|
||||||
opts,
|
|
||||||
True,
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
with open(file) as json_data:
|
with open(file) as json_data:
|
||||||
from pyjson5 import load as json5_load
|
from pyjson5 import load as json5_load
|
||||||
|
|
@ -316,23 +284,11 @@ def load_file(file: str, check_type=None) -> tuple[dict | list, bool, str]:
|
||||||
if check_type:
|
if check_type:
|
||||||
assert isinstance(opts, check_type) # noqa: S101
|
assert isinstance(opts, check_type) # noqa: S101
|
||||||
|
|
||||||
return (
|
return (opts, True, "")
|
||||||
opts,
|
|
||||||
True,
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
return (
|
return ({}, False, f"Failed to assert that the contents '{type(opts)}' are of type '{check_type}'.")
|
||||||
{},
|
|
||||||
False,
|
|
||||||
f"Failed to assert that the contents '{type(opts)}' are of type '{check_type}'.",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return (
|
return ({}, False, f"{e}")
|
||||||
{},
|
|
||||||
False,
|
|
||||||
f"{e}",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def check_id(file: pathlib.Path) -> bool | str:
|
def check_id(file: pathlib.Path) -> bool | str:
|
||||||
|
|
@ -340,10 +296,12 @@ def check_id(file: pathlib.Path) -> bool | str:
|
||||||
Check if we are able to get an id from the file name.
|
Check if we are able to get an id from the file name.
|
||||||
if so check if any video file with the same id exists.
|
if so check if any video file with the same id exists.
|
||||||
|
|
||||||
:param basePath: Base path to strip.
|
Args:
|
||||||
:param file: File to check.
|
file (pathlib.Path): File to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool|str: False if no file found, else the file path.
|
||||||
|
|
||||||
:return: False if no id found, otherwise the id.
|
|
||||||
"""
|
"""
|
||||||
match = re.search(r"(?<=\[)(?:youtube-)?(?P<id>[a-zA-Z0-9\-_]{11})(?=\])", file.stem, re.IGNORECASE)
|
match = re.search(r"(?<=\[)(?:youtube-)?(?P<id>[a-zA-Z0-9\-_]{11})(?=\])", file.stem, re.IGNORECASE)
|
||||||
if not match:
|
if not match:
|
||||||
|
|
@ -371,14 +329,18 @@ def ag(array: dict | list, path: list[str | int] | str | int, default: Any = Non
|
||||||
"""
|
"""
|
||||||
dict/array getter: Retrieve a value from a nested dict or object using a path.
|
dict/array getter: Retrieve a value from a nested dict or object using a path.
|
||||||
|
|
||||||
:param array_or_object: dict-like or object from which to retrieve values
|
Args:
|
||||||
:param path: string, list, or None. Represents the path to retrieve:
|
array (dict|list): dict-like or object from which to retrieve values.
|
||||||
- If None or empty string, returns the entire structure.
|
path (list|str|int): Represents the path to retrieve:
|
||||||
- If list, tries each path and returns the first found.
|
- If None or empty string, returns the entire structure.
|
||||||
- If string, navigates through nested dict keys separated by `separator`.
|
- If list, tries each path and returns the first found.
|
||||||
:param default: Value (or callable) returned if nothing is found.
|
- If string, navigates through nested dict keys separated by `separator`.
|
||||||
:param separator: Separator for nested paths in strings.
|
default (Any): Value (or callable) returned if nothing is found.
|
||||||
:return: The found value or the default if not found.
|
separator (str): Separator for nested paths in strings.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: The found value or the default if not found.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if path is None or path == "":
|
if path is None or path == "":
|
||||||
return array
|
return array
|
||||||
|
|
@ -523,8 +485,12 @@ def get_sidecar_subtitles(file: pathlib.Path) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Get sidecar files for the given file.
|
Get sidecar files for the given file.
|
||||||
|
|
||||||
:param file: File to get sidecar files for.
|
Args:
|
||||||
:return: List of sidecar files.
|
file (pathlib.Path): The video file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of sidecar files.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
files = []
|
files = []
|
||||||
|
|
||||||
|
|
|
||||||
102
app/library/YTDLPOpts.py
Normal file
102
app/library/YTDLPOpts.py
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
from .Presets import Presets
|
||||||
|
from .Singleton import Singleton
|
||||||
|
from .Utils import IGNORED_KEYS, calc_download_path, merge_dict
|
||||||
|
|
||||||
|
|
||||||
|
class YTDLPOpts(metaclass=Singleton):
|
||||||
|
item_opts: dict = {}
|
||||||
|
preset_opts: dict = {}
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
"""The instance of the class."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._config = Config.get_instance()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_instance() -> "YTDLPOpts":
|
||||||
|
"""
|
||||||
|
Get the instance of the class.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Presets: The instance of the class
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not YTDLPOpts._instance:
|
||||||
|
YTDLPOpts._instance = YTDLPOpts()
|
||||||
|
|
||||||
|
return YTDLPOpts._instance
|
||||||
|
|
||||||
|
def add(self, config: dict, from_user: bool = False):
|
||||||
|
for key, value in config.items():
|
||||||
|
if key in IGNORED_KEYS and from_user:
|
||||||
|
continue
|
||||||
|
self.item_opts[key] = value
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def preset(self, name: str, with_cookies: bool = False) -> "YTDLPOpts":
|
||||||
|
preset = Presets.get_instance().get(name=name)
|
||||||
|
if not preset or "default" == name:
|
||||||
|
return self
|
||||||
|
|
||||||
|
if preset.cookies and with_cookies:
|
||||||
|
file = Path(self._config.config_path, "cookies", f"{preset.id}.txt")
|
||||||
|
|
||||||
|
if not file.parent.exists():
|
||||||
|
file.parent.mkdir(parents=True)
|
||||||
|
|
||||||
|
with open(file, "w") as f:
|
||||||
|
f.write(preset.cookies)
|
||||||
|
|
||||||
|
self.preset_opts["cookiefile"] = str(file)
|
||||||
|
|
||||||
|
if preset.format:
|
||||||
|
self.preset_opts["format"] = preset.format
|
||||||
|
|
||||||
|
if preset.template:
|
||||||
|
self.preset_opts["outtmpl"] = {"default": preset.template, "chapter": self._config.output_template_chapter}
|
||||||
|
|
||||||
|
if preset.folder:
|
||||||
|
self.preset_opts["paths"] = {
|
||||||
|
"home": calc_download_path(base_path=self._config.download_path, folder=preset.folder),
|
||||||
|
"temp": self._config.temp_path,
|
||||||
|
}
|
||||||
|
|
||||||
|
if preset.postprocessors and isinstance(preset.postprocessors, list) and len(preset.postprocessors) > 0:
|
||||||
|
self.preset_opts["postprocessors"] = preset.postprocessors
|
||||||
|
|
||||||
|
if preset.args and isinstance(preset.args, dict) and len(preset.args) > 0:
|
||||||
|
for key, value in preset.args.items():
|
||||||
|
if key in IGNORED_KEYS:
|
||||||
|
continue
|
||||||
|
self.preset_opts[key] = value
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_all(self, keep: bool = False) -> dict:
|
||||||
|
default_opts = self._config.ytdl_options
|
||||||
|
default_opts["paths"] = {"home": self._config.download_path, "temp": self._config.temp_path}
|
||||||
|
default_opts["outtmpl"] = {
|
||||||
|
"default": self._config.output_template,
|
||||||
|
"chapter": self._config.output_template_chapter,
|
||||||
|
}
|
||||||
|
|
||||||
|
if "format" in default_opts or "format" in self.item_opts:
|
||||||
|
return merge_dict(default_opts, self.item_opts)
|
||||||
|
|
||||||
|
data = merge_dict(merge_dict(self.preset_opts, default_opts), self.item_opts)
|
||||||
|
|
||||||
|
if not keep:
|
||||||
|
self.presets_opts = {}
|
||||||
|
self.item_opts = {}
|
||||||
|
|
||||||
|
if "impersonate" in data:
|
||||||
|
from yt_dlp.networking.impersonate import ImpersonateTarget
|
||||||
|
|
||||||
|
data["impersonate"] = ImpersonateTarget.from_str(data["impersonate"])
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
@ -3,12 +3,18 @@
|
||||||
"id": "3e163c6c-64eb-4448-924f-814b629b3810",
|
"id": "3e163c6c-64eb-4448-924f-814b629b3810",
|
||||||
"name": "default",
|
"name": "default",
|
||||||
"format": "default",
|
"format": "default",
|
||||||
|
"folder": "",
|
||||||
|
"template": "",
|
||||||
|
"cookies": "",
|
||||||
"default": true
|
"default": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "5bf9c42b-8852-468a-99f5-915622dfba25",
|
"id": "5bf9c42b-8852-468a-99f5-915622dfba25",
|
||||||
"name": "Best video and audio",
|
"name": "Best video and audio",
|
||||||
"format": "bv+ba/b",
|
"format": "bv+ba/b",
|
||||||
|
"folder": "",
|
||||||
|
"template": "",
|
||||||
|
"cookies": "",
|
||||||
"default": true
|
"default": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -20,6 +26,9 @@
|
||||||
"vcodec:h264"
|
"vcodec:h264"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"folder": "",
|
||||||
|
"template": "",
|
||||||
|
"cookies": "",
|
||||||
"default": true
|
"default": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -31,6 +40,9 @@
|
||||||
"vcodec:h264"
|
"vcodec:h264"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"folder": "",
|
||||||
|
"template": "",
|
||||||
|
"cookies": "",
|
||||||
"default": true
|
"default": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -63,6 +75,9 @@
|
||||||
"when": "playlist"
|
"when": "playlist"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"folder": "",
|
||||||
|
"template": "",
|
||||||
|
"cookies": "",
|
||||||
"default": true
|
"default": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,8 @@ hr {
|
||||||
padding-top: 0.5em;
|
padding-top: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.play-overlay, .is-pointer {
|
.play-overlay,
|
||||||
|
.is-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,3 +255,7 @@ hr {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: top;
|
object-position: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-pre {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,8 +107,8 @@
|
||||||
Cookies
|
Cookies
|
||||||
</label>
|
</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea class="textarea" id="ytdlpCookies" v-model="ytdlpCookies"
|
<textarea class="textarea is-pre" id="ytdlpCookies" v-model="ytdlpCookies"
|
||||||
:disabled="!socket.isConnected || addInProgress"></textarea>
|
:disabled="!socket.isConnected || addInProgress" />
|
||||||
</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>
|
||||||
|
|
|
||||||
|
|
@ -75,100 +75,162 @@
|
||||||
|
|
||||||
<div class="column is-12">
|
<div class="column is-12">
|
||||||
<form id="presetForm" @submit.prevent="checkInfo()">
|
<form id="presetForm" @submit.prevent="checkInfo()">
|
||||||
<div class="box">
|
<div class="card">
|
||||||
<div class="columns is-multiline is-mobile">
|
<div class="card-header">
|
||||||
<div class="column is-12">
|
<div class="card-header-title">
|
||||||
<h1 class="title is-6" style="border-bottom: 1px solid #dbdbdb;">
|
<span class="icon-text">
|
||||||
<span class="icon-text">
|
<span class="icon"><i class="fa-solid" :class="reference ? 'fa-cog' : 'fa-plus'" /></span>
|
||||||
<span class="icon"><i class="fa-solid" :class="reference ? 'fa-cog' : 'fa-plus'" /></span>
|
<span>{{ reference ? 'Edit' : 'Add' }}</span>
|
||||||
<span>{{ reference ? 'Edit' : 'Add' }}</span>
|
</span>
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="column is-6-tablet is-12-mobile">
|
<div class="card-content">
|
||||||
<div class="field">
|
<div class="columns is-multiline is-mobile">
|
||||||
<label class="label is-inline" for="name" v-text="'Name'" />
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
<div class="control has-icons-left">
|
<div class="field">
|
||||||
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress">
|
<label class="label is-inline" for="name" v-text="'Name'" />
|
||||||
<span class="icon is-small is-left"><i class="fa-solid fa-n" /></span>
|
<div class="control has-icons-left">
|
||||||
</div>
|
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress">
|
||||||
<span class="help">
|
<span class="icon is-small is-left"><i class="fa-solid fa-n" /></span>
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
</div>
|
||||||
<span>The name to refers to this custom settings.</span>
|
<span class="help">
|
||||||
</span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
</div>
|
<span>The name to refers to this custom settings.</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="column is-6-tablet is-12-mobile">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label is-inline" for="format" v-text="'Format'" />
|
|
||||||
<div class="control has-icons-left">
|
|
||||||
<input type="text" class="input" id="format" v-model="form.format" :disabled="addInProgress">
|
|
||||||
<span class="icon is-small is-left"><i class="fa-solid fa-f" /></span>
|
|
||||||
</div>
|
|
||||||
<span class="help">
|
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
|
||||||
<span>The yt-dlp <code>[--format, -f]</code> video format code. see <NuxtLink
|
|
||||||
href="https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#format-selection" target="blank">this
|
|
||||||
url</NuxtLink> for more info.</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="column is-6-tablet is-12-mobile">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label is-inline" for="args" v-tooltip="'Extends current global yt-dlp config. (JSON)'">
|
|
||||||
JSON yt-dlp config
|
|
||||||
</label>
|
|
||||||
<div class="control">
|
|
||||||
<textarea class="textarea" id="args" v-model="form.args" :disabled="addInProgress" placeholder="{}" />
|
|
||||||
</div>
|
|
||||||
<span class="help">
|
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
|
||||||
<span>Extends current global yt-dlp config with given options. Some fields are ignored like
|
|
||||||
<code>cookiefile</code>, <code>paths</code>, and <code>outtmpl</code> etc. Warning: Use with caution
|
|
||||||
some of those options can break yt-dlp or the frontend.</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="column is-6-tablet is-12-mobile">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label is-inline" for="postprocessors" v-tooltip="'Things to do after download is done.'">
|
|
||||||
JSON yt-dlp Post-Processors
|
|
||||||
</label>
|
|
||||||
<div class="control">
|
|
||||||
<textarea class="textarea" id="postprocessors" v-model="form.postprocessors"
|
|
||||||
:disabled="addInProgress" />
|
|
||||||
</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>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label is-inline" for="format" v-text="'Format'" />
|
||||||
|
<div class="control has-icons-left">
|
||||||
|
<input type="text" class="input" id="format" v-model="form.format" :disabled="addInProgress">
|
||||||
|
<span class="icon is-small is-left"><i class="fa-solid fa-f" /></span>
|
||||||
|
</div>
|
||||||
|
<span class="help">
|
||||||
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
|
<span>The yt-dlp <code>[--format, -f]</code> video format code. see <NuxtLink
|
||||||
|
href="https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#format-selection" target="blank">this
|
||||||
|
url</NuxtLink> for more info.</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label is-inline" for="folder">
|
||||||
|
Default Download path
|
||||||
|
</label>
|
||||||
|
<div class="control has-icons-left">
|
||||||
|
<input type="text" class="input" id="folder" placeholder="Leave empty to use default download path"
|
||||||
|
v-model="form.folder" :disabled="addInProgress" list="folders">
|
||||||
|
<span class="icon is-small is-left"><i class="fa-solid fa-folder" /></span>
|
||||||
|
</div>
|
||||||
|
<span class="help">
|
||||||
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
|
<span>Use this folder if non is given with URL. Leave empty to use default download path. Default
|
||||||
|
download path <code>{{ config.app.download_path }}</code>.</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label is-inline" for="output_template">
|
||||||
|
Default Output template
|
||||||
|
</label>
|
||||||
|
<div class="control has-icons-left">
|
||||||
|
<input type="text" class="input" id="output_template" :disabled="addInProgress"
|
||||||
|
placeholder="Leave empty to use default template." v-model="form.template">
|
||||||
|
<span class="icon is-small is-left"><i class="fa-solid fa-file" /></span>
|
||||||
|
</div>
|
||||||
|
<span class="help">
|
||||||
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
|
<span>Use this output template if non are given with URL. if not set, it will defaults to
|
||||||
|
<code>{{ config.app.output_template }}</code>.
|
||||||
|
For more information <NuxtLink href="https://github.com/yt-dlp/yt-dlp#output-template"
|
||||||
|
target="_blank">visit this url</NuxtLink>.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label is-inline" for="args" v-tooltip="'Extends current global yt-dlp config. (JSON)'">
|
||||||
|
JSON yt-dlp config
|
||||||
|
</label>
|
||||||
|
<div class="control">
|
||||||
|
<textarea class="textarea" id="args" v-model="form.args" :disabled="addInProgress"
|
||||||
|
placeholder="{}" />
|
||||||
|
</div>
|
||||||
|
<span class="help">
|
||||||
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
|
<span>Extends current global yt-dlp config with given options. Some fields are ignored like
|
||||||
|
<code>cookiefile</code>, <code>paths</code>, and <code>outtmpl</code> etc. Warning: Use with
|
||||||
|
caution
|
||||||
|
some of those options can break yt-dlp or the frontend.</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label is-inline" for="postprocessors"
|
||||||
|
v-tooltip="'Things to do after download is done.'">
|
||||||
|
JSON yt-dlp Post-Processors
|
||||||
|
</label>
|
||||||
|
<div class="control">
|
||||||
|
<textarea class="textarea" id="postprocessors" v-model="form.postprocessors"
|
||||||
|
:disabled="addInProgress" placeholder="[]" />
|
||||||
|
</div>
|
||||||
|
<span class="help">
|
||||||
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
|
<span>
|
||||||
|
Post-processing operations, refer to <NuxtLink
|
||||||
|
href="https://github.com/yt-dlp/yt-dlp/tree/master/yt_dlp/postprocessor" target="blank">this url
|
||||||
|
</NuxtLink> for more info. It's easier for you to use the <b>Convert CLI options</b> to get what
|
||||||
|
you
|
||||||
|
want and it will auto-populate the fields if necessary.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-12">
|
||||||
|
<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"
|
||||||
|
placeholder="Leave empty to use default cookies" />
|
||||||
|
</div>
|
||||||
|
<span class="help">
|
||||||
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
|
<span>Use this cookies if non are given with the URL. 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 class="column is-12">
|
<div class="card-footer">
|
||||||
<div class="field is-grouped is-grouped-right">
|
<div class="card-footer-item">
|
||||||
<p class="control">
|
<button class="button is-fullwidth is-primary" :disabled="addInProgress" type="submit"
|
||||||
<button class="button is-primary" :disabled="addInProgress" type="submit"
|
:class="{ 'is-loading': addInProgress }" form="presetForm">
|
||||||
:class="{ 'is-loading': addInProgress }" form="presetForm">
|
<span class="icon"><i class="fa-solid fa-save" /></span>
|
||||||
<span class="icon"><i class="fa-solid fa-save" /></span>
|
<span>Save</span>
|
||||||
<span>Save</span>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</p>
|
<div class="card-footer-item">
|
||||||
<p class="control">
|
<button class="button is-fullwidth is-danger" @click="emitter('cancel')" :disabled="addInProgress"
|
||||||
<button class="button is-danger" @click="emitter('cancel')" :disabled="addInProgress" type="button">
|
type="button">
|
||||||
<span class="icon"><i class="fa-solid fa-times" /></span>
|
<span class="icon"><i class="fa-solid fa-times" /></span>
|
||||||
<span>Cancel</span>
|
<span>Cancel</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -176,6 +238,9 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<datalist id="folders" v-if="config?.folders">
|
||||||
|
<option v-for="dir in config.folders" :key="dir" :value="dir" />
|
||||||
|
</datalist>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -204,6 +269,7 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const config = useConfigStore()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const convertInProgress = ref(false)
|
const convertInProgress = ref(false)
|
||||||
const form = reactive(JSON.parse(JSON.stringify(props.preset)))
|
const form = reactive(JSON.parse(JSON.stringify(props.preset)))
|
||||||
|
|
@ -252,7 +318,7 @@ const checkInfo = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof copy.args === 'object') {
|
if (typeof copy.args === 'object') {
|
||||||
copy.config = JSON.stringify(copy.args, null, 2);
|
copy.args = JSON.stringify(copy.args, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof copy.postprocessors === 'object') {
|
if (typeof copy.postprocessors === 'object') {
|
||||||
|
|
@ -285,7 +351,7 @@ const convertOptions = async () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.format || form.args || form.postprocessors) {
|
if (form.format || form.args || form.postprocessors || form.template || form.folder) {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
@ -305,6 +371,14 @@ const convertOptions = async () => {
|
||||||
delete response.opts.format
|
delete response.opts.format
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.output_template) {
|
||||||
|
form.template = response.output_template
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.download_path) {
|
||||||
|
form.folder = response.download_path
|
||||||
|
}
|
||||||
|
|
||||||
if (response.opts.postprocessors) {
|
if (response.opts.postprocessors) {
|
||||||
form.postprocessors = JSON.stringify(response.opts.postprocessors, null, 2)
|
form.postprocessors = JSON.stringify(response.opts.postprocessors, null, 2)
|
||||||
delete response.opts.postprocessors
|
delete response.opts.postprocessors
|
||||||
|
|
@ -349,6 +423,14 @@ const importPreset = async () => {
|
||||||
form.postprocessors = JSON.stringify(preset.postprocessors, null, 2)
|
form.postprocessors = JSON.stringify(preset.postprocessors, null, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (preset.output_template) {
|
||||||
|
form.template = response.output_template
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preset.folder) {
|
||||||
|
form.folder = response.folder
|
||||||
|
}
|
||||||
|
|
||||||
json_preset.value = ''
|
json_preset.value = ''
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline" for="cookies" v-tooltip="'Netscape HTTP Cookie format.'">Cookies</label>
|
<label class="label is-inline" for="cookies" v-tooltip="'Netscape HTTP Cookie format.'">Cookies</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea class="textarea" id="cookies" v-model="form.cookies" :disabled="addInProgress"></textarea>
|
<textarea class="textarea is-pre" id="cookies" v-model="form.cookies" :disabled="addInProgress" />
|
||||||
</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>
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,7 @@ onMounted(async () => socket.isConnected ? await reloadContent(true) : '')
|
||||||
|
|
||||||
const copyItem = item => {
|
const copyItem = item => {
|
||||||
let data = JSON.parse(JSON.stringify(item))
|
let data = JSON.parse(JSON.stringify(item))
|
||||||
const keys = ['id', 'default', 'raw']
|
const keys = ['id', 'default', 'raw', 'cookies']
|
||||||
keys.forEach(key => {
|
keys.forEach(key => {
|
||||||
if (key in data) {
|
if (key in data) {
|
||||||
delete data[key]
|
delete data[key]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue