diff --git a/.vscode/settings.json b/.vscode/settings.json index 3875a991..d3f90560 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -45,6 +45,7 @@ "libjavascriptcoregtk", "libwebkit", "libx", + "matchtitle", "matroska", "mbed", "mccabe", @@ -71,6 +72,7 @@ "pywebview", "qtpy", "quicktime", + "rejecttitle", "rtime", "smhd", "socketio", diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py index 03ef709e..e946be2a 100644 --- a/app/library/DownloadQueue.py +++ b/app/library/DownloadQueue.py @@ -35,6 +35,7 @@ from .Utils import ( is_downloaded, load_cookies, str_to_dt, + ytdlp_reject, ) from .YTDLPOpts import YTDLPOpts @@ -203,7 +204,10 @@ class DownloadQueue(metaclass=Singleton): except Exception as e: LOG.error(f"Failed to cancel downloads. {e!s}") - async def _process_playlist(self, entry: dict, item: Item, already=None): + async def _process_playlist(self, entry: dict, item: Item, already=None, yt_params: dict | None = None): + if not yt_params: + yt_params = {} + entries = entry.get("entries", []) LOG.info(f"Processing '{entry.get('id')}: {entry.get('title')}' Playlist.") @@ -216,6 +220,11 @@ class DownloadQueue(metaclass=Singleton): try: LOG.debug(f"Processing entry {i}/{playlistCount} - ID: {etr.get('id')} - Title: {etr.get('title')}") + _status, _msg = ytdlp_reject(entry=etr, yt_params=yt_params) + if not _status: + LOG.debug(_msg) + return {"status": "ok", "msg": _msg} + extras = { "playlist": entry.get("title") or entry.get("id"), "playlist_count": playlistCount, @@ -424,7 +433,9 @@ class DownloadQueue(metaclass=Singleton): LOG.error(f"Failed to download item. '{e!s}'") return {"status": "error", "msg": str(e)} - async def _add_item(self, entry: dict, item: Item, already=None, logs: list | None = None): + async def _add_item( + self, entry: dict, item: Item, already=None, logs: list | None = None, yt_params: dict | None = None + ): """ Add an entry to the download queue. @@ -433,6 +444,7 @@ class DownloadQueue(metaclass=Singleton): item (Item): The item to add to the download queue. already (set): The set of already downloaded items. logs (list): The list of logs generated during information extraction. + yt_params (dict): The parameters for yt-dlp. Returns: dict: The status of the operation. @@ -444,7 +456,7 @@ class DownloadQueue(metaclass=Singleton): event_type = entry.get("_type", "video") if event_type.startswith("playlist"): - return await self._process_playlist(entry=entry, item=item, already=already) + return await self._process_playlist(entry=entry, item=item, already=already, yt_params=yt_params) if event_type.startswith("url"): return await self.add(item=item.new_with(url=entry.get("url")), already=already) @@ -556,6 +568,11 @@ class DownloadQueue(metaclass=Singleton): LOG.info(f"Condition '{condition.name}' matched for '{item.url}'.") return await self.add(item=item.new_with(requeued=True, cli=condition.cli), already=already) + _status, _msg = ytdlp_reject(entry=entry, yt_params=yt_conf) + if not _status: + LOG.debug(_msg) + return {"status": "ok", "msg": _msg} + end_time = time.perf_counter() - started LOG.debug(f"extract_info: for 'URL: {item.url}' is done in '{end_time:.3f}'. Length: '{len(entry)}/keys'.") except yt_dlp.utils.ExistingVideoReached as exc: @@ -578,7 +595,7 @@ class DownloadQueue(metaclass=Singleton): except Exception as e: LOG.error(f"Failed to remove cookie file '{yt_conf['cookiefile']}'. {e!s}") - return await self._add_item(entry=entry, item=item, already=already, logs=logs) + return await self._add_item(entry=entry, item=item, already=already, logs=logs, yt_params=yt_conf) async def cancel(self, ids: list[str]) -> dict[str, str]: """ diff --git a/app/library/Utils.py b/app/library/Utils.py index 43fa2c0b..92505b47 100644 --- a/app/library/Utils.py +++ b/app/library/Utils.py @@ -17,7 +17,7 @@ from typing import TypeVar import yt_dlp from Crypto.Cipher import AES -from yt_dlp.utils import match_str +from yt_dlp.utils import age_restricted, match_str from .LogWrapper import LogWrapper @@ -1325,3 +1325,50 @@ def str_to_dt(time_str: str, now=None) -> datetime: raise ValueError(msg) return dt + + +def ytdlp_reject(entry: dict, yt_params: dict) -> tuple[bool, str]: + """ + Implement yt-dlp reject filter logic. + + Args: + entry (dict): The entry to check. + yt_params (dict): The yt-dlp parameters containing filters. + + Returns: + tuple[bool, str]: A tuple where the first element is True if the entry passes the filters, or False if it does not, + and the second element is a message explaining the reason for rejection or an empty string if it passes. + + """ + if title := entry.get("title"): + if (matchtitle := yt_params.get("matchtitle")) and not re.search(matchtitle, title, re.IGNORECASE): + return (False, f'"{title}" title did not match pattern "{matchtitle}". Skipping download.') + + if (rejecttitle := yt_params.get("rejecttitle")) and re.search(rejecttitle, title, re.IGNORECASE): + return (False, f'"{title}" title matched reject pattern "{rejecttitle}". Skipping download.') + + date = entry.get("upload_date") + date_range = yt_params.get("daterange") + if (date and date_range) and date not in date_range: + return (False, f"Upload date '{date}' is not in range '{date_range}'.") + + view_count = entry.get("view_count") + if view_count is not None: + min_views = yt_params.get("min_views") + if min_views is not None and view_count < min_views: + return ( + False, + f"Skipping {entry.get('title', 'video')}, because it has not reached minimum view count ({view_count}/{min_views}).", + ) + + max_views = yt_params.get("max_views") + if max_views is not None and view_count > max_views: + return ( + False, + f"Skipping {entry.get('title', 'video')}, because it has exceeded maximum view count ({view_count}/{max_views}).", + ) + + if entry.get("age_limit") and age_restricted(entry.get("age_limit"), yt_params.get("age_limit")): + return (False, f'Video "{entry.get("title", "unknown")}" is age restricted.') + + return (True, "")