diff --git a/app/library/Download.py b/app/library/Download.py index 29474493..ff9c790b 100644 --- a/app/library/Download.py +++ b/app/library/Download.py @@ -4,18 +4,20 @@ import logging import multiprocessing import os import re +import signal import time from datetime import UTC, datetime from email.utils import formatdate from pathlib import Path -import yt_dlp +import yt_dlp.utils from .config import Config from .Events import EventBus, Events from .ffprobe import ffprobe from .ItemDTO import ItemDTO from .Utils import delete_dir, extract_info, extract_ytdlp_logs, load_cookies +from .ytdlp import YTDLP from .YTDLPOpts import YTDLPOpts @@ -264,10 +266,17 @@ class Download: params["logger"] = NestedLogger(self.logger) - cls = yt_dlp.YoutubeDL(params=params) + cls = YTDLP(params=params) self.started_time = int(time.time()) + def mark_cancelled(*_): + cls._interrupted = True + cls.to_screen("[info] Interrupt received, exiting cleanly...") + raise SystemExit(130) # noqa: TRY301 + + signal.signal(signal.SIGUSR1, mark_cancelled) + if isinstance(self.info_dict, dict) and len(self.info_dict) > 1: self.logger.debug(f"Downloading '{self.info.url}' using pre-info.") cls.process_ie_result( @@ -275,10 +284,10 @@ class Download: download=True, extra_info={k: v for k, v in self.info.extras.items() if k not in self.info_dict}, ) - ret = cls._download_retcode + ret: int = cls._download_retcode else: self.logger.debug(f"Downloading using url: {self.info.url}") - ret = cls.download(url_list=[self.info.url]) + ret: int = cls.download(url_list=[self.info.url]) self.status_queue.put({"id": self.id, "status": "finished" if ret == 0 else "error"}) except yt_dlp.utils.ExistingVideoReached as exc: @@ -333,7 +342,7 @@ class Download: return False self.cancel_in_progress = True - procId = self.proc.ident + procId: int | None = self.proc.ident self.logger.info(f"Closing PID='{procId}' download process.") @@ -387,7 +396,10 @@ class Download: try: self.logger.info(f"Killing download process: '{self.proc.ident}'.") - self.proc.kill() + if self.proc.pid and "posix" == os.name: + os.kill(self.proc.pid, signal.SIGUSR1) + else: + self.proc.kill() return True except Exception as e: self.logger.error(f"Failed to kill process: '{self.proc.ident}'. {e}") @@ -398,9 +410,9 @@ class Download: if self.temp_keep is True or not self.temp_path: return - if "finished" != self.info.status and self.is_live: + if "finished" != self.info.status and self.info.downloaded_bytes > 0: self.logger.warning( - f"Keeping live temp folder '{self.temp_path}', as the reported status is not finished '{self.info.status}'." + f"Keeping temp folder '{self.temp_path}', as the reported status is not finished '{self.info.status}'." ) return diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py index fb1bf668..ff4f4fd0 100644 --- a/app/library/DownloadQueue.py +++ b/app/library/DownloadQueue.py @@ -10,7 +10,7 @@ from pathlib import Path from sqlite3 import Connection from typing import TYPE_CHECKING -import yt_dlp +import yt_dlp.utils from aiohttp import web from .ag_utils import ag diff --git a/app/library/Utils.py b/app/library/Utils.py index 18e90ee3..8f9a8cdc 100644 --- a/app/library/Utils.py +++ b/app/library/Utils.py @@ -15,11 +15,11 @@ from http.cookiejar import MozillaCookieJar from pathlib import Path from typing import TypeVar -import yt_dlp from Crypto.Cipher import AES from yt_dlp.utils import age_restricted, match_str from .LogWrapper import LogWrapper +from .ytdlp import YTDLP LOG: logging.Logger = logging.getLogger("Utils") @@ -47,7 +47,7 @@ REMOVE_KEYS: list = [ }, ] -YTDLP_INFO_CLS: yt_dlp.YoutubeDL = None +YTDLP_INFO_CLS: YTDLP = None ALLOWED_SUBS_EXTENSIONS: tuple[str] = (".srt", ".vtt", ".ass") @@ -189,7 +189,7 @@ def extract_info( if no_archive and "download_archive" in params: del params["download_archive"] - data = yt_dlp.YoutubeDL(params=params).extract_info(url, download=False) + data = YTDLP(params=params).extract_info(url, download=False) if data and follow_redirect and "_type" in data and "url" == data["_type"]: return extract_info( @@ -208,7 +208,7 @@ def extract_info( if not data["is_premiere"]: data["is_premiere"] = "video" == data.get("media_type") and "is_upcoming" == data.get("live_status") - return yt_dlp.YoutubeDL.sanitize_info(data) if sanitize_info else data + return YTDLP.sanitize_info(data) if sanitize_info else data def merge_dict(source: dict, destination: dict) -> dict: @@ -1103,7 +1103,7 @@ def get_archive_id(url: str) -> tuple[bool, dict[str | None, str | None, str | N } if YTDLP_INFO_CLS is None: - YTDLP_INFO_CLS = yt_dlp.YoutubeDL( + YTDLP_INFO_CLS = YTDLP( params={ "color": "no_color", "extract_flat": True, diff --git a/app/library/ytdlp.py b/app/library/ytdlp.py new file mode 100644 index 00000000..8c5cc719 --- /dev/null +++ b/app/library/ytdlp.py @@ -0,0 +1,12 @@ +import yt_dlp + + +class YTDLP(yt_dlp.YoutubeDL): + _interrupted = False + + def _delete_downloaded_files(self, *args, **kwargs): + if self._interrupted: + self.to_screen("[info] Cancelled — skipping temp cleanup.") + return None + + return super()._delete_downloaded_files(*args, **kwargs) diff --git a/ui/@types/conditions.d.ts b/ui/@types/conditions.d.ts new file mode 100644 index 00000000..a6d440d3 --- /dev/null +++ b/ui/@types/conditions.d.ts @@ -0,0 +1,12 @@ +export type ConditionItem = { + id?: string + name: string + filter: string + cli: string + [key: string]: any +} + +export type ImportedConditionItem = ConditionItem & { + _type: string + _version: string +} diff --git a/ui/@types/item.d.ts b/ui/@types/item.d.ts new file mode 100644 index 00000000..547d2666 --- /dev/null +++ b/ui/@types/item.d.ts @@ -0,0 +1,20 @@ +export type item_request = { + /** Unique identifier for the item */ + id?: string|null, + /** URL of the item to download */ + url: string, + /** Preset to use for the download */ + preset?: string, + /** Where to save the downloaded item */ + folder?: string, + /** Output template for the downloaded item */ + template?: string, + /** Additional command line options for yt-dlp */ + cli?: string, + /** Cookies file for the download */ + cookies?: string, + /** Auto start the download */ + auto_start?: boolean, + /** Extras data for the item */ + extras?: Record, +} diff --git a/ui/@types/responses.d.ts b/ui/@types/responses.d.ts new file mode 100644 index 00000000..377ed414 --- /dev/null +++ b/ui/@types/responses.d.ts @@ -0,0 +1,18 @@ +export type error_response = { + /** The error message */ + error: string, +} + +export type convert_args_response = { + /** The converted CLI args */ + opts?: Record, + /** The output template if was provided */ + output_template?: string, + /** The download path if was provided */ + download_path?: string, + /** The download format if was provided */ + format?: string, + /** The removed options from the original CLI args if any. */ + removed_options?: string[], +} + diff --git a/ui/components/ConditionForm.vue b/ui/components/ConditionForm.vue index 39619f5d..97537ab6 100644 --- a/ui/components/ConditionForm.vue +++ b/ui/components/ConditionForm.vue @@ -206,77 +206,37 @@ - diff --git a/ui/components/GetInfo.vue b/ui/components/GetInfo.vue index d1dccd20..4dee4779 100644 --- a/ui/components/GetInfo.vue +++ b/ui/components/GetInfo.vue @@ -1,3 +1,9 @@ + + - diff --git a/ui/components/LateLoader.vue b/ui/components/LateLoader.vue index f3921e86..337494f7 100644 --- a/ui/components/LateLoader.vue +++ b/ui/components/LateLoader.vue @@ -1,76 +1,77 @@ - diff --git a/ui/components/Message.vue b/ui/components/Message.vue index 4209ce2d..ff4293be 100644 --- a/ui/components/Message.vue +++ b/ui/components/Message.vue @@ -1,67 +1,45 @@ - diff --git a/ui/components/NotificationForm.vue b/ui/components/NotificationForm.vue index 81e06ffe..15599088 100644 --- a/ui/components/NotificationForm.vue +++ b/ui/components/NotificationForm.vue @@ -351,7 +351,7 @@ const importItem = async () => { } if (form.target) { - if (false === box.confirm('This will overwrite the current form fields. Are you sure?', true)) { + if (false === box.confirm('Overwrite the current form fields?', true)) { return } } diff --git a/ui/components/Queue.vue b/ui/components/Queue.vue index ce62eaaa..75ccf661 100644 --- a/ui/components/Queue.vue +++ b/ui/components/Queue.vue @@ -523,7 +523,7 @@ const updateProgress = (item) => { } const confirmCancel = item => { - if (true !== box.confirm(`Are you sure you want to cancel (${item.title})?`)) { + if (true !== box.confirm(`Cancel '${item.title}'?`)) { return false } cancelItems(item._id) @@ -531,7 +531,7 @@ const confirmCancel = item => { } const cancelSelected = () => { - if (true !== box.confirm('Are you sure you want to cancel selected items?')) { + if (true !== box.confirm(`Cancel '${selectedElms.value.length}' selected items?`)) { return false; } cancelItems(selectedElms.value) diff --git a/ui/components/Settings.vue b/ui/components/Settings.vue index 336b22d5..658556dc 100644 --- a/ui/components/Settings.vue +++ b/ui/components/Settings.vue @@ -51,7 +51,7 @@
+
+ +
+
+ +
+
+
+
@@ -126,24 +139,20 @@
- diff --git a/ui/components/TaskForm.vue b/ui/components/TaskForm.vue index 0e5c3a13..bb7463d0 100644 --- a/ui/components/TaskForm.vue +++ b/ui/components/TaskForm.vue @@ -328,6 +328,9 @@ onMounted(() => { if (!props.task?.preset || '' === props.task.preset) { form.preset = toRaw(config.app.default_preset) } + if (typeof form.auto_start === 'undefined' || form.auto_start === null) { + form.auto_start = true + } }) const checkInfo = async () => { @@ -384,7 +387,7 @@ const importItem = async () => { } if (form.url || form.timer) { - if (false === box.confirm('This will overwrite the current form fields. Are you sure?', true)) { + if (false === box.confirm('Overwrite the current form fields?', true)) { return } } diff --git a/ui/pages/conditions.vue b/ui/pages/conditions.vue index 6c3c2a54..f590572d 100644 --- a/ui/pages/conditions.vue +++ b/ui/pages/conditions.vue @@ -16,7 +16,7 @@

- @@ -30,8 +30,8 @@

- +
@@ -114,18 +114,19 @@
- diff --git a/ui/pages/index.vue b/ui/pages/index.vue index 72f005fd..af30088a 100644 --- a/ui/pages/index.vue +++ b/ui/pages/index.vue @@ -18,27 +18,28 @@

-

-

-

@@ -47,6 +48,7 @@ @click="() => changeDisplay()"> + {{ display_style === 'cards' ? 'Cards' : 'List' }}

@@ -66,15 +68,19 @@ +