From 4ff7b20fcf41400a3107767d17c747df5d11e51d Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Fri, 29 Aug 2025 13:15:59 +0200 Subject: [PATCH 01/17] Update docker-compose.yml - Include mac port info warning --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 898fb1dc..9d353dd8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,6 +84,7 @@ services: # Comment out ports: when using behind a reverse proxy , enable networks: etc. + # Mac users! Use "127.0.0.1:5050:5000" (port 5050) so theres no conflict with Airplay etc. (https://github.com/dgtlmoon/changedetection.io/issues/3401) ports: - 127.0.0.1:5000:5000 restart: unless-stopped From cd8e1151182e83c0f854e78c66591d3a17e8bb04 Mon Sep 17 00:00:00 2001 From: Giuseppe Rota <403432+grota@users.noreply.github.com> Date: Sat, 6 Sep 2025 11:47:16 +0200 Subject: [PATCH 02/17] Enable "last_viewed" field in the watch API. (#3403) --- changedetectionio/api/__init__.py | 3 +- changedetectionio/api/api_schema.py | 7 ++++ changedetectionio/tests/test_api.py | 13 ++++++-- docs/api-spec.yaml | 52 +++++++++++++++++------------ docs/api_v1/index.html | 14 ++++---- 5 files changed, 58 insertions(+), 31 deletions(-) diff --git a/changedetectionio/api/__init__.py b/changedetectionio/api/__init__.py index 33a26ce6..82f6e337 100644 --- a/changedetectionio/api/__init__.py +++ b/changedetectionio/api/__init__.py @@ -13,6 +13,7 @@ schema = api_schema.build_watch_json_schema(watch_base_config) schema_create_watch = copy.deepcopy(schema) schema_create_watch['required'] = ['url'] +del schema_create_watch['properties']['last_viewed'] schema_update_watch = copy.deepcopy(schema) schema_update_watch['additionalProperties'] = False @@ -51,7 +52,7 @@ def validate_openapi_request(operation_id): try: spec = get_openapi_spec() openapi_request = FlaskOpenAPIRequest(request) - result = spec.unmarshal_request(openapi_request, operation_id) + result = spec.unmarshal_request(openapi_request) if result.errors: abort(400, message=f"OpenAPI validation failed: {result.errors}") return f(*args, **kwargs) diff --git a/changedetectionio/api/api_schema.py b/changedetectionio/api/api_schema.py index c181d6a0..ac948675 100644 --- a/changedetectionio/api/api_schema.py +++ b/changedetectionio/api/api_schema.py @@ -78,6 +78,13 @@ def build_watch_json_schema(d): ]: schema['properties'][v]['anyOf'].append({'type': 'string', "maxLength": 5000}) + for v in ['last_viewed']: + schema['properties'][v] = { + "type": "integer", + "description": "Unix timestamp in seconds of the last time the watch was viewed.", + "minimum": 0 + } + # None or Boolean schema['properties']['track_ldjson_price_data']['anyOf'].append({'type': 'boolean'}) diff --git a/changedetectionio/tests/test_api.py b/changedetectionio/tests/test_api.py index c8fe2c55..c019b532 100644 --- a/changedetectionio/tests/test_api.py +++ b/changedetectionio/tests/test_api.py @@ -311,7 +311,7 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage): "value": "." # contains anything } ], - "conditions_match_logic": "ALL" + "conditions_match_logic": "ALL", } ), headers={'content-type': 'application/json', 'x-api-key': api_key}, @@ -328,6 +328,7 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage): ) watch_uuid = list(res.json.keys())[0] + assert not res.json[watch_uuid].get('viewed'), 'A newly created watch can only be unviewed' # Check in the edit page just to be sure res = client.get( @@ -341,7 +342,12 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage): res = client.put( url_for("watch", uuid=watch_uuid), headers={'x-api-key': api_key, 'content-type': 'application/json'}, - data=json.dumps({"title": "new title", 'time_between_check': {'minutes': 552}, 'headers': {'cookie': 'all eaten'}}), + data=json.dumps({ + "title": "new title", + 'time_between_check': {'minutes': 552}, + 'headers': {'cookie': 'all eaten'}, + 'last_viewed': int(time.time()) + }), ) assert res.status_code == 200, "HTTP PUT update was sent OK" @@ -351,6 +357,7 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage): headers={'x-api-key': api_key} ) assert res.json.get('title') == 'new title' + assert res.json.get('viewed'), 'With the timestamp greater than "changed" a watch can be updated to viewed' # Check in the edit page just to be sure res = client.get( @@ -383,7 +390,7 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage): def test_api_import(client, live_server, measure_memory_usage): - + api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') res = client.post( diff --git a/docs/api-spec.yaml b/docs/api-spec.yaml index b89ddf49..11584984 100644 --- a/docs/api-spec.yaml +++ b/docs/api-spec.yaml @@ -119,14 +119,9 @@ components: Enter your API key in the "Authorize" button above to automatically populate all code examples. schemas: - Watch: + WatchBase: type: object properties: - uuid: - type: string - format: uuid - description: Unique identifier for the web page change monitor (watch) - readOnly: true url: type: string format: uri @@ -229,25 +224,40 @@ components: maxLength: 5000 required: [operation, selector, optional_value] description: Browser automation steps - last_checked: - type: integer - description: Unix timestamp of last check - readOnly: true - last_changed: - type: integer - description: Unix timestamp of last change - readOnly: true - last_error: - type: string - description: Last error message - readOnly: true required: - url + Watch: + allOf: + - $ref: '#/components/schemas/WatchBase' + - type: object + properties: + uuid: + type: string + format: uuid + description: Unique identifier for the web page change monitor (watch) + readOnly: true + last_checked: + type: integer + description: Unix timestamp of last check + readOnly: true + last_changed: + type: integer + description: Unix timestamp of last change + readOnly: true + last_error: + type: string + description: Last error message + readOnly: true + last_viewed: + type: integer + description: Unix timestamp in seconds of the last time the watch was viewed. Setting it to a value higher than `last_changed` in the "Update watch" endpoint marks the watch as viewed. + minimum: 0 + CreateWatch: allOf: - - $ref: '#/components/schemas/Watch' - - type: object + - $ref: '#/components/schemas/WatchBase' + - type: 'object' required: - url @@ -576,7 +586,7 @@ paths: delete: operationId: deleteWatch - tags: [Watch Management] + tags: [Watch Management] summary: Delete watch description: Delete a web page change monitor (watch) and all related history x-code-samples: diff --git a/docs/api_v1/index.html b/docs/api_v1/index.html index 4f1bc4e2..90c12549 100644 --- a/docs/api_v1/index.html +++ b/docs/api_v1/index.html @@ -451,9 +451,9 @@ data-styled.g138[id="sc-enPhjR"]{content:"SikXG,"}/*!sc*/ -231.5279,231.248 -231.873,231.248 -0.3451,0 -104.688, -104.0616 -231.873,-231.248 z " fill="currentColor">

ChangeDetection.io API (0.1.0)

Download OpenAPI specification:

ChangeDetection.io Web page monitoring and notifications API

REST API for managing Page watches, Group tags, and Notifications.

-

changedetection.io can be driven by its built in simple API, in the examples below you will also find curl command line examples to help you.

+

changedetection.io can be driven by its built in simple API, in the examples below you will also find curl command line and python examples to help you get started faster.

Where to find my API key?

The API key can be easily found under the SETTINGS then API tab of changedetection.io dashboard.
Simply click the API key to automatically copy it to your clipboard.

@@ -562,7 +562,7 @@ notification preferences, and content filtering options.

" class="sc-eVqvcJ sc-fszimp kIppRw drqpJr">

Custom server

{protocol}://{host}/api/v1/watch/{uuid}

Request samples

curl -X GET "http://localhost:5000/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f" \
   -H "x-api-key: YOUR_API_KEY"
-

Response samples

Content type
{
  • "uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f",
  • "title": "string",
  • "tag": "string",
  • "tags": [
    ],
  • "paused": true,
  • "muted": true,
  • "method": "GET",
  • "fetch_backend": "html_requests",
  • "headers": {
    },
  • "body": "string",
  • "proxy": "string",
  • "webdriver_delay": 0,
  • "webdriver_js_execute_code": "string",
  • "time_between_check": {
    },
  • "notification_urls": [
    ],
  • "notification_title": "string",
  • "notification_body": "string",
  • "notification_format": "Text",
  • "track_ldjson_price_data": true,
  • "browser_steps": [
    ],
  • "last_checked": 0,
  • "last_changed": 0,
  • "last_error": "string"
}

Update watch

Response samples

Content type
{
  • "title": "string",
  • "tag": "string",
  • "tags": [
    ],
  • "paused": true,
  • "muted": true,
  • "method": "GET",
  • "fetch_backend": "html_requests",
  • "headers": {
    },
  • "body": "string",
  • "proxy": "string",
  • "webdriver_delay": 0,
  • "webdriver_js_execute_code": "string",
  • "time_between_check": {
    },
  • "notification_urls": [
    ],
  • "notification_title": "string",
  • "notification_body": "string",
  • "notification_format": "Text",
  • "track_ldjson_price_data": true,
  • "browser_steps": [
    ],
  • "uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f",
  • "last_checked": 0,
  • "last_changed": 0,
  • "last_error": "string",
  • "last_viewed": 0
}

Update watch

Update an existing web page change monitor (watch) using JSON. Accepts the same structure as returned in get single watch information.

Authorizations:
ApiKeyAuth
path Parameters
uuid
required
string <uuid>

Web page change monitor (watch) unique ID

@@ -604,8 +604,10 @@ notification preferences, and content filtering options.

" class="sc-eVqvcJ sc-fszimp kIppRw drqpJr">

Format for notifications

track_ldjson_price_data
boolean

Whether to track JSON-LD price data

-
Array of objects
Array of objects

Browser automation steps

+
last_viewed
integer >= 0

Unix timestamp in seconds of the last time the watch was viewed. Setting it to a value higher than last_changed in the "Update watch" endpoint marks the watch as viewed.

Responses

Custom server

-
{protocol}://{host}/api/v1/watch/{uuid}

Request samples

Content type
application/json
{
  • "title": "string",
  • "tag": "string",
  • "tags": [
    ],
  • "paused": true,
  • "muted": true,
  • "method": "GET",
  • "fetch_backend": "html_requests",
  • "headers": {
    },
  • "body": "string",
  • "proxy": "string",
  • "webdriver_delay": 0,
  • "webdriver_js_execute_code": "string",
  • "time_between_check": {
    },
  • "notification_urls": [
    ],
  • "notification_title": "string",
  • "notification_body": "string",
  • "notification_format": "Text",
  • "track_ldjson_price_data": true,
  • "browser_steps": [
    ]
}

Delete watch

{protocol}://{host}/api/v1/watch/{uuid}

Request samples

Content type
application/json
{
  • "title": "string",
  • "tag": "string",
  • "tags": [
    ],
  • "paused": true,
  • "muted": true,
  • "method": "GET",
  • "fetch_backend": "html_requests",
  • "headers": {
    },
  • "body": "string",
  • "proxy": "string",
  • "webdriver_delay": 0,
  • "webdriver_js_execute_code": "string",
  • "time_between_check": {
    },
  • "notification_urls": [
    ],
  • "notification_title": "string",
  • "notification_body": "string",
  • "notification_format": "Text",
  • "track_ldjson_price_data": true,
  • "browser_steps": [
    ],
  • "last_viewed": 0
}

Delete watch

Delete a web page change monitor (watch) and all related history

Authorizations:
ApiKeyAuth
path Parameters
@@ -155,7 +157,12 @@ document.addEventListener('DOMContentLoaded', function() { {% endif %}
- {{watch.title if watch.title is not none and watch.title|length > 0 else watch.url}}   + {% if system_use_url_watchlist or watch.get('use_page_title_in_list') %} + {{watch.label}} + {% else %} + {{watch.url}} + {% endif %} +   {%- if watch['processor'] == 'text_json_diff' -%} diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py index edf71636..ab39af30 100644 --- a/changedetectionio/forms.py +++ b/changedetectionio/forms.py @@ -28,6 +28,8 @@ from wtforms.validators import ValidationError from validators.url import url as url_validator +from changedetectionio.widgets import TernaryNoneBooleanField + # default # each select
uuid
required
string <uuid>

Web page change monitor (watch) unique ID

@@ -914,7 +916,7 @@ counts, uptime information, and version details.

-H "x-api-key: YOUR_API_KEY"

Response samples

Content type
application/json
{
  • "watch_count": 42,
  • "tag_count": 5,
  • "uptime": "2 days, 3:45:12",
  • "version": "0.50.10"
}
@@ -72,15 +72,16 @@
Some sites use JavaScript to create the content, for this you should use the Chrome/WebDriver Fetcher
Variables are supported in the URL (help and examples here).
+
+ {{ render_field(form.tags) }} + Organisational tag/group name used in the main listing page +
{{ render_field(form.processor) }}
- {{ render_field(form.title, class="m-d") }} -
-
- {{ render_field(form.tags) }} - Organisational tag/group name used in the main listing page + {{ render_field(form.title, class="m-d", placeholder=watch.label) }} + Automatically uses the page title if found, you can also use your own title/description here
@@ -101,15 +102,16 @@

-
- {{ render_checkbox_field(form.extract_title_as_title) }} -
+
{{ render_checkbox_field(form.filter_failure_notification_send) }} Sends a notification when the filter can no longer be seen on the page, good for knowing when the page changed and your filter will not work anymore.
+
+ {{ render_ternary_field(form.use_page_title_in_list) }} +
@@ -262,7 +264,7 @@ Math: {{ 1 + 1 }}") }}
- {{ render_checkbox_field(form.notification_muted) }} + {{ render_ternary_field(form.notification_muted, BooleanField=true) }}
{% if watch_needs_selenium_or_playwright %}
@@ -469,11 +471,11 @@ Math: {{ 1 + 1 }}") }}
{{ render_button(form.save_button) }} Delete + class="pure-button button-error ">Delete {% if watch.history_n %}Clear History{% endif %} + class="pure-button button-error">Clear History{% endif %} Clone & Edit + class="pure-button">Clone & Edit
diff --git a/changedetectionio/blueprint/watchlist/templates/watch-overview.html b/changedetectionio/blueprint/watchlist/templates/watch-overview.html index 1c6ab0ba..d4483c3b 100644 --- a/changedetectionio/blueprint/watchlist/templates/watch-overview.html +++ b/changedetectionio/blueprint/watchlist/templates/watch-overview.html @@ -118,7 +118,8 @@ document.addEventListener('DOMContentLoaded', function() { {%- set checking_now = is_checking_now(watch) -%} {%- set history_n = watch.history_n -%} {%- set favicon = watch.get_favicon_filename() -%} - {# Mirror in changedetectionio/static/js/realtime.js for the frontend #} + {%- set system_use_url_watchlist = datastore.data['settings']['application']['ui'].get('use_page_title_in_list') -%} + {# Class settings mirrored in changedetectionio/static/js/realtime.js for the frontend #} {%- set row_classes = [ loop.cycle('pure-table-odd', 'pure-table-even'), 'processor-' ~ watch['processor'], @@ -133,7 +134,8 @@ document.addEventListener('DOMContentLoaded', function() { 'checking-now' if checking_now else '', 'notification_muted' if watch.notification_muted else '', 'single-history' if history_n == 1 else '', - 'multiple-history' if history_n >= 2 else '', + 'multiple-history' if history_n >= 2 else '', + 'use-html-title' if system_use_url_watchlist else 'no-html-title', ] -%}
{{ loop.index+pagination.skip }}