Compare commits

..

1 Commits

Author SHA1 Message Date
dgtlmoon
141aea07b8 JSONP - Attempt to strip out JSONP 2026-03-15 21:53:46 +01:00
11 changed files with 819 additions and 447 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -422,28 +422,3 @@ def test_plaintext_even_if_xml_content_and_can_apply_filters(client, live_server
assert b'&lt;foobar' not in res.data assert b'&lt;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)

View File

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

File diff suppressed because it is too large Load Diff

View File

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