removed deprecated features and flags

This commit is contained in:
arabcoders 2025-09-12 20:38:09 +03:00
parent 669f70d9b8
commit f490c78179
25 changed files with 146 additions and 363 deletions

View file

@ -26,7 +26,7 @@ live streams, and includes features like scheduling downloads, sending notificat
* Supports `curl-cffi`. See [yt-dlp documentation](https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#impersonation) * Supports `curl-cffi`. See [yt-dlp documentation](https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#impersonation)
* Bundled `pot provider plugin`. See [yt-dlp documentation](https://github.com/yt-dlp/yt-dlp/wiki/PO-Token-Guide) * Bundled `pot provider plugin`. See [yt-dlp documentation](https://github.com/yt-dlp/yt-dlp/wiki/PO-Token-Guide)
* Automatic updates for `yt-dlp` and custom `pip` packages. * Automatic updates for `yt-dlp` and custom `pip` packages.
* Conditions feature. * Conditions feature to apply custom options based on `yt-dlp` returned info.
* Custom browser extensions, bookmarklets and iOS shortcuts to send links to YTPTube instance. * Custom browser extensions, bookmarklets and iOS shortcuts to send links to YTPTube instance.
* A bundled executable version for Windows, macOS and Linux. `For non-docker users`. * A bundled executable version for Windows, macOS and Linux. `For non-docker users`.

View file

@ -489,9 +489,7 @@ class Download:
and self.info.downloaded_bytes and self.info.downloaded_bytes
and self.info.downloaded_bytes > 0 and self.info.downloaded_bytes > 0
): ):
self.logger.warning( self.logger.warning(f"Keeping temp folder '{self.temp_path}'. {self.info.status=}.")
f"Keeping temp folder '{self.temp_path}', as the reported status is not finished '{self.info.status}'."
)
return return
tmp_dir = Path(self.temp_path) tmp_dir = Path(self.temp_path)

View file

@ -152,9 +152,6 @@ class YTDLPOpts:
self._item_cli = [] self._item_cli = []
merge: list[str] = [] merge: list[str] = []
if self._config._ytdlp_cli_mutable and len(self._config._ytdlp_cli_mutable) > 1:
merge.append(self._config._ytdlp_cli_mutable)
if self._preset_cli and len(self._preset_cli) > 1: if self._preset_cli and len(self._preset_cli) > 1:
merge.append(self._preset_cli) merge.append(self._preset_cli)

View file

@ -7,13 +7,13 @@ import time
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from multiprocessing.managers import SyncManager from multiprocessing.managers import SyncManager
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Any
import coloredlogs import coloredlogs
from dotenv import load_dotenv from dotenv import load_dotenv
from .Singleton import Singleton from .Singleton import Singleton
from .Utils import FileLogFormatter, arg_converter from .Utils import FileLogFormatter
from .version import APP_BRANCH, APP_BUILD_DATE, APP_COMMIT_SHA, APP_VERSION from .version import APP_BRANCH, APP_BUILD_DATE, APP_COMMIT_SHA, APP_VERSION
if TYPE_CHECKING: if TYPE_CHECKING:
@ -96,9 +96,6 @@ class Config(metaclass=Singleton):
debugpy_port: int = 5678 debugpy_port: int = 5678
"""The port to use for the debugpy server.""" """The port to use for the debugpy server."""
socket_timeout: int = 30
"""The socket timeout to use for yt-dlp."""
extract_info_timeout: int = 70 extract_info_timeout: int = 70
"""The timeout to use for extracting video information.""" """The timeout to use for extracting video information."""
@ -138,9 +135,6 @@ class Config(metaclass=Singleton):
ignore_ui: bool = False ignore_ui: bool = False
"Ignore the UI and run the application in the background." "Ignore the UI and run the application in the background."
basic_mode: bool = False
"Run the frontend in basic mode."
default_preset: str = "default" default_preset: str = "default"
"The default preset to use when no preset is specified." "The default preset to use when no preset is specified."
@ -159,21 +153,12 @@ class Config(metaclass=Singleton):
console_enabled: bool = False console_enabled: bool = False
"Enable direct access to yt-dlp console." "Enable direct access to yt-dlp console."
browser_enabled: bool = True
"Enable file browser access."
browser_control_enabled: bool = False browser_control_enabled: bool = False
"Enable file browser control access." "Enable file browser control access."
ytdlp_auto_update: bool = True ytdlp_auto_update: bool = True
"""Enable in-place auto update of yt-dlp package.""" """Enable in-place auto update of yt-dlp package."""
ytdlp_cli: str = ""
"""The command line options to use for yt-dlp."""
_ytdlp_cli_mutable: str = ""
"""The command line options to use for yt-dlp."""
ytdlp_version: str | None = None ytdlp_version: str | None = None
"""The version of yt-dlp to use, if not set, the latest version will be used.""" """The version of yt-dlp to use, if not set, the latest version will be used."""
@ -206,8 +191,6 @@ class Config(metaclass=Singleton):
_immutable: tuple = ( _immutable: tuple = (
"ytdl_options", "ytdl_options",
"started", "started",
"ytdlp_cli",
"_ytdlp_cli_mutable",
"is_native", "is_native",
"app_version", "app_version",
"app_commit_sha", "app_commit_sha",
@ -219,7 +202,6 @@ class Config(metaclass=Singleton):
_int_vars: tuple = ( _int_vars: tuple = (
"port", "port",
"max_workers", "max_workers",
"socket_timeout",
"extract_info_timeout", "extract_info_timeout",
"debugpy_port", "debugpy_port",
"playlist_items_concurrency", "playlist_items_concurrency",
@ -238,10 +220,8 @@ class Config(metaclass=Singleton):
"ignore_ui", "ignore_ui",
"ui_update_title", "ui_update_title",
"pip_ignore_updates", "pip_ignore_updates",
"basic_mode",
"file_logging", "file_logging",
"console_enabled", "console_enabled",
"browser_enabled",
"browser_control_enabled", "browser_control_enabled",
"ytdlp_auto_update", "ytdlp_auto_update",
"prevent_premiere_live", "prevent_premiere_live",
@ -257,13 +237,10 @@ class Config(metaclass=Singleton):
"remove_files", "remove_files",
"ui_update_title", "ui_update_title",
"max_workers", "max_workers",
"basic_mode",
"default_preset", "default_preset",
"instance_title", "instance_title",
"console_enabled", "console_enabled",
"browser_enabled",
"browser_control_enabled", "browser_control_enabled",
"ytdlp_cli",
"file_logging", "file_logging",
"base_path", "base_path",
"is_native", "is_native",
@ -351,7 +328,7 @@ class Config(metaclass=Singleton):
if not self.base_path.endswith("/"): if not self.base_path.endswith("/"):
self.base_path += "/" self.base_path += "/"
numeric_level = getattr(logging, self.log_level.upper(), None) numeric_level: int | None = getattr(logging, self.log_level.upper(), None)
if not isinstance(numeric_level, int): if not isinstance(numeric_level, int):
msg = f"Invalid log level '{self.log_level}' specified." msg = f"Invalid log level '{self.log_level}' specified."
raise TypeError(msg) raise TypeError(msg)
@ -372,55 +349,26 @@ class Config(metaclass=Singleton):
debugpy.listen(("0.0.0.0", self.debugpy_port), in_process_debug_adapter=True) debugpy.listen(("0.0.0.0", self.debugpy_port), in_process_debug_adapter=True)
LOG.info(f"starting debugpy server on '0.0.0.0:{self.debugpy_port}'.") LOG.info(f"starting debugpy server on '0.0.0.0:{self.debugpy_port}'.")
except ImportError: except ImportError:
LOG.error("debugpy package not found, please install it with 'pip install debugpy'.") LOG.error("debugpy package not found, install it with 'uv sync'.")
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}")
opts_file: Path = Path(self.config_path) / "ytdlp.cli" if (Path(self.config_path) / "ytdlp.cli").exists():
if opts_file.exists() and opts_file.stat().st_size > 2: LOG.error("Support for ./ytdlp.cli file is removed, migrate to presets and remove the file.")
LOG.warning(
"Usage of 'ytdlp.cli' global config file is deprecated and will be removed in future releases. please migrate to presets."
)
with open(opts_file) as f:
self.ytdlp_cli = f.read().strip()
if self.ytdlp_cli:
self._ytdlp_cli_mutable = self.ytdlp_cli
try:
arg_converter(args=self.ytdlp_cli, level=True)
except Exception as e:
msg = f"Failed to parse yt-dlp cli options from '{opts_file}'. '{e!s}'."
raise ValueError(msg) from e
self._ytdlp_cli_mutable += f"\n--socket-timeout {self.socket_timeout}"
if self.keep_archive:
LOG.warning(
"The global 'keep_archive' option is deprecated and will be removed in future releases. please use presets instead."
)
archive_file: Path = Path(self.archive_file)
if not archive_file.exists():
LOG.info(f"Creating archive file '{archive_file}'.")
archive_file.touch(exist_ok=True)
LOG.info(f"keep archive option is enabled. Using archive file '{archive_file}' by default.")
self._ytdlp_cli_mutable += f"\n--download-archive {archive_file.as_posix()!s}"
if self.temp_keep: if self.temp_keep:
LOG.info("Keep temp files option is enabled.") LOG.warning("Keep temp files option is enabled.")
if self.auth_password and self.auth_username: if self.auth_password and self.auth_username:
LOG.warning(f"Basic authentication enabled with username '{self.auth_username}'.") LOG.info(f"Authentication enabled with username '{self.auth_username}'.")
if self.basic_mode:
LOG.info("The frontend is running in basic mode.")
if self.file_logging: if self.file_logging:
log_level_file = getattr(logging, self.log_level_file.upper(), None) log_level_file: int | None = getattr(logging, self.log_level_file.upper(), None)
if not isinstance(log_level_file, int): if not isinstance(log_level_file, int):
msg = f"Invalid file log level '{self.log_level_file}' specified." msg = f"Invalid file log level '{self.log_level_file}' specified."
raise TypeError(msg) raise TypeError(msg)
loggingPath = Path(self.config_path) / "logs" loggingPath: Path = Path(self.config_path) / "logs"
if not loggingPath.exists(): if not loggingPath.exists():
loggingPath.mkdir(parents=True, exist_ok=True) loggingPath.mkdir(parents=True, exist_ok=True)
@ -448,15 +396,17 @@ class Config(metaclass=Singleton):
self.started = time.time() self.started = time.time()
logging.getLogger("httpcore").setLevel(logging.INFO) _log_levels = (
for _tool in ("httpx", "urllib3.connectionpool", "apprise"): ("httpx", logging.WARNING),
logging.getLogger(_tool).setLevel(logging.WARNING) ("urllib3.connectionpool", logging.WARNING),
("apprise", logging.WARNING),
("httpcore", logging.INFO),
)
for _tool, _level in _log_levels:
logging.getLogger(_tool).setLevel(_level)
# check env
if self.app_env not in ("production", "development"): if self.app_env not in ("production", "development"):
msg: str = ( msg: str = f"Invalid app environment '{self.app_env}' specified. Must be 'production' or 'development'."
f"Invalid application environment '{self.app_env}' specified. Must be 'production' or 'development'."
)
raise ValueError(msg) raise ValueError(msg)
if "dev-master" == self.app_version: if "dev-master" == self.app_version:
@ -470,7 +420,7 @@ class Config(metaclass=Singleton):
if attribute.startswith("_"): if attribute.startswith("_"):
continue continue
value = getattr(vClass, attribute) value: Any = getattr(vClass, attribute)
if not callable(value): if not callable(value):
attrs[attribute] = value attrs[attribute] = value
@ -496,23 +446,6 @@ class Config(metaclass=Singleton):
""" """
return "production" == self.app_env return "production" == self.app_env
def get_ytdlp_args(self) -> dict:
"""
Get the yt-dlp command line options as a dictionary.
Returns:
dict: The yt-dlp command line options.
Deprecated:
Usage of global ytdlp.cli file is deprecated, please use presets instead.
"""
try:
return arg_converter(args=self._ytdlp_cli_mutable, level=True)
except Exception as e:
msg = f"Invalid ytdlp.cli options for yt-dlp. '{e!s}'."
raise ValueError(msg) from e
def frontend(self) -> dict: def frontend(self) -> dict:
""" """
Returns configuration variables relevant to the frontend. Returns configuration variables relevant to the frontend.
@ -521,23 +454,17 @@ class Config(metaclass=Singleton):
dict: A dictionary with the frontend configuration dict: A dictionary with the frontend configuration
""" """
data = {k: getattr(self, k) for k in self._frontend_vars} data: dict[str, Any] = {k: getattr(self, k) for k in self._frontend_vars}
ytdlp_args = self.get_ytdlp_args()
# TODO: this doesn't make sense, as each item might have it's own archive file or none at all.
if not data.get("keep_archive", False) and ytdlp_args.get("download_archive", None):
data["keep_archive"] = True
data["ytdlp_version"] = Config._ytdlp_version() data["ytdlp_version"] = Config._ytdlp_version()
return data return data
def get_replacers(self) -> dict: def get_replacers(self) -> dict[str, str]:
""" """
Get the variables that can be used in Command options for yt-dlp. Get the variables that can be used in Command options for yt-dlp.
Returns: Returns:
dict: The replacer variables. dict[str, str]: The replacer variables.
""" """
keys: tuple[str] = ("download_path", "temp_path", "config_path") keys: tuple[str] = ("download_path", "temp_path", "config_path")

View file

@ -129,9 +129,6 @@ async def file_browser(request: Request, config: Config, encoder: Encoder) -> Re
Response: The response object. Response: The response object.
""" """
if not config.browser_enabled:
return web.json_response(data={"error": "File browser is disabled."}, status=web.HTTPForbidden.status_code)
req_path: str = request.match_info.get("path") req_path: str = request.match_info.get("path")
req_path: str = "/" if not req_path else unquote_plus(req_path) req_path: str = "/" if not req_path else unquote_plus(req_path)
@ -190,9 +187,6 @@ async def path_actions(request: Request, config: Config) -> Response:
Response: The response object. Response: The response object.
""" """
if not config.browser_enabled:
return web.json_response(data={"error": "File browser is disabled."}, status=web.HTTPForbidden.status_code)
if not config.browser_control_enabled: if not config.browser_control_enabled:
return web.json_response( return web.json_response(
data={"error": "File browser actions is disabled."}, status=web.HTTPForbidden.status_code data={"error": "File browser actions is disabled."}, status=web.HTTPForbidden.status_code

View file

@ -153,13 +153,8 @@ async def conditions_test(request: Request, encoder: Encoder, cache: Cache, conf
preset: str = params.get("preset", config.default_preset) preset: str = params.get("preset", config.default_preset)
key: str = cache.hash(url + str(preset)) key: str = cache.hash(url + str(preset))
if not cache.has(key): if not cache.has(key):
opts = {} data: dict = extract_info(
if ytdlp_proxy := config.get_ytdlp_args().get("proxy", None): config=YTDLPOpts.get_instance().preset(name=preset).get_all(),
opts["proxy"] = ytdlp_proxy
ytdlp_opts = YTDLPOpts.get_instance().preset(name=preset).add(opts).get_all()
data = extract_info(
config=ytdlp_opts,
url=url, url=url,
debug=False, debug=False,
no_archive=True, no_archive=True,
@ -182,7 +177,7 @@ async def conditions_test(request: Request, encoder: Encoder, cache: Cache, conf
) )
try: try:
status = match_str(cond, data) status: bool = match_str(cond, data)
except Exception as e: except Exception as e:
LOG.exception(e) LOG.exception(e)
return web.json_response( return web.json_response(

View file

@ -2,6 +2,7 @@ import logging
import random import random
import time import time
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
import httpx import httpx
@ -14,6 +15,7 @@ from app.library.cache import Cache
from app.library.config import Config from app.library.config import Config
from app.library.router import route from app.library.router import route
from app.library.Utils import validate_url from app.library.Utils import validate_url
from app.library.YTDLPOpts import YTDLPOpts
LOG: logging.Logger = logging.getLogger(__name__) LOG: logging.Logger = logging.getLogger(__name__)
@ -33,7 +35,7 @@ async def get_thumbnail(request: Request, config: Config) -> Response:
Response: The response object. Response: The response object.
""" """
url = request.query.get("url") url: str | None = request.query.get("url")
if not url: if not url:
return web.json_response(data={"error": "URL is required."}, status=web.HTTPForbidden.status_code) return web.json_response(data={"error": "URL is required."}, status=web.HTTPForbidden.status_code)
@ -43,15 +45,28 @@ async def get_thumbnail(request: Request, config: Config) -> Response:
return web.json_response(data={"error": str(e)}, status=web.HTTPForbidden.status_code) return web.json_response(data={"error": str(e)}, status=web.HTTPForbidden.status_code)
try: try:
ytdlp_args: dict = YTDLPOpts.get_instance().preset(name=config.default_preset).get_all()
ytdlp_args = config.get_ytdlp_args() opts: dict[str, Any] = {
opts = {
"proxy": ytdlp_args.get("proxy", None),
"headers": { "headers": {
"User-Agent": ytdlp_args.get("user_agent", request.headers.get("User-Agent", random_user_agent())), "User-Agent": request.headers.get("User-Agent", ytdlp_args.get("user_agent", random_user_agent())),
}, },
} }
if proxy := ytdlp_args.get("proxy"):
opts["proxy"] = proxy
try:
from httpx_curl_cffi import AsyncCurlTransport, CurlOpt
opts["transport"] = AsyncCurlTransport(
impersonate="chrome",
default_headers=True,
curl_options={CurlOpt.FRESH_CONNECT: True},
)
opts.pop("headers", None)
except Exception:
pass
async with httpx.AsyncClient(**opts) as client: async with httpx.AsyncClient(**opts) as client:
LOG.debug(f"Fetching thumbnail from '{url}'.") LOG.debug(f"Fetching thumbnail from '{url}'.")
response = await client.request(method="GET", url=url, follow_redirects=True) response = await client.request(method="GET", url=url, follow_redirects=True)
@ -115,13 +130,16 @@ async def get_background(request: Request, config: Config, cache: Cache) -> Resp
}, },
) )
ytdlp_args = config.get_ytdlp_args() ytdlp_args: dict = YTDLPOpts.get_instance().preset(name=config.default_preset).get_all()
opts = { opts: dict[str, Any] = {
"proxy": ytdlp_args.get("proxy", None),
"headers": { "headers": {
"User-Agent": ytdlp_args.get("user_agent", random_user_agent()), "User-Agent": request.headers.get("User-Agent", ytdlp_args.get("user_agent", random_user_agent())),
}, },
} }
if proxy := ytdlp_args.get("proxy"):
opts["proxy"] = proxy
try: try:
from httpx_curl_cffi import AsyncCurlTransport, CurlOpt from httpx_curl_cffi import AsyncCurlTransport, CurlOpt
@ -166,7 +184,7 @@ async def get_background(request: Request, config: Config, cache: Cache) -> Resp
status=web.HTTPInternalServerError.status_code, status=web.HTTPInternalServerError.status_code,
) )
data = { data: dict[str, Any] = {
"content": response.content, "content": response.content,
"backend": urlparse(backend).netloc, "backend": urlparse(backend).netloc,
"headers": { "headers": {

View file

@ -150,9 +150,6 @@ async def get_info(request: Request, cache: Cache, config: Config) -> Response:
} }
return web.json_response(body=json.dumps(data, indent=4, default=str), status=web.HTTPOk.status_code) return web.json_response(body=json.dumps(data, indent=4, default=str), status=web.HTTPOk.status_code)
if ytdlp_proxy := config.get_ytdlp_args().get("proxy", None):
opts = opts.add({"proxy": ytdlp_proxy})
logs: list = [] logs: list = []
ytdlp_opts: dict = { ytdlp_opts: dict = {
@ -203,13 +200,14 @@ async def get_info(request: Request, cache: Cache, config: Config) -> Response:
"expires": time.time() + 300, "expires": time.time() + 300,
} }
is_archived = False data["is_archived"] = False
if (archive_file := ytdlp_opts.get("download_archive")) and (
archive_id := get_archive_id(url=url).get("archive_id")
):
is_archived: bool = len(archive_read(archive_file, [archive_id])) > 0
data["is_archived"] = is_archived archive_file: str | None = ytdlp_opts.get("download_archive")
data["archive_file"] = archive_file if archive_file else None
if archive_file and (archive_id := get_archive_id(url=url).get("archive_id")):
data["archive_id"] = archive_id
data["is_archived"] = len(archive_read(archive_file, [archive_id])) > 0
data = OrderedDict(sorted(data.items(), key=lambda item: len(str(item[1])))) data = OrderedDict(sorted(data.items(), key=lambda item: len(str(item[1]))))

View file

@ -209,4 +209,9 @@ testpaths = ["app/tests"]
addopts = "-v --tb=short" addopts = "-v --tb=short"
[dependency-groups] [dependency-groups]
dev = ["pytest>=8.4.2", "pytest-asyncio>=1.1.0", "ruff>=0.13.0"] dev = [
"debugpy>=1.8.16",
"pytest>=8.4.2",
"pytest-asyncio>=1.1.0",
"ruff>=0.13.0",
]

View file

@ -83,7 +83,8 @@
</div> </div>
<span class="help is-bold"> <span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span>yt-dlp <code>[--match-filters]</code> logic.</span> <span>yt-dlp <code>--match-filters</code> logic with <code>OR</code>, <code>||</code>
support.</span>
</span> </span>
</div> </div>
</div> </div>
@ -211,7 +212,7 @@
</div> </div>
</div> </div>
</div> </div>
<span class="help"> <span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span>The url to test the filter against.</span> <span>The url to test the filter against.</span>
</span> </span>
@ -226,9 +227,10 @@
<input type="text" class="input" id="filter" v-model="form.filter" :disabled="test_data.in_progress" <input type="text" class="input" id="filter" v-model="form.filter" :disabled="test_data.in_progress"
placeholder="availability = 'needs_auth' & channel_id = 'channel_id'" required> placeholder="availability = 'needs_auth' & channel_id = 'channel_id'" required>
</div> </div>
<span class="help"> <span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span>The yt-dlp <code>[--match-filters]</code> filter logic.</span><br> <span>yt-dlp <code>--match-filters</code> logic with <code>OR</code>, <code>||</code>
support.</span>
</span> </span>
</div> </div>

View file

@ -175,7 +175,7 @@
<span class="icon"><i class="fa-solid fa-trash-can" /></span> <span class="icon"><i class="fa-solid fa-trash-can" /></span>
</button> </button>
</div> </div>
<div class="control is-expanded" v-if="item.url && !config.app.basic_mode"> <div class="control is-expanded" v-if="item.url">
<Dropdown icons="fa-solid fa-cogs" @open_state="(s: boolean) => table_container = !s" <Dropdown icons="fa-solid fa-cogs" @open_state="(s: boolean) => table_container = !s"
:button_classes="'is-small'" label="Actions"> :button_classes="'is-small'" label="Actions">
<template v-if="'finished' === item.status && item.filename"> <template v-if="'finished' === item.status && item.filename">
@ -347,7 +347,7 @@
</a> </a>
</div> </div>
<div class="column" v-if="!config.app.basic_mode"> <div class="column">
<Dropdown icons="fa-solid fa-cogs" label="Actions"> <Dropdown icons="fa-solid fa-cogs" label="Actions">
<template v-if="'finished' === item.status && item.filename"> <template v-if="'finished' === item.status && item.filename">
<NuxtLink @click="playVideo(item)" class="dropdown-item"> <NuxtLink @click="playVideo(item)" class="dropdown-item">
@ -366,14 +366,12 @@
<hr class="dropdown-divider" /> <hr class="dropdown-divider" />
</template> </template>
<NuxtLink class="dropdown-item" @click="emitter('getInfo', item.url, item.preset, item.cli)" <NuxtLink class="dropdown-item" @click="emitter('getInfo', item.url, item.preset, item.cli)">
v-if="!config.app.basic_mode">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span>yt-dlp Information</span> <span>yt-dlp Information</span>
</NuxtLink> </NuxtLink>
<NuxtLink class="dropdown-item" @click="emitter('getItemInfo', item._id)" <NuxtLink class="dropdown-item" @click="emitter('getItemInfo', item._id)">
v-if="!config.app.basic_mode">
<span class="icon"><i class="fa-solid fa-info-circle" /></span> <span class="icon"><i class="fa-solid fa-info-circle" /></span>
<span>Local Information</span> <span>Local Information</span>
</NuxtLink> </NuxtLink>

View file

@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div class="column is-4-tablet is-12-mobile" v-if="!config.app.basic_mode"> <div class="column is-4-tablet is-12-mobile">
<div class="field has-addons"> <div class="field has-addons">
<div class="control" @click="show_description = !show_description"> <div class="control" @click="show_description = !show_description">
<label class="button is-static"> <label class="button is-static">
@ -54,7 +54,7 @@
</div> </div>
</div> </div>
<div class="column is-6-tablet is-12-mobile" v-if="!config.app.basic_mode"> <div class="column is-6-tablet is-12-mobile">
<div class="field has-addons" v-tooltip="'Folder relative to ' + config.app.download_path"> <div class="field has-addons" v-tooltip="'Folder relative to ' + config.app.download_path">
<div class="control"> <div class="control">
<label class="button is-static"> <label class="button is-static">
@ -70,7 +70,7 @@
</div> </div>
</div> </div>
<div class="column" v-if="!config.app.basic_mode"> <div class="column">
<button type="button" class="button is-info" @click="showAdvanced = !showAdvanced" <button type="button" class="button is-info" @click="showAdvanced = !showAdvanced"
:class="{ 'is-loading': !socket.isConnected }" :disabled="!socket.isConnected"> :class="{ 'is-loading': !socket.isConnected }" :disabled="!socket.isConnected">
<span class="icon"><i class="fa-solid fa-cog" /></span> <span class="icon"><i class="fa-solid fa-cog" /></span>
@ -79,7 +79,7 @@
</div> </div>
<div class="column is-12" <div class="column is-12"
v-if="show_description && !config.app.basic_mode && !hasFormatInConfig && get_preset(form.preset)?.description"> v-if="show_description && !hasFormatInConfig && get_preset(form.preset)?.description">
<div class="is-overflow-auto" style="max-height: 150px;"> <div class="is-overflow-auto" style="max-height: 150px;">
<div class="is-ellipsis is-clickable" @click="expand_description"> <div class="is-ellipsis is-clickable" @click="expand_description">
<span class="icon"><i class="fa-solid fa-info" /></span> {{ get_preset(form.preset)?.description }} <span class="icon"><i class="fa-solid fa-info" /></span> {{ get_preset(form.preset)?.description }}
@ -88,7 +88,7 @@
</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">
<div class="column is-3-tablet is-12-mobile"> <div class="column is-3-tablet is-12-mobile">
<DLInput id="force_download" type="bool" label="Force download" <DLInput id="force_download" type="bool" label="Force download"
v-model="dlFields['--no-download-archive']" icon="fa-solid fa-download" v-model="dlFields['--no-download-archive']" icon="fa-solid fa-download"
@ -137,7 +137,7 @@
</div> </div>
<div class="column is-6-tablet is-12-mobile"> <div class="column is-6-tablet is-12-mobile">
<DLInput id="ytdlpCookies" type="text" label="Cookies for yt-dlp" v-model="form.cookies" <DLInput id="ytdlpCookies" type="text" label="Cookies" v-model="form.cookies"
icon="fa-solid fa-cookie" :disabled="!socket.isConnected || addInProgress" icon="fa-solid fa-cookie" :disabled="!socket.isConnected || addInProgress"
:placeholder="getDefault('cookies', '')"> :placeholder="getDefault('cookies', '')">
<template #help> <template #help>
@ -290,37 +290,35 @@ const addDownload = async () => {
return false; return false;
} }
if (false === config.app.basic_mode) { if (dlFields.value && Object.keys(dlFields.value).length > 0) {
if (dlFields.value && Object.keys(dlFields.value).length > 0) { const joined = []
const joined = [] for (const [key, value] of Object.entries(dlFields.value)) {
for (const [key, value] of Object.entries(dlFields.value)) { if (false === is_valid(key)) {
if (false === is_valid(key)) { continue
continue
}
if ([undefined, null, '', false].includes(value as any)) {
continue
}
const keyRegex = new RegExp(`(^|\\s)${key}(\\s|$)`);
if (form_cli && keyRegex.test(form_cli)) {
continue;
}
joined.push(true === value ? `${key}` : `${key} ${value}`)
} }
if (joined.length > 0) { if ([undefined, null, '', false].includes(value as any)) {
form_cli = form_cli ? `${form_cli} ${joined.join(' ')}` : joined.join(' ') continue
} }
const keyRegex = new RegExp(`(^|\\s)${key}(\\s|$)`);
if (form_cli && keyRegex.test(form_cli)) {
continue;
}
joined.push(true === value ? `${key}` : `${key} ${value}`)
} }
if (form_cli && form_cli.trim()) { if (joined.length > 0) {
const options = await convertOptions(form_cli) form_cli = form_cli ? `${form_cli} ${joined.join(' ')}` : joined.join(' ')
if (null === options) { }
return
} }
if (form_cli && form_cli.trim()) {
const options = await convertOptions(form_cli)
if (null === options) {
return
} }
} }
@ -333,12 +331,12 @@ const addDownload = async () => {
const data = { const data = {
url: url, url: url,
preset: config.app.basic_mode ? config.app.default_preset : form.value.preset, preset: form.value.preset || config.app.default_preset,
folder: config.app.basic_mode ? null : form.value.folder, folder: form.value.folder,
template: config.app.basic_mode ? null : form.value.template, template: form.value.template,
cookies: config.app.basic_mode ? '' : form.value.cookies, cookies: form.value.cookies,
cli: config.app.basic_mode ? null : form_cli, cli: form_cli,
auto_start: config.app.basic_mode ? true : auto_start.value auto_start: auto_start.value
} as item_request } as item_request
if (form.value?.extras && Object.keys(form.value.extras).length > 0) { if (form.value?.extras && Object.keys(form.value.extras).length > 0) {

View file

@ -147,19 +147,17 @@
</NuxtLink> </NuxtLink>
</template> </template>
<template v-if="!config.app.basic_mode"> <hr class="dropdown-divider" />
<hr class="dropdown-divider" />
<NuxtLink class="dropdown-item" @click="emitter('getInfo', item.url, item.preset, item.cli)"> <NuxtLink class="dropdown-item" @click="emitter('getInfo', item.url, item.preset, item.cli)">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span>yt-dlp Information</span> <span>yt-dlp Information</span>
</NuxtLink> </NuxtLink>
<NuxtLink class="dropdown-item" @click="emitter('getItemInfo', item._id)"> <NuxtLink class="dropdown-item" @click="emitter('getItemInfo', item._id)">
<span class="icon"><i class="fa-solid fa-info-circle" /></span> <span class="icon"><i class="fa-solid fa-info-circle" /></span>
<span>Local Information</span> <span>Local Information</span>
</NuxtLink> </NuxtLink>
</template>
</Dropdown> </Dropdown>
</td> </td>
</tr> </tr>
@ -272,17 +270,15 @@
<span class="icon"><i class="fa-solid fa-play" /></span> <span class="icon"><i class="fa-solid fa-play" /></span>
<span>Play video</span> <span>Play video</span>
</NuxtLink> </NuxtLink>
<hr class="dropdown-divider" v-if="!config.app.basic_mode" /> <hr class="dropdown-divider" />
</template> </template>
<NuxtLink class="dropdown-item" @click="emitter('getInfo', item.url, item.preset, item.cli)" <NuxtLink class="dropdown-item" @click="emitter('getInfo', item.url, item.preset, item.cli)">
v-if="!config.app.basic_mode">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span>yt-dlp Information</span> <span>yt-dlp Information</span>
</NuxtLink> </NuxtLink>
<NuxtLink class="dropdown-item" @click="emitter('getItemInfo', item._id)" <NuxtLink class="dropdown-item" @click="emitter('getItemInfo', item._id)">
v-if="!config.app.basic_mode">
<span class="icon"><i class="fa-solid fa-info-circle" /></span> <span class="icon"><i class="fa-solid fa-info-circle" /></span>
<span>Local Information</span> <span>Local Information</span>
</NuxtLink> </NuxtLink>

View file

@ -24,9 +24,8 @@
</div> </div>
<div class="navbar-menu is-unselectable" :class="{ 'is-active': showMenu }"> <div class="navbar-menu is-unselectable" :class="{ 'is-active': showMenu }">
<div class="navbar-start" v-if="!config.app.basic_mode"> <div class="navbar-start">
<NuxtLink class="navbar-item" to="/browser" @click.prevent="(e: MouseEvent) => changeRoute(e)" <NuxtLink class="navbar-item" to="/browser" @click.prevent="(e: MouseEvent) => changeRoute(e)">
v-if="config.app.browser_enabled">
<span class="icon"><i class="fa-solid fa-folder-tree" /></span> <span class="icon"><i class="fa-solid fa-folder-tree" /></span>
<span>Files</span> <span>Files</span>
</NuxtLink> </NuxtLink>
@ -55,7 +54,7 @@
</div> </div>
<div class="navbar-end"> <div class="navbar-end">
<div class="navbar-item has-dropdown" v-if="!config.app.basic_mode"> <div class="navbar-item has-dropdown">
<a class="navbar-link" @click="(e: MouseEvent) => openMenu(e)"> <a class="navbar-link" @click="(e: MouseEvent) => openMenu(e)">
<span class="icon"><i class="fas fa-tools" /></span> <span class="icon"><i class="fas fa-tools" /></span>
<span>Other</span> <span>Other</span>

View file

@ -49,9 +49,6 @@
</p> </p>
</div> </div>
</div> </div>
<div class="is-hidden-mobile">
<span class="subtitle">Files Browser</span>
</div>
</div> </div>
</div> </div>
@ -278,14 +275,6 @@ watch(masterSelectAll, v => {
} }
}) })
watch(() => config.app.basic_mode, async v => {
if (!config.isLoaded() || !v) {
return
}
await navigateTo('/')
}, { immediate: true })
const filteredItems = computed<FileItem[]>(() => { const filteredItems = computed<FileItem[]>(() => {
if (!search.value) { if (!search.value) {
return sortedItems(items.value) return sortedItems(items.value)
@ -335,20 +324,6 @@ const sortedItems = (items: FileItem[]): FileItem[] => {
const model_item = ref<any>() const model_item = ref<any>()
const closeModel = (): void => { model_item.value = null } const closeModel = (): void => { model_item.value = null }
watch(() => config.app.basic_mode, async () => {
if (!config.app.basic_mode) {
return
}
await navigateTo('/')
})
watch(() => config.app.browser_enabled, async () => {
if (config.app.browser_enabled) {
return
}
await navigateTo('/')
})
watch(() => socket.isConnected, async () => { watch(() => socket.isConnected, async () => {
if (socket.isConnected && initialLoad.value) { if (socket.isConnected && initialLoad.value) {
await reloadContent(path.value, true) await reloadContent(path.value, true)

View file

@ -104,8 +104,10 @@
<div class="column is-12" v-if="items && items.length > 0 && !toggleForm"> <div class="column is-12" v-if="items && items.length > 0 && !toggleForm">
<Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle"> <Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle">
<ul> <ul>
<li>The filtering rely on yt-dlp <code>--match-filter</code> logic, whatever works there works here as well <li>Filtering is based on yt-dlps <code>--match-filter</code> logic. Any expression that works with yt-dlp
and uses the same logic boolean and operators.</li> will also work here, including the same boolean operators. We added extended support for the <code>OR</code>
( <code>||</code> ) operator, which yt-dlp does not natively support. This allows you to combine multiple
conditions more flexibly.</li>
<li> <li>
The primary use case for this feature is to apply custom cli arguments to specific returned info. The primary use case for this feature is to apply custom cli arguments to specific returned info.
</li> </li>
@ -116,7 +118,8 @@
</li> </li>
<li> <li>
The data which the filter is applied on is the same data that yt-dlp returns, simply, click on the The data which the filter is applied on is the same data that yt-dlp returns, simply, click on the
information button, and check the data to craft your filter. information button, and check the data to craft your filter. You will get instant feedback if the
filter matches or not.
</li> </li>
</ul> </ul>
</Message> </Message>
@ -130,7 +133,6 @@ import type { ConditionItem, ImportedConditionItem } from '~/types/conditions'
type ConditionItemWithUI = ConditionItem & { raw?: boolean } type ConditionItemWithUI = ConditionItem & { raw?: boolean }
const toast = useNotification() const toast = useNotification()
const config = useConfigStore()
const socket = useSocketStore() const socket = useSocketStore()
const box = useConfirm() const box = useConfirm()
const isMobile = useMediaQuery({ maxWidth: 1024 }) const isMobile = useMediaQuery({ maxWidth: 1024 })
@ -144,13 +146,6 @@ const initialLoad = ref(true)
const addInProgress = ref(false) const addInProgress = ref(false)
const remove_keys = ['in_progress', 'raw'] const remove_keys = ['in_progress', 'raw']
watch(() => config.app.basic_mode, async v => {
if (!config.isLoaded() || !v) {
return
}
await navigateTo("/")
}, { immediate: true })
watch(() => socket.isConnected, async () => { watch(() => socket.isConnected, async () => {
if (socket.isConnected && initialLoad.value) { if (socket.isConnected && initialLoad.value) {
await reloadContent(true) await reloadContent(true)

View file

@ -81,13 +81,6 @@ watch(() => isLoading.value, async value => {
focusInput() focusInput()
}, { immediate: true }) }, { immediate: true })
watch(() => config.app.basic_mode, async () => {
if (!config.isLoaded() || !config.app.basic_mode) {
return
}
await navigateTo('/')
}, { immediate: true })
watch(() => config.app.console_enabled, async () => { watch(() => config.app.console_enabled, async () => {
if (config.app.console_enabled) { if (config.app.console_enabled) {
return return
@ -108,7 +101,7 @@ const runCommand = async () => {
return return
} }
if (config.app.basic_mode || !config.app.console_enabled) { if (true !== config.app.console_enabled) {
await navigateTo('/') await navigateTo('/')
toast.error('Console is disabled in the configuration. Please enable it to use this feature.') toast.error('Console is disabled in the configuration. Please enable it to use this feature.')
return return
@ -183,13 +176,6 @@ const writer = (s: string) => {
const loader = () => isLoading.value = false const loader = () => isLoading.value = false
watch(() => config.app.basic_mode, async () => {
if (!config.app.basic_mode) {
return
}
await navigateTo('/')
})
onMounted(async () => { onMounted(async () => {
document.addEventListener('resize', handle_event); document.addEventListener('resize', handle_event);
focusInput() focusInput()

View file

@ -24,7 +24,7 @@
</button> </button>
</p> </p>
<p class="control" v-if="!config.app.basic_mode && false === config.app.basic_mode"> <p class="control">
<button class="button is-warning" @click="pauseDownload" v-if="false === config.paused"> <button class="button is-warning" @click="pauseDownload" v-if="false === config.paused">
<span class="icon"><i class="fas fa-pause" /></span> <span class="icon"><i class="fas fa-pause" /></span>
<span v-if="!isMobile">Pause</span> <span v-if="!isMobile">Pause</span>
@ -35,7 +35,7 @@
</button> </button>
</p> </p>
<p class="control" v-if="!config.app.basic_mode && false === config.app.basic_mode"> <p class="control">
<button class="button is-primary has-tooltip-bottom" @click="config.showForm = !config.showForm"> <button class="button is-primary has-tooltip-bottom" @click="config.showForm = !config.showForm">
<span class="icon"><i class="fa-solid fa-plus" /></span> <span class="icon"><i class="fa-solid fa-plus" /></span>
<span v-if="!isMobile">New Download</span> <span v-if="!isMobile">New Download</span>
@ -62,54 +62,7 @@
</div> </div>
</div> </div>
<div v-if="config.is_loaded" class="columns is-multiline"> <NewDownload v-if="config.showForm"
<div class="column is-12">
<DeprecatedNotice :version="config.app.app_version" title="Deprecation Notice" tone="warning"
icon="fas fa-exclamation-triangle fa-fade fa-spin-10">
<p>
The following environment variables and features are deprecated and will be removed in
<strong class="has-text-danger">v0.10.x</strong>
</p>
<ul>
<li>
The following ENVs <strong>YTP_KEEP_ARCHIVE</strong> and <strong>YTP_SOCKET_TIMEOUT</strong> will be
removed.
Their behavior will be part of the <strong>default presets</strong>. To keep your current behavior
<strong>and avoid re-downloading</strong>, please add the following <strong>Command options for
yt-dlp</strong> to your presets:
<code>--socket-timeout 30 --download-archive %(config_path)s/archive.log</code>
</li>
<li>
The global yt-dlp config file <strong>/config/ytdlp.cli</strong> will be removed. Please migrate to
presets.
</li>
<li>The <strong>archive.manual.log</strong> feature has been removed.</li>
</ul>
<p>
These changes help reduce confusion from multiple sources of truth. Going forward, <strong>presets</strong>
and the <strong>Command options for yt-dlp</strong> will be the single source of truth.
</p>
<p>
Notable changes in <strong>v0.10.x</strong>:
</p>
<ul>
<li>
The file browser feature is going to be enabled by default. and the associated ENV
<strong>YTP_BROWSER_ENABLED</strong> will be removed, <strong>YTP_BROWSER_CONTROL_ENABLED</strong> will
remain and
will default to <strong>false</strong>.
</li>
<li>
The <strong>Basic mode</strong> (which limited the interface to just the new download form) along it's
associated ENV <strong>YTP_BASIC_MODE</strong> is being removed. Everything except what is available
behind configurable flag will become part of the standard interface.
</li>
</ul>
</DeprecatedNotice>
</div>
</div>
<NewDownload v-if="config.showForm || config.app.basic_mode"
@getInfo="(url: string, preset: string = '', cli: string = '') => view_info(url, false, preset, cli)" @getInfo="(url: string, preset: string = '', cli: string = '') => view_info(url, false, preset, cli)"
:item="item_form" @clear_form="item_form = {}" /> :item="item_form" @clear_form="item_form = {}" />
<Queue @getInfo="(url: string, preset: string = '', cli: string = '') => view_info(url, false, preset, cli)" <Queue @getInfo="(url: string, preset: string = '', cli: string = '') => view_info(url, false, preset, cli)"
@ -128,7 +81,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import DeprecatedNotice from '~/components/DeprecatedNotice.vue'
import type { item_request } from '~/types/item' import type { item_request } from '~/types/item'
import type { StoreItem } from '~/types/store' import type { StoreItem } from '~/types/store'

View file

@ -155,13 +155,6 @@ watch(toggleFilter, () => {
} }
}); });
watch(() => config.app.basic_mode, async v => {
if (!config.isLoaded() || !v) {
return
}
await navigateTo('/')
}, { immediate: true })
watch(() => config.app.file_logging, async v => { watch(() => config.app.file_logging, async v => {
if (v) { if (v) {
return return

View file

@ -221,7 +221,6 @@ import { useStorage } from '@vueuse/core'
import type { notification, notificationImport } from '~/types/notification' import type { notification, notificationImport } from '~/types/notification'
const toast = useNotification() const toast = useNotification()
const config = useConfigStore()
const socket = useSocketStore() const socket = useSocketStore()
const box = useConfirm() const box = useConfirm()
const display_style = useStorage<string>("tasks_display_style", "cards") const display_style = useStorage<string>("tasks_display_style", "cards")
@ -244,13 +243,6 @@ const isLoading = ref(false)
const initialLoad = ref(true) const initialLoad = ref(true)
const addInProgress = ref(false) const addInProgress = ref(false)
watch(() => config.app.basic_mode, async v => {
if (!config.isLoaded() || !v) {
return
}
await navigateTo('/')
}, { immediate: true })
watch(() => socket.isConnected, async () => { watch(() => socket.isConnected, async () => {
if (socket.isConnected && initialLoad.value) { if (socket.isConnected && initialLoad.value) {
await reloadContent(true) await reloadContent(true)

View file

@ -39,8 +39,8 @@
</div> </div>
</div> </div>
<div class="is-hidden-mobile"> <div class="is-hidden-mobile">
<span class="subtitle">Custom presets. The presets are simply pre-defined yt-dlp settings <span class="subtitle">Presets are pre-defined command options for yt-dlp that you want to apply to given
that you want to apply to given download.</span> download.</span>
</div> </div>
</div> </div>
</div> </div>
@ -192,16 +192,7 @@
<div class="column is-12"> <div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle"> <Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle">
<ul> <ul>
<li> <li>When you export preset, it doesn't include <code>Cookies</code> field for security reasons.</li>
When you export preset, it doesn't include <code>Cookies</code> field for security reasons.
</li>
<li>
If you have created a global <code>config/ytdlp.cli</code> file, it will be appended to your exported
preset
<code><i class="fa-solid fa-terminal" /> Command options for yt-dlp</code> field for better
compatibility
and completeness.
</li>
</ul> </ul>
</Message> </Message>
</div> </div>
@ -233,13 +224,6 @@ const remove_keys = ['raw', 'toggle_description']
const presetsNoDefault = computed(() => presets.value.filter((t) => !t.default)) const presetsNoDefault = computed(() => presets.value.filter((t) => !t.default))
watch(() => config.app.basic_mode, async v => {
if (!config.isLoaded() || !v) {
return
}
await navigateTo('/')
}, { immediate: true })
watch(() => socket.isConnected, async () => { watch(() => socket.isConnected, async () => {
if (socket.isConnected && initialLoad.value) { if (socket.isConnected && initialLoad.value) {
await reloadContent(true) await reloadContent(true)
@ -391,11 +375,6 @@ const exportItem = (item: Preset) => {
} }
} }
if (config?.app?.ytdlp_cli) {
const val = `# exported from ytdlp.cli #\n${config.app.ytdlp_cli}\n# exported from ytdlp.cli #\n`
userData.cli = userData.cli ? val + '\n' + userData.cli : val
}
userData['_type'] = 'preset' userData['_type'] = 'preset'
userData['_version'] = '2.5' userData['_version'] = '2.5'

View file

@ -444,13 +444,6 @@ watch(masterSelectAll, value => {
} }
}) })
watch(() => config.app.basic_mode, async v => {
if (!config.isLoaded() || !v) {
return
}
await navigateTo('/')
}, { immediate: true })
watch(() => socket.isConnected, async () => { watch(() => socket.isConnected, async () => {
if (socket.isConnected && initialLoad.value) { if (socket.isConnected && initialLoad.value) {
socket.on('item_status', statusHandler) socket.on('item_status', statusHandler)

View file

@ -11,13 +11,10 @@ export const useConfigStore = defineStore('config', () => {
output_template: '', output_template: '',
ytdlp_version: '', ytdlp_version: '',
max_workers: 1, max_workers: 1,
basic_mode: true,
default_preset: 'default', default_preset: 'default',
instance_title: null, instance_title: null,
console_enabled: false, console_enabled: false,
browser_enabled: false,
browser_control_enabled: false, browser_control_enabled: false,
ytdlp_cli: '',
file_logging: false, file_logging: false,
is_native: false, is_native: false,
app_version: '', app_version: '',
@ -30,7 +27,7 @@ export const useConfigStore = defineStore('config', () => {
presets: [ presets: [
{ {
'name': 'default', 'name': 'default',
'description': 'Default preset for downloads', 'description': 'Default preset',
'folder': '', 'folder': '',
'template': '', 'template': '',
'cookies': '', 'cookies': '',

View file

@ -14,20 +14,14 @@ type AppConfig = {
ytdlp_version: string ytdlp_version: string
/** Maximum number of concurrent download workers */ /** Maximum number of concurrent download workers */
max_workers: number max_workers: number
/** Indicates if the app is in basic mode, which may limit some features */
basic_mode: boolean
/** Default preset name, e.g. "default" */ /** Default preset name, e.g. "default" */
default_preset: string default_preset: string
/** Instance title for the app, null if not set */ /** Instance title for the app, null if not set */
instance_title: string | null instance_title: string | null
/** Indicates if the console is enabled */ /** Indicates if the console is enabled */
console_enabled: boolean console_enabled: boolean
/** Indicates if the file browser is enabled */
browser_enabled: boolean
/** Indicates if the file browser control is enabled */ /** Indicates if the file browser control is enabled */
browser_control_enabled: boolean browser_control_enabled: boolean
/** Command options for yt-dlp */
ytdlp_cli: string
/** Indicates if file logging is enabled */ /** Indicates if file logging is enabled */
file_logging: boolean file_logging: boolean
/** Indicates if the app is running in a native environment (e.g., Electron) */ /** Indicates if the app is running in a native environment (e.g., Electron) */

View file

@ -1513,6 +1513,7 @@ installer = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "debugpy" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-asyncio" }, { name = "pytest-asyncio" },
{ name = "ruff" }, { name = "ruff" },
@ -1556,6 +1557,7 @@ provides-extras = ["installer"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "debugpy", specifier = ">=1.8.16" },
{ name = "pytest", specifier = ">=8.4.2" }, { name = "pytest", specifier = ">=8.4.2" },
{ name = "pytest-asyncio", specifier = ">=1.1.0" }, { name = "pytest-asyncio", specifier = ">=1.1.0" },
{ name = "ruff", specifier = ">=0.13.0" }, { name = "ruff", specifier = ">=0.13.0" },