diff --git a/changedetectionio/api/Watch.py b/changedetectionio/api/Watch.py index d3e790e5..78446897 100644 --- a/changedetectionio/api/Watch.py +++ b/changedetectionio/api/Watch.py @@ -139,58 +139,18 @@ class Watch(Resource): # Handle processor-config-* fields separately (save to JSON, not datastore) from changedetectionio import processors - processor_config_data = {} - regular_data = {} - for key, value in request.json.items(): - if key.startswith('processor_config_'): - config_key = key.replace('processor_config_', '') - if value: # Only save non-empty values - processor_config_data[config_key] = value - else: - regular_data[key] = value + # Make a mutable copy of request.json for modification + json_data = dict(request.json) + + # Extract and remove processor config fields from json_data + processor_config_data = processors.extract_processor_config_from_form_data(json_data) # Update watch with regular (non-processor-config) fields - watch.update(regular_data) + watch.update(json_data) - # Save processor config to JSON file if any config data exists - if processor_config_data: - try: - processor_name = request.json.get('processor', watch.get('processor')) - if processor_name: - # Create a processor instance to access config methods - from changedetectionio.processors import difference_detection_processor - processor_instance = difference_detection_processor(self.datastore, uuid) - # Use processor name as filename so each processor keeps its own config - config_filename = f'{processor_name}.json' - processor_instance.update_extra_watch_config(config_filename, processor_config_data) - logger.debug(f"API: Saved processor config to {config_filename}: {processor_config_data}") - - # Call optional edit_hook if processor has one - try: - import importlib - edit_hook_module_name = f'changedetectionio.processors.{processor_name}.edit_hook' - - try: - edit_hook = importlib.import_module(edit_hook_module_name) - logger.debug(f"API: Found edit_hook module for {processor_name}") - - if hasattr(edit_hook, 'on_config_save'): - logger.info(f"API: Calling edit_hook.on_config_save for {processor_name}") - # Call hook and get updated config - updated_config = edit_hook.on_config_save(watch, processor_config_data, self.datastore) - # Save updated config back to file - processor_instance.update_extra_watch_config(config_filename, updated_config) - logger.info(f"API: Edit hook updated config: {updated_config}") - else: - logger.debug(f"API: Edit hook module found but no on_config_save function") - except ModuleNotFoundError: - logger.debug(f"API: No edit_hook module for processor {processor_name} (this is normal)") - except Exception as hook_error: - logger.error(f"API: Edit hook error (non-fatal): {hook_error}", exc_info=True) - - except Exception as e: - logger.error(f"API: Failed to save processor config: {e}") + # Save processor config to JSON file + processors.save_processor_config(self.datastore, uuid, processor_config_data) return "OK", 200 diff --git a/changedetectionio/blueprint/ui/edit.py b/changedetectionio/blueprint/ui/edit.py index e2edd557..6e1c59be 100644 --- a/changedetectionio/blueprint/ui/edit.py +++ b/changedetectionio/blueprint/ui/edit.py @@ -144,58 +144,10 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe extra_update_obj['time_between_check'] = form.time_between_check.data - # Handle processor-config-* fields separately (save to JSON, not datastore) - processor_config_data = {} - fields_to_remove = [] - for field_name, field_value in form.data.items(): - if field_name.startswith('processor_config_'): - config_key = field_name.replace('processor_config_', '') - if field_value: # Only save non-empty values - processor_config_data[config_key] = field_value - fields_to_remove.append(field_name) - - # Save processor config to JSON file if any config data exists - if processor_config_data: - try: - processor_name = form.data.get('processor') - # Create a processor instance to access config methods - processor_instance = processors.difference_detection_processor(datastore, uuid) - # Use processor name as filename so each processor keeps its own config - config_filename = f'{processor_name}.json' - processor_instance.update_extra_watch_config(config_filename, processor_config_data) - logger.debug(f"Saved processor config to {config_filename}: {processor_config_data}") - - # Call optional edit_hook if processor has one - try: - # Try to import the edit_hook module from the processor package - import importlib - edit_hook_module_name = f'changedetectionio.processors.{processor_name}.edit_hook' - - try: - edit_hook = importlib.import_module(edit_hook_module_name) - logger.debug(f"Found edit_hook module for {processor_name}") - - if hasattr(edit_hook, 'on_config_save'): - logger.info(f"Calling edit_hook.on_config_save for {processor_name}") - watch_obj = datastore.data['watching'][uuid] - # Call hook and get updated config - updated_config = edit_hook.on_config_save(watch_obj, processor_config_data, datastore) - # Save updated config back to file - processor_instance.update_extra_watch_config(config_filename, updated_config) - logger.info(f"Edit hook updated config: {updated_config}") - else: - logger.debug(f"Edit hook module found but no on_config_save function") - except ModuleNotFoundError: - logger.debug(f"No edit_hook module for processor {processor_name} (this is normal)") - except Exception as hook_error: - logger.error(f"Edit hook error (non-fatal): {hook_error}", exc_info=True) - - except Exception as e: - logger.error(f"Failed to save processor config: {e}") - - # Remove processor-config-* fields from form.data before updating datastore - for field_name in fields_to_remove: - form.data.pop(field_name, None) + # Handle processor-config-* fields separately (save to JSON, not datastore) + # IMPORTANT: These must NOT be saved to url-watches.json, only to the processor-specific JSON file + processor_config_data = processors.extract_processor_config_from_form_data(form.data) + processors.save_processor_config(datastore, uuid, processor_config_data) # Ignore text form_ignore_text = form.ignore_text.data diff --git a/changedetectionio/processors/__init__.py b/changedetectionio/processors/__init__.py index bd658e88..9cb9332c 100644 --- a/changedetectionio/processors/__init__.py +++ b/changedetectionio/processors/__init__.py @@ -380,3 +380,76 @@ def get_processor_badge_css(): return '\n\n'.join(css_rules) + +def save_processor_config(datastore, watch_uuid, config_data): + """ + Save processor-specific configuration to JSON file. + + This is a shared helper function used by both the UI edit form and API endpoints + to consistently handle processor configuration storage. + + Args: + datastore: The application datastore instance + watch_uuid: UUID of the watch + config_data: Dictionary of configuration data to save (with processor_config_* prefix removed) + + Returns: + bool: True if saved successfully, False otherwise + """ + if not config_data: + return True + + try: + from changedetectionio.processors.base import difference_detection_processor + + # Get processor name from watch + watch = datastore.data['watching'].get(watch_uuid) + if not watch: + logger.error(f"Cannot save processor config: watch {watch_uuid} not found") + return False + + processor_name = watch.get('processor', 'text_json_diff') + + # Create a processor instance to access config methods + processor_instance = difference_detection_processor(datastore, watch_uuid) + + # Use processor name as filename so each processor keeps its own config + config_filename = f'{processor_name}.json' + processor_instance.update_extra_watch_config(config_filename, config_data) + + logger.debug(f"Saved processor config to {config_filename}: {config_data}") + return True + + except Exception as e: + logger.error(f"Failed to save processor config: {e}") + return False + + +def extract_processor_config_from_form_data(form_data): + """ + Extract processor_config_* fields from form data and return separate dicts. + + This is a shared helper function used by both the UI edit form and API endpoints + to consistently handle processor configuration extraction. + + IMPORTANT: This function modifies form_data in-place by removing processor_config_* fields. + + Args: + form_data: Dictionary of form data (will be modified in-place) + + Returns: + dict: Dictionary of processor config data (with processor_config_* prefix removed) + """ + processor_config_data = {} + + # Use list() to create a copy of keys since we're modifying the dict + for field_name in list(form_data.keys()): + if field_name.startswith('processor_config_'): + config_key = field_name.replace('processor_config_', '') + # Save all values (including empty strings) to allow explicit clearing of settings + processor_config_data[config_key] = form_data[field_name] + # Remove from form_data to prevent it from reaching datastore + del form_data[field_name] + + return processor_config_data +