mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-10-04 11:08:00 +00:00
An "extra browser" was a name + a ws(s):// endpoint in settings.requests.extra_browsers,
selected by a watch as the magic string 'extra_browser_<name>'. That string resolved to
html_webdriver plus a custom connection URL, which meant the protocol the endpoint was
spoken to came from env vars rather than from the entry: CDP over a WebSocket with
PLAYWRIGHT_DRIVER_URL set, CDP via pyppeteer with FAST_PUPPETEER_CHROME_FETCHER, and the
W3C WebDriver protocol over HTTP on a Selenium-only install - where a wss:// URL cannot
work at all. The form only ever accepted ws:// / wss://, so the feature was silently
broken on exactly the installs that could not honour it.
So it becomes an engine, html_external_cdp, which pins the protocol: a subclass of the
Playwright fetcher that takes its endpoint from the watch's browser config
(FetcherConfig.connection_url) instead of the environment. It is base-only
(ready_to_use=False) because an endpoint is required, so each endpoint is one browser
config ("variation") on the Browsers page - which is what the old settings list was.
update_36 migrates each extra_browsers row to such a variation, keyed by the SAME
'extra_browser_<name>' string watches already hold, so no watch, group override, API value
or global default needs rewriting; the legacy selector simply becomes a real browser-config
id. A row whose endpoint the model rejects is logged and skipped rather than taking the
update chain, and with it startup, down.
Knock-on cleanups, all of which delete a special case rather than add one:
- The proxy opt-out for custom endpoints is now Fetcher.ignores_proxy_setting, asked of
the engine, instead of a string-prefix test in call_browser().
- A live browser-steps / visual-selector session asks the engine where to connect
(Fetcher.browser_steps_connection_url, overridden by html_external_cdp) and refuses an
engine whose supports_browser_steps is False, instead of reading the env var itself and
silently stepping a browser the watch does not check with. That refusal is real: on a
Selenium install html_webdriver cannot drive a live session.
- is_valid_browser_selector() answers "may a watch store this in fetch_backend?" in one
place; the API (create/update/import), the quick-add form validator and the bulk "set
browser" operation each had their own copy, which is how they came to disagree about
whether a browser-config id was acceptable.
- api-spec.yaml's fetch_backend pattern enumerated extra_browser_* while rejecting
browser-config ids and every engine newer than html_webdriver. Valid values are
per-install, so the schema now bounds the string and the handlers do the real check.
- html_external_cdp registers unconditionally (unlike html_playwright_builtin): migrated
configs name it, so it must resolve even without the playwright library, or those
watches would quietly fetch with the plain HTTP client. The library is imported lazily
inside run(), and an unavailable engine now warns instead of falling back silently.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
110 lines
5.9 KiB
Python
110 lines
5.9 KiB
Python
from os import getenv
|
|
from copy import deepcopy
|
|
|
|
from changedetectionio.blueprint.menu_modes import MENU_SIDEBAR_ACTIONMODES_DEFAULT
|
|
from changedetectionio.blueprint.rss import RSS_FORMAT_TYPES, RSS_CONTENT_FORMAT_DEFAULT
|
|
from changedetectionio.model.Tags import TagsDict
|
|
|
|
from changedetectionio.notification import (
|
|
default_notification_body,
|
|
default_notification_format,
|
|
default_notification_title,
|
|
)
|
|
|
|
# Equal to or greater than this number of FilterNotFoundInResponse exceptions will trigger a filter-not-found notification
|
|
_FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT = 6
|
|
DEFAULT_SETTINGS_HEADERS_USERAGENT='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
|
|
|
|
|
|
|
|
class model(dict):
|
|
base_config = {
|
|
'note': "Hello! If you change this file manually, please be sure to restart your changedetection.io instance!",
|
|
'watching': {},
|
|
'settings': {
|
|
'headers': {
|
|
},
|
|
'requests': {
|
|
'extra_proxies': [], # Configurable extra proxies via the UI
|
|
'jitter_seconds': 0,
|
|
'proxy': None, # Preferred proxy connection
|
|
'time_between_check': {'weeks': None, 'days': None, 'hours': 3, 'minutes': None, 'seconds': None},
|
|
# NOTE: 'timeout' and 'default_ua' were migrated out to per-engine browser
|
|
# configs on the /browsers tab (update_35). The plain-client request timeout
|
|
# now defaults via DEFAULT_SETTINGS_REQUESTS_TIMEOUT (processor) and its default
|
|
# User-Agent via DEFAULT_SETTINGS_HEADERS_USERAGENT (requests fetcher).
|
|
'workers': int(getenv("DEFAULT_SETTINGS_REQUESTS_WORKERS", "5")), # Number of threads, lower is better for slow connections
|
|
},
|
|
'application': {
|
|
# Custom notification content
|
|
'all_paused': False,
|
|
'all_muted': False,
|
|
'api_access_token_enabled': True,
|
|
'base_url' : None,
|
|
'empty_pages_are_a_change': False,
|
|
'fetch_backend': getenv("DEFAULT_FETCH_BACKEND", "html_requests"),
|
|
'filter_failure_notification_threshold_attempts': _FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT,
|
|
'global_ignore_text': [], # List of text to ignore when calculating the comparison checksum
|
|
'global_subtractive_selectors': [],
|
|
'history_snapshot_max_length': None,
|
|
'ignore_whitespace': True,
|
|
'ignore_status_codes': False, #@todo implement, as ternary.
|
|
'ssim_threshold': '0.96', # Default SSIM threshold for screenshot comparison
|
|
'notification_body': default_notification_body,
|
|
'notification_format': default_notification_format,
|
|
'notification_title': default_notification_title,
|
|
'notification_urls': [], # Apprise URL list
|
|
'pager_size': 50,
|
|
'password': False,
|
|
'render_anchor_tag_content': False,
|
|
'rss_access_token': None,
|
|
'rss_content_format': RSS_CONTENT_FORMAT_DEFAULT,
|
|
'rss_template_type': 'system_default',
|
|
'rss_template_override': None,
|
|
'rss_diff_length': 5,
|
|
'rss_hide_muted_watches': True,
|
|
'rss_reader_mode': False,
|
|
'scheduler_timezone_default': None, # Default IANA timezone name
|
|
'schema_version' : 0,
|
|
'shared_diff_access': False,
|
|
'strip_ignored_lines': False,
|
|
'tags': None, # Initialized in __init__ with real datastore_path
|
|
# All LLM settings now live nested under application.llm.* (post-migration update_31).
|
|
# Defaults come from LLMSettings.model_validate({}) at read time —
|
|
# no need to pre-seed an empty {} here.
|
|
'webdriver_delay': None , # Extra delay in seconds before extracting text
|
|
'ui': {
|
|
'use_page_title_in_list': True,
|
|
'use_share_watch': False, # "Share watch" link is a power-user feature, off by default
|
|
'open_diff_in_new_tab': True,
|
|
'socket_io_enabled': True,
|
|
'favicons_enabled': True,
|
|
'timeago_format': 'long', # 'long' = "1 minute ago", 'short' = "1m ago"
|
|
'sidebar_mode': MENU_SIDEBAR_ACTIONMODES_DEFAULT, # one of blueprint.menu_modes.MENU_SIDEBAR_ACTIONMODES
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
def __init__(self, *arg, datastore_path=None, **kw):
|
|
super(model, self).__init__(*arg, **kw)
|
|
# Capture any tags data passed in before base_config overwrites the structure
|
|
existing_tags = self.get('settings', {}).get('application', {}).get('tags') or {}
|
|
# CRITICAL: deepcopy to avoid sharing mutable objects between instances
|
|
self.update(deepcopy(self.base_config))
|
|
# TagsDict requires the real datastore_path at runtime (cannot be set at class-definition time)
|
|
if datastore_path is None:
|
|
raise ValueError("App.model() requires 'datastore_path' keyword argument")
|
|
self['settings']['application']['tags'] = TagsDict(existing_tags, datastore_path=datastore_path)
|
|
|
|
|
|
def parse_headers_from_text_file(filepath):
|
|
headers = {}
|
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
for l in f.readlines():
|
|
l = l.strip()
|
|
if not l.startswith('#') and ':' in l:
|
|
(k, v) = l.split(':', 1) # Split only on the first colon
|
|
headers[k.strip()] = v.strip()
|
|
|
|
return headers |