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)
* 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.
* 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.
* 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 > 0
):
self.logger.warning(
f"Keeping temp folder '{self.temp_path}', as the reported status is not finished '{self.info.status}'."
)
self.logger.warning(f"Keeping temp folder '{self.temp_path}'. {self.info.status=}.")
return
tmp_dir = Path(self.temp_path)

View file

@ -152,9 +152,6 @@ class YTDLPOpts:
self._item_cli = []
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:
merge.append(self._preset_cli)

View file

@ -7,13 +7,13 @@ import time
from logging.handlers import TimedRotatingFileHandler
from multiprocessing.managers import SyncManager
from pathlib import Path
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
import coloredlogs
from dotenv import load_dotenv
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
if TYPE_CHECKING:
@ -96,9 +96,6 @@ class Config(metaclass=Singleton):
debugpy_port: int = 5678
"""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
"""The timeout to use for extracting video information."""
@ -138,9 +135,6 @@ class Config(metaclass=Singleton):
ignore_ui: bool = False
"Ignore the UI and run the application in the background."
basic_mode: bool = False
"Run the frontend in basic mode."
default_preset: str = "default"
"The default preset to use when no preset is specified."
@ -159,21 +153,12 @@ class Config(metaclass=Singleton):
console_enabled: bool = False
"Enable direct access to yt-dlp console."
browser_enabled: bool = True
"Enable file browser access."
browser_control_enabled: bool = False
"Enable file browser control access."
ytdlp_auto_update: bool = True
"""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
"""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 = (
"ytdl_options",
"started",
"ytdlp_cli",
"_ytdlp_cli_mutable",
"is_native",
"app_version",
"app_commit_sha",
@ -219,7 +202,6 @@ class Config(metaclass=Singleton):
_int_vars: tuple = (
"port",
"max_workers",
"socket_timeout",
"extract_info_timeout",
"debugpy_port",
"playlist_items_concurrency",
@ -238,10 +220,8 @@ class Config(metaclass=Singleton):
"ignore_ui",
"ui_update_title",
"pip_ignore_updates",
"basic_mode",
"file_logging",
"console_enabled",
"browser_enabled",
"browser_control_enabled",
"ytdlp_auto_update",
"prevent_premiere_live",
@ -257,13 +237,10 @@ class Config(metaclass=Singleton):
"remove_files",
"ui_update_title",
"max_workers",
"basic_mode",
"default_preset",
"instance_title",
"console_enabled",
"browser_enabled",
"browser_control_enabled",
"ytdlp_cli",
"file_logging",
"base_path",
"is_native",
@ -351,7 +328,7 @@ class Config(metaclass=Singleton):
if not self.base_path.endswith("/"):
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):
msg = f"Invalid log level '{self.log_level}' specified."
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)
LOG.info(f"starting debugpy server on '0.0.0.0:{self.debugpy_port}'.")
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:
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 opts_file.exists() and opts_file.stat().st_size > 2:
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 (Path(self.config_path) / "ytdlp.cli").exists():
LOG.error("Support for ./ytdlp.cli file is removed, migrate to presets and remove the file.")
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:
LOG.warning(f"Basic authentication enabled with username '{self.auth_username}'.")
if self.basic_mode:
LOG.info("The frontend is running in basic mode.")
LOG.info(f"Authentication enabled with username '{self.auth_username}'.")
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):
msg = f"Invalid file log level '{self.log_level_file}' specified."
raise TypeError(msg)
loggingPath = Path(self.config_path) / "logs"
loggingPath: Path = Path(self.config_path) / "logs"
if not loggingPath.exists():
loggingPath.mkdir(parents=True, exist_ok=True)
@ -448,15 +396,17 @@ class Config(metaclass=Singleton):
self.started = time.time()
logging.getLogger("httpcore").setLevel(logging.INFO)
for _tool in ("httpx", "urllib3.connectionpool", "apprise"):
logging.getLogger(_tool).setLevel(logging.WARNING)
_log_levels = (
("httpx", 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"):
msg: str = (
f"Invalid application environment '{self.app_env}' specified. Must be 'production' or 'development'."
)
msg: str = f"Invalid app environment '{self.app_env}' specified. Must be 'production' or 'development'."
raise ValueError(msg)
if "dev-master" == self.app_version:
@ -470,7 +420,7 @@ class Config(metaclass=Singleton):
if attribute.startswith("_"):
continue
value = getattr(vClass, attribute)
value: Any = getattr(vClass, attribute)
if not callable(value):
attrs[attribute] = value
@ -496,23 +446,6 @@ class Config(metaclass=Singleton):
"""
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:
"""
Returns configuration variables relevant to the frontend.
@ -521,23 +454,17 @@ class Config(metaclass=Singleton):
dict: A dictionary with the frontend configuration
"""
data = {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: dict[str, Any] = {k: getattr(self, k) for k in self._frontend_vars}
data["ytdlp_version"] = Config._ytdlp_version()
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.
Returns:
dict: The replacer variables.
dict[str, str]: The replacer variables.
"""
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.
"""
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 = "/" 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.
"""
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:
return web.json_response(
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)
key: str = cache.hash(url + str(preset))
if not cache.has(key):
opts = {}
if ytdlp_proxy := config.get_ytdlp_args().get("proxy", None):
opts["proxy"] = ytdlp_proxy
ytdlp_opts = YTDLPOpts.get_instance().preset(name=preset).add(opts).get_all()
data = extract_info(
config=ytdlp_opts,
data: dict = extract_info(
config=YTDLPOpts.get_instance().preset(name=preset).get_all(),
url=url,
debug=False,
no_archive=True,
@ -182,7 +177,7 @@ async def conditions_test(request: Request, encoder: Encoder, cache: Cache, conf
)
try:
status = match_str(cond, data)
status: bool = match_str(cond, data)
except Exception as e:
LOG.exception(e)
return web.json_response(

View file

@ -2,6 +2,7 @@ import logging
import random
import time
from datetime import UTC, datetime
from typing import Any
from urllib.parse import urlparse
import httpx
@ -14,6 +15,7 @@ from app.library.cache import Cache
from app.library.config import Config
from app.library.router import route
from app.library.Utils import validate_url
from app.library.YTDLPOpts import YTDLPOpts
LOG: logging.Logger = logging.getLogger(__name__)
@ -33,7 +35,7 @@ async def get_thumbnail(request: Request, config: Config) -> Response:
Response: The response object.
"""
url = request.query.get("url")
url: str | None = request.query.get("url")
if not url:
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)
try:
ytdlp_args = config.get_ytdlp_args()
opts = {
"proxy": ytdlp_args.get("proxy", None),
ytdlp_args: dict = YTDLPOpts.get_instance().preset(name=config.default_preset).get_all()
opts: dict[str, Any] = {
"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:
LOG.debug(f"Fetching thumbnail from '{url}'.")
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()
opts = {
"proxy": ytdlp_args.get("proxy", None),
ytdlp_args: dict = YTDLPOpts.get_instance().preset(name=config.default_preset).get_all()
opts: dict[str, Any] = {
"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:
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,
)
data = {
data: dict[str, Any] = {
"content": response.content,
"backend": urlparse(backend).netloc,
"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)
if ytdlp_proxy := config.get_ytdlp_args().get("proxy", None):
opts = opts.add({"proxy": ytdlp_proxy})
logs: list = []
ytdlp_opts: dict = {
@ -203,13 +200,14 @@ async def get_info(request: Request, cache: Cache, config: Config) -> Response:
"expires": time.time() + 300,
}
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"] = False
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]))))

View file

@ -209,4 +209,9 @@ testpaths = ["app/tests"]
addopts = "-v --tb=short"
[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>
<span class="help is-bold">
<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>
</div>
</div>
@ -211,7 +212,7 @@
</div>
</div>
</div>
<span class="help">
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The url to test the filter against.</span>
</span>
@ -226,9 +227,10 @@
<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>
</div>
<span class="help">
<span class="help is-bold">
<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>
</div>

View file

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

View file

@ -25,7 +25,7 @@
</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="control" @click="show_description = !show_description">
<label class="button is-static">
@ -54,7 +54,7 @@
</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="control">
<label class="button is-static">
@ -70,7 +70,7 @@
</div>
</div>
<div class="column" v-if="!config.app.basic_mode">
<div class="column">
<button type="button" class="button is-info" @click="showAdvanced = !showAdvanced"
:class="{ 'is-loading': !socket.isConnected }" :disabled="!socket.isConnected">
<span class="icon"><i class="fa-solid fa-cog" /></span>
@ -79,7 +79,7 @@
</div>
<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-ellipsis is-clickable" @click="expand_description">
<span class="icon"><i class="fa-solid fa-info" /></span> {{ get_preset(form.preset)?.description }}
@ -88,7 +88,7 @@
</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">
<DLInput id="force_download" type="bool" label="Force download"
v-model="dlFields['--no-download-archive']" icon="fa-solid fa-download"
@ -137,7 +137,7 @@
</div>
<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"
:placeholder="getDefault('cookies', '')">
<template #help>
@ -290,37 +290,35 @@ const addDownload = async () => {
return false;
}
if (false === config.app.basic_mode) {
if (dlFields.value && Object.keys(dlFields.value).length > 0) {
const joined = []
for (const [key, value] of Object.entries(dlFields.value)) {
if (false === is_valid(key)) {
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 (dlFields.value && Object.keys(dlFields.value).length > 0) {
const joined = []
for (const [key, value] of Object.entries(dlFields.value)) {
if (false === is_valid(key)) {
continue
}
if (joined.length > 0) {
form_cli = form_cli ? `${form_cli} ${joined.join(' ')}` : joined.join(' ')
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 (form_cli && form_cli.trim()) {
const options = await convertOptions(form_cli)
if (null === options) {
return
}
if (joined.length > 0) {
form_cli = form_cli ? `${form_cli} ${joined.join(' ')}` : joined.join(' ')
}
}
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 = {
url: url,
preset: config.app.basic_mode ? config.app.default_preset : form.value.preset,
folder: config.app.basic_mode ? null : form.value.folder,
template: config.app.basic_mode ? null : form.value.template,
cookies: config.app.basic_mode ? '' : form.value.cookies,
cli: config.app.basic_mode ? null : form_cli,
auto_start: config.app.basic_mode ? true : auto_start.value
preset: form.value.preset || config.app.default_preset,
folder: form.value.folder,
template: form.value.template,
cookies: form.value.cookies,
cli: form_cli,
auto_start: auto_start.value
} as item_request
if (form.value?.extras && Object.keys(form.value.extras).length > 0) {

View file

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

View file

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

View file

@ -49,9 +49,6 @@
</p>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">Files Browser</span>
</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[]>(() => {
if (!search.value) {
return sortedItems(items.value)
@ -335,20 +324,6 @@ const sortedItems = (items: FileItem[]): FileItem[] => {
const model_item = ref<any>()
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 () => {
if (socket.isConnected && initialLoad.value) {
await reloadContent(path.value, true)

View file

@ -104,8 +104,10 @@
<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">
<ul>
<li>The filtering rely on yt-dlp <code>--match-filter</code> logic, whatever works there works here as well
and uses the same logic boolean and operators.</li>
<li>Filtering is based on yt-dlps <code>--match-filter</code> logic. Any expression that works with yt-dlp
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>
The primary use case for this feature is to apply custom cli arguments to specific returned info.
</li>
@ -116,7 +118,8 @@
</li>
<li>
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>
</ul>
</Message>
@ -130,7 +133,6 @@ import type { ConditionItem, ImportedConditionItem } from '~/types/conditions'
type ConditionItemWithUI = ConditionItem & { raw?: boolean }
const toast = useNotification()
const config = useConfigStore()
const socket = useSocketStore()
const box = useConfirm()
const isMobile = useMediaQuery({ maxWidth: 1024 })
@ -144,13 +146,6 @@ const initialLoad = ref(true)
const addInProgress = ref(false)
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 () => {
if (socket.isConnected && initialLoad.value) {
await reloadContent(true)

View file

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

View file

@ -24,7 +24,7 @@
</button>
</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">
<span class="icon"><i class="fas fa-pause" /></span>
<span v-if="!isMobile">Pause</span>
@ -35,7 +35,7 @@
</button>
</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">
<span class="icon"><i class="fa-solid fa-plus" /></span>
<span v-if="!isMobile">New Download</span>
@ -62,54 +62,7 @@
</div>
</div>
<div v-if="config.is_loaded" class="columns is-multiline">
<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"
<NewDownload v-if="config.showForm"
@getInfo="(url: string, preset: string = '', cli: string = '') => view_info(url, false, preset, cli)"
:item="item_form" @clear_form="item_form = {}" />
<Queue @getInfo="(url: string, preset: string = '', cli: string = '') => view_info(url, false, preset, cli)"
@ -128,7 +81,6 @@
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import DeprecatedNotice from '~/components/DeprecatedNotice.vue'
import type { item_request } from '~/types/item'
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 => {
if (v) {
return

View file

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

View file

@ -39,8 +39,8 @@
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">Custom presets. The presets are simply pre-defined yt-dlp settings
that you want to apply to given download.</span>
<span class="subtitle">Presets are pre-defined command options for yt-dlp that you want to apply to given
download.</span>
</div>
</div>
</div>
@ -192,16 +192,7 @@
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle">
<ul>
<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>
<li>When you export preset, it doesn't include <code>Cookies</code> field for security reasons.</li>
</ul>
</Message>
</div>
@ -233,13 +224,6 @@ const remove_keys = ['raw', 'toggle_description']
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 () => {
if (socket.isConnected && initialLoad.value) {
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['_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 () => {
if (socket.isConnected && initialLoad.value) {
socket.on('item_status', statusHandler)

View file

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

View file

@ -14,20 +14,14 @@ type AppConfig = {
ytdlp_version: string
/** Maximum number of concurrent download workers */
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: string
/** Instance title for the app, null if not set */
instance_title: string | null
/** Indicates if the console is enabled */
console_enabled: boolean
/** Indicates if the file browser is enabled */
browser_enabled: boolean
/** Indicates if the file browser control is enabled */
browser_control_enabled: boolean
/** Command options for yt-dlp */
ytdlp_cli: string
/** Indicates if file logging is enabled */
file_logging: boolean
/** Indicates if the app is running in a native environment (e.g., Electron) */

View file

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