mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2025-11-18 15:36:11 +00:00
Compare commits
2 Commits
rss-watch-
...
API-add-se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64ccf0b4af | ||
|
|
27928f374b |
51
changedetectionio/api/Search.py
Normal file
51
changedetectionio/api/Search.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from flask_restful import Resource, abort
|
||||||
|
from flask import request
|
||||||
|
from . import auth
|
||||||
|
|
||||||
|
class Search(Resource):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
# datastore is a black box dependency
|
||||||
|
self.datastore = kwargs['datastore']
|
||||||
|
|
||||||
|
@auth.check_token
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
@api {get} /api/v1/search Search for watches
|
||||||
|
@apiDescription Search watches by URL or title text
|
||||||
|
@apiExample {curl} Example usage:
|
||||||
|
curl "http://localhost:5000/api/v1/search?q=https://example.com/page1" -H"x-api-key:813031b16330fe25e3780cf0325daa45"
|
||||||
|
curl "http://localhost:5000/api/v1/search?q=https://example.com/page1?tag=Favourites" -H"x-api-key:813031b16330fe25e3780cf0325daa45"
|
||||||
|
curl "http://localhost:5000/api/v1/search?q=https://example.com?partial=true" -H"x-api-key:813031b16330fe25e3780cf0325daa45"
|
||||||
|
@apiName Search
|
||||||
|
@apiGroup Watch Management
|
||||||
|
@apiQuery {String} q Search query to match against watch URLs and titles
|
||||||
|
@apiQuery {String} [tag] Optional name of tag to limit results (name not UUID)
|
||||||
|
@apiQuery {String} [partial] Allow partial matching of URL query
|
||||||
|
@apiSuccess (200) {Object} JSON Object containing matched watches
|
||||||
|
"""
|
||||||
|
query = request.args.get('q', '').strip()
|
||||||
|
tag_limit = request.args.get('tag', '').strip()
|
||||||
|
from changedetectionio.strtobool import strtobool
|
||||||
|
partial = bool(strtobool(request.args.get('partial', '0'))) if 'partial' in request.args else False
|
||||||
|
|
||||||
|
# Require a search query
|
||||||
|
if not query:
|
||||||
|
abort(400, message="Search query 'q' parameter is required")
|
||||||
|
|
||||||
|
# Use the search function from the datastore
|
||||||
|
matching_uuids = self.datastore.search_watches_for_url(query=query, tag_limit=tag_limit, partial=partial)
|
||||||
|
|
||||||
|
# Build the response with watch details
|
||||||
|
results = {}
|
||||||
|
for uuid in matching_uuids:
|
||||||
|
watch = self.datastore.data['watching'].get(uuid)
|
||||||
|
results[uuid] = {
|
||||||
|
'last_changed': watch.last_changed,
|
||||||
|
'last_checked': watch['last_checked'],
|
||||||
|
'last_error': watch['last_error'],
|
||||||
|
'title': watch['title'],
|
||||||
|
'url': watch['url'],
|
||||||
|
'viewed': watch.viewed
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, 200
|
||||||
@@ -34,6 +34,7 @@ from loguru import logger
|
|||||||
from changedetectionio import __version__
|
from changedetectionio import __version__
|
||||||
from changedetectionio import queuedWatchMetaData
|
from changedetectionio import queuedWatchMetaData
|
||||||
from changedetectionio.api import Watch, WatchHistory, WatchSingleHistory, CreateWatch, Import, SystemInfo, Tag, Tags
|
from changedetectionio.api import Watch, WatchHistory, WatchSingleHistory, CreateWatch, Import, SystemInfo, Tag, Tags
|
||||||
|
from changedetectionio.api.Search import Search
|
||||||
from .time_handler import is_within_schedule
|
from .time_handler import is_within_schedule
|
||||||
|
|
||||||
datastore = None
|
datastore = None
|
||||||
@@ -275,6 +276,9 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
|
|
||||||
watch_api.add_resource(Tag, '/api/v1/tag', '/api/v1/tag/<string:uuid>',
|
watch_api.add_resource(Tag, '/api/v1/tag', '/api/v1/tag/<string:uuid>',
|
||||||
resource_class_kwargs={'datastore': datastore})
|
resource_class_kwargs={'datastore': datastore})
|
||||||
|
|
||||||
|
watch_api.add_resource(Search, '/api/v1/search',
|
||||||
|
resource_class_kwargs={'datastore': datastore})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -631,6 +631,41 @@ class ChangeDetectionStore:
|
|||||||
if watch.get('processor') == processor_name:
|
if watch.get('processor') == processor_name:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def search_watches_for_url(self, query, tag_limit=None, partial=False):
|
||||||
|
"""Search watches by URL, title, or error messages
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (str): Search term to match against watch URLs, titles, and error messages
|
||||||
|
tag_limit (str, optional): Optional tag name to limit search results
|
||||||
|
partial: (bool, optional): sub-string matching
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of UUIDs of watches that match the search criteria
|
||||||
|
"""
|
||||||
|
matching_uuids = []
|
||||||
|
query = query.lower().strip()
|
||||||
|
tag = self.tag_exists_by_name(tag_limit) if tag_limit else False
|
||||||
|
|
||||||
|
for uuid, watch in self.data['watching'].items():
|
||||||
|
# Filter by tag if requested
|
||||||
|
if tag_limit:
|
||||||
|
if not tag.get('uuid') in watch.get('tags', []):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Search in URL, title, or error messages
|
||||||
|
if partial:
|
||||||
|
if ((watch.get('title') and query in watch.get('title').lower()) or
|
||||||
|
query in watch.get('url', '').lower() or
|
||||||
|
(watch.get('last_error') and query in watch.get('last_error').lower())):
|
||||||
|
matching_uuids.append(uuid)
|
||||||
|
else:
|
||||||
|
if ((watch.get('title') and query == watch.get('title').lower()) or
|
||||||
|
query == watch.get('url', '').lower() or
|
||||||
|
(watch.get('last_error') and query == watch.get('last_error').lower())):
|
||||||
|
matching_uuids.append(uuid)
|
||||||
|
|
||||||
|
return matching_uuids
|
||||||
|
|
||||||
def get_unique_notification_tokens_available(self):
|
def get_unique_notification_tokens_available(self):
|
||||||
# Ask each type of watch if they have any extra notification token to add to the validation
|
# Ask each type of watch if they have any extra notification token to add to the validation
|
||||||
|
|||||||
101
changedetectionio/tests/test_api_search.py
Normal file
101
changedetectionio/tests/test_api_search.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
from copy import copy
|
||||||
|
|
||||||
|
from flask import url_for
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from .util import live_server_setup, wait_for_all_checks
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_search(client, live_server):
|
||||||
|
live_server_setup(live_server)
|
||||||
|
api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token')
|
||||||
|
|
||||||
|
watch_data = {}
|
||||||
|
# Add some test watches
|
||||||
|
urls = [
|
||||||
|
'https://example.com/page1',
|
||||||
|
'https://example.org/testing',
|
||||||
|
'https://test-site.com/example'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Import the test URLs
|
||||||
|
res = client.post(
|
||||||
|
url_for("imports.import_page"),
|
||||||
|
data={"urls": "\r\n".join(urls)},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"3 Imported" in res.data
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
|
# Get a listing, it will be the first one
|
||||||
|
watches_response = client.get(
|
||||||
|
url_for("createwatch"),
|
||||||
|
headers={'x-api-key': api_key}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Add a title to one watch for title search testing
|
||||||
|
for uuid, watch in watches_response.json.items():
|
||||||
|
|
||||||
|
watch_data = client.get(url_for("watch", uuid=uuid),
|
||||||
|
follow_redirects=True,
|
||||||
|
headers={'x-api-key': api_key}
|
||||||
|
)
|
||||||
|
|
||||||
|
if urls[0] == watch_data.json['url']:
|
||||||
|
# HTTP PUT ( UPDATE an existing watch )
|
||||||
|
client.put(
|
||||||
|
url_for("watch", uuid=uuid),
|
||||||
|
headers={'x-api-key': api_key, 'content-type': 'application/json'},
|
||||||
|
data=json.dumps({'title': 'Example Title Test'}),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test search by URL
|
||||||
|
res = client.get(url_for("search")+"?q=https://example.com/page1", headers={'x-api-key': api_key, 'content-type': 'application/json'})
|
||||||
|
assert len(res.json) == 1
|
||||||
|
assert list(res.json.values())[0]['url'] == urls[0]
|
||||||
|
|
||||||
|
# Test search by URL - partial should NOT match without ?partial=true flag
|
||||||
|
res = client.get(url_for("search")+"?q=https://example", headers={'x-api-key': api_key, 'content-type': 'application/json'})
|
||||||
|
assert len(res.json) == 0
|
||||||
|
|
||||||
|
|
||||||
|
# Test search by title
|
||||||
|
res = client.get(url_for("search")+"?q=Example Title Test", headers={'x-api-key': api_key, 'content-type': 'application/json'})
|
||||||
|
assert len(res.json) == 1
|
||||||
|
assert list(res.json.values())[0]['url'] == urls[0]
|
||||||
|
assert list(res.json.values())[0]['title'] == 'Example Title Test'
|
||||||
|
|
||||||
|
# Test search that should return multiple results (partial = true)
|
||||||
|
res = client.get(url_for("search")+"?q=https://example&partial=true", headers={'x-api-key': api_key, 'content-type': 'application/json'})
|
||||||
|
assert len(res.json) == 2
|
||||||
|
|
||||||
|
# Test empty search
|
||||||
|
res = client.get(url_for("search")+"?q=", headers={'x-api-key': api_key, 'content-type': 'application/json'})
|
||||||
|
assert res.status_code == 400
|
||||||
|
|
||||||
|
# Add a tag to test search with tag filter
|
||||||
|
tag_name = 'test-tag'
|
||||||
|
res = client.post(
|
||||||
|
url_for("tag"),
|
||||||
|
data=json.dumps({"title": tag_name}),
|
||||||
|
headers={'content-type': 'application/json', 'x-api-key': api_key}
|
||||||
|
)
|
||||||
|
assert res.status_code == 201
|
||||||
|
tag_uuid = res.json['uuid']
|
||||||
|
|
||||||
|
# Add the tag to one watch
|
||||||
|
for uuid, watch in watches_response.json.items():
|
||||||
|
if urls[2] == watch['url']:
|
||||||
|
client.put(
|
||||||
|
url_for("watch", uuid=uuid),
|
||||||
|
headers={'x-api-key': api_key, 'content-type': 'application/json'},
|
||||||
|
data=json.dumps({'tags': [tag_uuid]}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Test search with tag filter and q
|
||||||
|
res = client.get(url_for("search") + f"?q={urls[2]}&tag={tag_name}", headers={'x-api-key': api_key, 'content-type': 'application/json'})
|
||||||
|
assert len(res.json) == 1
|
||||||
|
assert list(res.json.values())[0]['url'] == urls[2]
|
||||||
|
|
||||||
Reference in New Issue
Block a user