From d03e04db19d86001999d313d84acc205892baac2 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Fri, 4 Apr 2025 02:07:56 +0300 Subject: [PATCH 1/8] minor changes to webui display --- app/library/config.py | 11 ++++++++--- ui/components/History.vue | 19 +++++++++++++++++-- ui/pages/presets.vue | 14 ++++++++++++++ ui/stores/ConfigStore.js | 1 + 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/app/library/config.py b/app/library/config.py index 800a4907..761ec81e 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -148,6 +148,9 @@ class Config: ytdlp_auto_update: bool = False """Enable in-place auto update of yt-dlp package.""" + ytdlp_cli: str = "" + """The command line options to use for yt-dlp.""" + pictures_backends: list[str] = [ "https://unsplash.it/1920/1080?random", "https://picsum.photos/1920/1080", @@ -171,6 +174,7 @@ class Config: "tasks", "new_version_available", "started", + "ytdlp_cli", ) "The variables that are immutable." @@ -217,6 +221,7 @@ class Config: "sentry_dsn", "console_enabled", "browser_enabled", + "ytdlp_cli", ) "The variables that are relevant to the frontend." @@ -323,12 +328,12 @@ class Config: if os.path.exists(opts_file) and os.path.getsize(opts_file) > 2: LOG.info(f"Loading yt-dlp custom options from '{opts_file}'.") with open(opts_file) as f: - ytdlp_cli_opts = f.read().strip() - if ytdlp_cli_opts: + self.ytdlp_cli = f.read().strip() + if self.ytdlp_cli: try: removed_options = [] self.ytdl_options = arg_converter( - args=ytdlp_cli_opts, + args=self.ytdlp_cli, level=1, removed_options=removed_options, ) diff --git a/ui/components/History.vue b/ui/components/History.vue index d852e64b..f6fca351 100644 --- a/ui/components/History.vue +++ b/ui/components/History.vue @@ -22,7 +22,7 @@ -
+
@@ -455,6 +455,21 @@ const hasCompleted = computed(() => { return false }) +const hasDownloaded = computed(() => { + if (Object.keys(stateStore.history)?.length < 0) { + return false + } + + for (const key in stateStore.history) { + const element = stateStore.history[key] + if (element.status === 'finished' && element.filename) { + console.log(element); + return true + } + } + return false +}) + const deleteSelectedItems = () => { if (selectedElms.value.length < 1) { toast.error('No items selected.') diff --git a/ui/pages/presets.vue b/ui/pages/presets.vue index c0197658..a9bebb5e 100644 --- a/ui/pages/presets.vue +++ b/ui/pages/presets.vue @@ -127,6 +127,11 @@ div.is-centered {
  • When you export preset, it doesn't include Cookies field for security reasons.
  • +
  • + If you have created a global config/ytdlp.cli file, it will be appended to your exported preset + Command arguments for yt-dlp field for better compatibility + and completeness. +
  • @@ -307,6 +312,15 @@ const exportItem = item => { userData[key] = data[key] } + if (config?.app?.ytdlp_cli) { + const val = `# exported from ytdlp.cli #\n${config.app.ytdlp_cli}\n# exported from ytdlp.cli #\n` + if (userData.cli) { + userData.cli = val + "\n" + userData.cli + } else { + userData.cli = val + } + } + userData['_type'] = 'preset' userData['_version'] = '2.0' diff --git a/ui/stores/ConfigStore.js b/ui/stores/ConfigStore.js index 35aa31be..033b0902 100644 --- a/ui/stores/ConfigStore.js +++ b/ui/stores/ConfigStore.js @@ -18,6 +18,7 @@ const CONFIG_KEYS = { sentry_dsn: null, console_enabled: false, browser_enabled: false, + ytdlp_cli: '', }, presets: [ { From f17a27d35c918c9107364eb54c2511c10dd0e0ae Mon Sep 17 00:00:00 2001 From: arabcoders Date: Fri, 4 Apr 2025 02:08:11 +0300 Subject: [PATCH 2/8] fixed bug preventing the user from importing a preset --- ui/components/PresetForm.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/components/PresetForm.vue b/ui/components/PresetForm.vue index 0cbd7439..64dc703d 100644 --- a/ui/components/PresetForm.vue +++ b/ui/components/PresetForm.vue @@ -331,7 +331,7 @@ const importItem = async () => { try { const item = JSON.parse(val) - if (item?._type || 'preset' !== item._type) { + if (!item?._type || 'preset' !== item._type) { toast.error(`Invalid import string. Expected type 'preset', got '${item._type ?? 'unknown'}'.`) return } From 48e388b0fec47d09902c15e3e7cbc2084f1a1d91 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Fri, 4 Apr 2025 20:18:03 +0300 Subject: [PATCH 3/8] Add logs viewing and enable file logging by default --- .vscode/settings.json | 2 +- app/library/Events.py | 3 + app/library/HttpAPI.py | 40 ++- app/library/HttpSocket.py | 97 +++++++- app/library/Utils.py | 107 ++++++++ app/library/config.py | 29 ++- ui/assets/css/style.css | 19 ++ ui/components/NewDownload.vue | 5 +- ui/layouts/default.vue | 31 ++- ui/pages/{changeslog.vue => changelog.vue} | 6 +- ui/pages/logs.vue | 275 +++++++++++++++++++++ ui/stores/ConfigStore.js | 1 + 12 files changed, 588 insertions(+), 27 deletions(-) rename ui/pages/{changeslog.vue => changelog.vue} (93%) create mode 100644 ui/pages/logs.vue diff --git a/.vscode/settings.json b/.vscode/settings.json index 6fa6b457..d1dff94b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,7 +18,6 @@ "arrowless", "attl", "autonumber", - "changeslog", "consoletitle", "cookiesfrombrowser", "copyts", @@ -49,6 +48,7 @@ "preferredquality", "printtraffic", "quicktime", + "timespec", "tmpfilename", "upgrader", "urandom", diff --git a/app/library/Events.py b/app/library/Events.py index 156b3a65..0fb1afbd 100644 --- a/app/library/Events.py +++ b/app/library/Events.py @@ -111,6 +111,9 @@ class Events: PRESETS_UPDATE = "presets_update" SCHEDULE_ADD = "schedule_add" + SUBSCRIBED = "subscribed" + UNSUBSCRIBED = "unsubscribed" + def get_all() -> list: """ Get all the events. diff --git a/app/library/HttpAPI.py b/app/library/HttpAPI.py index 51a02169..b2a3a470 100644 --- a/app/library/HttpAPI.py +++ b/app/library/HttpAPI.py @@ -46,6 +46,7 @@ from .Utils import ( get_file_sidecar, get_files, get_mime_type, + read_logfile, validate_url, validate_uuid, ) @@ -73,7 +74,8 @@ class HttpAPI(Common): "/presets", "/tasks", "/notifications", - "/changeslog", + "/changelog", + "/logs", "/browser", "/browser/{path:.*}", ] @@ -728,6 +730,42 @@ class HttpAPI(Common): dumps=self.encoder.encode, ) + @route("GET", "api/logs") + async def logs(self, request: Request) -> Response: + """ + Get recent logs + + Args: + request (Request): The request object. + + Returns: + Response: The response object. + + """ + if not self.config.file_logging: + return web.json_response( + data={"error": "File logging is not enabled."}, status=web.HTTPNotFound.status_code + ) + + offset = int(request.query.get("offset", 0)) + limit = int(request.query.get("limit", 50)) + if limit < 1 or limit > 150: + limit = 50 + + return web.json_response( + data={ + "logs": await read_logfile( + file=os.path.join(self.config.config_path, "logs", "app.log"), + offset=offset, + limit=limit, + ), + "offset": offset, + "limit": limit, + }, + status=web.HTTPOk.status_code, + dumps=self.encoder.encode, + ) + @route("GET", "api/presets") async def presets(self, request: Request) -> Response: """ diff --git a/app/library/HttpSocket.py b/app/library/HttpSocket.py index fae33413..40191789 100644 --- a/app/library/HttpSocket.py +++ b/app/library/HttpSocket.py @@ -19,7 +19,7 @@ from .encoder import Encoder from .Events import Event, EventBus, Events, error from .ItemDTO import Item from .Presets import Presets -from .Utils import is_downloaded +from .Utils import is_downloaded, tail_log LOG = logging.getLogger("socket_api") @@ -33,6 +33,12 @@ class HttpSocket(Common): sio: socketio.AsyncServer queue: DownloadQueue + subscribers: dict[str, list[str]] = {} + """Event subscriber list.""" + + log_task = None + """Task to tail the log file.""" + def __init__( self, queue: DownloadQueue | None = None, @@ -288,3 +294,92 @@ class HttpSocket(Common): async def resume(self, *_, **__): self.queue.resume() await self._notify.emit(Events.PAUSED, data={"paused": False, "at": time.time()}) + + @ws_event + async def subscribe(self, sid: str, event: str): + """ + Subscribe to a specific event. + + Args: + sid (str): The session ID of the client. + event (str): The event to subscribe to. + + """ + if not isinstance(event, str): + await self._notify.emit(Events.ERROR, data=error("Invalid event."), to=sid) + return + + if event not in self.subscribers: + self.subscribers[event] = [] + + if sid not in self.subscribers[event]: + self.subscribers[event].append(sid) + LOG.debug(f"Client '{sid}' subscribed to event '{event}'.") + await self.sio.emit(Events.SUBSCRIBED, data={"event": event}, to=sid) + + async def emit_logs(data: dict): + await self.subscribe_emit(event=event, data=data) + + if "log_lines" == event and self.log_task is None: + LOG.debug("Starting log tailing task.") + self.log_task = asyncio.create_task( + tail_log( + file=os.path.join(self.config.config_path, "logs", "app.log"), + emitter=emit_logs, + ), + name="tail_log", + ) + + @ws_event + async def unsubscribe(self, sid: str, event: str): + """ + Unsubscribe from a specific event. + + Args: + sid (str): The session ID of the client. + event (str): The event to unsubscribe from. + + """ + if event not in self.subscribers: + return + + if sid not in self.subscribers[event]: + return + + self.subscribers[event].remove(sid) + await self.sio.emit(Events.UNSUBSCRIBED, data={"event": event}, to=sid) + + LOG.debug(f"Client '{sid}' unsubscribed from event '{event}'.") + + if "log_lines" != event or not self.log_task or "log_lines" not in self.subscribers: + return + + if len(self.subscribers["log_lines"]) < 1: + try: + LOG.debug("Stopping log tailing task.") + self.log_task.cancel() + self.log_task = None + except asyncio.CancelledError: + pass + + @ws_event + async def disconnect(self, sid: str): + """ + Handle client disconnection. + + Args: + sid (str): The session ID of the client. + + """ + LOG.debug(f"Client '{sid}' disconnected.") + + for event in self.subscribers: + if sid in self.subscribers[event]: + await self.unsubscribe(sid=sid, event=event) + + async def subscribe_emit(self, event: str, data: dict): + if event not in self.subscribers or len(self.subscribers[event]) < 1: + return + + for sid in self.subscribers[event]: + await self.sio.emit(event=event, data=data, to=sid) diff --git a/app/library/Utils.py b/app/library/Utils.py index 0e5ae8b8..b8e79806 100644 --- a/app/library/Utils.py +++ b/app/library/Utils.py @@ -70,11 +70,18 @@ FILES_TYPE: list = [ {"rx": re.compile(r"\.(nfo|json|jpg|torrent|\.info\.json)$", re.IGNORECASE), "type": "metadata"}, ] +DATETIME_PATTERN = re.compile(r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:\d{2}))\s?") + class StreamingError(Exception): """Raised when an error occurs during streaming.""" +class FileLogFormatter(logging.Formatter): + def formatTime(self, record, datefmt=None): # noqa: ARG002, N802 + return datetime.datetime.fromtimestamp(record.created).astimezone().isoformat(timespec="milliseconds") + + def calc_download_path(base_path: str, folder: str | None = None, create_path: bool = True) -> str: """ Calculates download path and prevents folder traversal. @@ -944,3 +951,103 @@ def strip_newline(string: str) -> str: res = re.sub(r"(\r\n|\r|\n)", " ", string) return res.strip() if res else "" + + +async def read_logfile(file: str, offset: int = 0, limit: int = 50) -> list[str]: + """ + Read log file and return limited lines from the end. + + Args: + file (str): The log file path. + offset (int): The number of lines to skip from the end. + limit (int): The number of lines to read. + + Returns: + list[str]: A list of log lines. + + """ + from anyio import open_file + + if not os.path.exists(file): + return [] + + from hashlib import sha256 + + try: + async with await open_file(file, "rb") as f: + await f.seek(0, os.SEEK_END) + size = await f.tell() + + block_size = 1024 + block_end = size + buffer = b"" + lines = [] + + while len(lines) < (offset + limit) and block_end > 0: + block_start = max(0, block_end - block_size) + await f.seek(block_start) + chunk = await f.read(block_end - block_start) + buffer = chunk + buffer + lines = buffer.splitlines() + block_end = block_start + + selected_lines = lines[-(offset + limit) :] if offset else lines[-limit:] + if offset: + selected_lines = selected_lines[:-offset] or [] + + result = [] + for line in selected_lines: + line_bytes = line if isinstance(line, bytes) else line.encode() + msg = line.decode(errors="replace") + dt_match = DATETIME_PATTERN.match(msg) + + result.append( + { + "id": sha256(line_bytes).hexdigest(), + "line": msg[dt_match.end() :] if dt_match else msg, + "datetime": dt_match.group(1) if dt_match else None, + } + ) + + return result + + except Exception: + return [] + + +async def tail_log(file: str, emitter: callable, sleep_time: float = 0.5): + """ + Continuously read a log file and emit new lines. + + Args: + file (str): The log file path. + emitter (callable): A callable to emit new lines. + sleep_time (float): The time to sleep between reads. + + """ + from asyncio import sleep as asyncio_sleep + from hashlib import sha256 + + from anyio import open_file + + if not os.path.exists(file): + return + + async with await open_file(file, "rb") as f: + await f.seek(0, os.SEEK_END) + while True: + line = await f.readline() + if not line: + await asyncio_sleep(sleep_time) + continue + + msg = line.decode(errors="replace") + dt_match = DATETIME_PATTERN.match(msg) + + await emitter( + { + "id": sha256(line if isinstance(line, bytes) else line.encode()).hexdigest(), + "line": msg[dt_match.end() :] if dt_match else msg, + "datetime": dt_match.group(1) if dt_match else None, + } + ) diff --git a/app/library/config.py b/app/library/config.py index 761ec81e..39558720 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -4,14 +4,14 @@ import os import re import sys import time -from logging.handlers import RotatingFileHandler +from logging.handlers import TimedRotatingFileHandler from multiprocessing.managers import SyncManager from pathlib import Path import coloredlogs from dotenv import load_dotenv -from .Utils import arg_converter +from .Utils import FileLogFormatter, arg_converter from .version import APP_VERSION @@ -55,6 +55,9 @@ class Config: log_level: str = "info" """The log level to use for the application.""" + log_level_file: str = "info" + """The log level to use for the file logging.""" + max_workers: int = 1 """The maximum number of workers to use for downloading.""" @@ -130,7 +133,7 @@ class Config: instance_title: str | None = None "The title of the instance." - file_logging: bool = False + file_logging: bool = True "Enable file logging." sentry_dsn: str | None = None @@ -222,6 +225,7 @@ class Config: "console_enabled", "browser_enabled", "ytdlp_cli", + "file_logging", ) "The variables that are relevant to the frontend." @@ -377,11 +381,22 @@ class Config: LOG.info("The frontend is running in basic mode.") if self.file_logging: - handler = RotatingFileHandler( - os.path.join(self.config_path, "app.log"), maxBytes=1 * 1024 * 1024, backupCount=3 + log_level_file = 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 = os.path.join(self.config_path, "logs") + if not os.path.exists(loggingPath): + os.makedirs(loggingPath, exist_ok=True) + + handler = TimedRotatingFileHandler( + filename=os.path.join(self.config_path, "logs", "app.log"), + when="midnight", + backupCount=3, ) - handler.setLevel(logging.ERROR) - formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + handler.setLevel(log_level_file) + formatter = FileLogFormatter("%(asctime)s [%(levelname)s.%(name)s]: %(message)s") handler.setFormatter(formatter) logging.getLogger().addHandler(handler) diff --git a/ui/assets/css/style.css b/ui/assets/css/style.css index dced2c82..9292c447 100644 --- a/ui/assets/css/style.css +++ b/ui/assets/css/style.css @@ -51,11 +51,26 @@ hr { border-bottom: 1px dotted; } +.has-tooltip { + cursor: help; + border-bottom: 1px dotted; +} + +@media (prefers-color-scheme: light) { + .has-tooltip { + border-bottom-color: #000; + } +} + @media (prefers-color-scheme: dark) { * { unicode-bidi: plaintext; } + .has-tooltip { + border-bottom-color: rgba(255, 255, 255, 0.3); + } + .container { padding: 1em; margin-top: 1em; @@ -266,6 +281,10 @@ hr { white-space: pre; } +.is-pre-wrap { + white-space: pre-wrap; +} + .has-text-bold { font-weight: bold; } diff --git a/ui/components/NewDownload.vue b/ui/components/NewDownload.vue index 8ff3f473..2ec67041 100644 --- a/ui/components/NewDownload.vue +++ b/ui/components/NewDownload.vue @@ -77,7 +77,6 @@
    -
    -
    +
    -
    +
    - - -
    diff --git a/ui/pages/changeslog.vue b/ui/pages/changelog.vue similarity index 93% rename from ui/pages/changeslog.vue rename to ui/pages/changelog.vue index fc0ec2b7..18e08777 100644 --- a/ui/pages/changeslog.vue +++ b/ui/pages/changelog.vue @@ -2,12 +2,12 @@
    - + - Changes log + CHANGELOG
    - This page displays the application change logs. + This page display the latest changes and updates from the project.
    diff --git a/ui/pages/logs.vue b/ui/pages/logs.vue new file mode 100644 index 00000000..477ec92f --- /dev/null +++ b/ui/pages/logs.vue @@ -0,0 +1,275 @@ + + + + diff --git a/ui/stores/ConfigStore.js b/ui/stores/ConfigStore.js index 033b0902..6f14346b 100644 --- a/ui/stores/ConfigStore.js +++ b/ui/stores/ConfigStore.js @@ -19,6 +19,7 @@ const CONFIG_KEYS = { console_enabled: false, browser_enabled: false, ytdlp_cli: '', + file_logging: false, }, presets: [ { From 737b923a97ff7c94d1a9addcac6cfe106c6302c2 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Fri, 4 Apr 2025 20:21:40 +0300 Subject: [PATCH 4/8] remove debug statement --- ui/pages/logs.vue | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/pages/logs.vue b/ui/pages/logs.vue index 477ec92f..efe48e7e 100644 --- a/ui/pages/logs.vue +++ b/ui/pages/logs.vue @@ -242,8 +242,6 @@ const scrollToBottom = () => { onMounted(async () => { await fetchLogs() socket.emit('subscribe', 'log_lines') - window.socket = socket - socket.on('log_lines', data => { if (logs.value.length >= maxLogLimit) { logs.value.shift() From 93e830f029cfa3853638a4b45c945f36973402cb Mon Sep 17 00:00:00 2001 From: arabcoders Date: Fri, 4 Apr 2025 22:09:58 +0300 Subject: [PATCH 5/8] in some specific cases, users were unable to delete items from history --- app/library/DataStore.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/library/DataStore.py b/app/library/DataStore.py index 7356a67d..eb92e4a5 100644 --- a/app/library/DataStore.py +++ b/app/library/DataStore.py @@ -70,9 +70,9 @@ class DataStore: for row in cursor: rowDate = datetime.strptime(row["created_at"], "%Y-%m-%d %H:%M:%S") # noqa: DTZ007 data, _ = clean_item(json.loads(row["data"]), keys=ItemDTO.removed_fields()) - key: str = data.pop("_id") + data.pop("_id", None) item: ItemDTO = ItemDTO(**data) - item._id = key + item._id = row["id"] item.datetime = formatdate(rowDate.replace(tzinfo=UTC).timestamp()) items.append((row["id"], item)) From 260901cfa909ef30f33475e12372386cd27f1df7 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Fri, 4 Apr 2025 22:53:21 +0300 Subject: [PATCH 6/8] minor changes to newDownload form --- app/library/DownloadQueue.py | 10 ++- ui/assets/css/style.css | 4 ++ ui/components/History.vue | 26 +++++--- ui/components/NewDownload.vue | 121 +++++++++++++++++++++------------- ui/error.vue | 4 +- ui/pages/index.vue | 24 ++++++- 6 files changed, 128 insertions(+), 61 deletions(-) diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py index d9e894e6..5e87678d 100644 --- a/app/library/DownloadQueue.py +++ b/app/library/DownloadQueue.py @@ -18,7 +18,7 @@ from .AsyncPool import AsyncPool from .config import Config from .DataStore import DataStore from .Download import Download -from .Events import EventBus, Events +from .Events import EventBus, Events, info from .ItemDTO import Item, ItemDTO from .Presets import Presets from .Singleton import Singleton @@ -307,6 +307,14 @@ class DownloadQueue(metaclass=Singleton): dlInfo.info.status = "not_live" itemDownload = self.done.put(dlInfo) NotifyEvent = Events.COMPLETED + log_message = f"{dl.title or dl.id or dl._id}: stream is not live yet." + if dlInfo.info.live_in: + log_message += f" Will start in {dlInfo.info.live_in}." + + await self._notify.emit( + Events.LOG_INFO, + data=info(msg=log_message, data=itemDownload.info.serialize()), + ) elif self.config.allow_manifestless is False and is_manifestless is True: dlInfo.info.status = "error" dlInfo.info.error = "Video is in post-live manifestless mode." diff --git a/ui/assets/css/style.css b/ui/assets/css/style.css index 9292c447..db506ea5 100644 --- a/ui/assets/css/style.css +++ b/ui/assets/css/style.css @@ -309,3 +309,7 @@ hr { .is-auto { unicode-bidi: embed; } + +.is-justify-self-end { + justify-self: end; +} diff --git a/ui/components/History.vue b/ui/components/History.vue index f6fca351..3e424c24 100644 --- a/ui/components/History.vue +++ b/ui/components/History.vue @@ -153,7 +153,7 @@
    @@ -283,7 +283,7 @@
    - + Re-queue @@ -371,13 +371,13 @@ import moment from 'moment' import { useStorage } from '@vueuse/core' import { makeDownload, formatBytes } from '~/utils/index' -import toast from '~/plugins/toast' import { isEmbedable, getEmbedable } from '~/utils/embedable' -const emitter = defineEmits(['getInfo']) +const emitter = defineEmits(['getInfo', 'add_new']) const config = useConfigStore() const stateStore = useStateStore() const socket = useSocketStore() +const toast = useToast() const selectedElms = ref([]) const masterSelectAll = ref(false) @@ -624,9 +624,7 @@ const removeItem = item => { }) } -const reQueueItem = item => { - socket.emit('item_delete', { id: item._id, remove_file: false }) - +const reQueueItem = (item, event = null) => { let extras = {} if (item.extras) { @@ -637,7 +635,7 @@ const reQueueItem = item => { }) } - socket.emit('add_url', { + const item_req = { url: item.url, preset: item.preset, folder: item.folder, @@ -645,7 +643,17 @@ const reQueueItem = item => { template: item.template, cli: item?.cli, extras: extras - }) + }; + + socket.emit('item_delete', { id: item._id, remove_file: false }) + + if (event && (event?.altKey && true === event?.altKey)) { + toast.info('Removed the item from history, and added it to the new download form.') + emitter('add_new', item_req) + return + } + + socket.emit('add_url', item_req) } const pImg = e => e.target.naturalHeight > e.target.naturalWidth ? e.target.classList.add('image-portrait') : null diff --git a/ui/components/NewDownload.vue b/ui/components/NewDownload.vue index 2ec67041..dc013a87 100644 --- a/ui/components/NewDownload.vue +++ b/ui/components/NewDownload.vue @@ -11,7 +11,7 @@
    + :disabled="!socket.isConnected || addInProgress" v-model="form.url">
    @@ -29,7 +29,7 @@
    @@ -63,7 +63,7 @@
    @@ -85,7 +85,7 @@ Output Template
    -
    @@ -104,7 +104,7 @@ Command arguments for yt-dlp
    -