diff --git a/README.md b/README.md index d401acb21f..ca931aba3f 100644 --- a/README.md +++ b/README.md @@ -451,7 +451,9 @@ ## Video Selection: those that have a like count more than 100 (or the like field is not available) and also has a description that contains the - phrase "cats & dogs" (ignoring case) + phrase "cats & dogs" (ignoring case). Use + "--match-filter -" to interactively ask + whether to download each video --no-match-filter Do not use generic video filter (default) --no-playlist Download only the video, if the URL refers to a video and a playlist diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index 4351699b6a..78345f87aa 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -413,6 +413,8 @@ class YoutubeDL: every video. If it returns a message, the video is ignored. If it returns None, the video is downloaded. + If it returns utils.NO_DEFAULT, the user is interactively + asked whether to download the video. match_filter_func in utils.py is one example for this. no_color: Do not emit color codes in output. geo_bypass: Bypass geographic restriction via faking X-Forwarded-For @@ -878,6 +880,7 @@ def trouble(self, message=None, tb=None, is_error=True): Styles = Namespace( HEADERS='yellow', EMPHASIS='light blue', + FILENAME='green', ID='green', DELIM='blue', ERROR='red', @@ -1303,7 +1306,17 @@ def check_filter(): except TypeError: # For backward compatibility ret = None if incomplete else match_filter(info_dict) - if ret is not None: + if ret is NO_DEFAULT: + while True: + filename = self._format_screen(self.prepare_filename(info_dict), self.Styles.FILENAME) + reply = input(self._format_screen( + f'Download "{filename}"? (Y/n): ', self.Styles.EMPHASIS)).lower().strip() + if reply in {'y', ''}: + return None + elif reply == 'n': + return f'Skipping {video_title}' + return True + elif ret is not None: return ret return None diff --git a/yt_dlp/minicurses.py b/yt_dlp/minicurses.py index 9fd679a48d..a867fd2898 100644 --- a/yt_dlp/minicurses.py +++ b/yt_dlp/minicurses.py @@ -69,6 +69,7 @@ def format_text(text, f): raise SyntaxError(f'Invalid format {" ".join(tokens)!r} in {f!r}') if fg_color or bg_color: + text = text.replace(CONTROL_SEQUENCES['RESET'], f'{fg_color}{bg_color}') return f'{fg_color}{bg_color}{text}{CONTROL_SEQUENCES["RESET"]}' else: return text diff --git a/yt_dlp/options.py b/yt_dlp/options.py index 73bc88b898..725ab89dbe 100644 --- a/yt_dlp/options.py +++ b/yt_dlp/options.py @@ -471,7 +471,8 @@ def _dict_from_options_callback( '!is_live --match-filter "like_count>?100 & description~=\'(?i)\\bcats \\& dogs\\b\'" ' 'matches only videos that are not live OR those that have a like count more than 100 ' '(or the like field is not available) and also has a description ' - 'that contains the phrase "cats & dogs" (ignoring case)')) + 'that contains the phrase "cats & dogs" (ignoring case). ' + 'Use "--match-filter -" to interactively ask whether to download each video')) selection.add_option( '--no-match-filter', metavar='FILTER', dest='match_filter', action='store_const', const=None, diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index 7faee62ac8..0612139e02 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -3407,11 +3407,15 @@ def match_str(filter_str, dct, incomplete=False): def match_filter_func(filters): if not filters: return None - filters = variadic(filters) + filters = set(variadic(filters)) - def _match_func(info_dict, *args, **kwargs): - if any(match_str(f, info_dict, *args, **kwargs) for f in filters): - return None + interactive = '-' in filters + if interactive: + filters.remove('-') + + def _match_func(info_dict, incomplete=False): + if not filters or any(match_str(f, info_dict, incomplete) for f in filters): + return NO_DEFAULT if interactive and not incomplete else None else: video_title = info_dict.get('title') or info_dict.get('id') or 'video' filter_str = ') | ('.join(map(str.strip, filters))