Compare commits

...

12 Commits

Author SHA1 Message Date
dgtlmoon
0ade4307b0 0.50.12
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
2025-09-07 16:15:16 +02:00
dgtlmoon
8c03b65dc6 Fix - Filters in tags/groups were being added to watches on each check - #3406 fix list update (#3407) 2025-09-07 15:33:18 +02:00
Jeff Hedlund
8a07459e43 UI - Added "unread" view filter (#3393)
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
2025-09-06 11:47:57 +02:00
Giuseppe Rota
cd8e115118 Enable "last_viewed" field in the watch API. (#3403) 2025-09-06 11:47:16 +02:00
dgtlmoon
4ff7b20fcf Update docker-compose.yml - Include mac port info warning
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-08-29 13:15:59 +02:00
dgtlmoon
8120f00148 0.50.11
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/amd64 (alpine) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm64 (alpine) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/amd64 (main) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm/v7 (main) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm/v8 (main) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm64 (main) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm64/v8 (main) (push) Has been cancelled
2025-08-28 22:11:36 +02:00
dependabot[bot]
127abf49f1 Bump cryptography from 43.0.1 to 44.0.1 (#3399) 2025-08-28 21:20:15 +02:00
dgtlmoon
db81c3c5e2 Cryptography library - pinning version 2025-08-28 20:41:59 +02:00
dgtlmoon
9952af7a52 UI - Improving "real-time updates offline" message 2025-08-28 20:35:20 +02:00
dgtlmoon
790577c1b6 Build - Adding new cryptography library, solving apprise plugin issues (#3398) #3397 2025-08-28 20:29:21 +02:00
dgtlmoon
bab362fb7d Update api-spec.yaml
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/amd64 (alpine) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm64 (alpine) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/amd64 (main) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm/v7 (main) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm/v8 (main) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm64 (main) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm64/v8 (main) (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
2025-08-28 14:38:19 +02:00
dgtlmoon
a177d02406 API - API endpoint call validation against OpenAPI specification YML also (#3386) 2025-08-28 14:36:28 +02:00
24 changed files with 335 additions and 197 deletions

View File

@@ -15,6 +15,10 @@ jobs:
ruff check . --select E9,F63,F7,F82
# Complete check with errors treated as warnings
ruff check . --exit-zero
- name: Validate OpenAPI spec
run: |
pip install openapi-spec-validator
python3 -c "from openapi_spec_validator import validate_spec; import yaml; validate_spec(yaml.safe_load(open('docs/api-spec.yaml')))"
test-application-3-10:
needs: lint-code

View File

@@ -5,7 +5,6 @@ ARG PYTHON_VERSION=3.11
FROM python:${PYTHON_VERSION}-slim-bookworm AS builder
# See `cryptography` pin comment in requirements.txt
ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1
RUN apt-get update && apt-get install -y --no-install-recommends \
g++ \
@@ -17,6 +16,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libxslt-dev \
make \
patch \
pkg-config \
zlib1g-dev
RUN mkdir /install
@@ -26,6 +26,14 @@ COPY requirements.txt /requirements.txt
# Use cache mounts and multiple wheel sources for faster ARM builds
ENV PIP_CACHE_DIR=/tmp/pip-cache
# Help Rust find OpenSSL for cryptography package compilation on ARM
ENV PKG_CONFIG_PATH="/usr/lib/pkgconfig:/usr/lib/arm-linux-gnueabihf/pkgconfig:/usr/lib/aarch64-linux-gnu/pkgconfig"
ENV PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1
ENV OPENSSL_DIR="/usr"
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
ENV OPENSSL_INCLUDE_DIR="/usr/include/openssl"
# Additional environment variables for cryptography Rust build
ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1
RUN --mount=type=cache,target=/tmp/pip-cache \
pip install \
--extra-index-url https://www.piwheels.org/simple \

View File

@@ -2,7 +2,7 @@
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
__version__ = '0.50.10'
__version__ = '0.50.12'
from changedetectionio.strtobool import strtobool
from json.decoder import JSONDecodeError

View File

@@ -3,7 +3,7 @@ from changedetectionio.strtobool import strtobool
from flask_restful import abort, Resource
from flask import request
import validators
from . import auth
from . import auth, validate_openapi_request
class Import(Resource):
@@ -12,6 +12,7 @@ class Import(Resource):
self.datastore = kwargs['datastore']
@auth.check_token
@validate_openapi_request('importWatches')
def post(self):
"""Import a list of watched URLs."""

View File

@@ -1,9 +1,7 @@
from flask_expects_json import expects_json
from flask_restful import Resource
from . import auth
from flask_restful import abort, Resource
from flask_restful import Resource, abort
from flask import request
from . import auth
from . import auth, validate_openapi_request
from . import schema_create_notification_urls, schema_delete_notification_urls
class Notifications(Resource):
@@ -12,6 +10,7 @@ class Notifications(Resource):
self.datastore = kwargs['datastore']
@auth.check_token
@validate_openapi_request('getNotifications')
def get(self):
"""Return Notification URL List."""
@@ -22,6 +21,7 @@ class Notifications(Resource):
}, 200
@auth.check_token
@validate_openapi_request('addNotifications')
@expects_json(schema_create_notification_urls)
def post(self):
"""Create Notification URLs."""
@@ -49,6 +49,7 @@ class Notifications(Resource):
return {'notification_urls': added_urls}, 201
@auth.check_token
@validate_openapi_request('replaceNotifications')
@expects_json(schema_create_notification_urls)
def put(self):
"""Replace Notification URLs."""
@@ -71,6 +72,7 @@ class Notifications(Resource):
return {'notification_urls': clean_urls}, 200
@auth.check_token
@validate_openapi_request('deleteNotifications')
@expects_json(schema_delete_notification_urls)
def delete(self):
"""Delete Notification URLs."""

View File

@@ -1,6 +1,6 @@
from flask_restful import Resource, abort
from flask import request
from . import auth
from . import auth, validate_openapi_request
class Search(Resource):
def __init__(self, **kwargs):
@@ -8,6 +8,7 @@ class Search(Resource):
self.datastore = kwargs['datastore']
@auth.check_token
@validate_openapi_request('searchWatches')
def get(self):
"""Search for watches by URL or title text."""
query = request.args.get('q', '').strip()

View File

@@ -1,5 +1,5 @@
from flask_restful import Resource
from . import auth
from . import auth, validate_openapi_request
class SystemInfo(Resource):
@@ -9,6 +9,7 @@ class SystemInfo(Resource):
self.update_q = kwargs['update_q']
@auth.check_token
@validate_openapi_request('getSystemInfo')
def get(self):
"""Return system info."""
import time

View File

@@ -7,7 +7,7 @@ from flask import request
from . import auth
# Import schemas from __init__.py
from . import schema_tag, schema_create_tag, schema_update_tag
from . import schema_tag, schema_create_tag, schema_update_tag, validate_openapi_request
class Tag(Resource):
@@ -19,6 +19,7 @@ class Tag(Resource):
# Get information about a single tag
# curl http://localhost:5000/api/v1/tag/<string:uuid>
@auth.check_token
@validate_openapi_request('getTag')
def get(self, uuid):
"""Get data for a single tag/group, toggle notification muting, or recheck all."""
from copy import deepcopy
@@ -50,6 +51,7 @@ class Tag(Resource):
return tag
@auth.check_token
@validate_openapi_request('deleteTag')
def delete(self, uuid):
"""Delete a tag/group and remove it from all watches."""
if not self.datastore.data['settings']['application']['tags'].get(uuid):
@@ -66,6 +68,7 @@ class Tag(Resource):
return 'OK', 204
@auth.check_token
@validate_openapi_request('updateTag')
@expects_json(schema_update_tag)
def put(self, uuid):
"""Update tag information."""
@@ -80,6 +83,7 @@ class Tag(Resource):
@auth.check_token
@validate_openapi_request('createTag')
# Only cares for {'title': 'xxxx'}
def post(self):
"""Create a single tag/group."""
@@ -100,6 +104,7 @@ class Tags(Resource):
self.datastore = kwargs['datastore']
@auth.check_token
@validate_openapi_request('listTags')
def get(self):
"""List tags/groups."""
result = {}

View File

@@ -11,7 +11,7 @@ from . import auth
import copy
# Import schemas from __init__.py
from . import schema, schema_create_watch, schema_update_watch
from . import schema, schema_create_watch, schema_update_watch, validate_openapi_request
class Watch(Resource):
@@ -25,6 +25,7 @@ class Watch(Resource):
# @todo - version2 - ?muted and ?paused should be able to be called together, return the watch struct not "OK"
# ?recheck=true
@auth.check_token
@validate_openapi_request('getWatch')
def get(self, uuid):
"""Get information about a single watch, recheck, pause, or mute."""
from copy import deepcopy
@@ -57,6 +58,7 @@ class Watch(Resource):
return watch
@auth.check_token
@validate_openapi_request('deleteWatch')
def delete(self, uuid):
"""Delete a watch and related history."""
if not self.datastore.data['watching'].get(uuid):
@@ -66,6 +68,7 @@ class Watch(Resource):
return 'OK', 204
@auth.check_token
@validate_openapi_request('updateWatch')
@expects_json(schema_update_watch)
def put(self, uuid):
"""Update watch information."""
@@ -91,6 +94,7 @@ class WatchHistory(Resource):
# Get a list of available history for a watch by UUID
# curl http://localhost:5000/api/v1/watch/<string:uuid>/history
@auth.check_token
@validate_openapi_request('getWatchHistory')
def get(self, uuid):
"""Get a list of all historical snapshots available for a watch."""
watch = self.datastore.data['watching'].get(uuid)
@@ -105,6 +109,7 @@ class WatchSingleHistory(Resource):
self.datastore = kwargs['datastore']
@auth.check_token
@validate_openapi_request('getWatchSnapshot')
def get(self, uuid, timestamp):
"""Get single snapshot from watch."""
watch = self.datastore.data['watching'].get(uuid)
@@ -138,6 +143,7 @@ class WatchFavicon(Resource):
self.datastore = kwargs['datastore']
@auth.check_token
@validate_openapi_request('getWatchFavicon')
def get(self, uuid):
"""Get favicon for a watch."""
watch = self.datastore.data['watching'].get(uuid)
@@ -172,6 +178,7 @@ class CreateWatch(Resource):
self.update_q = kwargs['update_q']
@auth.check_token
@validate_openapi_request('createWatch')
@expects_json(schema_create_watch)
def post(self):
"""Create a single watch."""
@@ -207,6 +214,7 @@ class CreateWatch(Resource):
return "Invalid or unsupported URL", 400
@auth.check_token
@validate_openapi_request('listWatches')
def get(self):
"""List watches."""
list = {}

View File

@@ -1,4 +1,9 @@
import copy
import yaml
import functools
from flask import request, abort
from openapi_core import OpenAPI
from openapi_core.contrib.flask import FlaskOpenAPIRequest
from . import api_schema
from ..model import watch_base
@@ -8,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
@@ -25,6 +31,38 @@ schema_create_notification_urls['required'] = ['notification_urls']
schema_delete_notification_urls = copy.deepcopy(schema_notification_urls)
schema_delete_notification_urls['required'] = ['notification_urls']
# Load OpenAPI spec for validation
_openapi_spec = None
def get_openapi_spec():
global _openapi_spec
if _openapi_spec is None:
import os
spec_path = os.path.join(os.path.dirname(__file__), '../../docs/api-spec.yaml')
with open(spec_path, 'r') as f:
spec_dict = yaml.safe_load(f)
_openapi_spec = OpenAPI.from_dict(spec_dict)
return _openapi_spec
def validate_openapi_request(operation_id):
"""Decorator to validate incoming requests against OpenAPI spec."""
def decorator(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
try:
spec = get_openapi_spec()
openapi_request = FlaskOpenAPIRequest(request)
result = spec.unmarshal_request(openapi_request)
if result.errors:
abort(400, message=f"OpenAPI validation failed: {result.errors}")
return f(*args, **kwargs)
except Exception as e:
# If OpenAPI validation fails, log but don't break existing functionality
print(f"OpenAPI validation warning for {operation_id}: {e}")
return f(*args, **kwargs)
return wrapper
return decorator
# Import all API resources
from .Watch import Watch, WatchHistory, WatchSingleHistory, CreateWatch, WatchFavicon
from .Tags import Tags, Tag

View File

@@ -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'})

View File

@@ -44,12 +44,16 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
# Sort by last_changed and add the uuid which is usually the key..
sorted_watches = []
with_errors = request.args.get('with_errors') == "1"
unread_only = request.args.get('unread') == "1"
errored_count = 0
search_q = request.args.get('q').strip().lower() if request.args.get('q') else False
for uuid, watch in datastore.data['watching'].items():
if with_errors and not watch.get('last_error'):
continue
if unread_only and (watch.viewed or watch.last_changed == 0) :
continue
if active_tag_uuid and not active_tag_uuid in watch['tags']:
continue
if watch.get('last_error'):

View File

@@ -245,6 +245,9 @@ document.addEventListener('DOMContentLoaded', function() {
<a href="{{url_for('ui.mark_all_viewed', tag=active_tag_uuid) }}" class="pure-button button-tag " id="mark-all-viewed">Mark all viewed in '{{active_tag.title}}'</a>
</li>
{%- endif -%}
<li id="post-list-unread" class="{%- if has_unviewed -%}has-unviewed{%- endif -%}" style="display: none;" >
<a href="{{url_for('watchlist.index', unread=1, tag=request.args.get('tag')) }}" class="pure-button button-tag">Unread</a>
</li>
<li>
<a href="{{ url_for('ui.form_watch_checknow', tag=active_tag_uuid, with_errors=request.args.get('with_errors',0)) }}" class="pure-button button-tag" id="recheck-all">Recheck
all {% if active_tag_uuid %} in '{{active_tag.title}}'{%endif%}</a>

View File

@@ -251,8 +251,7 @@ class perform_site_check(difference_detection_processor):
update_obj["last_check_status"] = self.fetcher.get_last_status_code()
# 615 Extract text by regex
extract_text = watch.get('extract_text', [])
extract_text += self.datastore.get_tag_overrides_for_watch(uuid=watch.get('uuid'), attr='extract_text')
extract_text = list(dict.fromkeys(watch.get('extract_text', []) + self.datastore.get_tag_overrides_for_watch(uuid=watch.get('uuid'), attr='extract_text')))
if len(extract_text) > 0:
regex_matched_output = []
for s_re in extract_text:
@@ -311,8 +310,7 @@ class perform_site_check(difference_detection_processor):
############ Blocking rules, after checksum #################
blocked = False
trigger_text = watch.get('trigger_text', [])
trigger_text += self.datastore.get_tag_overrides_for_watch(uuid=watch.get('uuid'), attr='trigger_text')
trigger_text = list(dict.fromkeys(watch.get('trigger_text', []) + self.datastore.get_tag_overrides_for_watch(uuid=watch.get('uuid'), attr='trigger_text')))
if len(trigger_text):
# Assume blocked
blocked = True
@@ -326,8 +324,7 @@ class perform_site_check(difference_detection_processor):
if result:
blocked = False
text_should_not_be_present = watch.get('text_should_not_be_present', [])
text_should_not_be_present += self.datastore.get_tag_overrides_for_watch(uuid=watch.get('uuid'), attr='text_should_not_be_present')
text_should_not_be_present = list(dict.fromkeys(watch.get('text_should_not_be_present', []) + self.datastore.get_tag_overrides_for_watch(uuid=watch.get('uuid'), attr='text_should_not_be_present')))
if len(text_should_not_be_present):
# If anything matched, then we should block a change from happening
result = html_tools.strip_ignore_text(content=str(stripped_text_from_html),

View File

@@ -153,6 +153,7 @@ $(document).ready(function () {
// Tabs at bottom of list
$('#post-list-mark-views').toggleClass("has-unviewed", general_stats.has_unviewed);
$('#post-list-unread').toggleClass("has-unviewed", general_stats.has_unviewed);
$('#post-list-with-errors').toggleClass("has-error", general_stats.count_errors !== 0)
$('#post-list-with-errors a').text(`With errors (${ general_stats.count_errors })`);

View File

@@ -24,6 +24,9 @@ body.checking-now {
#post-list-mark-views.has-unviewed {
display: inline-block !important;
}
#post-list-unread.has-unviewed {
display: inline-block !important;
}
}

View File

@@ -1130,11 +1130,12 @@ ul {
}
#realtime-conn-error {
position: absolute;
position: fixed;
bottom: 0;
left: 30px;
left: 0;
background: var(--color-warning);
padding: 10px;
font-size: 0.8rem;
color: #fff;
opacity: 0.8;
}

File diff suppressed because one or more lines are too long

View File

@@ -236,7 +236,7 @@
<script src="{{url_for('static_content', group='js', filename='toggle-theme.js')}}" defer></script>
<div id="checking-now-fixed-tab" style="display: none;"><span class="spinner"></span><span>&nbsp;Checking now</span></div>
<div id="realtime-conn-error" style="display:none">Offline</div>
<div id="realtime-conn-error" style="display:none">Real-time updates offline</div>
</body>
</html>

View File

@@ -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(

View File

@@ -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

View File

@@ -6,7 +6,7 @@ info:
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?
@@ -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
@@ -328,6 +338,7 @@ components:
paths:
/watch:
get:
operationId: listWatches
tags: [Watch Management]
summary: List all watches
description: Return concise list of available web page change monitors (watches) and basic info
@@ -390,6 +401,7 @@ paths:
last_checked: 1640998800
last_changed: 1640995200
post:
operationId: createWatch
tags: [Watch Management]
summary: Create a new watch
description: Create a single web page change monitor (watch). Requires at least 'url' to be set.
@@ -453,7 +465,7 @@ paths:
/watch/{uuid}:
get:
operationId: getSingleWatch
operationId: getWatch
tags: [Watch Management]
summary: Get single watch
description: Retrieve web page change monitor (watch) information and set muted/paused status. Returns the FULL Watch JSON.
@@ -518,7 +530,7 @@ paths:
operationId: updateWatch
tags: [Watch Management]
summary: Update watch
description: Update an existing web page change monitor (watch) using JSON. Accepts the same structure as returned in [get single watch information](#operation/getSingleWatch).
description: Update an existing web page change monitor (watch) using JSON. Accepts the same structure as returned in [get single watch information](#operation/getWatch).
x-code-samples:
- lang: 'curl'
source: |
@@ -573,7 +585,8 @@ paths:
description: Server error
delete:
tags: [Watch Management]
operationId: deleteWatch
tags: [Watch Management]
summary: Delete watch
description: Delete a web page change monitor (watch) and all related history
x-code-samples:
@@ -608,6 +621,7 @@ paths:
/watch/{uuid}/history:
get:
operationId: getWatchHistory
tags: [Watch History]
summary: Get watch history
description: Get a list of all historical snapshots available for a web page change monitor (watch)
@@ -647,6 +661,7 @@ paths:
/watch/{uuid}/history/{timestamp}:
get:
operationId: getWatchSnapshot
tags: [Snapshots]
summary: Get single snapshot
description: Get single snapshot from web page change monitor (watch). Use 'latest' for the most recent snapshot.
@@ -699,6 +714,7 @@ paths:
/watch/{uuid}/favicon:
get:
operationId: getWatchFavicon
tags: [Favicon]
summary: Get watch favicon
description: Get the favicon for a web page change monitor (watch) as displayed in the watch overview list.
@@ -738,6 +754,7 @@ paths:
/tags:
get:
operationId: listTags
tags: [Group / Tag Management]
summary: List all tags
description: Return list of available tags/groups
@@ -774,8 +791,59 @@ paths:
notification_urls: ["discord://webhook_id/webhook_token"]
notification_muted: false
/tag:
post:
operationId: createTag
tags: [Group / Tag Management]
summary: Create tag
description: Create a single tag/group
x-code-samples:
- lang: 'curl'
source: |
curl -X POST "http://localhost:5000/api/v1/tag" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Important Sites"
}'
- lang: 'Python'
source: |
import requests
headers = {
'x-api-key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
}
data = {'title': 'Important Sites'}
response = requests.post('http://localhost:5000/api/v1/tag',
headers=headers, json=data)
print(response.json())
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Tag'
example:
title: "Important Sites"
responses:
'201':
description: Tag created successfully
content:
application/json:
schema:
type: object
properties:
uuid:
type: string
format: uuid
description: UUID of the created tag
'400':
description: Invalid or unsupported tag
/tag/{uuid}:
get:
operationId: getTag
tags: [Group / Tag Management]
summary: Get single tag
description: Retrieve tag information, set notification_muted status, recheck all web page change monitors (watches) in tag.
@@ -827,6 +895,7 @@ paths:
description: Tag not found
put:
operationId: updateTag
tags: [Group / Tag Management]
summary: Update tag
description: Update an existing tag using JSON
@@ -877,6 +946,7 @@ paths:
description: Server error
delete:
operationId: deleteTag
tags: [Group / Tag Management]
summary: Delete tag
description: Delete a tag/group and remove it from all web page change monitors (watches)
@@ -905,48 +975,10 @@ paths:
'200':
description: Tag deleted successfully
post:
tags: [Group / Tag Management]
summary: Create tag
description: Create a single tag/group
x-code-samples:
- lang: 'curl'
source: |
curl -X POST "http://localhost:5000/api/v1/tag/550e8400-e29b-41d4-a716-446655440000" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Important Sites"
}'
- lang: 'Python'
source: |
import requests
headers = {
'x-api-key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
}
tag_uuid = '550e8400-e29b-41d4-a716-446655440000'
data = {'title': 'Important Sites'}
response = requests.post(f'http://localhost:5000/api/v1/tag/{tag_uuid}',
headers=headers, json=data)
print(response.text)
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Tag'
example:
title: "Important Sites"
responses:
'200':
description: Tag created successfully
'500':
description: Server error
/notifications:
get:
operationId: getNotifications
tags: [Notifications]
summary: Get notification URLs
description: Return the notification URL list from the configuration
@@ -971,6 +1003,7 @@ paths:
$ref: '#/components/schemas/NotificationUrls'
post:
operationId: addNotifications
tags: [Notifications]
summary: Add notification URLs
description: Add one or more notification URLs to the configuration
@@ -1024,6 +1057,7 @@ paths:
description: Invalid input
put:
operationId: replaceNotifications
tags: [Notifications]
summary: Replace notification URLs
description: Replace all notification URLs with the provided list (can be empty)
@@ -1071,6 +1105,7 @@ paths:
description: Invalid input
delete:
operationId: deleteNotifications
tags: [Notifications]
summary: Delete notification URLs
description: Delete one or more notification URLs from the configuration
@@ -1115,6 +1150,7 @@ paths:
/search:
get:
operationId: searchWatches
tags: [Search]
summary: Search watches
description: Search web page change monitors (watches) by URL or title text
@@ -1169,6 +1205,7 @@ paths:
/import:
post:
operationId: importWatches
tags: [Import]
summary: Import watch URLs
description: Import a list of URLs to monitor. Accepts line-separated URLs in request body.
@@ -1239,6 +1276,7 @@ paths:
/systeminfo:
get:
operationId: getSystemInfo
tags: [System Information]
summary: Get system information
description: Return information about the current system state
@@ -1265,4 +1303,4 @@ paths:
watch_count: 42
tag_count: 5
uptime: "2 days, 3:45:12"
version: "0.50.10"
version: "0.50.10"

File diff suppressed because one or more lines are too long

View File

@@ -41,13 +41,16 @@ jsonpath-ng~=1.5.3
# Notification library
apprise==1.9.3
# - Needed for apprise/spush, and maybe others? hopefully doesnt trigger a rust compile.
# - Requires extra wheel for rPi, adds build time for arm/v8 which is not in piwheels
# Pinned to 43.0.1 for ARM compatibility (45.x may not have pre-built ARM wheels)
# Also pinned because dependabot wants specific versions
cryptography==44.0.1
# apprise mqtt https://github.com/dgtlmoon/changedetection.io/issues/315
# use any version other than 2.0.x due to https://github.com/eclipse/paho.mqtt.python/issues/814
paho-mqtt!=2.0.*
# Requires extra wheel for rPi
#cryptography~=42.0.8
# Used for CSS filtering
beautifulsoup4>=4.0.0
@@ -89,6 +92,9 @@ pytest-flask ~=1.2
# Anything 4.0 and up but not 5.0
jsonschema ~= 4.0
# OpenAPI validation support
openapi-core[flask] >= 0.19.0
loguru