mirror of
				https://github.com/dgtlmoon/changedetection.io.git
				synced 2025-10-30 22:27:52 +00:00 
			
		
		
		
	Compare commits
	
		
			16 Commits
		
	
	
		
			timezone-i
			...
			pluggy-2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 65bc76f11b | ||
|   | 1aa0070ae2 | ||
|   | 0ab3a83a11 | ||
|   | 42c6f8fc37 | ||
|   | 06744dbd3a | ||
|   | c6433815e4 | ||
|   | ce97d67ecf | ||
|   | 25778a8102 | ||
|   | b88998feea | ||
|   | 494740e3f8 | ||
|   | 2769abf374 | ||
|   | 690b16b710 | ||
|   | 8563126287 | ||
|   | f6c667b0a8 | ||
|   | 774923f67d | ||
|   | 432ee1236d | 
| @@ -1,6 +1,7 @@ | ||||
| recursive-include changedetectionio/api * | ||||
| recursive-include changedetectionio/blueprint * | ||||
| recursive-include changedetectionio/model * | ||||
| recursive-include changedetectionio/plugins * | ||||
| recursive-include changedetectionio/processors * | ||||
| recursive-include changedetectionio/res * | ||||
| recursive-include changedetectionio/static * | ||||
|   | ||||
| @@ -101,6 +101,7 @@ class Fetcher(): | ||||
|     error = None | ||||
|     fetcher_description = "No description" | ||||
|     headers = {} | ||||
|     is_plaintext = None | ||||
|     instock_data = None | ||||
|     instock_data_js = "" | ||||
|     status_code = None | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| #!/usr/bin/python3 | ||||
|  | ||||
| from changedetectionio import queuedWatchMetaData | ||||
| from changedetectionio import queuedWatchMetaData, html_tools, __version__ | ||||
| from copy import deepcopy | ||||
| from distutils.util import strtobool | ||||
| from feedgen.feed import FeedGenerator | ||||
| @@ -35,8 +35,6 @@ from flask import ( | ||||
| ) | ||||
|  | ||||
| from flask_paginate import Pagination, get_page_parameter | ||||
|  | ||||
| from changedetectionio import html_tools, __version__ | ||||
| from changedetectionio.api import api_v1 | ||||
|  | ||||
| datastore = None | ||||
| @@ -50,6 +48,18 @@ extra_stylesheets = [] | ||||
| update_q = queue.PriorityQueue() | ||||
| notification_q = queue.Queue() | ||||
|  | ||||
|  | ||||
| def get_plugin_manager(): | ||||
|     import pluggy | ||||
|     from changedetectionio.plugins import hookspecs | ||||
|     from changedetectionio.plugins import whois as whois_plugin | ||||
|  | ||||
|     pm = pluggy.PluginManager("changedetectionio_plugin") | ||||
|     pm.add_hookspecs(hookspecs) | ||||
|     pm.load_setuptools_entrypoints("changedetectionio_plugin") | ||||
|     pm.register(whois_plugin) | ||||
|     return pm | ||||
|  | ||||
| app = Flask(__name__, | ||||
|             static_url_path="", | ||||
|             static_folder="static", | ||||
| @@ -96,7 +106,6 @@ def init_app_secret(datastore_path): | ||||
|  | ||||
|     return secret | ||||
|  | ||||
|  | ||||
| @app.template_global() | ||||
| def get_darkmode_state(): | ||||
|     css_dark_mode = request.cookies.get('css_dark_mode', 'false') | ||||
| @@ -629,7 +638,6 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|             form.fetch_backend.choices.append(p) | ||||
|  | ||||
|         form.fetch_backend.choices.append(("system", 'System settings default')) | ||||
|  | ||||
|         # form.browser_steps[0] can be assumed that we 'goto url' first | ||||
|  | ||||
|         if datastore.proxy_list is None: | ||||
| @@ -730,6 +738,8 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|             if (watch.get('fetch_backend') == 'system' and system_uses_webdriver) or watch.get('fetch_backend') == 'html_webdriver' or watch.get('fetch_backend', '').startswith('extra_browser_'): | ||||
|                 is_html_webdriver = True | ||||
|  | ||||
|             processor_config = next((p[2] for p in processors.available_processors() if p[0] == watch.get('processor')), None) | ||||
|  | ||||
|             # Only works reliably with Playwright | ||||
|             visualselector_enabled = os.getenv('PLAYWRIGHT_DRIVER_URL', False) and is_html_webdriver | ||||
|             output = render_template("edit.html", | ||||
| @@ -744,6 +754,7 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|                                      is_html_webdriver=is_html_webdriver, | ||||
|                                      jq_support=jq_support, | ||||
|                                      playwright_enabled=os.getenv('PLAYWRIGHT_DRIVER_URL', False), | ||||
|                                      processor_config=processor_config, | ||||
|                                      settings_application=datastore.data['settings']['application'], | ||||
|                                      using_global_webdriver_wait=default['webdriver_delay'] is None, | ||||
|                                      uuid=uuid, | ||||
| @@ -824,11 +835,14 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|                 flash("An error occurred, please see below.", "error") | ||||
|  | ||||
|         output = render_template("settings.html", | ||||
|                                  form=form, | ||||
|                                  hide_remove_pass=os.getenv("SALTED_PASS", False), | ||||
|                                  api_key=datastore.data['settings']['application'].get('api_access_token'), | ||||
|                                  emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False), | ||||
|                                  settings_application=datastore.data['settings']['application']) | ||||
|                                  form=form, | ||||
|                                  hide_remove_pass=os.getenv("SALTED_PASS", False), | ||||
|                                  settings_application=datastore.data['settings']['application'], | ||||
|                                  plugins=[] | ||||
|  | ||||
|                                  ) | ||||
|  | ||||
|         return output | ||||
|  | ||||
|   | ||||
| @@ -410,7 +410,7 @@ class quickWatchForm(Form): | ||||
|     url = fields.URLField('URL', validators=[validateURL()]) | ||||
|     tags = StringTagUUID('Group tag', [validators.Optional()]) | ||||
|     watch_submit_button = SubmitField('Watch', render_kw={"class": "pure-button pure-button-primary"}) | ||||
|     processor = RadioField(u'Processor', choices=processors.available_processors(), default="text_json_diff") | ||||
|     processor = RadioField(u'Processor', choices=[t[:2] for t in processors.available_processors()], default="text_json_diff") | ||||
|     edit_and_watch_submit_button = SubmitField('Edit > Watch', render_kw={"class": "pure-button pure-button-primary"}) | ||||
|  | ||||
|  | ||||
| @@ -427,7 +427,7 @@ class commonSettingsForm(Form): | ||||
|                                                                                                                                     message="Should contain one or more seconds")]) | ||||
| class importForm(Form): | ||||
|     from . import processors | ||||
|     processor = RadioField(u'Processor', choices=processors.available_processors(), default="text_json_diff") | ||||
|     processor = RadioField(u'Processor', choices=[t[:2] for t in processors.available_processors()], default="text_json_diff") | ||||
|     urls = TextAreaField('URLs') | ||||
|     xlsx_file = FileField('Upload .xlsx file', validators=[FileAllowed(['xlsx'], 'Must be .xlsx file!')]) | ||||
|     file_mapping = SelectField('File mapping', [validators.DataRequired()], choices={('wachete', 'Wachete mapping'), ('custom','Custom mapping')}) | ||||
|   | ||||
| @@ -38,6 +38,7 @@ class model(dict): | ||||
|                     'notification_format': default_notification_format, | ||||
|                     'notification_title': default_notification_title, | ||||
|                     'notification_urls': [], # Apprise URL list | ||||
|                     'plugins': [], # list of dict, keyed by plugin name, with dict of the config and enabled true/false | ||||
|                     'pager_size': 50, | ||||
|                     'password': False, | ||||
|                     'render_anchor_tag_content': False, | ||||
|   | ||||
							
								
								
									
										6
									
								
								changedetectionio/plugins/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								changedetectionio/plugins/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| import pluggy | ||||
|  | ||||
| hookimpl = pluggy.HookimplMarker("changedetectionio_plugin") | ||||
| """Marker to be imported and used in plugins (and for own implementations)""" | ||||
|  | ||||
| x=1 | ||||
							
								
								
									
										20
									
								
								changedetectionio/plugins/hookspecs.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								changedetectionio/plugins/hookspecs.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| import pluggy | ||||
| from changedetectionio.store import ChangeDetectionStore | ||||
|  | ||||
| hookspec = pluggy.HookspecMarker("changedetectionio_plugin") | ||||
|  | ||||
|  | ||||
| @hookspec | ||||
| def extra_processor(): | ||||
|     """Defines a new fetch method | ||||
|  | ||||
|     :return: a tuples, (machine_name, description) | ||||
|     """ | ||||
|  | ||||
| @hookspec(firstresult=True) | ||||
| def processor_call(processor_name: str, datastore: ChangeDetectionStore, watch_uuid: str): | ||||
|     """ | ||||
|     Call processors with processor name | ||||
|     :param processor_name: as defined in extra_processors | ||||
|     :return: data? | ||||
|     """ | ||||
							
								
								
									
										53
									
								
								changedetectionio/plugins/whois.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								changedetectionio/plugins/whois.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| """ | ||||
| Whois information lookup | ||||
| - Fetches using whois | ||||
| - Extends the 'text_json_diff' so that text filters can still be used with whois information | ||||
|  | ||||
| @todo publish to pypi and github as a separate plugin | ||||
| """ | ||||
|  | ||||
| from ..plugins import hookimpl | ||||
| import changedetectionio.processors.text_json_diff as text_json_diff | ||||
| from changedetectionio import content_fetcher | ||||
|  | ||||
| # would be changedetectionio.plugins in other apps | ||||
|  | ||||
| class text_json_filtering_whois(text_json_diff.perform_site_check): | ||||
|  | ||||
|     def __init__(self, *args, datastore, watch_uuid, **kwargs): | ||||
|         super().__init__(*args, datastore=datastore, watch_uuid=watch_uuid, **kwargs) | ||||
|  | ||||
|     def call_browser(self): | ||||
|         import whois | ||||
|         # the whois data | ||||
|         self.fetcher = content_fetcher.Fetcher() | ||||
|         self.fetcher.is_plaintext = True | ||||
|  | ||||
|         from urllib.parse import urlparse | ||||
|         parsed = urlparse(self.watch.link) | ||||
|         w = whois.whois(parsed.hostname) | ||||
|         self.fetcher.content= w.text | ||||
|  | ||||
| @hookimpl | ||||
| def extra_processor(): | ||||
|     """ | ||||
|     Advertise a new processor | ||||
|     :return: | ||||
|     """ | ||||
|     from changedetectionio.processors import default_processor_config | ||||
|     processor_config = dict(default_processor_config) | ||||
|     # Which UI elements are not used | ||||
|     processor_config['needs_request_fetch_method'] = False | ||||
|     processor_config['needs_browsersteps'] = False | ||||
|     processor_config['needs_visualselector'] = False | ||||
|     return ('plugin_processor_whois', "Whois domain information fetch", processor_config) | ||||
|  | ||||
| # @todo When a watch chooses this extra_process processor, the watch should ONLY use this one. | ||||
| #       (one watch can only have one extra_processor) | ||||
| @hookimpl | ||||
| def processor_call(processor_name, datastore, watch_uuid): | ||||
|     if processor_name == 'plugin_processor_whois': # could be removed, see above note | ||||
|         x = text_json_filtering_whois(datastore=datastore, watch_uuid=watch_uuid) | ||||
|         return x | ||||
|     return None | ||||
|  | ||||
| @@ -7,6 +7,15 @@ from copy import deepcopy | ||||
| from distutils.util import strtobool | ||||
| from loguru import logger | ||||
|  | ||||
| # Which UI elements in settings the processor requires | ||||
| # For example, restock monitor isnt compatible with visualselector and filters | ||||
| default_processor_config = { | ||||
|     'needs_request_fetch_method': True, | ||||
|     'needs_browsersteps': True, | ||||
|     'needs_visualselector': True, | ||||
|     'needs_filters': True, | ||||
| } | ||||
|  | ||||
| class difference_detection_processor(): | ||||
|  | ||||
|     browser_steps = None | ||||
| @@ -132,6 +141,15 @@ class difference_detection_processor(): | ||||
|  | ||||
| def available_processors(): | ||||
|     from . import restock_diff, text_json_diff | ||||
|     x=[('text_json_diff', text_json_diff.name), ('restock_diff', restock_diff.name)] | ||||
|     # @todo Make this smarter with introspection of sorts. | ||||
|     from ..flask_app import get_plugin_manager | ||||
|     pm = get_plugin_manager() | ||||
|     x = [('text_json_diff', text_json_diff.name, dict(default_processor_config)), | ||||
|          ('restock_diff', restock_diff.name, dict(default_processor_config)) | ||||
|          ] | ||||
|  | ||||
|     plugin_choices = pm.hook.extra_processor() | ||||
|     if plugin_choices: | ||||
|         for p in plugin_choices: | ||||
|             x.append(p) | ||||
|  | ||||
|     return x | ||||
|   | ||||
| @@ -155,7 +155,7 @@ class perform_site_check(difference_detection_processor): | ||||
|             html_content = self.fetcher.content | ||||
|  | ||||
|             # If not JSON,  and if it's not text/plain.. | ||||
|             if 'text/plain' in self.fetcher.get_all_headers().get('content-type', '').lower(): | ||||
|             if 'text/plain' in self.fetcher.get_all_headers().get('content-type', '').lower() or self.fetcher.is_plaintext: | ||||
|                 # Don't run get_text or xpath/css filters on plaintext | ||||
|                 stripped_text_from_html = html_content | ||||
|             else: | ||||
|   | ||||
| @@ -39,12 +39,15 @@ | ||||
|         <ul> | ||||
|             <li class="tab" id=""><a href="#general">General</a></li> | ||||
|             <li class="tab"><a href="#request">Request</a></li> | ||||
|             {% if playwright_enabled %} | ||||
|             {% if playwright_enabled and processor_config['needs_browsersteps'] %} | ||||
|             <li class="tab"><a id="browsersteps-tab" href="#browser-steps">Browser Steps</a></li> | ||||
|             {% endif %} | ||||
|  | ||||
|             {% if watch['processor'] == 'text_json_diff' %} | ||||
|             {% if processor_config['needs_visualselector']  %} | ||||
|             <li class="tab"><a id="visualselector-tab" href="#visualselector">Visual Filter Selector</a></li> | ||||
|             {% endif %} | ||||
|  | ||||
|             {% if processor_config['needs_filters']  %} | ||||
|             <li class="tab"><a href="#filters-and-triggers">Filters & Triggers</a></li> | ||||
|             {% endif %} | ||||
|  | ||||
| @@ -67,16 +70,12 @@ | ||||
|                         {{ render_field(form.url, placeholder="https://...", required=true, class="m-d") }} | ||||
|                         <span class="pure-form-message-inline">Some sites use JavaScript to create the content, for this you should <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Fetching-pages-with-WebDriver">use the Chrome/WebDriver Fetcher</a></span><br> | ||||
|                         <span class="pure-form-message-inline">You can use variables in the URL, perfect for inserting the current date and other logic, <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Handling-variables-in-the-watched-URL">help and examples here</a></span><br> | ||||
|                         <span class="pure-form-message-inline"> | ||||
|                         {% if watch['processor'] == 'text_json_diff' %} | ||||
|                             Current mode: <strong>Webpage Text/HTML, JSON and PDF changes.</strong><br> | ||||
|                           <a href="{{url_for('edit_page', uuid=uuid)}}?switch_processor=restock_diff" class="pure-button button-xsmall">Switch to re-stock detection mode.</a> | ||||
|                         {% else %} | ||||
|                         Current mode: <strong>Re-stock detection.</strong><br> | ||||
|                           <a href="{{url_for('edit_page', uuid=uuid)}}?switch_processor=text_json_diff" class="pure-button button-xsmall">Switch to Webpage Text/HTML, JSON and PDF changes mode.</a> | ||||
|                         {% endif %} | ||||
|                         </span> | ||||
|  | ||||
|                     </div> | ||||
|                 <div class="pure-control-group"> | ||||
|                     <label for="title">Processing mode</label> | ||||
|                           {% for a in available_processors %} | ||||
|                             <a href="{{url_for('edit_page', uuid=uuid)}}?switch_processor={{ a[0] }}" class="pure-button button-xsmall {% if watch['processor'] == a[0] %}button-secondary{% endif %}">{{ a[1]}}.</a> | ||||
|                           {% endfor %} | ||||
|                     </div> | ||||
|                     <div class="pure-control-group"> | ||||
|                         {{ render_field(form.title, class="m-d") }} | ||||
| @@ -108,6 +107,7 @@ | ||||
|             </div> | ||||
|  | ||||
|             <div class="tab-pane-inner" id="request"> | ||||
|             {% if processor_config['needs_request_fetch_method']  %} | ||||
|                     <div class="pure-control-group inline-radio"> | ||||
|                         {{ render_field(form.fetch_backend, class="fetch-backend") }} | ||||
|                         <span class="pure-form-message-inline"> | ||||
| @@ -116,6 +116,7 @@ | ||||
|                             Tip: <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration#brightdata-proxy-support">Connect using Bright Data and Oxylabs Proxies, find out more here.</a> | ||||
|                         </span> | ||||
|                     </div> | ||||
|             {% endif  %} | ||||
|                 {% if form.proxy %} | ||||
|                     <div class="pure-control-group inline-radio"> | ||||
|                           <div>{{ form.proxy.label }} <a href="" id="check-all-proxies" class="pure-button button-secondary button-xsmall" >Check/Scan all</a></div> | ||||
| @@ -193,7 +194,7 @@ User-Agent: wonderbra 1.0") }} | ||||
|                     </div> | ||||
|             </fieldset> | ||||
|             </div> | ||||
|             {% if playwright_enabled %} | ||||
|             {% if playwright_enabled and processor_config['needs_browsersteps'] %} | ||||
|             <div class="tab-pane-inner" id="browser-steps"> | ||||
|                 <img class="beta-logo" src="{{url_for('static_content', group='images', filename='beta-logo.png')}}" alt="New beta functionality"> | ||||
|                 <fieldset> | ||||
| @@ -264,8 +265,10 @@ User-Agent: wonderbra 1.0") }} | ||||
|                 </fieldset> | ||||
|             </div> | ||||
|  | ||||
|             {% if watch['processor'] == 'text_json_diff' %} | ||||
|             {% if processor_config['needs_filters']  %} | ||||
|             <div class="tab-pane-inner" id="filters-and-triggers"> | ||||
|                 <div class="text-filtering"> | ||||
|                 <h3>Filter by HTML element</h3> | ||||
|                     <div class="pure-control-group"> | ||||
|                             <strong>Pro-tips:</strong><br> | ||||
|                             <ul> | ||||
| @@ -315,7 +318,7 @@ xpath://body/div/span[contains(@class, 'example-class')]", | ||||
|                                 href="https://github.com/dgtlmoon/changedetection.io/wiki/CSS-Selector-help">here for more CSS selector help</a>.<br> | ||||
|                 </span> | ||||
|                     </div> | ||||
|                 <fieldset class="pure-control-group"> | ||||
|                     <div class="pure-control-group"> | ||||
|                     {{ render_field(form.subtractive_selectors, rows=5, placeholder="header | ||||
| footer | ||||
| nav | ||||
| @@ -326,7 +329,8 @@ nav | ||||
|                           <li> Add multiple elements or CSS selectors per line to ignore multiple parts of the HTML. </li> | ||||
|                         </ul> | ||||
|                       </span> | ||||
|                 </fieldset> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="text-filtering"> | ||||
|                 <fieldset class="pure-group" id="text-filtering-type-options"> | ||||
|                     <h3>Text filtering</h3> | ||||
| @@ -423,7 +427,7 @@ Unavailable") }} | ||||
|             </div> | ||||
|             {% endif %} | ||||
|  | ||||
|             {% if watch['processor'] == 'text_json_diff' %} | ||||
|             {% if processor_config['needs_visualselector']  %} | ||||
|             <div class="tab-pane-inner visual-selector-ui" id="visualselector"> | ||||
|                 <img class="beta-logo" src="{{url_for('static_content', group='images', filename='beta-logo.png')}}" alt="New beta functionality"> | ||||
|  | ||||
|   | ||||
| @@ -22,6 +22,7 @@ | ||||
|             <li class="tab"><a href="#filters">Global Filters</a></li> | ||||
|             <li class="tab"><a href="#api">API</a></li> | ||||
|             <li class="tab"><a href="#proxies">CAPTCHA & Proxies</a></li> | ||||
|             <li class="tab"><a href="#plugins">Plugins</a></li> | ||||
|         </ul> | ||||
|     </div> | ||||
|     <div class="box-wrap inner"> | ||||
| @@ -243,6 +244,12 @@ nav | ||||
|                     {{ render_field(form.requests.form.extra_browsers) }} | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="tab-pane-inner" id="plugins"> | ||||
|                 available plugin on/off stuff here | ||||
|  | ||||
|                 how to let each one expose config? | ||||
|             </div> | ||||
|  | ||||
|             <div id="actions"> | ||||
|                 <div class="pure-control-group"> | ||||
|                     {{ render_button(form.save_button) }} | ||||
|   | ||||
| @@ -259,6 +259,13 @@ class update_worker(threading.Thread): | ||||
|                             update_handler = restock_diff.perform_site_check(datastore=self.datastore, | ||||
|                                                                              watch_uuid=uuid | ||||
|                                                                              ) | ||||
|                         elif processor.startswith('plugin_processor_'): | ||||
|                             from .flask_app import get_plugin_manager | ||||
|                             pm = get_plugin_manager() | ||||
|                             x = pm.hook.processor_call(processor_name=processor, datastore=self.datastore, watch_uuid=uuid) | ||||
|                             if x: | ||||
|                                 update_handler = x | ||||
|  | ||||
|                         else: | ||||
|                             # Used as a default and also by some tests | ||||
|                             update_handler = text_json_diff.perform_site_check(datastore=self.datastore, | ||||
|   | ||||
| @@ -73,4 +73,5 @@ pytest-flask ~=1.2 | ||||
| # Pin jsonschema version to prevent build errors on armv6 while rpds-py wheels aren't available (1708) | ||||
| jsonschema==4.17.3 | ||||
|  | ||||
| pluggy | ||||
| loguru | ||||
|   | ||||
		Reference in New Issue
	
	Block a user