diff --git a/.vscode/settings.json b/.vscode/settings.json index 3fcb2218..09c5c3df 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -80,6 +80,9 @@ "onefile", "pathex", "platformdirs", + "playlistend", + "playlistrandom", + "playlistreverse", "plexmatch", "postprocessor", "preferredcodec", diff --git a/app/library/Download.py b/app/library/Download.py index fff8db9f..73577a8d 100644 --- a/app/library/Download.py +++ b/app/library/Download.py @@ -229,7 +229,7 @@ class Download: # Safe-guard incase downloading take too long and the info expires. if self.info_dict and isinstance(self.info_dict, dict) and self.download_info_expires > 0: - _ts: int | None = self.info_dict.get("timestamp") + _ts: int | None = self.info_dict.get("epoch", self.info_dict.get("timestamp", None)) _ts = datetime.fromtimestamp(_ts, tz=UTC) if _ts else None if not _ts or (datetime.now(tz=UTC) - _ts).total_seconds() > self.download_info_expires: self.info_dict = None diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py index f2d0a8c0..77fc7f8b 100644 --- a/app/library/DownloadQueue.py +++ b/app/library/DownloadQueue.py @@ -305,9 +305,11 @@ class DownloadQueue(metaclass=Singleton): entries = entry.get("entries", []) - LOG.info(f"Processing '{entry.get('id')}: {entry.get('title')}' Playlist.") + LOG.info(f"Processing '{entry.get('id')}: {entry.get('title')} ({len(entries)})' Playlist.") + + playlistCount = entry.get("playlist_count") + playlistCount = int(playlistCount) if playlistCount else len(entries) - playlistCount = int(entry.get("playlist_count", len(entries))) results = [] playlist_keys = { @@ -325,11 +327,19 @@ class DownloadQueue(metaclass=Singleton): } async def playlist_processor(i: int, etr: dict): - await self.processors.acquire() try: - LOG.info( - f"Processing '{entry.get('title')}: {i}/{playlistCount}' - ID: {etr.get('id')} - Title: {etr.get('title')}" + item_name = ( + f"'{entry.get('title')}: {i}/{playlist_keys['n_entries']}' - '{etr.get('id')}: {etr.get('title')}'" ) + LOG.debug(f"Waiting to acquire lock for {item_name}") + await self.processors.acquire() + LOG.debug(f"Acquired lock for {item_name}") + + if self.is_paused(): + LOG.warning(f"Download is paused. Skipping processing of '{item_name}'.") + return {"status": "ok"} + + LOG.info(f"Processing '{item_name}'.") _status, _msg = ytdlp_reject(entry=etr, yt_params=yt_params) if not _status: diff --git a/app/library/Utils.py b/app/library/Utils.py index 26257ad4..b1f74ad8 100644 --- a/app/library/Utils.py +++ b/app/library/Utils.py @@ -57,7 +57,7 @@ REMOVE_KEYS: list = [ "opt_list_format": "-F, --list-formats", "opt_dump_agent": "--dump-user-agent", "opt_extractor_descriptions": "--extractor-descriptions", - "opt_list_impersonate_targets": "--list-impersonate-targets" + "opt_list_impersonate_targets": "--list-impersonate-targets", }, ] @@ -459,6 +459,7 @@ def arg_converter( level: int | bool | None = None, dumps: bool = False, removed_options: list | None = None, + keep_defaults: bool = False, ) -> dict: """ Convert yt-dlp options to a dictionary. @@ -468,6 +469,7 @@ def arg_converter( level (int|bool|None): Level of options to remove, True for all. dumps (bool): Dump options as JSON. removed_options (list|None): List of removed options. + keep_defaults (bool): Keep default options. Returns: dict: yt-dlp options dictionary. @@ -488,8 +490,7 @@ def arg_converter( default_opts = _default_opts([]).ydl_opts opts = yt_dlp.parse_options(shlex.split(args)).ydl_opts - - diff = {k: v for k, v in opts.items() if default_opts[k] != v} + diff = {k: v for k, v in opts.items() if default_opts[k] != v} if not keep_defaults else opts.items() if "postprocessors" in diff: diff["postprocessors"] = [pp for pp in diff["postprocessors"] if pp not in default_opts["postprocessors"]] diff --git a/app/library/ytdlp.py b/app/library/ytdlp.py index ecbc61e9..b2f8d224 100644 --- a/app/library/ytdlp.py +++ b/app/library/ytdlp.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import Any import yt_dlp @@ -47,13 +45,16 @@ def ytdlp_options() -> list[dict[str, Any]]: ), } for opt in opts - if ( - (getattr(opt, "_short_opts", []) or getattr(opt, "_long_opts", [])) - and getattr(opt, "help", None) - and "SUPPRESSHELP" not in getattr(opt, "help", "") - ) + if (getattr(opt, "_long_opts", []) and getattr(opt, "help", None)) ] - return collect(parser.option_list, "root") + [ + opts = collect(parser.option_list, "root") + [ entry for grp in parser.option_groups for entry in collect(grp.option_list, grp.title or "other") ] + + for opt in opts: + if "SUPPRESSHELP" in opt.get("description", ""): + opt["description"] = "No description available from yt-dlp." + + return opts + diff --git a/ui/app/components/Queue.vue b/ui/app/components/Queue.vue index 53ad7830..e22ef611 100644 --- a/ui/app/components/Queue.vue +++ b/ui/app/components/Queue.vue @@ -523,6 +523,7 @@ const cancelSelected = () => { } cancelItems(selectedElms.value) selectedElms.value = [] + masterSelectAll.value = false return true } diff --git a/ui/app/pages/index.vue b/ui/app/pages/index.vue index e135eb0d..771625c3 100644 --- a/ui/app/pages/index.vue +++ b/ui/app/pages/index.vue @@ -141,10 +141,17 @@ watch(() => stateStore.queue, () => { const pauseDownload = () => { dialog_confirm.value.visible = true - dialog_confirm.value.html_message = `Pause All non-active downloads?
- + dialog_confirm.value.html_message = ` + - This will not stop downloads that are currently in progress. + Pause All non-active downloads? + +
+ + ` dialog_confirm.value.confirm = () => { socket.emit('pause', {})