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? -
    -
    - {{ render_checkbox_field(form.application.form.rss_hide_muted_watches) }} -
    -
    - {{ render_field(form.application.form.rss_content_format) }} - Love RSS? Does your reader support HTML? Set it here -
    -
    - {{ render_checkbox_field(form.application.form.rss_reader_mode) }} - When watching RSS/Atom feeds, convert them into clean text for better change detection. -
    -
    @@ -230,6 +218,24 @@ nav

    +
    +
    + {{ render_checkbox_field(form.application.form.rss_hide_muted_watches) }} +
    +
    + {{ render_field(form.application.form.rss_content_format) }} + Love RSS? Does your reader support HTML? Set it here +
    +
    + {{ render_field(form.application.form.rss_diff_length) }} + Maximum number of history snapshots to include in the watch specific RSS feed. +
    +
    + {{ render_checkbox_field(form.application.form.rss_reader_mode) }} + For watching other RSS feeds - When watching RSS/Atom feeds, convert them into clean text for better change detection. +
    +
    +
    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