mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-08-24 07:06:37 +00:00
189 lines
9.2 KiB
Python
189 lines
9.2 KiB
Python
import os
|
|
import time
|
|
|
|
from flask import Blueprint, request, make_response, render_template, redirect, url_for, flash, session
|
|
from flask_paginate import Pagination, get_page_parameter
|
|
from flask_babel import gettext as _
|
|
|
|
from changedetectionio import forms
|
|
from changedetectionio import processors
|
|
from changedetectionio import worker_pool
|
|
from changedetectionio.store import ChangeDetectionStore
|
|
from changedetectionio.auth_decorator import login_optionally_required
|
|
# Shared filtering — the single source of truth, also used by the ui blueprint's
|
|
# bulk actions so a filtered view and the actions taken on it always agree.
|
|
from changedetectionio.blueprint.watchlist import filters as wl_filters
|
|
from changedetectionio.blueprint.watchlist.row_context import watch_row_context
|
|
|
|
def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMetaData):
|
|
watchlist_blueprint = Blueprint('watchlist', __name__, template_folder="templates")
|
|
|
|
@watchlist_blueprint.route("/", methods=['GET'])
|
|
@login_optionally_required
|
|
def index():
|
|
active_tag_req = request.args.get('tag', '').lower().strip()
|
|
active_tag_uuid = active_tag = None
|
|
|
|
# Be sure limit_tag is a uuid
|
|
if active_tag_req:
|
|
for uuid, tag in datastore.data['settings']['application'].get('tags', {}).items():
|
|
if active_tag_req == tag.get('title', '').lower().strip() or active_tag_req == uuid:
|
|
active_tag = tag
|
|
active_tag_uuid = uuid
|
|
break
|
|
|
|
# Redirect for the old rss path which used the /?rss=true
|
|
if request.args.get('rss'):
|
|
return redirect(url_for('rss.feed', tag=active_tag_uuid))
|
|
|
|
op = request.args.get('op')
|
|
if op:
|
|
uuid = request.args.get('uuid')
|
|
if op == 'pause':
|
|
datastore.data['watching'][uuid].toggle_pause()
|
|
elif op == 'mute':
|
|
datastore.data['watching'][uuid].toggle_mute()
|
|
|
|
datastore.data['watching'][uuid].commit()
|
|
return redirect(url_for('watchlist.index', tag = active_tag_uuid))
|
|
|
|
# Sort by last_changed and add the uuid which is usually the key..
|
|
sorted_watches = []
|
|
active_processor = request.args.get('processor', '').strip()
|
|
# Toolbar facet counts are tallied over the tag/processor/search context but
|
|
# independently of the active status toggle, so the .seg numbers stay stable
|
|
# as you switch between All / Unread / Deals / With errors.
|
|
errored_count = 0
|
|
deals_count = 0
|
|
unread_count = 0
|
|
processor_counts = {}
|
|
list_filters = wl_filters.list_filters_from_args(datastore, request.args)
|
|
for uuid, watch in datastore.data['watching'].items():
|
|
# The processor facet is counted over the tag/search base (ignoring the
|
|
# selected processor) so every detected processor shows up and you can
|
|
# switch between them.
|
|
if not wl_filters.watch_matches_tag(datastore, watch, list_filters):
|
|
continue
|
|
if not wl_filters.watch_passes_search(watch, list_filters):
|
|
continue
|
|
proc = watch.get('processor')
|
|
if proc:
|
|
processor_counts[proc] = processor_counts.get(proc, 0) + 1
|
|
|
|
# Narrow to the selected processor for the remaining facets + the list.
|
|
if list_filters['processor'] and proc != list_filters['processor']:
|
|
continue
|
|
if watch.get('last_error'):
|
|
errored_count += 1
|
|
if wl_filters.watch_is_deal(watch):
|
|
deals_count += 1
|
|
# Unread = changed and not yet viewed — same test the 'unread' status
|
|
# filter uses, so this count matches what clicking Unread shows.
|
|
if not (watch.viewed or watch.last_changed == 0):
|
|
unread_count += 1
|
|
if wl_filters.watch_passes_status(watch, list_filters):
|
|
sorted_watches.append(watch)
|
|
|
|
form = forms.quickWatchForm(request.form)
|
|
page = request.args.get(get_page_parameter(), type=int, default=1)
|
|
total_count = len(sorted_watches)
|
|
|
|
pagination = Pagination(page=page,
|
|
total=total_count,
|
|
per_page=datastore.data['settings']['application'].get('pager_size', 50),
|
|
css_framework="semantic",
|
|
display_msg=_('displaying <b>{start} - {end}</b> {record_name} in total <b>{total}</b>'),
|
|
record_name=_('records'))
|
|
|
|
sorted_tags = sorted(datastore.data['settings']['application'].get('tags').items(), key=lambda x: x[1]['title'])
|
|
|
|
from changedetectionio import content_fetchers
|
|
available_fetchers = content_fetchers.available_fetchers()
|
|
|
|
from changedetectionio.llm.evaluator import get_llm_config as _get_llm_config
|
|
from changedetectionio.llm.ui_strings import LLM_INTENT_WATCH_PLACEHOLDER
|
|
llm_configured = bool(_get_llm_config(datastore))
|
|
|
|
# Everything the row markup itself needs comes from here, shared with the Socket.IO
|
|
# row push so a live-updated row can't drift from the server-rendered one.
|
|
row_ctx = watch_row_context(datastore,
|
|
active_tag_uuid=active_tag_uuid,
|
|
queued_uuids=update_q.get_queued_uuids())
|
|
|
|
output = render_template(
|
|
"watch-overview.html",
|
|
**row_ctx,
|
|
active_tag=active_tag,
|
|
active_processor=active_processor,
|
|
checking_now_size=len(worker_pool.get_running_uuids()),
|
|
app_rss_token=datastore.data['settings']['application'].get('rss_access_token'),
|
|
errored_count=errored_count,
|
|
deals_count=deals_count,
|
|
unread_count=unread_count,
|
|
processor_counts=processor_counts,
|
|
# body classes for app-wide state; realtime.js keeps these in sync live
|
|
# (has-any-unviewed reveals the "Mark all viewed" button - see _watch_table.scss)
|
|
extra_classes=' '.join(filter(None, ['has-queue' if not update_q.empty() else '',
|
|
'llm-configured' if llm_configured else '',
|
|
'has-any-unviewed' if datastore.unread_changes_count else ''])),
|
|
form=form,
|
|
generate_tag_colors=processors.generate_processor_badge_colors,
|
|
wcag_text_color=processors.wcag_text_color,
|
|
guid=datastore.data['app_guid'],
|
|
available_fetchers=available_fetchers,
|
|
#header=_("todo - tag name etc"),
|
|
hosted_sticky=os.getenv("SALTED_PASS", False) == False,
|
|
now_time_server=round(time.time()),
|
|
pagination=pagination,
|
|
processor_badge_css=processors.get_processor_badge_css(),
|
|
processor_badge_texts=processors.get_processor_badge_texts(),
|
|
queue_size=update_q.qsize(),
|
|
# Active view filters (tag/processor/q/unread/...) so links (e.g. column sorting)
|
|
# can re-apply them and not drop the operator's current filtered view.
|
|
active_filters=wl_filters.filter_query_args(request.args),
|
|
search_q=request.args.get('q', '').strip(),
|
|
sort_attribute=request.args.get('sort') if request.args.get('sort') else request.cookies.get('sort'),
|
|
sort_order=request.args.get('order') if request.args.get('order') else request.cookies.get('order'),
|
|
tags=sorted_tags,
|
|
unread_changes_count=datastore.unread_changes_count,
|
|
watches=sorted_watches,
|
|
llm_configured=llm_configured,
|
|
llm_intent_watch_placeholder=LLM_INTENT_WATCH_PLACEHOLDER,
|
|
)
|
|
|
|
# Return freed template-building memory to the OS immediately.
|
|
# render_template allocates ~20MB of intermediate strings that are freed on return,
|
|
# but glibc keeps those pages mapped in its arenas as RSS. malloc_trim() forces
|
|
# glibc to release them, preventing RSS growth from concurrent Chrome connections.
|
|
try:
|
|
import ctypes
|
|
ctypes.CDLL('libc.so.6').malloc_trim(0)
|
|
except Exception:
|
|
pass
|
|
|
|
if session.get('share-link'):
|
|
del (session['share-link'])
|
|
|
|
resp = make_response(output)
|
|
|
|
# The template can run on cookie or url query info
|
|
if request.args.get('sort'):
|
|
resp.set_cookie('sort', request.args.get('sort'))
|
|
if request.args.get('order'):
|
|
resp.set_cookie('order', request.args.get('order'))
|
|
|
|
return resp
|
|
|
|
@watchlist_blueprint.route("/uuids", methods=['GET'])
|
|
@login_optionally_required
|
|
def uuids():
|
|
"""All watch UUIDs matching the current filter (tag/search/status/processor).
|
|
|
|
Backs the client "select all matching" feature: the watchlist page only
|
|
renders one page of rows, so to select across pages the browser fetches the
|
|
full matching id list from here and holds it in its selection store.
|
|
"""
|
|
from flask import jsonify
|
|
return jsonify({'uuids': wl_filters.matching_watch_uuids(datastore, request.args)})
|
|
|
|
return watchlist_blueprint |