diff --git a/docs/config.rst b/docs/config.rst index f9c06d8e..21385dda 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -152,6 +152,14 @@ file_convert ------------------------------------------------------------------------------- +match_filters +''''''''''''' +.. autoclass:: ytdl_sub.plugins.match_filters.MatchFiltersOptions() + :members: + :exclude-members: partial_validate + +------------------------------------------------------------------------------- + music_tags '''''''''' .. autoclass:: ytdl_sub.plugins.music_tags.MusicTagsOptions() diff --git a/src/ytdl_sub/config/preset_class_mappings.py b/src/ytdl_sub/config/preset_class_mappings.py index 0912776d..c4a8eb9e 100644 --- a/src/ytdl_sub/config/preset_class_mappings.py +++ b/src/ytdl_sub/config/preset_class_mappings.py @@ -15,6 +15,7 @@ from ytdl_sub.plugins.chapters import ChaptersPlugin from ytdl_sub.plugins.date_range import DateRangePlugin from ytdl_sub.plugins.file_convert import FileConvertPlugin from ytdl_sub.plugins.internal.view import ViewPlugin +from ytdl_sub.plugins.match_filters import MatchFiltersPlugin from ytdl_sub.plugins.music_tags import MusicTagsPlugin from ytdl_sub.plugins.nfo_tags import NfoTagsPlugin from ytdl_sub.plugins.output_directory_nfo_tags import OutputDirectoryNfoTagsPlugin @@ -121,6 +122,7 @@ class PluginMapping: "audio_extract": AudioExtractPlugin, "date_range": DateRangePlugin, "file_convert": FileConvertPlugin, + "match_filters": MatchFiltersPlugin, "music_tags": MusicTagsPlugin, "video_tags": VideoTagsPlugin, "nfo_tags": NfoTagsPlugin, diff --git a/src/ytdl_sub/plugins/match_filters.py b/src/ytdl_sub/plugins/match_filters.py new file mode 100644 index 00000000..2ad8aa1a --- /dev/null +++ b/src/ytdl_sub/plugins/match_filters.py @@ -0,0 +1,78 @@ +from typing import Any +from typing import Dict +from typing import List +from typing import Optional + +from yt_dlp import match_filter_func + +from ytdl_sub.plugins.plugin import Plugin +from ytdl_sub.plugins.plugin import PluginOptions +from ytdl_sub.utils.logger import Logger +from ytdl_sub.validators.validators import StringListValidator + +logger = Logger.get("match_filters") + + +class MatchFiltersOptions(PluginOptions): + """ + Set ``--match-filters``` to pass into yt-dlp to filter entries from being downloaded. + Uses the same syntax as yt-dlp. + + Usage: + + .. code-block:: yaml + + presets: + my_example_preset: + match_filters: + filters: "original_url!*=/shorts/" + + Supports one or multiple filters: + + .. code-block:: yaml + + presets: + my_example_preset: + match_filters: + filters: + - "original_url!*=/shorts/" + - "!is_live" + """ + + _required_keys = {"filters"} + + @classmethod + def partial_validate(cls, name: str, value: Any) -> None: + """Ensure filters looks right""" + if isinstance(value, dict): + value["filters"] = value.get("filters", [""]) + _ = cls(name, value) + + def __init__(self, name, value): + super().__init__(name, value) + self._filters = self._validate_key(key="filters", validator=StringListValidator).list + + @property + def filters(self) -> List[str]: + """ + The filters themselves. If used multiple times, the filter matches if at least one of the + conditions are met. + """ + return [validator.value for validator in self._filters] + + +class MatchFiltersPlugin(Plugin[MatchFiltersOptions]): + plugin_options_type = MatchFiltersOptions + + def ytdl_options(self) -> Optional[Dict]: + """ + Returns + ------- + match_filter after calling the utility function for it + """ + match_filters: List[str] = [] + for filter_str in self.plugin_options.filters: + logger.debug("Adding match-filter %s", filter_str) + match_filters.append(filter_str) + + return {"match_filter": match_filter_func(match_filters)} diff --git a/src/ytdl_sub/subscriptions/subscription_ytdl_options.py b/src/ytdl_sub/subscriptions/subscription_ytdl_options.py index b5c52be4..e922f1ed 100644 --- a/src/ytdl_sub/subscriptions/subscription_ytdl_options.py +++ b/src/ytdl_sub/subscriptions/subscription_ytdl_options.py @@ -12,6 +12,7 @@ from ytdl_sub.plugins.audio_extract import AudioExtractPlugin from ytdl_sub.plugins.chapters import ChaptersPlugin from ytdl_sub.plugins.date_range import DateRangePlugin from ytdl_sub.plugins.file_convert import FileConvertPlugin +from ytdl_sub.plugins.match_filters import MatchFiltersPlugin from ytdl_sub.plugins.plugin import Plugin from ytdl_sub.plugins.subtitles import SubtitlesPlugin from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive @@ -110,6 +111,7 @@ class SubscriptionYTDLOptions: self._global_options, self._output_options, self._plugin_ytdl_options(DateRangePlugin), + self._plugin_ytdl_options(MatchFiltersPlugin), self._user_ytdl_options, # user ytdl options... self._info_json_only_options, # then info_json_only options ) diff --git a/tests/e2e/plugins/test_match_filters.py b/tests/e2e/plugins/test_match_filters.py new file mode 100644 index 00000000..3f9cf029 --- /dev/null +++ b/tests/e2e/plugins/test_match_filters.py @@ -0,0 +1,36 @@ +import pytest + +from ytdl_sub.subscriptions.subscription import Subscription + + +@pytest.fixture +def preset_dict(output_directory): + return { + "preset": "music_video", + "download": {"url": "https://www.youtube.com/watch?v=2zYF9JLHDmA"}, + "output_options": {"output_directory": output_directory}, + # download the worst format so it is fast + "ytdl_options": { + "postprocessor_args": {"ffmpeg": ["-bitexact"]}, # Must add this for reproducibility + }, + "match_filters": {"filters": ["is_live", "view_count <=? 10"]}, + } + + +class TestFileConvert: + @pytest.mark.parametrize("dry_run", [True, False]) + def test_match_filters( + self, + music_video_config, + preset_dict, + output_directory, + dry_run, + ): + subscription = Subscription.from_dict( + config=music_video_config, + preset_name="match_filter_test", + preset_dict=preset_dict, + ) + + transaction_log = subscription.download(dry_run=dry_run) + assert transaction_log.is_empty