diff --git a/changedetectionio/blueprint/ui/__init__.py b/changedetectionio/blueprint/ui/__init__.py index a87aaaf6..e917e8d0 100644 --- a/changedetectionio/blueprint/ui/__init__.py +++ b/changedetectionio/blueprint/ui/__init__.py @@ -9,7 +9,7 @@ from changedetectionio.blueprint.ui.edit import construct_blueprint as construct from changedetectionio.blueprint.ui.notification import construct_blueprint as construct_notification_blueprint from changedetectionio.blueprint.ui.views import construct_blueprint as construct_views_blueprint -def construct_blueprint(datastore: ChangeDetectionStore, update_q, running_update_threads, queuedWatchMetaData, watch_check_completed): +def construct_blueprint(datastore: ChangeDetectionStore, update_q, running_update_threads, queuedWatchMetaData, watch_check_update): ui_blueprint = Blueprint('ui', __name__, template_folder="templates") # Register the edit blueprint @@ -21,10 +21,10 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, running_updat ui_blueprint.register_blueprint(notification_blueprint) # Register the views blueprint - views_blueprint = construct_views_blueprint(datastore, update_q, queuedWatchMetaData, watch_check_completed) + views_blueprint = construct_views_blueprint(datastore, update_q, queuedWatchMetaData, watch_check_update) ui_blueprint.register_blueprint(views_blueprint) - ui_ajax_blueprint = constuct_ui_ajax_blueprint(datastore, update_q, running_update_threads, queuedWatchMetaData, watch_check_completed) + ui_ajax_blueprint = constuct_ui_ajax_blueprint(datastore, update_q, running_update_threads, queuedWatchMetaData, watch_check_update) ui_blueprint.register_blueprint(ui_ajax_blueprint) # Import the login decorator @@ -252,7 +252,7 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, running_updat if uuids: for uuid in uuids: - watch_check_completed.send(watch_uuid=uuid) + watch_check_update.send(watch_uuid=uuid) return redirect(url_for('watchlist.index')) diff --git a/changedetectionio/blueprint/ui/ajax.py b/changedetectionio/blueprint/ui/ajax.py index 5d2352e3..bbe3464d 100644 --- a/changedetectionio/blueprint/ui/ajax.py +++ b/changedetectionio/blueprint/ui/ajax.py @@ -6,7 +6,7 @@ from flask import Blueprint, request, redirect, url_for, flash, render_template, from changedetectionio.store import ChangeDetectionStore -def constuct_ui_ajax_blueprint(datastore: ChangeDetectionStore, update_q, running_update_threads, queuedWatchMetaData, watch_check_completed): +def constuct_ui_ajax_blueprint(datastore: ChangeDetectionStore, update_q, running_update_threads, queuedWatchMetaData, watch_check_update): ui_ajax_blueprint = Blueprint('ajax', __name__, template_folder="templates", url_prefix='/ajax') # Import the login decorator @@ -25,9 +25,9 @@ def constuct_ui_ajax_blueprint(datastore: ChangeDetectionStore, update_q, runnin elif op == 'recheck': update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid})) - watch_check_completed = signal('watch_check_completed') - if watch_check_completed: - watch_check_completed.send(watch_uuid=uuid) + watch_check_update = signal('watch_check_update') + if watch_check_update: + watch_check_update.send(watch_uuid=uuid) return 'OK' diff --git a/changedetectionio/blueprint/ui/views.py b/changedetectionio/blueprint/ui/views.py index cf8341aa..efcdc03a 100644 --- a/changedetectionio/blueprint/ui/views.py +++ b/changedetectionio/blueprint/ui/views.py @@ -8,7 +8,7 @@ from changedetectionio.store import ChangeDetectionStore from changedetectionio.auth_decorator import login_optionally_required from changedetectionio import html_tools -def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMetaData, watch_check_completed): +def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMetaData, watch_check_update): views_blueprint = Blueprint('ui_views', __name__, template_folder="../ui/templates") @views_blueprint.route("/preview/", methods=['GET']) diff --git a/changedetectionio/custom_queue.py b/changedetectionio/custom_queue.py index 532855bc..d3891991 100644 --- a/changedetectionio/custom_queue.py +++ b/changedetectionio/custom_queue.py @@ -21,7 +21,7 @@ class SignalPriorityQueue(queue.PriorityQueue): if hasattr(item, 'item') and isinstance(item.item, dict) and 'uuid' in item.item: uuid = item.item['uuid'] # Get the signal and send it if it exists - watch_check_completed = signal('watch_check_completed') - if watch_check_completed: + watch_check_update = signal('watch_check_update') + if watch_check_update: # Send the watch_uuid parameter - watch_check_completed.send(watch_uuid=uuid) \ No newline at end of file + watch_check_update.send(watch_uuid=uuid) \ No newline at end of file diff --git a/changedetectionio/flask_app.py b/changedetectionio/flask_app.py index aaa8f6a0..05e9926d 100644 --- a/changedetectionio/flask_app.py +++ b/changedetectionio/flask_app.py @@ -33,7 +33,7 @@ from flask_cors import CORS # Create specific signals for application events # Make this a global singleton to avoid multiple signal objects -watch_check_completed = signal('watch_check_completed', doc='Signal sent when a watch check is completed') +watch_check_update = signal('watch_check_update', doc='Signal sent when a watch check is completed') from flask_wtf import CSRFProtect from loguru import logger import eventlet @@ -245,7 +245,7 @@ def changedetection_app(config=None, datastore_o=None): app.config['DATASTORE'] = datastore_o # Store the signal in the app config to ensure it's accessible everywhere - app.config['WATCH_CHECK_COMPLETED_SIGNAL'] = watch_check_completed + app.config['watch_check_update_SIGNAL'] = watch_check_update login_manager = flask_login.LoginManager(app) login_manager.login_view = 'login' @@ -472,7 +472,7 @@ def changedetection_app(config=None, datastore_o=None): # watchlist UI buttons etc import changedetectionio.blueprint.ui as ui - app.register_blueprint(ui.construct_blueprint(datastore, update_q, running_update_threads, queuedWatchMetaData, watch_check_completed)) + app.register_blueprint(ui.construct_blueprint(datastore, update_q, running_update_threads, queuedWatchMetaData, watch_check_update)) import changedetectionio.blueprint.watchlist as watchlist app.register_blueprint(watchlist.construct_blueprint(datastore=datastore, update_q=update_q, queuedWatchMetaData=queuedWatchMetaData), url_prefix='') diff --git a/changedetectionio/model/Watch.py b/changedetectionio/model/Watch.py index 10848a04..b862b5f2 100644 --- a/changedetectionio/model/Watch.py +++ b/changedetectionio/model/Watch.py @@ -126,9 +126,9 @@ class model(watch_base): 'remote_server_reply': None, 'track_ldjson_price_data': None }) - watch_check_completed = signal('watch_check_completed') - if watch_check_completed: - watch_check_completed.send(watch_uuid=self.get('uuid')) + watch_check_update = signal('watch_check_update') + if watch_check_update: + watch_check_update.send(watch_uuid=self.get('uuid')) return diff --git a/changedetectionio/realtime/socket_server.py b/changedetectionio/realtime/socket_server.py index b77e6d7e..fef700f8 100644 --- a/changedetectionio/realtime/socket_server.py +++ b/changedetectionio/realtime/socket_server.py @@ -5,17 +5,14 @@ import time import os from loguru import logger -from changedetectionio.flask_app import _jinja2_filter_datetime, watch_check_completed - - class SignalHandler: """A standalone class to receive signals""" def __init__(self, socketio_instance, datastore): self.socketio_instance = socketio_instance self.datastore = datastore - # Connect to the watch_check_completed signal - from changedetectionio.flask_app import watch_check_completed as wcc + # Connect to the watch_check_update signal + from changedetectionio.flask_app import watch_check_update as wcc wcc.connect(self.handle_signal, weak=False) logger.info("SignalHandler: Connected to signal from direct import") @@ -42,6 +39,7 @@ def handle_watch_update(socketio, **kwargs): # Emit the watch update to all connected clients from changedetectionio.flask_app import running_update_threads, update_q + from changedetectionio.flask_app import _jinja2_filter_datetime # Get list of watches that are currently running running_uuids = [] @@ -69,9 +67,10 @@ def handle_watch_update(socketio, **kwargs): 'notification_muted': True if watch.get('notification_muted') else False, 'unviewed': watch.has_unviewed, 'uuid': watch.get('uuid'), + 'event_timestamp': time.time() } socketio.emit("watch_update", watch_data) - logger.debug(f"Socket.IO: Emitted update for watch {watch.get('uuid')}") + logger.debug(f"Socket.IO: Emitted update for watch {watch.get('uuid')}, Checking now: {watch_data['checking_now']}") except Exception as e: logger.error(f"Socket.IO error in handle_watch_update: {str(e)}") diff --git a/changedetectionio/static/js/socket.js b/changedetectionio/static/js/socket.js index 8011d0d9..0123e6f5 100644 --- a/changedetectionio/static/js/socket.js +++ b/changedetectionio/static/js/socket.js @@ -43,7 +43,7 @@ $(document).ready(function () { // Listen for periodically emitted watch data socket.on('watch_update', function (watch) { - console.log(`Watch update ${watch.uuid}`); + console.log(`${watch.event_timestamp} - Watch update ${watch.uuid} - Checking now - ${watch.checking_now}`); const $watchRow = $('tr[data-watch-uuid="' + watch.uuid + '"]'); if ($watchRow.length) { diff --git a/changedetectionio/store.py b/changedetectionio/store.py index 7ee4c00c..f200bef3 100644 --- a/changedetectionio/store.py +++ b/changedetectionio/store.py @@ -167,9 +167,9 @@ class ChangeDetectionStore: self.data['watching'][uuid].update({'last_viewed': int(timestamp)}) self.needs_write = True - watch_check_completed = signal('watch_check_completed') - if watch_check_completed: - watch_check_completed.send(watch_uuid=uuid) + watch_check_update = signal('watch_check_update') + if watch_check_update: + watch_check_update.send(watch_uuid=uuid) def remove_password(self): self.__data['settings']['application']['password'] = False diff --git a/changedetectionio/tests/realtime/test_socketio.py b/changedetectionio/tests/realtime/test_socketio.py new file mode 100755 index 00000000..11ce6066 --- /dev/null +++ b/changedetectionio/tests/realtime/test_socketio.py @@ -0,0 +1,37 @@ +import asyncio +import socketio + +# URL of your Socket.IO server +SOCKETIO_URL = "http://localhost:5000" # Change as needed +SOCKETIO_PATH = "/socket.io" # Match the path used in your JS config + +# Number of clients to simulate +NUM_CLIENTS = 10 + +async def start_client(client_id: int): + sio = socketio.AsyncClient(reconnection_attempts=5, reconnection_delay=1) + + @sio.event + async def connect(): + print(f"[Client {client_id}] Connected") + + @sio.event + async def disconnect(): + print(f"[Client {client_id}] Disconnected") + + @sio.on("watch_update") + async def on_watch_update(watch): + print(f"[Client {client_id}] Received update: {watch}") + + try: + await sio.connect(SOCKETIO_URL, socketio_path=SOCKETIO_PATH, transports=["websocket", "polling"]) + await sio.wait() + except Exception as e: + print(f"[Client {client_id}] Connection error: {e}") + +async def main(): + clients = [start_client(i) for i in range(NUM_CLIENTS)] + await asyncio.gather(*clients) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/changedetectionio/update_worker.py b/changedetectionio/update_worker.py index 609c3a77..acd4759a 100644 --- a/changedetectionio/update_worker.py +++ b/changedetectionio/update_worker.py @@ -2,7 +2,7 @@ from .processors.exceptions import ProcessorException import changedetectionio.content_fetchers.exceptions as content_fetchers_exceptions from changedetectionio.processors.text_json_diff.processor import FilterNotFoundInResponse from changedetectionio import html_tools -from changedetectionio.flask_app import watch_check_completed +from changedetectionio.flask_app import watch_check_update import importlib import os @@ -272,7 +272,7 @@ class update_worker(threading.Thread): logger.info(f"Processing watch UUID {uuid} Priority {queued_item_data.priority} URL {watch['url']}") try: - watch_check_completed.send(watch_uuid=watch['uuid']) + watch_check_update.send(watch_uuid=watch['uuid']) # Processor is what we are using for detecting the "Change" processor = watch.get('processor', 'text_json_diff') @@ -595,8 +595,8 @@ class update_worker(threading.Thread): # Send signal for watch check completion with the watch data if watch: - logger.info(f"Sending watch_check_completed signal for UUID {watch['uuid']}") - watch_check_completed.send(watch_uuid=watch['uuid']) + logger.info(f"Sending watch_check_update signal for UUID {watch['uuid']}") + watch_check_update.send(watch_uuid=watch['uuid']) update_handler = None logger.debug(f"Watch {uuid} done in {time.time()-fetch_start_time:.2f}s")