WIP
This commit is contained in:
@@ -100,14 +100,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% for watch in (watches|sort(attribute=sort_attribute, reverse=sort_order == 'asc'))|pagination_slice(skip=pagination.skip) %}
|
{% for watch in (watches|sort(attribute=sort_attribute, reverse=sort_order == 'asc'))|pagination_slice(skip=pagination.skip) %}
|
||||||
|
|
||||||
{% set is_unviewed = watch.newest_history_key| int > watch.last_viewed and watch.history_n>=2 %}
|
|
||||||
{% set checking_now = is_checking_now(watch) %}
|
{% set checking_now = is_checking_now(watch) %}
|
||||||
<tr id="{{ watch.uuid }}" data-watch-uuid="{{ watch.uuid }}"
|
<tr id="{{ watch.uuid }}" data-watch-uuid="{{ watch.uuid }}"
|
||||||
class="{{ loop.cycle('pure-table-odd', 'pure-table-even') }} processor-{{ watch['processor'] }}
|
class="{{ loop.cycle('pure-table-odd', 'pure-table-even') }} processor-{{ watch['processor'] }}
|
||||||
{% if watch.last_error is defined and watch.last_error != False %}error{% endif %}
|
{% if watch.last_error is defined and watch.last_error != False %}error{% endif %}
|
||||||
{% if watch.last_notification_error is defined and watch.last_notification_error != False %}error{% endif %}
|
{% if watch.last_notification_error is defined and watch.last_notification_error != False %}error{% endif %}
|
||||||
{% if watch.paused is defined and watch.paused != False %}paused{% endif %}
|
{% if watch.paused is defined and watch.paused != False %}paused{% endif %}
|
||||||
{% if is_unviewed %}unviewed{% endif %}
|
{% if watch.has_unviewed %}unviewed{% endif %}
|
||||||
{% if watch.has_restock_info %} has-restock-info {% if watch['restock']['in_stock'] %}in-stock{% else %}not-in-stock{% endif %} {% else %}no-restock-info{% endif %}
|
{% if watch.has_restock_info %} has-restock-info {% if watch['restock']['in_stock'] %}in-stock{% else %}not-in-stock{% endif %} {% else %}no-restock-info{% endif %}
|
||||||
{% if watch.uuid in queued_uuids %}queued{% endif %}
|
{% if watch.uuid in queued_uuids %}queued{% endif %}
|
||||||
{% if checking_now %}checking-now{% endif %}
|
{% if checking_now %}checking-now{% endif %}
|
||||||
@@ -207,6 +206,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<a {% if watch.uuid in queued_uuids %}disabled="true"{% endif %} href="{{ url_for('ui.form_watch_checknow', uuid=watch.uuid, tag=request.args.get('tag')) }}"
|
<a {% if watch.uuid in queued_uuids %}disabled="true"{% endif %} href="{{ url_for('ui.form_watch_checknow', uuid=watch.uuid, tag=request.args.get('tag')) }}"
|
||||||
class="recheck pure-button pure-button-primary">{% if watch.uuid in queued_uuids %}Queued{% else %}Recheck{% endif %}</a>
|
class="recheck pure-button pure-button-primary">{% if watch.uuid in queued_uuids %}Queued{% else %}Recheck{% endif %}</a>
|
||||||
|
|
||||||
<a href="{{ url_for('ui.ui_edit.edit_page', uuid=watch.uuid, tag=active_tag_uuid)}}#general" class="pure-button pure-button-primary">Edit</a>
|
<a href="{{ url_for('ui.ui_edit.edit_page', uuid=watch.uuid, tag=active_tag_uuid)}}#general" class="pure-button pure-button-primary">Edit</a>
|
||||||
|
|
||||||
{% if watch.history_n >= 2 %}
|
{% if watch.history_n >= 2 %}
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
{% set open_diff_in_new_tab = datastore.data['settings']['application']['ui'].get('open_diff_in_new_tab') %}
|
{% set open_diff_in_new_tab = datastore.data['settings']['application']['ui'].get('open_diff_in_new_tab') %}
|
||||||
{% set target_attr = ' target="' ~ watch.uuid ~ '"' if open_diff_in_new_tab else '' %}
|
{% set target_attr = ' target="' ~ watch.uuid ~ '"' if open_diff_in_new_tab else '' %}
|
||||||
|
|
||||||
{% if is_unviewed %}
|
{% if watch.has_unviewed %}
|
||||||
<a href="{{ url_for('ui.ui_views.diff_history_page', uuid=watch.uuid, from_version=watch.get_from_version_based_on_last_viewed) }}" {{target_attr}} class="pure-button pure-button-primary diff-link">History</a>
|
<a href="{{ url_for('ui.ui_views.diff_history_page', uuid=watch.uuid, from_version=watch.get_from_version_based_on_last_viewed) }}" {{target_attr}} class="pure-button pure-button-primary diff-link">History</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('ui.ui_views.diff_history_page', uuid=watch.uuid)}}" {{target_attr}} class="pure-button pure-button-primary diff-link">History</a>
|
<a href="{{ url_for('ui.ui_views.diff_history_page', uuid=watch.uuid)}}" {{target_attr}} class="pure-button pure-button-primary diff-link">History</a>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import queue
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import timeago
|
import timeago
|
||||||
|
from blinker import signal
|
||||||
|
|
||||||
from changedetectionio.strtobool import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
from threading import Event
|
from threading import Event
|
||||||
@@ -28,6 +29,10 @@ from flask_login import current_user
|
|||||||
from flask_paginate import Pagination, get_page_parameter
|
from flask_paginate import Pagination, get_page_parameter
|
||||||
from flask_restful import abort, Api
|
from flask_restful import abort, Api
|
||||||
from flask_cors import CORS
|
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')
|
||||||
from flask_wtf import CSRFProtect
|
from flask_wtf import CSRFProtect
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
import eventlet
|
import eventlet
|
||||||
@@ -226,6 +231,9 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
# (instead of the global var)
|
# (instead of the global var)
|
||||||
app.config['DATASTORE'] = datastore_o
|
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
|
||||||
|
|
||||||
login_manager = flask_login.LoginManager(app)
|
login_manager = flask_login.LoginManager(app)
|
||||||
login_manager.login_view = 'login'
|
login_manager.login_view = 'login'
|
||||||
app.secret_key = init_app_secret(config['datastore_path'])
|
app.secret_key = init_app_secret(config['datastore_path'])
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ class model(watch_base):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_unviewed(self):
|
||||||
|
return int(self.newest_history_key) > int(self['last_viewed']) and self.__history_n >= 2
|
||||||
|
|
||||||
def ensure_data_dir_exists(self):
|
def ensure_data_dir_exists(self):
|
||||||
if not os.path.isdir(self.watch_data_dir):
|
if not os.path.isdir(self.watch_data_dir):
|
||||||
logger.debug(f"> Creating data dir {self.watch_data_dir}")
|
logger.debug(f"> Creating data dir {self.watch_data_dir}")
|
||||||
|
|||||||
@@ -4,10 +4,43 @@ import threading
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
import blinker
|
||||||
|
|
||||||
from changedetectionio.flask_app import _jinja2_filter_datetime
|
from changedetectionio.flask_app import _jinja2_filter_datetime, watch_check_completed
|
||||||
|
|
||||||
|
|
||||||
|
class SignalHandler:
|
||||||
|
"""A standalone class to receive signals"""
|
||||||
|
def __init__(self, socketio_instance):
|
||||||
|
self.socketio_instance = socketio_instance
|
||||||
|
|
||||||
|
# Get signal from app config
|
||||||
|
app_signal = socketio_instance.main_app.config.get('WATCH_CHECK_COMPLETED_SIGNAL')
|
||||||
|
if app_signal:
|
||||||
|
app_signal.connect(self.handle_signal, weak=False)
|
||||||
|
logger.info("SignalHandler: Connected to signal from app config")
|
||||||
|
else:
|
||||||
|
# Fallback if not in app config
|
||||||
|
from changedetectionio.flask_app import watch_check_completed as wcc
|
||||||
|
wcc.connect(self.handle_signal, weak=False)
|
||||||
|
logger.info("SignalHandler: Connected to signal from direct import")
|
||||||
|
|
||||||
|
def handle_signal(self, *args, **kwargs):
|
||||||
|
logger.info(f"SignalHandler: Signal received with {len(args)} args and {len(kwargs)} kwargs")
|
||||||
|
# Safely extract the watch UUID from kwargs
|
||||||
|
watch_uuid = kwargs.get('watch_uuid')
|
||||||
|
if watch_uuid:
|
||||||
|
# Get the datastore from the socket instance
|
||||||
|
datastore = self.socketio_instance.datastore
|
||||||
|
# Get the watch object from the datastore
|
||||||
|
watch = datastore.data['watching'].get(watch_uuid)
|
||||||
|
if watch:
|
||||||
|
# Forward to the socket instance with the watch parameter
|
||||||
|
self.socketio_instance.handle_watch_update(watch=watch)
|
||||||
|
logger.info(f"Signal handler processed watch UUID {watch_uuid}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Watch UUID {watch_uuid} not found in datastore")
|
||||||
|
|
||||||
class ChangeDetectionSocketIO:
|
class ChangeDetectionSocketIO:
|
||||||
def __init__(self, app, datastore):
|
def __init__(self, app, datastore):
|
||||||
self.main_app = app
|
self.main_app = app
|
||||||
@@ -29,75 +62,59 @@ class ChangeDetectionSocketIO:
|
|||||||
self.thread = None
|
self.thread = None
|
||||||
self.thread_lock = threading.Lock()
|
self.thread_lock = threading.Lock()
|
||||||
|
|
||||||
def start_background_task(self):
|
# Create a dedicated signal handler
|
||||||
"""Start the background task if it's not already running"""
|
self.signal_handler = SignalHandler(self)
|
||||||
with self.thread_lock:
|
|
||||||
if self.thread is None:
|
|
||||||
self.thread = threading.Thread(target=self.background_task)
|
|
||||||
self.thread.daemon = True
|
|
||||||
self.thread.start()
|
|
||||||
logger.info("Socket.IO: Started background task thread")
|
|
||||||
|
|
||||||
def handle_connect(self):
|
def handle_connect(self):
|
||||||
"""Handle client connection"""
|
"""Handle client connection"""
|
||||||
logger.info("Socket.IO: Client connected")
|
logger.info("Socket.IO: Client connected")
|
||||||
|
|
||||||
# Start the background task when the first client connects
|
|
||||||
self.start_background_task()
|
|
||||||
|
|
||||||
def handle_disconnect(self):
|
def handle_disconnect(self):
|
||||||
"""Handle client disconnection"""
|
"""Handle client disconnection"""
|
||||||
logger.info("Socket.IO: Client disconnected")
|
logger.info("Socket.IO: Client disconnected")
|
||||||
|
|
||||||
def background_task(self):
|
def handle_watch_update(self, **kwargs):
|
||||||
"""Background task that emits watch status periodically"""
|
"""Handle watch update signal from blinker"""
|
||||||
check_interval = 4 # seconds between updates
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
watch = kwargs.get('watch')
|
||||||
|
# Emit the watch update to all connected clients
|
||||||
with self.main_app.app_context():
|
with self.main_app.app_context():
|
||||||
while True:
|
from changedetectionio.flask_app import running_update_threads, update_q
|
||||||
try:
|
|
||||||
# Collect all watch data
|
|
||||||
watches_data = []
|
|
||||||
|
|
||||||
# Get list of watches that are currently running
|
# Get list of watches that are currently running
|
||||||
from changedetectionio.flask_app import running_update_threads
|
running_uuids = []
|
||||||
currently_checking = []
|
for t in running_update_threads:
|
||||||
|
if hasattr(t, 'current_uuid') and t.current_uuid:
|
||||||
|
running_uuids.append(t.current_uuid)
|
||||||
|
|
||||||
# Make a copy to avoid issues if the list changes
|
# Get list of watches in the queue
|
||||||
threads_snapshot = list(running_update_threads)
|
queue_list = []
|
||||||
for thread in threads_snapshot:
|
for q_item in update_q.queue:
|
||||||
if hasattr(thread, 'current_uuid') and thread.current_uuid:
|
if hasattr(q_item, 'item') and 'uuid' in q_item.item:
|
||||||
currently_checking.append(thread.current_uuid)
|
queue_list.append(q_item.item['uuid'])
|
||||||
self.socketio.emit("checking_now", list(currently_checking))
|
|
||||||
|
|
||||||
# Send all watch data periodically
|
# Create a simplified watch data object to send to clients
|
||||||
for uuid, watch in self.datastore.data['watching'].items():
|
watch_data = {
|
||||||
# Simplified watch data to avoid sending everything
|
'uuid': watch.get('uuid'),
|
||||||
simplified_data = {
|
'last_checked_text': _jinja2_filter_datetime(watch),
|
||||||
'uuid': uuid,
|
'last_checked': watch.get('last_checked'),
|
||||||
'last_checked': _jinja2_filter_datetime(watch),
|
'last_changed': watch.get('last_changed'),
|
||||||
# 'history_n': watch.history_n if hasattr(watch, 'history_n') else 0,
|
'queued': True if watch.get('uuid') in queue_list else False,
|
||||||
|
'checking_now': True if watch.get('uuid') in running_uuids else False,
|
||||||
|
'unviewed': watch.has_unviewed,
|
||||||
}
|
}
|
||||||
#watches_data.append(simplified_data)
|
self.socketio.emit("watch_update", watch_data)
|
||||||
|
logger.debug(f"Socket.IO: Emitted update for watch {watch.uuid}")
|
||||||
# Emit all watch data periodically
|
|
||||||
self.socketio.emit('watch_data', watches_data)
|
|
||||||
logger.debug(f"Socket.IO: Emitted watch data for {len(watches_data)} watches")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Socket.IO error in background task: {str(e)}")
|
logger.error(f"Socket.IO error in handle_watch_update: {str(e)}")
|
||||||
|
|
||||||
# Wait before next update
|
|
||||||
time.sleep(check_interval)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Socket.IO background task failed: {str(e)}")
|
|
||||||
|
|
||||||
def run(self, host='0.0.0.0', port=5005):
|
def run(self, host='0.0.0.0', port=5005):
|
||||||
"""Run the Socket.IO server on a separate port"""
|
"""Run the Socket.IO server on a separate port"""
|
||||||
# Start the background task when the server starts
|
# Start the background task when the server starts
|
||||||
self.start_background_task()
|
#self.start_background_task()
|
||||||
|
|
||||||
# Run the Socket.IO server
|
# Run the Socket.IO server
|
||||||
# Use 0.0.0.0 to listen on all interfaces
|
# Use 0.0.0.0 to listen on all interfaces
|
||||||
|
|||||||
@@ -31,60 +31,18 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Listen for periodically emitted watch data
|
// Listen for periodically emitted watch data
|
||||||
socket.on('watch_data', function(watches) {
|
socket.on('watch_update', function(watch) {
|
||||||
/* console.log('Received watch data updates');
|
console.log(`Watch update ${watch.uuid}`);
|
||||||
|
|
||||||
|
|
||||||
// Update all watches with their current data
|
|
||||||
watches.forEach(function(watch) {
|
|
||||||
const $watchRow = $('tr[data-watch-uuid="' + watch.uuid + '"]');
|
const $watchRow = $('tr[data-watch-uuid="' + watch.uuid + '"]');
|
||||||
if ($watchRow.length) {
|
if ($watchRow.length) {
|
||||||
updateWatchRow($watchRow, watch);
|
$($watchRow).toggleClass('checking-now', watch.checking_now);
|
||||||
|
$($watchRow).toggleClass('queued', watch.queued);
|
||||||
|
$($watchRow).toggleClass('unviewed', watch.unviewed);
|
||||||
}
|
}
|
||||||
});*/
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Function to update a watch row with new data
|
|
||||||
function updateWatchRow($row, data) {
|
|
||||||
// Update the last-checked time
|
|
||||||
return;
|
|
||||||
const $lastChecked = $row.find('.last-checked');
|
|
||||||
if ($lastChecked.length) {
|
|
||||||
// Update data-timestamp attribute
|
|
||||||
$lastChecked.attr('data-timestamp', data.last_checked);
|
|
||||||
|
|
||||||
// Only show timeago if not currently checking
|
|
||||||
if (!data.checking) {
|
|
||||||
let $timeagoSpan = $lastChecked.find('.timeago');
|
|
||||||
|
|
||||||
// If there's no timeago span yet, create one
|
|
||||||
if (!$timeagoSpan.length) {
|
|
||||||
$lastChecked.html('<span class="timeago"></span>');
|
|
||||||
$timeagoSpan = $lastChecked.find('.timeago');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.last_checked > 0) {
|
|
||||||
// Format as timeago if we have the timeago library available
|
|
||||||
if (typeof timeago !== 'undefined') {
|
|
||||||
$timeagoSpan.text(timeago.format(data.last_checked * 1000));
|
|
||||||
} else {
|
|
||||||
// Simple fallback if timeago isn't available
|
|
||||||
const date = new Date(data.last_checked * 1000);
|
|
||||||
$timeagoSpan.text(date.toLocaleString());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$lastChecked.text('Not yet');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Toggle the unviewed class based on viewed status
|
|
||||||
// $row.toggleClass('unviewed', data.unviewed_history === false);
|
|
||||||
|
|
||||||
// If the watch is currently being checked
|
|
||||||
// $row.toggleClass('checking-now', data.checking === true);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If Socket.IO fails to initialize, just log it and continue
|
// If Socket.IO fails to initialize, just log it and continue
|
||||||
console.log('Socket.IO initialization error:', e);
|
console.log('Socket.IO initialization error:', e);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from .processors.exceptions import ProcessorException
|
|||||||
import changedetectionio.content_fetchers.exceptions as content_fetchers_exceptions
|
import changedetectionio.content_fetchers.exceptions as content_fetchers_exceptions
|
||||||
from changedetectionio.processors.text_json_diff.processor import FilterNotFoundInResponse
|
from changedetectionio.processors.text_json_diff.processor import FilterNotFoundInResponse
|
||||||
from changedetectionio import html_tools
|
from changedetectionio import html_tools
|
||||||
|
from changedetectionio.flask_app import watch_check_completed
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
@@ -245,14 +246,13 @@ class update_worker(threading.Thread):
|
|||||||
|
|
||||||
while not self.app.config.exit.is_set():
|
while not self.app.config.exit.is_set():
|
||||||
update_handler = None
|
update_handler = None
|
||||||
|
watch = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
queued_item_data = self.q.get(block=False)
|
queued_item_data = self.q.get(block=False)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
uuid = queued_item_data.item.get('uuid')
|
uuid = queued_item_data.item.get('uuid')
|
||||||
fetch_start_time = round(time.time()) # Also used for a unique history key for now
|
fetch_start_time = round(time.time()) # Also used for a unique history key for now
|
||||||
self.current_uuid = uuid
|
self.current_uuid = uuid
|
||||||
@@ -272,6 +272,9 @@ class update_worker(threading.Thread):
|
|||||||
logger.info(f"Processing watch UUID {uuid} Priority {queued_item_data.priority} URL {watch['url']}")
|
logger.info(f"Processing watch UUID {uuid} Priority {queued_item_data.priority} URL {watch['url']}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
#watch_check_completed.send(sender=self, watch=watch)
|
||||||
|
watch_check_completed.send(watch_uuid=watch['uuid'])
|
||||||
|
|
||||||
# Processor is what we are using for detecting the "Change"
|
# Processor is what we are using for detecting the "Change"
|
||||||
processor = watch.get('processor', 'text_json_diff')
|
processor = watch.get('processor', 'text_json_diff')
|
||||||
|
|
||||||
@@ -588,12 +591,18 @@ class update_worker(threading.Thread):
|
|||||||
'check_count': count
|
'check_count': count
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
self.current_uuid = None # Done
|
self.current_uuid = None # Done
|
||||||
self.q.task_done()
|
self.q.task_done()
|
||||||
|
|
||||||
|
# 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'])
|
||||||
|
|
||||||
update_handler = None
|
update_handler = None
|
||||||
logger.debug(f"Watch {uuid} done in {time.time()-fetch_start_time:.2f}s")
|
logger.debug(f"Watch {uuid} done in {time.time()-fetch_start_time:.2f}s")
|
||||||
|
|
||||||
|
|
||||||
# Give the CPU time to interrupt
|
# Give the CPU time to interrupt
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
|||||||
@@ -118,3 +118,6 @@ psutil==7.0.0
|
|||||||
|
|
||||||
ruff >= 0.11.2
|
ruff >= 0.11.2
|
||||||
pre_commit >= 4.2.0
|
pre_commit >= 4.2.0
|
||||||
|
|
||||||
|
# For events between checking and socketio updates
|
||||||
|
blinker
|
||||||
|
|||||||
Reference in New Issue
Block a user