diff --git a/src/ytdl_sub/config/preset_class_mappings.py b/src/ytdl_sub/config/preset_class_mappings.py index 775ef15e..878faa46 100644 --- a/src/ytdl_sub/config/preset_class_mappings.py +++ b/src/ytdl_sub/config/preset_class_mappings.py @@ -108,7 +108,7 @@ class PluginMapping: "music_tags": MusicTagsPlugin, "nfo_tags": NfoTagsPlugin, "output_directory_nfo_tags": OutputDirectoryNfoTagsPlugin, - "regex_capture": RegexCapturePlugin, + "regex": RegexCapturePlugin, } @classmethod diff --git a/src/ytdl_sub/plugins/regex_capture.py b/src/ytdl_sub/plugins/regex_capture.py index e84740c0..aba5e8fc 100644 --- a/src/ytdl_sub/plugins/regex_capture.py +++ b/src/ytdl_sub/plugins/regex_capture.py @@ -6,10 +6,14 @@ from ytdl_sub.entries.entry import Entry from ytdl_sub.plugins.plugin import Plugin from ytdl_sub.plugins.plugin import PluginOptions from ytdl_sub.utils.exceptions import ValidationException +from ytdl_sub.utils.logger import Logger from ytdl_sub.validators.regex_validator import RegexListValidator from ytdl_sub.validators.strict_dict_validator import StrictDictValidator from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator +from ytdl_sub.validators.validators import BoolValidator + +logger = Logger.get(name="regex") def _source_var_name(source_variable: str, capture_group_idx: int) -> str: @@ -18,24 +22,24 @@ def _source_var_name(source_variable: str, capture_group_idx: int) -> str: class SourceVariableRegexCapture(StrictDictValidator): - _required_keys = {"capture"} + _required_keys = {"match"} _optional_keys = {"defaults"} def __init__(self, name, value): super().__init__(name, value) - self._capture = self._validate_key(key="capture", validator=RegexListValidator) + self._match = self._validate_key(key="match", validator=RegexListValidator) self._defaults = self._validate_key_if_present( key="defaults", validator=ListFormatterValidator ) # If defaults are to be used, ensure there are the same number of defaults as there are # capture groups - if self._defaults is not None and self._capture.num_capture_groups != len( + if self._defaults is not None and self._match.num_capture_groups != len( self._defaults.list ): raise self._validation_exception( f"number of defaults must match number of capture groups, " - f"{len(self._defaults.list)} != {self._capture.num_capture_groups}" + f"{len(self._defaults.list)} != {self._match.num_capture_groups}" ) @property @@ -45,7 +49,7 @@ class SourceVariableRegexCapture(StrictDictValidator): ------- List of regex captures """ - return self._capture + return self._match @property def has_defaults(self) -> bool: @@ -66,14 +70,30 @@ class SourceVariableRegexCapture(StrictDictValidator): return self._defaults.list if self.has_defaults else None -class RegexCaptureOptions(PluginOptions): +class FromSourceVariablesRegexCapture(StrictDictValidator): - _optional_keys = {"_"} + _optional_keys = Entry.source_variables() _allow_extra_keys = True def __init__(self, name, value): super().__init__(name, value) - self._source_variable_capture_dict: Dict[str, SourceVariableRegexCapture] = {} + self.source_variable_capture_dict: Dict[str, SourceVariableRegexCapture] = { + key: self._validate_key(key=key, validator=SourceVariableRegexCapture) + for key in self._keys + } + + +class RegexCaptureOptions(PluginOptions): + + _required_keys = {"from"} + _optional_keys = {"skip_if_match_fails"} + + def __init__(self, name, value): + super().__init__(name, value) + self._from = self._validate_key(key="from", validator=FromSourceVariablesRegexCapture) + self.skip_if_match_fails: bool = self._validate_key_if_present( + key="skip_if_match_fails", validator=BoolValidator, default=False + ).value def validate_with_source_variables(self, source_variables: List[str]) -> None: """ @@ -84,16 +104,12 @@ class RegexCaptureOptions(PluginOptions): source_variables Variables to check against the provided capture groups """ - for key in self._keys: + for key in self.source_variable_capture_dict.keys(): if key not in source_variables: raise self._validation_exception( f"cannot regex capture '{key}' because it is not a source variable" ) - self._source_variable_capture_dict[key] = self._validate_key( - key=key, validator=SourceVariableRegexCapture - ) - @property def source_variable_capture_dict(self) -> Dict[str, SourceVariableRegexCapture]: """ @@ -101,7 +117,7 @@ class RegexCaptureOptions(PluginOptions): ------- Dict of { source variable: capture options } """ - return self._source_variable_capture_dict + return self._from.source_variable_capture_dict def added_source_variables(self) -> List[str]: """ @@ -148,8 +164,16 @@ class RegexCapturePlugin(Plugin[RegexCaptureOptions]): # If no capture if maybe_capture is None: - # and no defaults, then error + # and no defaults if not regex_options.has_defaults: + # Skip the entry if toggled + if self.plugin_options.skip_if_match_fails: + logger.info( + "Entry with title '%s' failed to match regex, skipping.", entry.title + ) + return None + + # Otherwise, error raise ValidationException( f"Failed to capture {source_var} from an entry with the value:\n" f"{entry_variable_dict[source_var]}" diff --git a/src/ytdl_sub/validators/regex_validator.py b/src/ytdl_sub/validators/regex_validator.py index 3fd4f8b0..8a6fd79c 100644 --- a/src/ytdl_sub/validators/regex_validator.py +++ b/src/ytdl_sub/validators/regex_validator.py @@ -28,33 +28,20 @@ class RegexValidator(StringValidator): """ return self._compiled_regex.groups - def is_match(self, input_str: str) -> bool: - """ - Parameters - ---------- - input_str - String to match against the regex - - Returns - ------- - True if input_str matches. False otherwise. - """ - return self._compiled_regex.search(input_str) is not None - def capture(self, input_str: str) -> Optional[List[str]]: """ Parameters ---------- input_str - String to try to regex capture from + String to regex match Returns ------- - List of captures (will always be >= 1). None if there are no captures. + List of captures. If the regex has no capture groups, then the list will be emtpy. + None is returned if the input_str failed to match """ if match := self._compiled_regex.search(input_str): - if len(to_return := list(match.groups())) > 0: - return to_return + return list(match.groups()) return None @@ -81,19 +68,6 @@ class RegexListValidator(ListValidator[RegexValidator]): """ return self._num_capture_groups - def matches_any(self, input_str: str) -> bool: - """ - Parameters - ---------- - input_str - String to match against any regexes in the list - - Returns - ------- - True if at least one matches. False if none match - """ - return any(reg.is_match(input_str) for reg in self._list) - def capture_any(self, input_str: str) -> Optional[List[str]]: """ Parameters @@ -103,10 +77,9 @@ class RegexListValidator(ListValidator[RegexValidator]): Returns ------- - List of captures (will always be >= 1) on the first regex that matches. None if - no regexes match. + List of captures on the first regex that matches. None if no regexes match. """ for reg in self._list: - if maybe_capture := reg.capture(input_str): + if (maybe_capture := reg.capture(input_str)) is not None: return maybe_capture return None diff --git a/tests/e2e/plugins/test_regex_capture.py b/tests/e2e/plugins/test_regex_capture.py index e4e1b7b1..65de1964 100644 --- a/tests/e2e/plugins/test_regex_capture.py +++ b/tests/e2e/plugins/test_regex_capture.py @@ -17,22 +17,25 @@ def regex_capture_subscription_dict(output_directory): "format": "best[height<=480]", "postprocessor_args": {"ffmpeg": ["-bitexact"]}, # Must add this for reproducibility }, - "regex_capture": { - "title": { - "capture": [ - "should not cap (.+) - (.+)", - ".*\\[(.+) - (.+)]", - ], + "regex": { + "skip_if_match_fails": False, + "from": { + "title": { + "match": [ + "should not cap (.+) - (.+)", + ".*\\[(.+) - (.+)]", + ], + }, + "description": {"match": [".*http:\\/\\/(.+).com.*"]}, + "upload_date_standardized": { + "match": ["([0-9]+)-([0-9]+)-27"], + "defaults": [ + "First", + "Second", + ], + }, + "artist": {"match": ["Never (.*) capture"], "defaults": ["Always default"]}, }, - "description": {"capture": [".*http:\\/\\/(.+).com.*"]}, - "upload_date_standardized": { - "capture": ["([0-9]+)-([0-9]+)-27"], - "defaults": [ - "First", - "Second", - ], - }, - "artist": {"capture": ["Never (.*) capture"], "defaults": ["Always default"]}, }, "nfo_tags": { "tags": { diff --git a/tests/unit/plugins/test_regex_capture.py b/tests/unit/plugins/test_regex_capture.py index e69de29b..8b137891 100644 --- a/tests/unit/plugins/test_regex_capture.py +++ b/tests/unit/plugins/test_regex_capture.py @@ -0,0 +1 @@ +