mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-03-23 20:28:04 +00:00
Compare commits
1 Commits
master
...
JSONP-supp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
141aea07b8 |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
||||||
# Semver means never use .01, or 00. Should be .1.
|
# Semver means never use .01, or 00. Should be .1.
|
||||||
__version__ = '0.54.6'
|
__version__ = '0.54.5'
|
||||||
|
|
||||||
from changedetectionio.strtobool import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
|
|||||||
@@ -40,13 +40,12 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
|||||||
contents = ''
|
contents = ''
|
||||||
now = time.time()
|
now = time.time()
|
||||||
try:
|
try:
|
||||||
import asyncio
|
|
||||||
processor_module = importlib.import_module("changedetectionio.processors.text_json_diff.processor")
|
processor_module = importlib.import_module("changedetectionio.processors.text_json_diff.processor")
|
||||||
update_handler = processor_module.perform_site_check(datastore=datastore,
|
update_handler = processor_module.perform_site_check(datastore=datastore,
|
||||||
watch_uuid=uuid
|
watch_uuid=uuid
|
||||||
)
|
)
|
||||||
|
|
||||||
asyncio.run(update_handler.call_browser(preferred_proxy_id=preferred_proxy))
|
update_handler.call_browser(preferred_proxy_id=preferred_proxy)
|
||||||
# title, size is len contents not len xfer
|
# title, size is len contents not len xfer
|
||||||
except content_fetcher_exceptions.Non200ErrorCodeReceived as e:
|
except content_fetcher_exceptions.Non200ErrorCodeReceived as e:
|
||||||
if e.status_code == 404:
|
if e.status_code == 404:
|
||||||
|
|||||||
@@ -154,8 +154,9 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<br>
|
<br>
|
||||||
{{ _('Tip:') }} <a href="{{ url_for('settings.settings_page')}}#proxies">{{ _('Connect using Bright Data proxies, find out more here.') }}</a>
|
{{ _('Tip:') }} <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration#brightdata-proxy-support">{{ _('Connect using Bright Data and Oxylabs Proxies, find out more here.') }}</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -351,7 +352,7 @@ nav
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p><strong>{{ _('Tip') }}</strong>: {{ _('"Residential" and "Mobile" proxy type can be more successful than "Data Center" for blocked websites.') }}</p>
|
<p><strong>{{ _('Tip') }}</strong>: {{ _('"Residential" and "Mobile" proxy type can be more successfull than "Data Center" for blocked websites.') }}</p>
|
||||||
|
|
||||||
<div class="pure-control-group" id="extra-proxies-setting">
|
<div class="pure-control-group" id="extra-proxies-setting">
|
||||||
{{ render_fieldlist_with_inline_errors(form.requests.form.extra_proxies) }}
|
{{ render_fieldlist_with_inline_errors(form.requests.form.extra_proxies) }}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
from babel.numbers import parse_decimal
|
from babel.numbers import parse_decimal
|
||||||
from changedetectionio.model.Watch import model as BaseWatch
|
from changedetectionio.model.Watch import model as BaseWatch
|
||||||
from decimal import Decimal, InvalidOperation
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -11,8 +10,6 @@ supports_browser_steps = True
|
|||||||
supports_text_filters_and_triggers = True
|
supports_text_filters_and_triggers = True
|
||||||
supports_text_filters_and_triggers_elements = True
|
supports_text_filters_and_triggers_elements = True
|
||||||
supports_request_type = True
|
supports_request_type = True
|
||||||
_price_re = re.compile(r"Price:\s*(\d+(?:\.\d+)?)", re.IGNORECASE)
|
|
||||||
|
|
||||||
|
|
||||||
class Restock(dict):
|
class Restock(dict):
|
||||||
|
|
||||||
@@ -66,17 +63,6 @@ class Restock(dict):
|
|||||||
|
|
||||||
super().__setitem__(key, value)
|
super().__setitem__(key, value)
|
||||||
|
|
||||||
def get_price_from_history_str(history_str):
|
|
||||||
m = _price_re.search(history_str)
|
|
||||||
if not m:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
return str(Decimal(m.group(1)))
|
|
||||||
except InvalidOperation:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class Watch(BaseWatch):
|
class Watch(BaseWatch):
|
||||||
def __init__(self, *arg, **kw):
|
def __init__(self, *arg, **kw):
|
||||||
super().__init__(*arg, **kw)
|
super().__init__(*arg, **kw)
|
||||||
@@ -90,27 +76,13 @@ class Watch(BaseWatch):
|
|||||||
def extra_notification_token_values(self):
|
def extra_notification_token_values(self):
|
||||||
values = super().extra_notification_token_values()
|
values = super().extra_notification_token_values()
|
||||||
values['restock'] = self.get('restock', {})
|
values['restock'] = self.get('restock', {})
|
||||||
|
|
||||||
values['restock']['previous_price'] = None
|
|
||||||
if self.history_n >= 2:
|
|
||||||
history = self.history
|
|
||||||
if history and len(history) >=2:
|
|
||||||
"""Unfortunately for now timestamp is stored as string key"""
|
|
||||||
sorted_keys = sorted(list(history), key=lambda x: int(x))
|
|
||||||
sorted_keys.reverse()
|
|
||||||
|
|
||||||
price_str = self.get_history_snapshot(timestamp=sorted_keys[-1])
|
|
||||||
if price_str:
|
|
||||||
values['restock']['previous_price'] = get_price_from_history_str(price_str)
|
|
||||||
return values
|
return values
|
||||||
|
|
||||||
def extra_notification_token_placeholder_info(self):
|
def extra_notification_token_placeholder_info(self):
|
||||||
values = super().extra_notification_token_placeholder_info()
|
values = super().extra_notification_token_placeholder_info()
|
||||||
|
|
||||||
values.append(('restock.price', "Price detected"))
|
values.append(('restock.price', "Price detected"))
|
||||||
values.append(('restock.in_stock', "In stock status"))
|
|
||||||
values.append(('restock.original_price', "Original price at first check"))
|
values.append(('restock.original_price', "Original price at first check"))
|
||||||
values.append(('restock.previous_price', "Previous price in history"))
|
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|||||||
@@ -199,31 +199,8 @@ def handle_watch_update(socketio, **kwargs):
|
|||||||
logger.error(f"Socket.IO error in handle_watch_update: {str(e)}")
|
logger.error(f"Socket.IO error in handle_watch_update: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
def _suppress_werkzeug_ws_abrupt_disconnect_noise():
|
|
||||||
"""Patch BaseWSGIServer.log to suppress the AssertionError traceback that fires when
|
|
||||||
a browser closes a WebSocket connection mid-handshake (e.g. closing a tab).
|
|
||||||
The exception is caught inside run_wsgi and routed to self.server.log() — it never
|
|
||||||
propagates out, so wrapping run_wsgi doesn't help. Patching the log method is the
|
|
||||||
only reliable intercept point. The error is cosmetic: Socket.IO already handles the
|
|
||||||
disconnect correctly via its own disconnect handler and timeout logic."""
|
|
||||||
try:
|
|
||||||
from werkzeug.serving import BaseWSGIServer
|
|
||||||
_original_log = BaseWSGIServer.log
|
|
||||||
|
|
||||||
def _filtered_log(self, type, message, *args):
|
|
||||||
if type == 'error' and 'write() before start_response' in message:
|
|
||||||
return
|
|
||||||
_original_log(self, type, message, *args)
|
|
||||||
|
|
||||||
BaseWSGIServer.log = _filtered_log
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def init_socketio(app, datastore):
|
def init_socketio(app, datastore):
|
||||||
"""Initialize SocketIO with the main Flask app"""
|
"""Initialize SocketIO with the main Flask app"""
|
||||||
_suppress_werkzeug_ws_abrupt_disconnect_noise()
|
|
||||||
|
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|||||||
@@ -116,14 +116,6 @@ $(document).ready(function () {
|
|||||||
$('#realtime-conn-error').show();
|
$('#realtime-conn-error').show();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tell the server we're leaving cleanly so it can release the connection
|
|
||||||
// immediately rather than waiting for a timeout.
|
|
||||||
// Note: this only fires for voluntary closes (tab/window close, navigation away).
|
|
||||||
// Hard kills, crashes and network drops will still timeout normally on the server.
|
|
||||||
window.addEventListener('beforeunload', function () {
|
|
||||||
socket.disconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('queue_size', function (data) {
|
socket.on('queue_size', function (data) {
|
||||||
console.log(`${data.event_timestamp} - Queue size update: ${data.q_length}`);
|
console.log(`${data.event_timestamp} - Queue size update: ${data.q_length}`);
|
||||||
if(queueSizePagerInfoText) {
|
if(queueSizePagerInfoText) {
|
||||||
|
|||||||
@@ -422,28 +422,3 @@ def test_plaintext_even_if_xml_content_and_can_apply_filters(client, live_server
|
|||||||
assert b'<foobar' not in res.data
|
assert b'<foobar' not in res.data
|
||||||
|
|
||||||
res = delete_all_watches(client)
|
res = delete_all_watches(client)
|
||||||
|
|
||||||
|
|
||||||
def test_last_error_cleared_on_same_checksum(client, live_server, datastore_path):
|
|
||||||
"""last_error should be cleared even when content is unchanged (checksumFromPreviousCheckWasTheSame path)"""
|
|
||||||
set_original_response(datastore_path=datastore_path)
|
|
||||||
|
|
||||||
uuid = client.application.config.get('DATASTORE').add_watch(url=url_for('test_endpoint', _external=True))
|
|
||||||
|
|
||||||
# First check - establishes baseline checksum
|
|
||||||
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
|
||||||
wait_for_all_checks(client)
|
|
||||||
|
|
||||||
# Inject a stale last_error directly (simulates a prior failed check)
|
|
||||||
datastore = client.application.config.get('DATASTORE')
|
|
||||||
datastore.update_watch(uuid=uuid, update_obj={'last_error': 'Some previous error'})
|
|
||||||
assert datastore.data['watching'][uuid].get('last_error') == 'Some previous error'
|
|
||||||
|
|
||||||
# Second check - same content, so checksumFromPreviousCheckWasTheSame will fire
|
|
||||||
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
|
||||||
wait_for_all_checks(client)
|
|
||||||
|
|
||||||
# last_error must be cleared even though no change was detected
|
|
||||||
assert datastore.data['watching'][uuid].get('last_error') == False
|
|
||||||
|
|
||||||
delete_all_watches(client)
|
|
||||||
|
|||||||
@@ -350,7 +350,6 @@ def test_change_with_notification_values(client, live_server, measure_memory_usa
|
|||||||
res = client.get(url_for("settings.settings_page"))
|
res = client.get(url_for("settings.settings_page"))
|
||||||
|
|
||||||
assert b'{{restock.original_price}}' in res.data
|
assert b'{{restock.original_price}}' in res.data
|
||||||
assert b'{{restock.previous_price}}' in res.data
|
|
||||||
assert b'Original price at first check' in res.data
|
assert b'Original price at first check' in res.data
|
||||||
|
|
||||||
#####################
|
#####################
|
||||||
@@ -359,7 +358,7 @@ def test_change_with_notification_values(client, live_server, measure_memory_usa
|
|||||||
url_for("settings.settings_page"),
|
url_for("settings.settings_page"),
|
||||||
data={"application-notification_urls": notification_url,
|
data={"application-notification_urls": notification_url,
|
||||||
"application-notification_title": "title new price {{restock.price}}",
|
"application-notification_title": "title new price {{restock.price}}",
|
||||||
"application-notification_body": "new price {{restock.price}} previous price {{restock.previous_price}} instock {{restock.in_stock}}",
|
"application-notification_body": "new price {{restock.price}}",
|
||||||
"application-notification_format": default_notification_format,
|
"application-notification_format": default_notification_format,
|
||||||
"requests-time_between_check-minutes": 180,
|
"requests-time_between_check-minutes": 180,
|
||||||
'application-fetch_backend': "html_requests"},
|
'application-fetch_backend': "html_requests"},
|
||||||
@@ -373,6 +372,8 @@ def test_change_with_notification_values(client, live_server, measure_memory_usa
|
|||||||
|
|
||||||
assert b"Settings updated." in res.data
|
assert b"Settings updated." in res.data
|
||||||
|
|
||||||
|
|
||||||
|
set_original_response(props_markup=instock_props[0], price='960.45', datastore_path=datastore_path)
|
||||||
# A change in price, should trigger a change by default
|
# A change in price, should trigger a change by default
|
||||||
set_original_response(props_markup=instock_props[0], price='1950.45', datastore_path=datastore_path)
|
set_original_response(props_markup=instock_props[0], price='1950.45', datastore_path=datastore_path)
|
||||||
client.get(url_for("ui.form_watch_checknow"))
|
client.get(url_for("ui.form_watch_checknow"))
|
||||||
@@ -383,7 +384,6 @@ def test_change_with_notification_values(client, live_server, measure_memory_usa
|
|||||||
notification = f.read()
|
notification = f.read()
|
||||||
assert "new price 1950.45" in notification
|
assert "new price 1950.45" in notification
|
||||||
assert "title new price 1950.45" in notification
|
assert "title new price 1950.45" in notification
|
||||||
assert "previous price 960.45" in notification
|
|
||||||
|
|
||||||
## Now test the "SEND TEST NOTIFICATION" is working
|
## Now test the "SEND TEST NOTIFICATION" is working
|
||||||
os.unlink(os.path.join(datastore_path, "notification.txt"))
|
os.unlink(os.path.join(datastore_path, "notification.txt"))
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -284,9 +284,6 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
|
|||||||
logger.debug(f'[{uuid}] - checksumFromPreviousCheckWasTheSame - Checksum from previous check was the same, nothing todo here.')
|
logger.debug(f'[{uuid}] - checksumFromPreviousCheckWasTheSame - Checksum from previous check was the same, nothing todo here.')
|
||||||
# Reset the edited flag since we successfully completed the check
|
# Reset the edited flag since we successfully completed the check
|
||||||
watch.reset_watch_edited_flag()
|
watch.reset_watch_edited_flag()
|
||||||
# Page was fetched successfully - clear any previous error state
|
|
||||||
datastore.update_watch(uuid=uuid, update_obj={'last_error': False})
|
|
||||||
cleanup_error_artifacts(uuid, datastore)
|
|
||||||
|
|
||||||
except content_fetchers_exceptions.BrowserConnectError as e:
|
except content_fetchers_exceptions.BrowserConnectError as e:
|
||||||
datastore.update_watch(uuid=uuid,
|
datastore.update_watch(uuid=uuid,
|
||||||
|
|||||||
Reference in New Issue
Block a user