diff --git a/babel.cfg b/babel.cfg index f0c1b8759..6ad037780 100644 --- a/babel.cfg +++ b/babel.cfg @@ -1,5 +1,6 @@ [python: **.py] -keywords = _:1,_l:1,gettext:1 +keywords = _ _l gettext [jinja2: **/templates/**.html] encoding = utf-8 +keywords = _ _l gettext diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py index e9a72c37e..f4b5a1d53 100644 --- a/changedetectionio/forms.py +++ b/changedetectionio/forms.py @@ -798,6 +798,7 @@ class processor_text_json_diff_form(commonSettingsForm): subtractive_selectors = StringListField(_l('Remove elements'), [ValidateCSSJSONXPATHInput(allow_json=False)]) + extract_lines_containing = StringListField(_l('Extract lines containing'), [validators.Optional()]) extract_text = StringListField(_l('Extract text'), [ValidateListRegex()]) title = StringField(_l('Title'), default='') diff --git a/changedetectionio/model/__init__.py b/changedetectionio/model/__init__.py index a16a35523..ffbdbbb7c 100644 --- a/changedetectionio/model/__init__.py +++ b/changedetectionio/model/__init__.py @@ -186,6 +186,7 @@ class watch_base(dict): 'consecutive_filter_failures': 0, # Every time the CSS/xPath filter cannot be located, reset when all is fine. 'content-type': None, 'date_created': None, + 'extract_lines_containing': [], # Keep only lines containing these substrings (plain text, case-insensitive) 'extract_text': [], # Extract text by regex after filters 'fetch_backend': 'system', # plaintext, playwright etc 'fetch_time': 0.0, diff --git a/changedetectionio/processors/text_json_diff/processor.py b/changedetectionio/processors/text_json_diff/processor.py index 77ef2ba86..16fa5a917 100644 --- a/changedetectionio/processors/text_json_diff/processor.py +++ b/changedetectionio/processors/text_json_diff/processor.py @@ -85,6 +85,10 @@ class FilterConfig: self._subtractive_selectors_cache = [*tag_selectors, *watch_selectors, *global_selectors] return self._subtractive_selectors_cache + @property + def extract_lines_containing(self): + return self._get_merged_rules('extract_lines_containing') + @property def extract_text(self): return self._get_merged_rules('extract_text') @@ -135,6 +139,17 @@ class ContentTransformer: text = text.replace("\n\n", "\n") return '\n'.join(sorted(text.splitlines(), key=lambda x: x.lower())) + @staticmethod + def extract_lines_containing(text, substrings): + """Keep only lines that contain at least one of the given substrings (case-insensitive).""" + needles = [s.lower() for s in substrings if s.strip()] + if not needles: + return text + return '\n'.join( + line for line in text.splitlines() + if any(needle in line.lower() for needle in needles) + ) + @staticmethod def extract_by_regex(text, regex_patterns): """Extract text matching regex patterns.""" @@ -503,6 +518,10 @@ class perform_site_check(difference_detection_processor): update_obj["last_check_status"] = self.fetcher.get_last_status_code() + # === LINE FILTER (plain-text substring) === + if filter_config.extract_lines_containing: + stripped_text = transformer.extract_lines_containing(stripped_text, filter_config.extract_lines_containing) + # === REGEX EXTRACTION === if filter_config.extract_text: extracted = transformer.extract_by_regex(stripped_text, filter_config.extract_text) diff --git a/changedetectionio/templates/edit/text-options.html b/changedetectionio/templates/edit/text-options.html index 1af63a7ec..ca666eca6 100644 --- a/changedetectionio/templates/edit/text-options.html +++ b/changedetectionio/templates/edit/text-options.html @@ -49,6 +49,21 @@ Unavailable") }} +