From e4baca112793b15bf5513b12b68f81675f51856d Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Mon, 6 Oct 2025 09:14:14 +0200 Subject: [PATCH 1/7] 0.50.15 --- changedetectionio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changedetectionio/__init__.py b/changedetectionio/__init__.py index 0d48769a7..a6f8d1465 100644 --- a/changedetectionio/__init__.py +++ b/changedetectionio/__init__.py @@ -2,7 +2,7 @@ # Read more https://github.com/dgtlmoon/changedetection.io/wiki -__version__ = '0.50.14' +__version__ = '0.50.15' from changedetectionio.strtobool import strtobool from json.decoder import JSONDecodeError From 0c9c475f3249df72cff0036b128a423aee5b85fe Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Mon, 6 Oct 2025 15:39:07 +0200 Subject: [PATCH 2/7] Fixing bad detection of text text/plain in previous release, adding automated test (#3460) --- .../processors/text_json_diff/processor.py | 4 +- changedetectionio/tests/test_backend.py | 77 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/changedetectionio/processors/text_json_diff/processor.py b/changedetectionio/processors/text_json_diff/processor.py index 62630ed0a..45d644213 100644 --- a/changedetectionio/processors/text_json_diff/processor.py +++ b/changedetectionio/processors/text_json_diff/processor.py @@ -154,10 +154,10 @@ class perform_site_check(difference_detection_processor): self.fetcher.content = html_tools.workarounds_for_obfuscations(self.fetcher.content) html_content = self.fetcher.content content_type = self.fetcher.get_all_headers().get('content-type', '').lower() - is_attachment = 'attachment' in self.fetcher.get_all_headers().get('content-disposition', '').lower() + is_attachment = 'attachment' in self.fetcher.get_all_headers().get('content-disposition', '').lower() or 'octet-stream' in content_type # Try to detect better mime types if its a download or not announced as HTML - if is_attachment or 'octet-stream' in content_type or not 'html' in content_type: + if is_attachment: logger.debug(f"Got a reply that may be a download or possibly a text attachment, checking..") try: import magic diff --git a/changedetectionio/tests/test_backend.py b/changedetectionio/tests/test_backend.py index 2636466e1..1447e7bf3 100644 --- a/changedetectionio/tests/test_backend.py +++ b/changedetectionio/tests/test_backend.py @@ -167,6 +167,18 @@ def test_check_basic_change_detection_functionality(client, live_server, measure assert b'Deleted' in res.data def test_non_text_mime_or_downloads(client, live_server, measure_memory_usage): + """ + + https://github.com/dgtlmoon/changedetection.io/issues/3434 + I noticed that a watched website can be monitored fine as long as the server sends content-type: text/plain; charset=utf-8, + but once the server sends content-type: application/octet-stream (which is usually done to force the browser to show the Download dialog), + changedetection somehow ignores all line breaks and treats the document file as if everything is on one line. + + :param client: + :param live_server: + :param measure_memory_usage: + :return: + """ with open("test-datastore/endpoint-content.txt", "w") as f: f.write("""some random text that should be split by line and not parsed with html_to_text @@ -215,3 +227,68 @@ got it\r\n res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True) + +def test_standard_text_plain(client, live_server, measure_memory_usage): + """ + + https://github.com/dgtlmoon/changedetection.io/issues/3434 + I noticed that a watched website can be monitored fine as long as the server sends content-type: text/plain; charset=utf-8, + but once the server sends content-type: application/octet-stream (which is usually done to force the browser to show the Download dialog), + changedetection somehow ignores all line breaks and treats the document file as if everything is on one line. + + The real bug here can be that it will try to process plain-text as HTML, losing + + :param client: + :param live_server: + :param measure_memory_usage: + :return: + """ + with open("test-datastore/endpoint-content.txt", "w") as f: + f.write("""some random text that should be split by line +and not parsed with html_to_text +Even this title should stay because we are just plain text +this way we know that it correctly parsed as plain text +\r\n +ok\r\n +got it\r\n +""") + + test_url = url_for('test_endpoint', content_type="text/plain", _external=True) + + # Add our URL to the import page + res = client.post( + url_for("imports.import_page"), + data={"urls": test_url}, + follow_redirects=True + ) + + assert b"1 Imported" in res.data + + wait_for_all_checks(client) + + ### check the front end + res = client.get( + url_for("ui.ui_views.preview_page", uuid="first"), + follow_redirects=True + ) + assert b"some random text that should be split by line\n" in res.data + #### + + # Check the snapshot by API that it has linefeeds too + watch_uuid = next(iter(live_server.app.config['DATASTORE'].data['watching'])) + api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') + res = client.get( + url_for("watchhistory", uuid=watch_uuid), + headers={'x-api-key': api_key}, + ) + + # Fetch a snapshot by timestamp, check the right one was found + res = client.get( + url_for("watchsinglehistory", uuid=watch_uuid, timestamp=list(res.json.keys())[-1]), + headers={'x-api-key': api_key}, + ) + assert b"some random text that should be split by line\n" in res.data + assert b"Even this title should stay because we are just plain text" in res.data + + res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True) + From 754febfd33f40c75a3d5a530d7b5e45f63633b7f Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Mon, 6 Oct 2025 15:39:23 +0200 Subject: [PATCH 3/7] 0.50.16 --- changedetectionio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changedetectionio/__init__.py b/changedetectionio/__init__.py index a6f8d1465..18075f992 100644 --- a/changedetectionio/__init__.py +++ b/changedetectionio/__init__.py @@ -2,7 +2,7 @@ # Read more https://github.com/dgtlmoon/changedetection.io/wiki -__version__ = '0.50.15' +__version__ = '0.50.16' from changedetectionio.strtobool import strtobool from json.decoder import JSONDecodeError From 584b6e378d8cc112bd12f9b7469baf98a06f5478 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Thu, 9 Oct 2025 00:03:13 +0200 Subject: [PATCH 4/7] Dependabot tweaks --- .github/dependabot.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7920383a1..32bbe708c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,6 +11,4 @@ updates: - package-ecosystem: pip directory: / schedule: - interval: "daily" - allow: - - dependency-name: "apprise" + interval: "weekly" From f7dfc9bbb8d0a0816ae6c8bd46b9d86ea419ff47 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Thu, 9 Oct 2025 00:14:28 +0200 Subject: [PATCH 5/7] Refactor content type detection, fixing more xpath issues for RSS types (#3465) #3462 #3391 --- changedetectionio/processors/magic.py | 138 ++++++++++++++++++ .../processors/text_json_diff/processor.py | 78 +++++----- changedetectionio/tests/test_backend.py | 3 + changedetectionio/tests/test_group.py | 2 - .../tests/test_history_consistency.py | 3 +- .../tests/test_xpath_selector.py | 82 ++++++++++- 6 files changed, 253 insertions(+), 53 deletions(-) create mode 100644 changedetectionio/processors/magic.py diff --git a/changedetectionio/processors/magic.py b/changedetectionio/processors/magic.py new file mode 100644 index 000000000..bdfda2ded --- /dev/null +++ b/changedetectionio/processors/magic.py @@ -0,0 +1,138 @@ +""" +Content Type Detection and Stream Classification + +This module provides intelligent content-type detection for changedetection.io. +It addresses the common problem where HTTP Content-Type headers are missing, incorrect, +or too generic, which would otherwise cause the wrong processor to be used. + +The guess_stream_type class combines: +1. HTTP Content-Type headers (when available and reliable) +2. Python-magic library for MIME detection (analyzing actual file content) +3. Content-based pattern matching for text formats (HTML tags, XML declarations, etc.) + +This multi-layered approach ensures accurate detection of RSS feeds, JSON, HTML, PDF, +plain text, CSV, YAML, and XML formats - even when servers provide misleading headers. + +Used by: processors/text_json_diff/processor.py and other content processors +""" + +# When to apply the 'cdata to real HTML' hack +RSS_XML_CONTENT_TYPES = [ + "application/rss+xml", + "application/rdf+xml", + "text/xml", + "application/xml", + "application/atom+xml", + "text/rss+xml", # rare, non-standard + "application/x-rss+xml", # legacy (older feed software) + "application/x-atom+xml", # legacy (older Atom) +] + +# JSON Content-types +JSON_CONTENT_TYPES = [ + "application/activity+json", + "application/feed+json", + "application/json", + "application/ld+json", + "application/vnd.api+json", +] + +# CSV Content-types +CSV_CONTENT_TYPES = [ + "text/csv", + "application/csv", +] + +# Generic XML Content-types (non-RSS/Atom) +XML_CONTENT_TYPES = [ + "text/xml", + "application/xml", +] + +# YAML Content-types +YAML_CONTENT_TYPES = [ + "text/yaml", + "text/x-yaml", + "application/yaml", + "application/x-yaml", +] + +HTML_PATTERNS = [' + + + + RSS Feed + + + + + + + en-us + water News RSS + + 🍁 Lets go discount +

ok heres the description

+ + + + Wed, 08 Oct 2025 15:28:55 +0000 + https://store.waterpowered.com/news/app/1643320/view/511845698831908921 + +
+
+
""" + + with open("test-datastore/endpoint-content.txt", "w") as f: + f.write(test_return_data) + + return None @@ -575,3 +605,47 @@ def test_xpath_20_function_string_join_matches(client, live_server, measure_memo client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True) + +def _subtest_xpath_rss(client, content_type='text/html'): + + # Add our URL to the import page + test_url = url_for('test_endpoint', content_type=content_type, _external=True) + res = client.post( + url_for("ui.ui_views.form_quick_watch_add"), + data={"url": test_url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'}, + follow_redirects=True + ) + + assert b"Watch added in Paused state, saving will unpause" in res.data + + res = client.post( + url_for("ui.ui_edit.edit_page", uuid="first", unpause_on_save=1), + data={ + "url": test_url, + "include_filters": "xpath://item", + "tags": '', + "fetch_backend": "html_requests", + "time_between_check_use_default": "y", + }, + follow_redirects=True + ) + + assert b"unpaused" in res.data + wait_for_all_checks(client) + + res = client.get( + url_for("ui.ui_views.preview_page", uuid="first"), + follow_redirects=True + ) + + assert b"Lets go discount" in res.data, f"When testing for Lets go discount called with content type '{content_type}'" + assert b"Events and Announcements" not in res.data, f"When testing for Lets go discount called with content type '{content_type}'" # It should not be here because thats not our selector target + + client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True) + +# Be sure all-in-the-wild types of RSS feeds work with xpath +def test_rss_xpath(client, live_server): + for feed_header in ['', '']: + set_rss_atom_feed_response(header=feed_header) + for content_type in RSS_XML_CONTENT_TYPES: + _subtest_xpath_rss(client, content_type=content_type) From 4aa5bb6da3cbe9a5621328f0b6f3bb42d34ccf81 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Thu, 9 Oct 2025 00:19:02 +0200 Subject: [PATCH 6/7] 0.50.17 --- changedetectionio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changedetectionio/__init__.py b/changedetectionio/__init__.py index 18075f992..255322c0e 100644 --- a/changedetectionio/__init__.py +++ b/changedetectionio/__init__.py @@ -2,7 +2,7 @@ # Read more https://github.com/dgtlmoon/changedetection.io/wiki -__version__ = '0.50.16' +__version__ = '0.50.17' from changedetectionio.strtobool import strtobool from json.decoder import JSONDecodeError From 78fa47f6f8f4f313eb28b97e3b1d8ac52763da8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 00:50:53 +0200 Subject: [PATCH 7/7] Bump psutil from 7.0.0 to 7.1.0 (#3469) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d448f7aa4..cf95181d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -135,7 +135,7 @@ tzdata pluggy ~= 1.5 # Needed for testing, cross-platform for process and system monitoring -psutil==7.0.0 +psutil==7.1.0 ruff >= 0.11.2 pre_commit >= 4.2.0