Changedetection.io features a flexible plugin system powered by pluggy that allows you to extend functionality without modifying the core codebase.
Plugin Types
Currently supported plugin types:
UI Stats Tab Plugins Add custom statistics and visualizations to the Edit page Stats tab
Processor Plugins Create custom change detection processors for specialized use cases
UI Stats Tab Plugins
These plugins add custom content to the Stats tab in the watch Edit page. Useful for displaying custom analytics, metrics, or visualizations.
Creating a Stats Plugin
Create plugin file
Create a Python file in your plugin directory: import pluggy
from loguru import logger
# Register as a changedetection.io plugin
global_hookimpl = pluggy.HookimplMarker( "changedetectionio" )
Implement the hook
Use the ui_edit_stats_extras hook to add your content: @global_hookimpl
def ui_edit_stats_extras ( watch ):
"""Add custom content to the stats tab"""
# Calculate your statistics
my_stat = calculate_something(watch)
# Return HTML as a string
html = f """
<div class="my-plugin-stats">
<h4>My Custom Statistics</h4>
<p>Statistic value: { my_stat } </p>
</div>
"""
return html
Access watch data
The watch object provides access to all watch properties: def calculate_something ( watch ):
# Access watch properties
url = watch.get( 'url' )
history_count = len (watch.history)
# Get latest snapshot
if watch.history.keys():
latest_key = list (watch.history.keys())[ - 1 ]
content = watch.get_history_snapshot( timestamp = latest_key)
word_count = len (content.split())
return word_count
return 0
Complete Example: Word Count Plugin
Here’s a full working example that adds word count statistics:
import pluggy
from loguru import logger
global_hookimpl = pluggy.HookimplMarker( "changedetectionio" )
def count_words_in_history ( watch ):
"""Count words in the latest snapshot"""
try :
if not watch.history.keys():
return 0
latest_key = list (watch.history.keys())[ - 1 ]
latest_content = watch.get_history_snapshot( timestamp = latest_key)
return len (latest_content.split())
except Exception as e:
logger.error( f "Error counting words: { str (e) } " )
return 0
@global_hookimpl
def ui_edit_stats_extras ( watch ):
"""Add word count to the Stats tab"""
word_count = count_words_in_history(watch)
html = f """
<div class="word-count-stats">
<h4>Content Analysis</h4>
<table class="pure-table">
<tbody>
<tr>
<td>Word count (latest snapshot)</td>
<td> { word_count :,} </td>
</tr>
</tbody>
</table>
</div>
"""
return html
Processor Plugins
Create custom processors for specialized change detection scenarios.
Processor Architecture
Processors must implement specific methods and can extend the API schema:
custom_processor/__init__.py
from changedetectionio.processors.base import ProcessorBase
class MyCustomProcessor ( ProcessorBase ):
"""Custom processor for specialized monitoring"""
def run ( self , uuid , preferred_proxy = None , check_jitter = False ):
"""
Main processing method called for each check.
Returns:
- changed_detected (bool)
- update_obj (dict): Data to update in watch
- contents (str): Processed content for storage
"""
# Fetch content
fetcher = self .get_fetcher()
contents = fetcher.run(uuid, preferred_proxy)
# Your custom processing logic
processed = self .process_content(contents)
# Detect changes
changed = self .detect_changes(processed)
return changed, {}, processed
def process_content ( self , contents ):
"""Implement your custom processing logic"""
# Example: extract specific data
return contents
def detect_changes ( self , contents ):
"""Implement your change detection logic"""
# Compare with previous check
return True
API Schema Extension
Processors can extend the Watch API to accept custom configuration:
components :
schemas :
processor_config_my_custom :
type : object
properties :
custom_threshold :
type : number
description : Custom threshold value
default : 10
enable_feature :
type : boolean
description : Enable custom feature
default : false
This makes processor_config_my_custom available in watch API calls:
curl -X POST http://localhost:5000/api/v1/watch \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"processor": "my_custom",
"processor_config_my_custom": {
"custom_threshold": 20,
"enable_feature": true
}
}'
Plugin Loading
Built-in Plugin Directories
Plugins are automatically loaded from:
changedetectionio/processors/ - Processor plugins
Additional directories defined in pluggy_interface.py
External Plugin Packages
Install plugins from PyPI or pip packages using the EXTRA_PACKAGES environment variable:
docker run -d \
-e EXTRA_PACKAGES="changedetection.io-osint-processor my-custom-plugin" \
-p 5000:5000 \
dgtlmoon/changedetection.io
services :
changedetection :
image : dgtlmoon/changedetection.io
environment :
- EXTRA_PACKAGES=changedetection.io-osint-processor my-custom-plugin
EXTRA_PACKAGES = "my-custom-plugin" changedetection.io
Setuptools Entry Points
Package your plugin for distribution with setuptools:
from setuptools import setup, find_packages
setup(
name = "changedetection-my-plugin" ,
version = "1.0.0" ,
packages = find_packages(),
install_requires = [
"changedetection.io" ,
],
entry_points = {
"changedetectionio" : [
"my_plugin = my_plugin.module:MyPlugin" ,
],
},
)
Hook Specifications
Available Hooks
More hooks will be added in future versions. Check pluggy_interface.py for the latest hook specifications.
Best Practices
Error Handling
Always wrap plugin code in try-except blocks:
@global_hookimpl
def ui_edit_stats_extras ( watch ):
try :
result = calculate_something(watch)
return generate_html(result)
except Exception as e:
logger.error( f "Plugin error: { str (e) } " )
return "<p>Error calculating statistics</p>"
Keep processing lightweight (Stats tab loads for every watch edit)
Cache expensive calculations
Use async operations for external API calls
Avoid blocking operations
Logging
Use loguru for consistent logging:
from loguru import logger
logger.info( "Plugin initialized" )
logger.debug( f "Processing watch: { watch.get( 'url' ) } " )
logger.error( f "Error: { str (e) } " )
Example Use Cases
Display SEO-related statistics: @global_hookimpl
def ui_edit_stats_extras ( watch ):
content = get_latest_snapshot(watch)
# Calculate SEO metrics
title_count = content.count( '<title>' )
meta_desc = extract_meta_description(content)
h1_count = content.count( '<h1' )
return f """
<div class="seo-stats">
<h4>SEO Analysis</h4>
<table class="pure-table">
<tr><td>Title tags</td><td> { title_count } </td></tr>
<tr><td>Meta description</td><td> { meta_desc } </td></tr>
<tr><td>H1 headings</td><td> { h1_count } </td></tr>
</table>
</div>
"""
Calculate content readability metrics: import textstat
@global_hookimpl
def ui_edit_stats_extras ( watch ):
content = get_text_content(watch)
flesch = textstat.flesch_reading_ease(content)
grade = textstat.flesch_kincaid_grade(content)
return f """
<div class="readability-stats">
<h4>Readability Metrics</h4>
<p>Flesch Score: { flesch :.1f} </p>
<p>Grade Level: { grade :.1f} </p>
</div>
"""
Processor that sends alerts to external systems: class CustomAlertProcessor ( ProcessorBase ):
def run ( self , uuid , preferred_proxy = None ):
# Fetch and process
fetcher = self .get_fetcher()
contents = fetcher.run(uuid)
# Custom logic
if self .detect_critical_change(contents):
self .send_external_alert(uuid)
return True , {}, contents
Troubleshooting
Plugin Not Loading
Check plugin file is in correct directory
Verify global_hookimpl decorator is used
Check logs for import errors: docker logs changedetection
Ensure plugin has no syntax errors
Stats Not Appearing
Verify hook returns valid HTML string
Check browser console for JavaScript errors
Ensure watch object has history data
Test with simple HTML first
Package Installation Fails
Check package name in EXTRA_PACKAGES is correct
Verify package exists on PyPI
Check for dependency conflicts
Review startup logs for pip errors
Next Steps
Processors Learn about built-in processors
Custom Fetchers Create custom content fetchers