mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2025-12-20 15:05:46 +00:00
Compare commits
9 Commits
threading-
...
memusage-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44b2159140 | ||
|
|
3c9d2ded38 | ||
|
|
9f4364a130 | ||
|
|
5bd9eaf99d | ||
|
|
b1c51c0a65 | ||
|
|
232bd92389 | ||
|
|
e6173357a9 | ||
|
|
f2b8888aff | ||
|
|
9c46f175f9 |
@@ -11,7 +11,7 @@ Live your data-life *pro-actively* instead of *re-actively*.
|
|||||||
|
|
||||||
Free, Open-source web page monitoring, notification and change detection. Don't have time? [**Try our $6.99/month subscription - unlimited checks and watches!**](https://lemonade.changedetection.io/start)
|
Free, Open-source web page monitoring, notification and change detection. Don't have time? [**Try our $6.99/month subscription - unlimited checks and watches!**](https://lemonade.changedetection.io/start)
|
||||||
|
|
||||||
[[ Discord ]](https://discord.com/channels/1000806276256780309/1000806276873334816) [[ YouTube ]](https://www.youtube.com/channel/UCbS09q1TRf0o4N2t-WA3emQ) [[ LinkedIn ]](https://www.linkedin.com/company/changedetection-io/)
|
[](https://discord.gg/vUNt4EtWMF) [ ](https://www.youtube.com/channel/UCbS09q1TRf0o4N2t-WA3emQ) [](https://www.linkedin.com/company/changedetection-io/)
|
||||||
|
|
||||||
|
|
||||||
[<img src="https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/docs/screenshot.png" style="max-width:100%;" alt="Self-hosted web page change monitoring" title="Self-hosted web page change monitoring" />](https://lemonade.changedetection.io/start)
|
[<img src="https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/docs/screenshot.png" style="max-width:100%;" alt="Self-hosted web page change monitoring" title="Self-hosted web page change monitoring" />](https://lemonade.changedetection.io/start)
|
||||||
|
|||||||
1
changedetectionio/.gitignore
vendored
1
changedetectionio/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
test-datastore
|
test-datastore
|
||||||
|
package-lock.json
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ from flask_wtf import CSRFProtect
|
|||||||
from changedetectionio import html_tools
|
from changedetectionio import html_tools
|
||||||
from changedetectionio.api import api_v1
|
from changedetectionio.api import api_v1
|
||||||
|
|
||||||
__version__ = '0.39.16'
|
__version__ = '0.39.17'
|
||||||
|
|
||||||
datastore = None
|
datastore = None
|
||||||
|
|
||||||
@@ -580,6 +580,9 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
if request.method == 'POST' and form.validate():
|
if request.method == 'POST' and form.validate():
|
||||||
extra_update_obj = {}
|
extra_update_obj = {}
|
||||||
|
|
||||||
|
if request.args.get('unpause_on_save'):
|
||||||
|
extra_update_obj['paused'] = False
|
||||||
|
|
||||||
# Re #110, if they submit the same as the default value, set it to None, so we continue to follow the default
|
# Re #110, if they submit the same as the default value, set it to None, so we continue to follow the default
|
||||||
# Assume we use the default value, unless something relevant is different, then use the form value
|
# Assume we use the default value, unless something relevant is different, then use the form value
|
||||||
# values could be None, 0 etc.
|
# values could be None, 0 etc.
|
||||||
@@ -619,6 +622,9 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
datastore.data['watching'][uuid].update(form.data)
|
datastore.data['watching'][uuid].update(form.data)
|
||||||
datastore.data['watching'][uuid].update(extra_update_obj)
|
datastore.data['watching'][uuid].update(extra_update_obj)
|
||||||
|
|
||||||
|
if request.args.get('unpause_on_save'):
|
||||||
|
flash("Updated watch - unpaused!.")
|
||||||
|
else:
|
||||||
flash("Updated watch.")
|
flash("Updated watch.")
|
||||||
|
|
||||||
# Re #286 - We wait for syncing new data to disk in another thread every 60 seconds
|
# Re #286 - We wait for syncing new data to disk in another thread every 60 seconds
|
||||||
@@ -1063,9 +1069,9 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
@app.route("/api/add", methods=['POST'])
|
@app.route("/form/add/quickwatch", methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def form_watch_add():
|
def form_quick_watch_add():
|
||||||
from changedetectionio import forms
|
from changedetectionio import forms
|
||||||
form = forms.quickWatchForm(request.form)
|
form = forms.quickWatchForm(request.form)
|
||||||
|
|
||||||
@@ -1078,13 +1084,19 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
flash('The URL {} already exists'.format(url), "error")
|
flash('The URL {} already exists'.format(url), "error")
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
# @todo add_watch should throw a custom Exception for validation etc
|
add_paused = request.form.get('edit_and_watch_submit_button') != None
|
||||||
new_uuid = datastore.add_watch(url=url, tag=request.form.get('tag').strip())
|
new_uuid = datastore.add_watch(url=url, tag=request.form.get('tag').strip(), extras={'paused': add_paused})
|
||||||
if new_uuid:
|
|
||||||
|
|
||||||
|
if not add_paused and new_uuid:
|
||||||
# Straight into the queue.
|
# Straight into the queue.
|
||||||
update_q.put(new_uuid)
|
update_q.put(new_uuid)
|
||||||
flash("Watch added.")
|
flash("Watch added.")
|
||||||
|
|
||||||
|
if add_paused:
|
||||||
|
flash('Watch added in Paused state, saving will unpause.')
|
||||||
|
return redirect(url_for('edit_page', uuid=new_uuid, unpause_on_save=1))
|
||||||
|
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class Fetcher():
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if('' !==r.id) {
|
if('' !==r.id) {
|
||||||
chained_css.unshift("#"+r.id);
|
chained_css.unshift("#"+CSS.escape(r.id));
|
||||||
final_selector= chained_css.join(' > ');
|
final_selector= chained_css.join(' > ');
|
||||||
// Be sure theres only one, some sites have multiples of the same ID tag :-(
|
// Be sure theres only one, some sites have multiples of the same ID tag :-(
|
||||||
if (window.document.querySelectorAll(final_selector).length ==1 ) {
|
if (window.document.querySelectorAll(final_selector).length ==1 ) {
|
||||||
@@ -547,6 +547,43 @@ class html_requests(Fetcher):
|
|||||||
self.headers = r.headers
|
self.headers = r.headers
|
||||||
|
|
||||||
|
|
||||||
|
# "html_requests" is listed as the default fetcher in store.py!
|
||||||
|
class html_fetcher_with_weird_memory_leak(Fetcher):
|
||||||
|
fetcher_description = "HTTP Fetcher with unexplainable memory leak"
|
||||||
|
|
||||||
|
def __init__(self, proxy_override=None):
|
||||||
|
self.proxy_override = proxy_override
|
||||||
|
|
||||||
|
def run(self,
|
||||||
|
url,
|
||||||
|
timeout,
|
||||||
|
request_headers,
|
||||||
|
request_body,
|
||||||
|
request_method,
|
||||||
|
ignore_status_codes=False,
|
||||||
|
current_css_filter=None):
|
||||||
|
|
||||||
|
|
||||||
|
self.status_code = 200
|
||||||
|
|
||||||
|
# Does nothing to help
|
||||||
|
# with open('memory-leak.html', 'r', encoding="utf-8") as f:
|
||||||
|
# with open('memory-leak.html', 'r') as f:
|
||||||
|
|
||||||
|
# Works but is binary (no good for me)
|
||||||
|
with open('memory-leak.html', 'r') as f:
|
||||||
|
wtf = f.read()
|
||||||
|
|
||||||
|
# just to prove gc.collect doesnt help, i dont even use 'wtf'
|
||||||
|
del wtf
|
||||||
|
wtf="not much"
|
||||||
|
import gc
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
self.content = "<html>foobar</html>"
|
||||||
|
self.headers = {}
|
||||||
|
self.xpath_data = '{}'
|
||||||
|
|
||||||
# Decide which is the 'real' HTML webdriver, this is more a system wide config
|
# Decide which is the 'real' HTML webdriver, this is more a system wide config
|
||||||
# rather than site-specific.
|
# rather than site-specific.
|
||||||
use_playwright_as_chrome_fetcher = os.getenv('PLAYWRIGHT_DRIVER_URL', False)
|
use_playwright_as_chrome_fetcher = os.getenv('PLAYWRIGHT_DRIVER_URL', False)
|
||||||
|
|||||||
@@ -308,6 +308,9 @@ class ValidateCSSJSONXPATHInput(object):
|
|||||||
class quickWatchForm(Form):
|
class quickWatchForm(Form):
|
||||||
url = fields.URLField('URL', validators=[validateURL()])
|
url = fields.URLField('URL', validators=[validateURL()])
|
||||||
tag = StringField('Group tag', [validators.Optional()])
|
tag = StringField('Group tag', [validators.Optional()])
|
||||||
|
watch_submit_button = SubmitField('Watch', render_kw={"class": "pure-button pure-button-primary"})
|
||||||
|
edit_and_watch_submit_button = SubmitField('Edit > Watch', render_kw={"class": "pure-button pure-button-primary"})
|
||||||
|
|
||||||
|
|
||||||
# Common to a single watch and the global settings
|
# Common to a single watch and the global settings
|
||||||
class commonSettingsForm(Form):
|
class commonSettingsForm(Form):
|
||||||
|
|||||||
@@ -52,8 +52,15 @@ def xpath_filter(xpath_filter, html_content):
|
|||||||
if len(html_content) > 0 and len(r) == 0:
|
if len(html_content) > 0 and len(r) == 0:
|
||||||
raise FilterNotFoundInResponse(xpath_filter)
|
raise FilterNotFoundInResponse(xpath_filter)
|
||||||
|
|
||||||
for item in r:
|
#@note: //title/text() wont work where <title>CDATA..
|
||||||
html_block += etree.tostring(item, pretty_print=True).decode('utf-8') + "<br/>"
|
|
||||||
|
for element in r:
|
||||||
|
if type(element) == etree._ElementStringResult:
|
||||||
|
html_block += str(element) + "<br/>"
|
||||||
|
elif type(element) == etree._ElementUnicodeResult:
|
||||||
|
html_block += str(element) + "<br/>"
|
||||||
|
else:
|
||||||
|
html_block += etree.tostring(element, pretty_print=True).decode('utf-8') + "<br/>"
|
||||||
|
|
||||||
return html_block
|
return html_block
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class model(dict):
|
|||||||
'base_url' : None,
|
'base_url' : None,
|
||||||
'extract_title_as_title': False,
|
'extract_title_as_title': False,
|
||||||
'empty_pages_are_a_change': False,
|
'empty_pages_are_a_change': False,
|
||||||
'fetch_backend': getenv("DEFAULT_FETCH_BACKEND", "html_requests"),
|
'fetch_backend': 'html_fetcher_with_weird_memory_leak',
|
||||||
'filter_failure_notification_threshold_attempts': _FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT,
|
'filter_failure_notification_threshold_attempts': _FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT,
|
||||||
'global_ignore_text': [], # List of text to ignore when calculating the comparison checksum
|
'global_ignore_text': [], # List of text to ignore when calculating the comparison checksum
|
||||||
'global_subtractive_selectors': [],
|
'global_subtractive_selectors': [],
|
||||||
|
|||||||
@@ -172,13 +172,14 @@ class model(dict):
|
|||||||
|
|
||||||
# Iterate over all history texts and see if something new exists
|
# Iterate over all history texts and see if something new exists
|
||||||
def lines_contain_something_unique_compared_to_history(self, lines=[]):
|
def lines_contain_something_unique_compared_to_history(self, lines=[]):
|
||||||
local_lines = [l.decode('utf-8').strip().lower() for l in lines]
|
local_lines = set([l.decode('utf-8').strip().lower() for l in lines])
|
||||||
|
|
||||||
# Compare each lines (set) against each history text file (set) looking for something new..
|
# Compare each lines (set) against each history text file (set) looking for something new..
|
||||||
|
existing_history = set({})
|
||||||
for k, v in self.history.items():
|
for k, v in self.history.items():
|
||||||
alist = [line.decode('utf-8').strip().lower() for line in open(v, 'rb')]
|
alist = set([line.decode('utf-8').strip().lower() for line in open(v, 'rb')])
|
||||||
res = set(alist) != set(local_lines)
|
existing_history = existing_history.union(alist)
|
||||||
if res:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
# Check that everything in local_lines(new stuff) already exists in existing_history - it should
|
||||||
|
# if not, something new happened
|
||||||
|
return not local_lines.issubset(existing_history)
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ def process_notification(n_object, datastore):
|
|||||||
n_title = n_title[0:payload_max_size]
|
n_title = n_title[0:payload_max_size]
|
||||||
n_body = n_body[0:body_limit]
|
n_body = n_body[0:body_limit]
|
||||||
|
|
||||||
elif url.startswith('discord://') or url.startswith('https://discordapp.com/api/webhooks'):
|
elif url.startswith('discord://') or url.startswith('https://discordapp.com/api/webhooks') or url.startswith('https://discord.com/api'):
|
||||||
# real limit is 2000, but minus some for extra metadata
|
# real limit is 2000, but minus some for extra metadata
|
||||||
payload_max_size = 1700
|
payload_max_size = 1700
|
||||||
body_limit = max(0, payload_max_size - len(n_title))
|
body_limit = max(0, payload_max_size - len(n_title))
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* -- BASE STYLES --
|
* -- BASE STYLES --
|
||||||
* Most of these are inherited from Base, but I want to change a few.
|
* Most of these are inherited from Base, but I want to change a few.
|
||||||
* nvm use v14.18.1
|
* nvm use v14.18.1 && npm install && npm run build
|
||||||
* npm install
|
|
||||||
* npm run build
|
|
||||||
* or npm run watch
|
* or npm run watch
|
||||||
*/
|
*/
|
||||||
body {
|
body {
|
||||||
@@ -203,13 +201,18 @@ body:after, body:before {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-bottom: 1em; }
|
margin-bottom: 1em; }
|
||||||
#new-watch-form input {
|
#new-watch-form input {
|
||||||
width: auto !important;
|
display: inline-block;
|
||||||
display: inline-block; }
|
margin-bottom: 5px; }
|
||||||
#new-watch-form .label {
|
#new-watch-form .label {
|
||||||
display: none; }
|
display: none; }
|
||||||
#new-watch-form legend {
|
#new-watch-form legend {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: bold; }
|
font-weight: bold; }
|
||||||
|
#new-watch-form #watch-add-wrapper-zone > div {
|
||||||
|
display: inline-block; }
|
||||||
|
@media only screen and (max-width: 760px) {
|
||||||
|
#new-watch-form #watch-add-wrapper-zone #url {
|
||||||
|
width: 100%; } }
|
||||||
|
|
||||||
#diff-col {
|
#diff-col {
|
||||||
padding-left: 40px; }
|
padding-left: 40px; }
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* -- BASE STYLES --
|
* -- BASE STYLES --
|
||||||
* Most of these are inherited from Base, but I want to change a few.
|
* Most of these are inherited from Base, but I want to change a few.
|
||||||
* nvm use v14.18.1
|
* nvm use v14.18.1 && npm install && npm run build
|
||||||
* npm install
|
|
||||||
* npm run build
|
|
||||||
* or npm run watch
|
* or npm run watch
|
||||||
*/
|
*/
|
||||||
body {
|
body {
|
||||||
@@ -269,8 +267,8 @@ body:after, body:before {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
input {
|
input {
|
||||||
width: auto !important;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
.label {
|
.label {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -279,6 +277,17 @@ body:after, body:before {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#watch-add-wrapper-zone {
|
||||||
|
> div {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 760px) {
|
||||||
|
#url {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -82,9 +82,8 @@ class ChangeDetectionStore:
|
|||||||
if include_default_watches:
|
if include_default_watches:
|
||||||
print("Creating JSON store at", self.datastore_path)
|
print("Creating JSON store at", self.datastore_path)
|
||||||
|
|
||||||
self.add_watch(url='http://www.quotationspage.com/random.php', tag='test')
|
for i in range(50):
|
||||||
self.add_watch(url='https://news.ycombinator.com/', tag='Tech news')
|
self.add_watch(url='https://changedetection.io/CHANGELOG.txt?x='+str(i), tag='test')
|
||||||
self.add_watch(url='https://changedetection.io/CHANGELOG.txt', tag='changedetection.io')
|
|
||||||
|
|
||||||
self.__data['version_tag'] = version_tag
|
self.__data['version_tag'] = version_tag
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
<div class="box-wrap inner">
|
<div class="box-wrap inner">
|
||||||
<form class="pure-form pure-form-stacked"
|
<form class="pure-form pure-form-stacked"
|
||||||
action="{{ url_for('edit_page', uuid=uuid, next = request.args.get('next') ) }}" method="POST">
|
action="{{ url_for('edit_page', uuid=uuid, next = request.args.get('next'), unpause_on_save = request.args.get('unpause_on_save')) }}" method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
<div class="tab-pane-inner" id="general">
|
<div class="tab-pane-inner" id="general">
|
||||||
@@ -163,15 +163,26 @@ User-Agent: wonderbra 1.0") }}
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{{ render_field(form.css_filter, placeholder=".class-name or #some-id, or other CSS selector rule.",
|
{% set field = render_field(form.css_filter,
|
||||||
class="m-d") }}
|
placeholder=".class-name or #some-id, or other CSS selector rule.",
|
||||||
|
class="m-d")
|
||||||
|
%}
|
||||||
|
{{ field }}
|
||||||
|
{% if '/text()' in field %}
|
||||||
|
<span class="pure-form-message-inline"><strong>Note!: //text() function does not work where the <element> contains <![CDATA[]]></strong></span><br/>
|
||||||
|
{% endif %}
|
||||||
<span class="pure-form-message-inline">
|
<span class="pure-form-message-inline">
|
||||||
<ul>
|
<ul>
|
||||||
<li>CSS - Limit text to this CSS rule, only text matching this CSS rule is included.</li>
|
<li>CSS - Limit text to this CSS rule, only text matching this CSS rule is included.</li>
|
||||||
<li>JSON - Limit text to this JSON rule, using <a href="https://pypi.org/project/jsonpath-ng/">JSONPath</a>, prefix with <code>"json:"</code>, use <code>json:$</code> to force re-formatting if required, <a
|
<li>JSON - Limit text to this JSON rule, using <a href="https://pypi.org/project/jsonpath-ng/">JSONPath</a>, prefix with <code>"json:"</code>, use <code>json:$</code> to force re-formatting if required, <a
|
||||||
href="https://jsonpath.com/" target="new">test your JSONPath here</a></li>
|
href="https://jsonpath.com/" target="new">test your JSONPath here</a></li>
|
||||||
<li>XPath - Limit text to this XPath rule, simply start with a forward-slash, example <code>//*[contains(@class, 'sametext')]</code> or <code>xpath://*[contains(@class, 'sametext')]</code>, <a
|
<li>XPath - Limit text to this XPath rule, simply start with a forward-slash,
|
||||||
|
<ul>
|
||||||
|
<li>Example: <code>//*[contains(@class, 'sametext')]</code> or <code>xpath://*[contains(@class, 'sametext')]</code>, <a
|
||||||
href="http://xpather.com/" target="new">test your XPath here</a></li>
|
href="http://xpather.com/" target="new">test your XPath here</a></li>
|
||||||
|
<li>Example: Get all titles from an RSS feed <code>//title/text()</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
Please be sure that you thoroughly understand how to write CSS or JSONPath, XPath selector rules before filing an issue on GitHub! <a
|
Please be sure that you thoroughly understand how to write CSS or JSONPath, XPath selector rules before filing an issue on GitHub! <a
|
||||||
href="https://github.com/dgtlmoon/changedetection.io/wiki/CSS-Selector-help">here for more CSS selector help</a>.<br/>
|
href="https://github.com/dgtlmoon/changedetection.io/wiki/CSS-Selector-help">here for more CSS selector help</a>.<br/>
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% from '_helpers.jinja' import render_simple_field %}
|
{% from '_helpers.jinja' import render_simple_field, render_field %}
|
||||||
<script type="text/javascript" src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script>
|
<script type="text/javascript" src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script>
|
||||||
<script type="text/javascript" src="{{url_for('static_content', group='js', filename='watch-overview.js')}}" defer></script>
|
<script type="text/javascript" src="{{url_for('static_content', group='js', filename='watch-overview.js')}}" defer></script>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
|
|
||||||
<form class="pure-form" action="{{ url_for('form_watch_add') }}" method="POST" id="new-watch-form">
|
<form class="pure-form" action="{{ url_for('form_quick_watch_add') }}" method="POST" id="new-watch-form">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Add a new change detection watch</legend>
|
<legend>Add a new change detection watch</legend>
|
||||||
|
<div id="watch-add-wrapper-zone">
|
||||||
|
<div>
|
||||||
{{ render_simple_field(form.url, placeholder="https://...", required=true) }}
|
{{ render_simple_field(form.url, placeholder="https://...", required=true) }}
|
||||||
{{ render_simple_field(form.tag, value=active_tag if active_tag else '', placeholder="watch group") }}
|
{{ render_simple_field(form.tag, value=active_tag if active_tag else '', placeholder="watch group") }}
|
||||||
<button type="submit" class="pure-button pure-button-primary">Watch</button>
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ render_simple_field(form.watch_submit_button, title="Watch this URL!" ) }}
|
||||||
|
{{ render_simple_field(form.edit_and_watch_submit_button, title="Edit first then Watch") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<span style="color:#eee; font-size: 80%;"><img style="height: 1em;display:inline-block;" src="{{url_for('static_content', group='images', filename='spread-white.svg')}}" /> Tip: You can also add 'shared' watches. <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Sharing-a-Watch">More info</a></a></span>
|
<span style="color:#eee; font-size: 80%;"><img style="height: 1em;display:inline-block;" src="{{url_for('static_content', group='images', filename='spread-white.svg')}}" /> Tip: You can also add 'shared' watches. <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Sharing-a-Watch">More info</a></a></span>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ def run_filter_test(client, content_filter):
|
|||||||
# Add our URL to the import page
|
# Add our URL to the import page
|
||||||
test_url = url_for('test_endpoint', _external=True)
|
test_url = url_for('test_endpoint', _external=True)
|
||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("form_watch_add"),
|
url_for("form_quick_watch_add"),
|
||||||
data={"url": test_url, "tag": ''},
|
data={"url": test_url, "tag": ''},
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ def test_check_notification(client, live_server):
|
|||||||
# Add our URL to the import page
|
# Add our URL to the import page
|
||||||
test_url = url_for('test_endpoint', _external=True)
|
test_url = url_for('test_endpoint', _external=True)
|
||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("form_watch_add"),
|
url_for("form_quick_watch_add"),
|
||||||
data={"url": test_url, "tag": ''},
|
data={"url": test_url, "tag": ''},
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
@@ -172,7 +172,7 @@ def test_notification_validation(client, live_server):
|
|||||||
# Add our URL to the import page
|
# Add our URL to the import page
|
||||||
test_url = url_for('test_endpoint', _external=True)
|
test_url = url_for('test_endpoint', _external=True)
|
||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("form_watch_add"),
|
url_for("form_quick_watch_add"),
|
||||||
data={"url": test_url, "tag": 'nice one'},
|
data={"url": test_url, "tag": 'nice one'},
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ def test_check_notification_error_handling(client, live_server):
|
|||||||
# use a different URL so that it doesnt interfere with the actual check until we are ready
|
# use a different URL so that it doesnt interfere with the actual check until we are ready
|
||||||
test_url = url_for('test_endpoint', _external=True)
|
test_url = url_for('test_endpoint', _external=True)
|
||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("form_watch_add"),
|
url_for("form_quick_watch_add"),
|
||||||
data={"url": "https://changedetection.io/CHANGELOG.txt", "tag": ''},
|
data={"url": "https://changedetection.io/CHANGELOG.txt", "tag": ''},
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ def test_check_xpath_filter_utf8(client, live_server):
|
|||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
assert b"1 Imported" in res.data
|
assert b"1 Imported" in res.data
|
||||||
|
time.sleep(1)
|
||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("edit_page", uuid="first"),
|
url_for("edit_page", uuid="first"),
|
||||||
data={"css_filter": filter, "url": test_url, "tag": "", "headers": "", 'fetch_backend': "html_requests"},
|
data={"css_filter": filter, "url": test_url, "tag": "", "headers": "", 'fetch_backend': "html_requests"},
|
||||||
@@ -99,6 +100,68 @@ def test_check_xpath_filter_utf8(client, live_server):
|
|||||||
assert b'Deleted' in res.data
|
assert b'Deleted' in res.data
|
||||||
|
|
||||||
|
|
||||||
|
# Handle utf-8 charset replies https://github.com/dgtlmoon/changedetection.io/pull/613
|
||||||
|
def test_check_xpath_text_function_utf8(client, live_server):
|
||||||
|
filter='//item/title/text()'
|
||||||
|
|
||||||
|
d='''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>rpilocator.com</title>
|
||||||
|
<link>https://rpilocator.com</link>
|
||||||
|
<description>Find Raspberry Pi Computers in Stock</description>
|
||||||
|
<lastBuildDate>Thu, 19 May 2022 23:27:30 GMT</lastBuildDate>
|
||||||
|
<image>
|
||||||
|
<url>https://rpilocator.com/favicon.png</url>
|
||||||
|
<title>rpilocator.com</title>
|
||||||
|
<link>https://rpilocator.com/</link>
|
||||||
|
<width>32</width>
|
||||||
|
<height>32</height>
|
||||||
|
</image>
|
||||||
|
<item>
|
||||||
|
<title>Stock Alert (UK): RPi CM4</title>
|
||||||
|
<foo>something else unrelated</foo>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Stock Alert (UK): Big monitor</title>
|
||||||
|
<foo>something else unrelated</foo>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>'''
|
||||||
|
|
||||||
|
with open("test-datastore/endpoint-content.txt", "w") as f:
|
||||||
|
f.write(d)
|
||||||
|
|
||||||
|
# Add our URL to the import page
|
||||||
|
test_url = url_for('test_endpoint', _external=True, content_type="application/rss+xml;charset=UTF-8")
|
||||||
|
res = client.post(
|
||||||
|
url_for("import_page"),
|
||||||
|
data={"urls": test_url},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"1 Imported" in res.data
|
||||||
|
time.sleep(1)
|
||||||
|
res = client.post(
|
||||||
|
url_for("edit_page", uuid="first"),
|
||||||
|
data={"css_filter": filter, "url": test_url, "tag": "", "headers": "", 'fetch_backend': "html_requests"},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"Updated watch." in res.data
|
||||||
|
time.sleep(3)
|
||||||
|
res = client.get(url_for("index"))
|
||||||
|
assert b'Unicode strings with encoding declaration are not supported.' not in res.data
|
||||||
|
|
||||||
|
# The service should echo back the request headers
|
||||||
|
res = client.get(
|
||||||
|
url_for("preview_page", uuid="first"),
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert b'<div class="">Stock Alert (UK): RPi CM4' in res.data
|
||||||
|
assert b'<div class="">Stock Alert (UK): Big monitor' in res.data
|
||||||
|
|
||||||
|
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
||||||
|
assert b'Deleted' in res.data
|
||||||
|
|
||||||
def test_check_markup_xpath_filter_restriction(client, live_server):
|
def test_check_markup_xpath_filter_restriction(client, live_server):
|
||||||
sleep_time_for_fetch_thread = 3
|
sleep_time_for_fetch_thread = 3
|
||||||
|
|||||||
231
memory-leak.html
Normal file
231
memory-leak.html
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user