From 8de0a78fa765002b6fbfe0df94757d86922c4062 Mon Sep 17 00:00:00 2001
From: dgtlmoon
Date: Thu, 13 Nov 2025 19:09:10 +0100
Subject: [PATCH] RSS Feed length
---
changedetectionio/blueprint/rss/_util.py | 8 ++-
.../blueprint/rss/single_watch.py | 71 ++++++++++++++-----
.../settings/templates/settings.html | 32 +++++----
changedetectionio/forms.py | 6 +-
changedetectionio/model/App.py | 1 +
5 files changed, 83 insertions(+), 35 deletions(-)
diff --git a/changedetectionio/blueprint/rss/_util.py b/changedetectionio/blueprint/rss/_util.py
index 47c0ef885..8aa56dd2e 100644
--- a/changedetectionio/blueprint/rss/_util.py
+++ b/changedetectionio/blueprint/rss/_util.py
@@ -46,7 +46,7 @@ def generate_watch_guid(watch):
return f"{watch['uuid']}/{watch.last_changed}"
-def generate_watch_diff_content(watch, dates, rss_content_format, datastore):
+def generate_watch_diff_content(watch, dates, rss_content_format, datastore, date_index_from=-2, date_index_to=-1):
"""
Generate HTML diff content for a watch given its history dates.
Returns tuple of (content, watch_label).
@@ -56,6 +56,8 @@ def generate_watch_diff_content(watch, dates, rss_content_format, datastore):
dates: List of history snapshot dates
rss_content_format: Format for RSS content (html or text)
datastore: The ChangeDetectionStore instance
+ date_index_from: Index of the "from" date in the dates list (default: -2)
+ date_index_to: Index of the "to" date in the dates list (default: -1)
Returns:
Tuple of (content, watch_label) - the rendered HTML content and watch label
@@ -70,8 +72,8 @@ def generate_watch_diff_content(watch, dates, rss_content_format, datastore):
try:
html_diff = diff.render_diff(
- previous_version_file_contents=watch.get_history_snapshot(timestamp=dates[-2]),
- newest_version_file_contents=watch.get_history_snapshot(timestamp=dates[-1]),
+ previous_version_file_contents=watch.get_history_snapshot(timestamp=dates[date_index_from]),
+ newest_version_file_contents=watch.get_history_snapshot(timestamp=dates[date_index_to]),
include_equal=False
)
diff --git a/changedetectionio/blueprint/rss/single_watch.py b/changedetectionio/blueprint/rss/single_watch.py
index 84b7bcc85..6b496c278 100644
--- a/changedetectionio/blueprint/rss/single_watch.py
+++ b/changedetectionio/blueprint/rss/single_watch.py
@@ -18,8 +18,9 @@ def construct_single_watch_routes(rss_blueprint, datastore):
@rss_blueprint.route("/watch/", methods=['GET'])
def rss_single_watch(uuid):
"""
- Display the most recent change for a single watch as RSS feed.
- Returns RSS XML with a single entry showing the diff between the last two snapshots.
+ Display the most recent changes for a single watch as RSS feed.
+ Returns RSS XML with multiple entries showing diffs between consecutive snapshots.
+ The number of entries is controlled by the rss_diff_length setting.
"""
# Always requires token set
app_rss_token = datastore.data['settings']['application'].get('rss_access_token')
@@ -42,29 +43,65 @@ def construct_single_watch_routes(rss_blueprint, datastore):
# Add uuid to watch for proper functioning
watch['uuid'] = uuid
- # Generate the diff content using the shared helper function
- content, watch_label = generate_watch_diff_content(watch, dates, rss_content_format, datastore)
+ # Get the number of diffs to include (default: 5)
+ rss_diff_length = datastore.data['settings']['application'].get('rss_diff_length', 5)
- # Create RSS feed with single entry
+ # Calculate how many diffs we can actually show (limited by available history)
+ # We need at least 2 snapshots to create 1 diff
+ max_possible_diffs = len(dates) - 1
+ num_diffs = min(rss_diff_length, max_possible_diffs) if rss_diff_length > 0 else max_possible_diffs
+
+ # Create RSS feed
fg = FeedGenerator()
fg.title(f'changedetection.io - {watch.label}')
fg.description('Changes')
fg.link(href='https://changedetection.io')
- # Add single entry for this watch
- guid = generate_watch_guid(watch)
- fe = fg.add_entry()
+ # Loop through history and create RSS entries for each diff
+ for i in range(num_diffs):
+ # Calculate indices for this diff (working backwards from newest)
+ # i=0: compare dates[-2] to dates[-1] (most recent change)
+ # i=1: compare dates[-3] to dates[-2] (previous change)
+ # etc.
+ date_index_to = -(i + 1)
+ date_index_from = -(i + 2)
- # Include a link to the diff page
- diff_link = {'href': url_for('ui.ui_views.diff_history_page', uuid=watch['uuid'], _external=True)}
- fe.link(link=diff_link)
+ try:
+ # Generate the diff content for this pair of snapshots
+ content, watch_label = generate_watch_diff_content(
+ watch, dates, rss_content_format, datastore,
+ date_index_from=date_index_from,
+ date_index_to=date_index_to
+ )
- fe.title(title=watch_label)
- fe.content(content=content, type='CDATA')
- fe.guid(guid, permalink=False)
- dt = datetime.datetime.fromtimestamp(int(watch.newest_history_key))
- dt = dt.replace(tzinfo=pytz.UTC)
- fe.pubDate(dt)
+ # Create a unique GUID for this specific diff
+ timestamp_to = dates[date_index_to]
+ timestamp_from = dates[date_index_from]
+ guid = f"{watch['uuid']}/{timestamp_to}"
+
+ fe = fg.add_entry()
+
+ # Include a link to the diff page with specific versions
+ diff_link = {'href': url_for('ui.ui_views.diff_history_page',
+ uuid=watch['uuid'],
+ from_version=timestamp_from,
+ to_version=timestamp_to,
+ _external=True)}
+ fe.link(link=diff_link)
+
+ # Add timestamp info to title to distinguish different diffs
+ fe.title(title=f"{watch_label} - Change {i+1}")
+ fe.content(content=content, type='CDATA')
+ fe.guid(guid, permalink=False)
+
+ # Use the timestamp of the "to" snapshot for pubDate
+ dt = datetime.datetime.fromtimestamp(int(timestamp_to))
+ dt = dt.replace(tzinfo=pytz.UTC)
+ fe.pubDate(dt)
+
+ except (IndexError, FileNotFoundError) as e:
+ # Skip this diff if we can't generate it
+ continue
response = make_response(fg.rss_str())
response.headers.set('Content-Type', 'application/rss+xml;charset=utf-8')
diff --git a/changedetectionio/blueprint/settings/templates/settings.html b/changedetectionio/blueprint/settings/templates/settings.html
index 6bbe604a3..0577b98e6 100644
--- a/changedetectionio/blueprint/settings/templates/settings.html
+++ b/changedetectionio/blueprint/settings/templates/settings.html
@@ -24,6 +24,7 @@
Global Filters
UI Options
API
+ RSS
Time & Date
CAPTCHA & Proxies
@@ -72,19 +73,6 @@
{{ render_checkbox_field(form.application.form.empty_pages_are_a_change) }}
When a request returns no content, or the HTML does not contain any text, is this considered a change?
-
@@ -230,6 +218,24 @@ nav
+
+
Ensure the settings below are correct, they are used to manage the time schedule for checking your web page watches.
diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py
index 51760efed..49e6e6009 100644
--- a/changedetectionio/forms.py
+++ b/changedetectionio/forms.py
@@ -1009,8 +1009,10 @@ class globalSettingsApplicationForm(commonSettingsForm):
rss_hide_muted_watches = BooleanField('Hide muted watches from RSS feed', default=True,
validators=[validators.Optional()])
- rss_reader_mode = BooleanField('RSS reader mode ', default=False,
- validators=[validators.Optional()])
+ rss_reader_mode = BooleanField('RSS reader mode ', default=False, validators=[validators.Optional()])
+ rss_diff_length = IntegerField(label='Number of changes to show in watch RSS feed',
+ render_kw={"style": "width: 5em;"},
+ validators=[validators.NumberRange(min=0, message="Should contain zero or more attempts")])
filter_failure_notification_threshold_attempts = IntegerField('Number of times the filter can be missing before sending a notification',
render_kw={"style": "width: 5em;"},
diff --git a/changedetectionio/model/App.py b/changedetectionio/model/App.py
index cbf81370c..21647f85c 100644
--- a/changedetectionio/model/App.py
+++ b/changedetectionio/model/App.py
@@ -55,6 +55,7 @@ class model(dict):
'render_anchor_tag_content': False,
'rss_access_token': None,
'rss_content_format': RSS_CONTENT_FORMAT_DEFAULT,
+ 'rss_diff_length': 5,
'rss_hide_muted_watches': True,
'rss_reader_mode': False,
'scheduler_timezone_default': None, # Default IANA timezone name