mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2025-11-13 04:56:39 +00:00
Compare commits
42 Commits
updating-j
...
lock-mqtt-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1d3362188 | ||
|
|
1ba29655f5 | ||
|
|
830a0a3a82 | ||
|
|
e110b3ee93 | ||
|
|
3ae9bfa6f9 | ||
|
|
6f3c3b7dfb | ||
|
|
74707909f1 | ||
|
|
d4dac23ba1 | ||
|
|
f9954f93f3 | ||
|
|
1a43b112dc | ||
|
|
db59bf73e1 | ||
|
|
8aac7bccbe | ||
|
|
9449c59fbb | ||
|
|
21f4ba2208 | ||
|
|
daef1cd036 | ||
|
|
56b365df40 | ||
|
|
8e5bf91965 | ||
|
|
1ae59551be | ||
|
|
a176468fb8 | ||
|
|
8fac593201 | ||
|
|
e3b8c0f5af | ||
|
|
514fd7f91e | ||
|
|
38c4768b92 | ||
|
|
6555d99044 | ||
|
|
e719dbd19b | ||
|
|
b28a8316cc | ||
|
|
e609a2d048 | ||
|
|
994d34c776 | ||
|
|
de776800e9 | ||
|
|
8b8ed58f20 | ||
|
|
79c6d765de | ||
|
|
c6db7fc90e | ||
|
|
bc587efae2 | ||
|
|
6ee6be1a5f | ||
|
|
c83485094b | ||
|
|
387ce32e6f | ||
|
|
6b9a788d75 | ||
|
|
14e632bc19 | ||
|
|
52c895b2e8 | ||
|
|
a62043e086 | ||
|
|
3d390b6ea4 | ||
|
|
301a40ca34 |
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -4,6 +4,10 @@ updates:
|
|||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
"caronc/apprise":
|
||||||
|
versioning-strategy: "increase"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
groups:
|
groups:
|
||||||
all:
|
all:
|
||||||
patterns:
|
patterns:
|
||||||
|
|||||||
13
.github/workflows/test-only.yml
vendored
13
.github/workflows/test-only.yml
vendored
@@ -72,7 +72,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
# Playwright via Sockpuppetbrowser fetch
|
# Playwright via Sockpuppetbrowser fetch
|
||||||
# tests/visualselector/test_fetch_data.py will do browser steps
|
# tests/visualselector/test_fetch_data.py will do browser steps
|
||||||
docker run --rm -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/fetchers/test_content.py && pytest tests/test_errorhandling.py && pytest tests/visualselector/test_fetch_data.py'
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_content.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_errorhandling.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/visualselector/test_fetch_data.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_custom_js_before_content.py'
|
||||||
|
|
||||||
|
|
||||||
- name: Playwright and SocketPuppetBrowser - Headers and requests
|
- name: Playwright and SocketPuppetBrowser - Headers and requests
|
||||||
run: |
|
run: |
|
||||||
@@ -87,8 +91,11 @@ jobs:
|
|||||||
# STRAIGHT TO CDP
|
# STRAIGHT TO CDP
|
||||||
- name: Pyppeteer and SocketPuppetBrowser - Specific tests in built container
|
- name: Pyppeteer and SocketPuppetBrowser - Specific tests in built container
|
||||||
run: |
|
run: |
|
||||||
# Playwright via Sockpuppetbrowser fetch
|
# Playwright via Sockpuppetbrowser fetch
|
||||||
docker run --rm -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" -e "FAST_PUPPETEER_CHROME_FETCHER=True" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/fetchers/test_content.py && pytest tests/test_errorhandling.py && pytest tests/visualselector/test_fetch_data.py'
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_content.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_errorhandling.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/visualselector/test_fetch_data.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_custom_js_before_content.py'
|
||||||
|
|
||||||
- name: Pyppeteer and SocketPuppetBrowser - Headers and requests checks
|
- name: Pyppeteer and SocketPuppetBrowser - Headers and requests checks
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ Contributing is always welcome!
|
|||||||
|
|
||||||
I am no professional flask developer, if you know a better way that something can be done, please let me know!
|
I am no professional flask developer, if you know a better way that something can be done, please let me know!
|
||||||
|
|
||||||
Otherwise, it's always best to PR into the `dev` branch.
|
Otherwise, it's always best to PR into the `master` branch.
|
||||||
|
|
||||||
Please be sure that all new functionality has a matching test!
|
Please be sure that all new functionality has a matching test!
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
recursive-include changedetectionio/api *
|
recursive-include changedetectionio/api *
|
||||||
recursive-include changedetectionio/blueprint *
|
recursive-include changedetectionio/blueprint *
|
||||||
|
recursive-include changedetectionio/content_fetchers *
|
||||||
recursive-include changedetectionio/model *
|
recursive-include changedetectionio/model *
|
||||||
recursive-include changedetectionio/processors *
|
recursive-include changedetectionio/processors *
|
||||||
recursive-include changedetectionio/res *
|
|
||||||
recursive-include changedetectionio/static *
|
recursive-include changedetectionio/static *
|
||||||
recursive-include changedetectionio/templates *
|
recursive-include changedetectionio/templates *
|
||||||
recursive-include changedetectionio/tests *
|
recursive-include changedetectionio/tests *
|
||||||
|
|||||||
@@ -91,6 +91,14 @@ We [recommend and use Bright Data](https://brightdata.grsm.io/n0r16zf7eivq) glob
|
|||||||
|
|
||||||
Please :star: star :star: this project and help it grow! https://github.com/dgtlmoon/changedetection.io/
|
Please :star: star :star: this project and help it grow! https://github.com/dgtlmoon/changedetection.io/
|
||||||
|
|
||||||
|
### We have a Chrome extension!
|
||||||
|
|
||||||
|
Easily add the current web page to your changedetection.io tool, simply install the extension and click "Sync" to connect it to your existing changedetection.io install.
|
||||||
|
|
||||||
|
[<img src="./docs/chrome-extension-screenshot.png" style="max-width:80%;" alt="Chrome Extension to easily add the current web-page to detect a change." title="Chrome Extension to easily add the current web-page to detect a change." />](https://chromewebstore.google.com/detail/changedetectionio-website/kefcfmgmlhmankjmnbijimhofdjekbop)
|
||||||
|
|
||||||
|
[Goto the Chrome Webstore to download the extension.](https://chromewebstore.google.com/detail/changedetectionio-website/kefcfmgmlhmankjmnbijimhofdjekbop)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
||||||
|
|
||||||
__version__ = '0.45.14'
|
__version__ = '0.45.20'
|
||||||
|
|
||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
import os
|
import os
|
||||||
#os.environ['EVENTLET_NO_GREENDNS'] = 'yes'
|
#os.environ['EVENTLET_NO_GREENDNS'] = 'yes'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
|
|
||||||
from flask_expects_json import expects_json
|
from flask_expects_json import expects_json
|
||||||
from changedetectionio import queuedWatchMetaData
|
from changedetectionio import queuedWatchMetaData
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
from flask import Blueprint, request, make_response
|
from flask import Blueprint, request, make_response
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import re
|
|||||||
from random import randint
|
from random import randint
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from changedetectionio.content_fetchers.base import manage_user_agent
|
||||||
|
|
||||||
# Two flags, tell the JS which of the "Selector" or "Value" field should be enabled in the front end
|
# Two flags, tell the JS which of the "Selector" or "Value" field should be enabled in the front end
|
||||||
# 0- off, 1- on
|
# 0- off, 1- on
|
||||||
browser_step_ui_config = {'Choose one': '0 0',
|
browser_step_ui_config = {'Choose one': '0 0',
|
||||||
@@ -178,6 +180,7 @@ class browsersteps_live_ui(steppable_browser_interface):
|
|||||||
stale = False
|
stale = False
|
||||||
# bump and kill this if idle after X sec
|
# bump and kill this if idle after X sec
|
||||||
age_start = 0
|
age_start = 0
|
||||||
|
headers = {}
|
||||||
|
|
||||||
# use a special driver, maybe locally etc
|
# use a special driver, maybe locally etc
|
||||||
command_executor = os.getenv(
|
command_executor = os.getenv(
|
||||||
@@ -192,7 +195,8 @@ class browsersteps_live_ui(steppable_browser_interface):
|
|||||||
|
|
||||||
browser_type = os.getenv("PLAYWRIGHT_BROWSER_TYPE", 'chromium').strip('"')
|
browser_type = os.getenv("PLAYWRIGHT_BROWSER_TYPE", 'chromium').strip('"')
|
||||||
|
|
||||||
def __init__(self, playwright_browser, proxy=None):
|
def __init__(self, playwright_browser, proxy=None, headers=None):
|
||||||
|
self.headers = headers or {}
|
||||||
self.age_start = time.time()
|
self.age_start = time.time()
|
||||||
self.playwright_browser = playwright_browser
|
self.playwright_browser = playwright_browser
|
||||||
if self.context is None:
|
if self.context is None:
|
||||||
@@ -206,16 +210,17 @@ class browsersteps_live_ui(steppable_browser_interface):
|
|||||||
|
|
||||||
# @todo handle multiple contexts, bind a unique id from the browser on each req?
|
# @todo handle multiple contexts, bind a unique id from the browser on each req?
|
||||||
self.context = self.playwright_browser.new_context(
|
self.context = self.playwright_browser.new_context(
|
||||||
# @todo
|
accept_downloads=False, # Should never be needed
|
||||||
# user_agent=request_headers['User-Agent'] if request_headers.get('User-Agent') else 'Mozilla/5.0',
|
bypass_csp=True, # This is needed to enable JavaScript execution on GitHub and others
|
||||||
# proxy=self.proxy,
|
extra_http_headers=self.headers,
|
||||||
# This is needed to enable JavaScript execution on GitHub and others
|
ignore_https_errors=True,
|
||||||
bypass_csp=True,
|
proxy=proxy,
|
||||||
# Should never be needed
|
service_workers=os.getenv('PLAYWRIGHT_SERVICE_WORKERS', 'allow'),
|
||||||
accept_downloads=False,
|
# Should be `allow` or `block` - sites like YouTube can transmit large amounts of data via Service Workers
|
||||||
proxy=proxy
|
user_agent=manage_user_agent(headers=self.headers),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
self.page = self.context.new_page()
|
self.page = self.context.new_page()
|
||||||
|
|
||||||
# self.page.set_default_navigation_timeout(keep_open)
|
# self.page.set_default_navigation_timeout(keep_open)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from playwright.sync_api import PlaywrightContextManager
|
from playwright.sync_api import PlaywrightContextManager
|
||||||
import asyncio
|
|
||||||
|
|
||||||
# So playwright wants to run as a context manager, but we do something horrible and hacky
|
# So playwright wants to run as a context manager, but we do something horrible and hacky
|
||||||
# we are holding the session open for as long as possible, then shutting it down, and opening a new one
|
# we are holding the session open for as long as possible, then shutting it down, and opening a new one
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
from flask import Blueprint, flash, redirect, url_for
|
from flask import Blueprint, flash, redirect, url_for
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
from changedetectionio.store import ChangeDetectionStore
|
from changedetectionio.store import ChangeDetectionStore
|
||||||
|
|||||||
@@ -12,9 +12,15 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
|||||||
from .form import SingleTag
|
from .form import SingleTag
|
||||||
add_form = SingleTag(request.form)
|
add_form = SingleTag(request.form)
|
||||||
sorted_tags = sorted(datastore.data['settings']['application'].get('tags').items(), key=lambda x: x[1]['title'])
|
sorted_tags = sorted(datastore.data['settings']['application'].get('tags').items(), key=lambda x: x[1]['title'])
|
||||||
|
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
tag_count = Counter(tag for watch in datastore.data['watching'].values() if watch.get('tags') for tag in watch['tags'])
|
||||||
|
|
||||||
output = render_template("groups-overview.html",
|
output = render_template("groups-overview.html",
|
||||||
form=add_form,
|
|
||||||
available_tags=sorted_tags,
|
available_tags=sorted_tags,
|
||||||
|
form=add_form,
|
||||||
|
tag_count=tag_count
|
||||||
)
|
)
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
|
{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
|
||||||
{% from '_common_fields.jinja' import render_common_settings_form %}
|
{% from '_common_fields.jinja' import render_common_settings_form %}
|
||||||
<script>
|
<script>
|
||||||
const notification_base_url="{{url_for('ajax_callback_send_notification_test', watch_uuid=uuid)}}";
|
const notification_base_url="{{url_for('ajax_callback_send_notification_test', mode="group-settings")}}";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
|
<th># Watches</th>
|
||||||
<th>Tag / Label name</th>
|
<th>Tag / Label name</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -45,7 +46,8 @@
|
|||||||
<td class="watch-controls">
|
<td class="watch-controls">
|
||||||
<a class="link-mute state-{{'on' if tag.notification_muted else 'off'}}" href="{{url_for('tags.mute', uuid=tag.uuid)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="Mute notifications" title="Mute notifications" class="icon icon-mute" ></a>
|
<a class="link-mute state-{{'on' if tag.notification_muted else 'off'}}" href="{{url_for('tags.mute', uuid=tag.uuid)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="Mute notifications" title="Mute notifications" class="icon icon-mute" ></a>
|
||||||
</td>
|
</td>
|
||||||
<td class="title-col inline">{{tag.title}}</td>
|
<td>{{ "{:,}".format(tag_count[uuid]) if uuid in tag_count else 0 }}</td>
|
||||||
|
<td class="title-col inline"> <a href="{{url_for('index', tag=uuid) }}">{{ tag.title }}</a></td>
|
||||||
<td>
|
<td>
|
||||||
<a class="pure-button pure-button-primary" href="{{ url_for('tags.form_tag_edit', uuid=uuid) }}">Edit</a>
|
<a class="pure-button pure-button-primary" href="{{ url_for('tags.form_tag_edit', uuid=uuid) }}">Edit</a>
|
||||||
<a class="pure-button pure-button-primary" href="{{ url_for('tags.delete', uuid=uuid) }}" title="Deletes and removes tag">Delete</a>
|
<a class="pure-button pure-button-primary" href="{{ url_for('tags.delete', uuid=uuid) }}" title="Deletes and removes tag">Delete</a>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
|
from loguru import logger
|
||||||
from changedetectionio.content_fetchers.exceptions import BrowserStepsStepException
|
from changedetectionio.content_fetchers.exceptions import BrowserStepsStepException
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -29,10 +29,15 @@ def available_fetchers():
|
|||||||
# rather than site-specific.
|
# rather than site-specific.
|
||||||
use_playwright_as_chrome_fetcher = os.getenv('PLAYWRIGHT_DRIVER_URL', False)
|
use_playwright_as_chrome_fetcher = os.getenv('PLAYWRIGHT_DRIVER_URL', False)
|
||||||
if use_playwright_as_chrome_fetcher:
|
if use_playwright_as_chrome_fetcher:
|
||||||
|
# @note - For now, browser steps always uses playwright
|
||||||
if not strtobool(os.getenv('FAST_PUPPETEER_CHROME_FETCHER', 'False')):
|
if not strtobool(os.getenv('FAST_PUPPETEER_CHROME_FETCHER', 'False')):
|
||||||
|
logger.debug('Using Playwright library as fetcher')
|
||||||
from .playwright import fetcher as html_webdriver
|
from .playwright import fetcher as html_webdriver
|
||||||
else:
|
else:
|
||||||
|
logger.debug('Using direct Python Puppeteer library as fetcher')
|
||||||
from .puppeteer import fetcher as html_webdriver
|
from .puppeteer import fetcher as html_webdriver
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
logger.debug("Falling back to selenium as fetcher")
|
||||||
from .webdriver_selenium import fetcher as html_webdriver
|
from .webdriver_selenium import fetcher as html_webdriver
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,40 @@ from loguru import logger
|
|||||||
from changedetectionio.content_fetchers import BrowserStepsStepException
|
from changedetectionio.content_fetchers import BrowserStepsStepException
|
||||||
|
|
||||||
|
|
||||||
|
def manage_user_agent(headers, current_ua=''):
|
||||||
|
"""
|
||||||
|
Basic setting of user-agent
|
||||||
|
|
||||||
|
NOTE!!!!!! The service that does the actual Chrome fetching should handle any anti-robot techniques
|
||||||
|
THERE ARE MANY WAYS THAT IT CAN BE DETECTED AS A ROBOT!!
|
||||||
|
This does not take care of
|
||||||
|
- Scraping of 'navigator' (platform, productSub, vendor, oscpu etc etc) browser object (navigator.appVersion) etc
|
||||||
|
- TCP/IP fingerprint JA3 etc
|
||||||
|
- Graphic rendering fingerprinting
|
||||||
|
- Your IP being obviously in a pool of bad actors
|
||||||
|
- Too many requests
|
||||||
|
- Scraping of SCH-UA browser replies (thanks google!!)
|
||||||
|
- Scraping of ServiceWorker, new window calls etc
|
||||||
|
|
||||||
|
See https://filipvitas.medium.com/how-to-set-user-agent-header-with-puppeteer-js-and-not-fail-28c7a02165da
|
||||||
|
Puppeteer requests https://github.com/dgtlmoon/pyppeteerstealth
|
||||||
|
|
||||||
|
:param page:
|
||||||
|
:param headers:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
# Ask it what the user agent is, if its obviously ChromeHeadless, switch it to the default
|
||||||
|
ua_in_custom_headers = next((v for k, v in headers.items() if k.lower() == "user-agent"), None)
|
||||||
|
if ua_in_custom_headers:
|
||||||
|
return ua_in_custom_headers
|
||||||
|
|
||||||
|
if not ua_in_custom_headers and current_ua:
|
||||||
|
current_ua = current_ua.replace('HeadlessChrome', 'Chrome')
|
||||||
|
return current_ua
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class Fetcher():
|
class Fetcher():
|
||||||
browser_connection_is_custom = None
|
browser_connection_is_custom = None
|
||||||
browser_connection_url = None
|
browser_connection_url = None
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import os
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from changedetectionio.content_fetchers.base import Fetcher
|
|
||||||
|
from changedetectionio.content_fetchers.base import Fetcher, manage_user_agent
|
||||||
from changedetectionio.content_fetchers.exceptions import PageUnloadable, Non200ErrorCodeReceived, EmptyReply, ScreenshotUnavailable
|
from changedetectionio.content_fetchers.exceptions import PageUnloadable, Non200ErrorCodeReceived, EmptyReply, ScreenshotUnavailable
|
||||||
|
|
||||||
class fetcher(Fetcher):
|
class fetcher(Fetcher):
|
||||||
@@ -102,19 +103,16 @@ class fetcher(Fetcher):
|
|||||||
# Set user agent to prevent Cloudflare from blocking the browser
|
# Set user agent to prevent Cloudflare from blocking the browser
|
||||||
# Use the default one configured in the App.py model that's passed from fetch_site_status.py
|
# Use the default one configured in the App.py model that's passed from fetch_site_status.py
|
||||||
context = browser.new_context(
|
context = browser.new_context(
|
||||||
user_agent={k.lower(): v for k, v in request_headers.items()}.get('user-agent', None),
|
accept_downloads=False, # Should never be needed
|
||||||
|
bypass_csp=True, # This is needed to enable JavaScript execution on GitHub and others
|
||||||
|
extra_http_headers=request_headers,
|
||||||
|
ignore_https_errors=True,
|
||||||
proxy=self.proxy,
|
proxy=self.proxy,
|
||||||
# This is needed to enable JavaScript execution on GitHub and others
|
service_workers=os.getenv('PLAYWRIGHT_SERVICE_WORKERS', 'allow'), # Should be `allow` or `block` - sites like YouTube can transmit large amounts of data via Service Workers
|
||||||
bypass_csp=True,
|
user_agent=manage_user_agent(headers=request_headers),
|
||||||
# Should be `allow` or `block` - sites like YouTube can transmit large amounts of data via Service Workers
|
|
||||||
service_workers=os.getenv('PLAYWRIGHT_SERVICE_WORKERS', 'allow'),
|
|
||||||
# Should never be needed
|
|
||||||
accept_downloads=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.page = context.new_page()
|
self.page = context.new_page()
|
||||||
if len(request_headers):
|
|
||||||
context.set_extra_http_headers(request_headers)
|
|
||||||
|
|
||||||
# Listen for all console events and handle errors
|
# Listen for all console events and handle errors
|
||||||
self.page.on("console", lambda msg: print(f"Playwright console: Watch URL: {url} {msg.type}: {msg.text} {msg.args}"))
|
self.page.on("console", lambda msg: print(f"Playwright console: Watch URL: {url} {msg.type}: {msg.text} {msg.args}"))
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import websockets.exceptions
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from changedetectionio.content_fetchers.base import Fetcher
|
|
||||||
|
from changedetectionio.content_fetchers.base import Fetcher, manage_user_agent
|
||||||
from changedetectionio.content_fetchers.exceptions import PageUnloadable, Non200ErrorCodeReceived, EmptyReply, BrowserFetchTimedOut, BrowserConnectError
|
from changedetectionio.content_fetchers.exceptions import PageUnloadable, Non200ErrorCodeReceived, EmptyReply, BrowserFetchTimedOut, BrowserConnectError
|
||||||
|
|
||||||
|
|
||||||
@@ -100,10 +101,11 @@ class fetcher(Fetcher):
|
|||||||
else:
|
else:
|
||||||
self.page = await browser.newPage()
|
self.page = await browser.newPage()
|
||||||
|
|
||||||
|
await self.page.setUserAgent(manage_user_agent(headers=request_headers, current_ua=await self.page.evaluate('navigator.userAgent')))
|
||||||
|
|
||||||
await self.page.setBypassCSP(True)
|
await self.page.setBypassCSP(True)
|
||||||
if request_headers:
|
if request_headers:
|
||||||
await self.page.setExtraHTTPHeaders(request_headers)
|
await self.page.setExtraHTTPHeaders(request_headers)
|
||||||
# @todo check user-agent worked
|
|
||||||
|
|
||||||
# SOCKS5 with authentication is not supported (yet)
|
# SOCKS5 with authentication is not supported (yet)
|
||||||
# https://github.com/microsoft/playwright/issues/10567
|
# https://github.com/microsoft/playwright/issues/10567
|
||||||
@@ -212,8 +214,12 @@ class fetcher(Fetcher):
|
|||||||
logger.error('ERROR: Failed to get viewport-only reduced screenshot :(')
|
logger.error('ERROR: Failed to get viewport-only reduced screenshot :(')
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
|
# It's good to log here in the case that the browser crashes on shutting down but we still get the data we need
|
||||||
|
logger.success(f"Fetching '{url}' complete, closing page")
|
||||||
await self.page.close()
|
await self.page.close()
|
||||||
|
logger.success(f"Fetching '{url}' complete, closing browser")
|
||||||
await browser.close()
|
await browser.close()
|
||||||
|
logger.success(f"Fetching '{url}' complete, exiting puppeteer fetch.")
|
||||||
|
|
||||||
async def main(self, **kwargs):
|
async def main(self, **kwargs):
|
||||||
await self.fetch_page(**kwargs)
|
await self.fetch_page(**kwargs)
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ function isItemInStock() {
|
|||||||
'as soon as stock is available',
|
'as soon as stock is available',
|
||||||
'ausverkauft', // sold out
|
'ausverkauft', // sold out
|
||||||
'available for back order',
|
'available for back order',
|
||||||
'back-order or out of stock',
|
'awaiting stock',
|
||||||
'back in stock soon',
|
'back in stock soon',
|
||||||
|
'back-order or out of stock',
|
||||||
'backordered',
|
'backordered',
|
||||||
'benachrichtigt mich', // notify me
|
'benachrichtigt mich', // notify me
|
||||||
'brak na stanie',
|
'brak na stanie',
|
||||||
@@ -57,16 +58,20 @@ function isItemInStock() {
|
|||||||
'sold-out',
|
'sold-out',
|
||||||
'temporarily out of stock',
|
'temporarily out of stock',
|
||||||
'temporarily unavailable',
|
'temporarily unavailable',
|
||||||
|
'there were no search results for',
|
||||||
|
'this item is currently unavailable',
|
||||||
'tickets unavailable',
|
'tickets unavailable',
|
||||||
'tijdelijk uitverkocht',
|
'tijdelijk uitverkocht',
|
||||||
'unavailable tickets',
|
'unavailable tickets',
|
||||||
'vorbestellung ist bald möglich',
|
'vorbestellung ist bald möglich',
|
||||||
|
'we couldn\'t find any products that match',
|
||||||
'we do not currently have an estimate of when this product will be back in stock.',
|
'we do not currently have an estimate of when this product will be back in stock.',
|
||||||
'we don\'t know when or if this item will be back in stock.',
|
'we don\'t know when or if this item will be back in stock.',
|
||||||
|
'we were not able to find a match',
|
||||||
'zur zeit nicht an lager',
|
'zur zeit nicht an lager',
|
||||||
'品切れ',
|
'品切れ',
|
||||||
'已售完',
|
|
||||||
'已售',
|
'已售',
|
||||||
|
'已售完',
|
||||||
'품절'
|
'품절'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import queue
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from threading import Event
|
from threading import Event
|
||||||
|
|
||||||
@@ -30,6 +30,7 @@ from flask_compress import Compress as FlaskCompress
|
|||||||
from flask_login import current_user
|
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_wtf import CSRFProtect
|
from flask_wtf import CSRFProtect
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -53,6 +54,9 @@ app = Flask(__name__,
|
|||||||
static_folder="static",
|
static_folder="static",
|
||||||
template_folder="templates")
|
template_folder="templates")
|
||||||
|
|
||||||
|
# Enable CORS, especially useful for the Chrome extension to operate from anywhere
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
# Super handy for compressing large BrowserSteps responses and others
|
# Super handy for compressing large BrowserSteps responses and others
|
||||||
FlaskCompress(app)
|
FlaskCompress(app)
|
||||||
|
|
||||||
@@ -512,21 +516,38 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
|
|
||||||
watch = datastore.data['watching'].get(watch_uuid) if watch_uuid else None
|
watch = datastore.data['watching'].get(watch_uuid) if watch_uuid else None
|
||||||
|
|
||||||
# validate URLS
|
notification_urls = request.form['notification_urls'].strip().splitlines()
|
||||||
if not len(request.form['notification_urls'].strip()):
|
|
||||||
return make_response({'error': 'No Notification URLs set'}, 400)
|
|
||||||
|
|
||||||
for server_url in request.form['notification_urls'].splitlines():
|
if not notification_urls:
|
||||||
if len(server_url.strip()):
|
logger.debug("Test notification - Trying by group/tag in the edit form if available")
|
||||||
if not apobj.add(server_url):
|
# On an edit page, we should also fire off to the tags if they have notifications
|
||||||
message = '{} is not a valid AppRise URL.'.format(server_url)
|
if request.form.get('tags') and request.form['tags'].strip():
|
||||||
return make_response({'error': message}, 400)
|
for k in request.form['tags'].split(','):
|
||||||
|
tag = datastore.tag_exists_by_name(k.strip())
|
||||||
|
notification_urls = tag.get('notifications_urls') if tag and tag.get('notifications_urls') else None
|
||||||
|
|
||||||
|
is_global_settings_form = request.args.get('mode', '') == 'global-settings'
|
||||||
|
is_group_settings_form = request.args.get('mode', '') == 'group-settings'
|
||||||
|
if not notification_urls and not is_global_settings_form and not is_group_settings_form:
|
||||||
|
# In the global settings, use only what is typed currently in the text box
|
||||||
|
logger.debug("Test notification - Trying by global system settings notifications")
|
||||||
|
if datastore.data['settings']['application'].get('notification_urls'):
|
||||||
|
notification_urls = datastore.data['settings']['application']['notification_urls']
|
||||||
|
|
||||||
|
|
||||||
|
if not notification_urls:
|
||||||
|
return 'No Notification URLs set/found'
|
||||||
|
|
||||||
|
for n_url in notification_urls:
|
||||||
|
if len(n_url.strip()):
|
||||||
|
if not apobj.add(n_url):
|
||||||
|
return f'Error - {n_url} is not a valid AppRise URL.'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# use the same as when it is triggered, but then override it with the form test values
|
# use the same as when it is triggered, but then override it with the form test values
|
||||||
n_object = {
|
n_object = {
|
||||||
'watch_url': request.form['window_url'],
|
'watch_url': request.form['window_url'],
|
||||||
'notification_urls': request.form['notification_urls'].splitlines()
|
'notification_urls': notification_urls
|
||||||
}
|
}
|
||||||
|
|
||||||
# Only use if present, if not set in n_object it should use the default system value
|
# Only use if present, if not set in n_object it should use the default system value
|
||||||
@@ -545,7 +566,7 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return make_response({'error': str(e)}, 400)
|
return make_response({'error': str(e)}, 400)
|
||||||
|
|
||||||
return 'OK'
|
return 'OK - Sent test notifications'
|
||||||
|
|
||||||
|
|
||||||
@app.route("/clear_history/<string:uuid>", methods=['GET'])
|
@app.route("/clear_history/<string:uuid>", methods=['GET'])
|
||||||
@@ -582,6 +603,12 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
output = render_template("clear_all_history.html")
|
output = render_template("clear_all_history.html")
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
def _watch_has_tag_options_set(watch):
|
||||||
|
"""This should be fixed better so that Tag is some proper Model, a tag is just a Watch also"""
|
||||||
|
for tag_uuid, tag in datastore.data['settings']['application'].get('tags', {}).items():
|
||||||
|
if tag_uuid in watch.get('tags', []) and (tag.get('include_filters') or tag.get('subtractive_selectors')):
|
||||||
|
return True
|
||||||
|
|
||||||
@app.route("/edit/<string:uuid>", methods=['GET', 'POST'])
|
@app.route("/edit/<string:uuid>", methods=['GET', 'POST'])
|
||||||
@login_optionally_required
|
@login_optionally_required
|
||||||
# https://stackoverflow.com/questions/42984453/wtforms-populate-form-with-data-if-data-exists
|
# https://stackoverflow.com/questions/42984453/wtforms-populate-form-with-data-if-data-exists
|
||||||
@@ -752,6 +779,7 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
has_default_notification_urls=True if len(datastore.data['settings']['application']['notification_urls']) else False,
|
has_default_notification_urls=True if len(datastore.data['settings']['application']['notification_urls']) else False,
|
||||||
has_empty_checktime=using_default_check_time,
|
has_empty_checktime=using_default_check_time,
|
||||||
has_extra_headers_file=len(datastore.get_all_headers_in_textfile_for_watch(uuid=uuid)) > 0,
|
has_extra_headers_file=len(datastore.get_all_headers_in_textfile_for_watch(uuid=uuid)) > 0,
|
||||||
|
has_special_tag_options=_watch_has_tag_options_set(watch=watch),
|
||||||
is_html_webdriver=is_html_webdriver,
|
is_html_webdriver=is_html_webdriver,
|
||||||
jq_support=jq_support,
|
jq_support=jq_support,
|
||||||
playwright_enabled=os.getenv('PLAYWRIGHT_DRIVER_URL', False),
|
playwright_enabled=os.getenv('PLAYWRIGHT_DRIVER_URL', False),
|
||||||
@@ -1275,9 +1303,8 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
|
|
||||||
url = request.form.get('url').strip()
|
url = request.form.get('url').strip()
|
||||||
if datastore.url_exists(url):
|
if datastore.url_exists(url):
|
||||||
flash('The URL {} already exists'.format(url), "error")
|
flash(f'Warning, URL {url} already exists', "notice")
|
||||||
return redirect(url_for('index'))
|
|
||||||
|
|
||||||
add_paused = request.form.get('edit_and_watch_submit_button') != None
|
add_paused = request.form.get('edit_and_watch_submit_button') != None
|
||||||
processor = request.form.get('processor', 'text_json_diff')
|
processor = request.form.get('processor', 'text_json_diff')
|
||||||
new_uuid = datastore.add_watch(url=url, tag=request.form.get('tags').strip(), extras={'paused': add_paused, 'processor': processor})
|
new_uuid = datastore.add_watch(url=url, tag=request.form.get('tags').strip(), extras={'paused': add_paused, 'processor': processor})
|
||||||
@@ -1427,6 +1454,13 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid, 'skip_when_checksum_same': False}))
|
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid, 'skip_when_checksum_same': False}))
|
||||||
flash("{} watches queued for rechecking".format(len(uuids)))
|
flash("{} watches queued for rechecking".format(len(uuids)))
|
||||||
|
|
||||||
|
elif (op == 'clear-errors'):
|
||||||
|
for uuid in uuids:
|
||||||
|
uuid = uuid.strip()
|
||||||
|
if datastore.data['watching'].get(uuid):
|
||||||
|
datastore.data['watching'][uuid]["last_error"] = False
|
||||||
|
flash(f"{len(uuids)} watches errors cleared")
|
||||||
|
|
||||||
elif (op == 'clear-history'):
|
elif (op == 'clear-history'):
|
||||||
for uuid in uuids:
|
for uuid in uuids:
|
||||||
uuid = uuid.strip()
|
uuid = uuid.strip()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
|
|
||||||
from wtforms import (
|
from wtforms import (
|
||||||
BooleanField,
|
BooleanField,
|
||||||
|
|||||||
@@ -169,14 +169,14 @@ def xpath1_filter(xpath_filter, html_content, append_pretty_line_formatting=Fals
|
|||||||
# And where the matched result doesn't include something that will cause Inscriptis to add a newline
|
# And where the matched result doesn't include something that will cause Inscriptis to add a newline
|
||||||
# (This way each 'match' reliably has a new-line in the diff)
|
# (This way each 'match' reliably has a new-line in the diff)
|
||||||
# Divs are converted to 4 whitespaces by inscriptis
|
# Divs are converted to 4 whitespaces by inscriptis
|
||||||
if append_pretty_line_formatting and len(html_block) and (not hasattr( element, 'tag' ) or not element.tag in (['br', 'hr', 'div', 'p'])):
|
if append_pretty_line_formatting and len(html_block) and (not hasattr(element, 'tag') or not element.tag in (['br', 'hr', 'div', 'p'])):
|
||||||
html_block += TEXT_FILTER_LIST_LINE_SUFFIX
|
html_block += TEXT_FILTER_LIST_LINE_SUFFIX
|
||||||
|
|
||||||
if type(element) == etree._ElementStringResult:
|
# Some kind of text, UTF-8 or other
|
||||||
html_block += str(element)
|
if isinstance(element, (str, bytes)):
|
||||||
elif type(element) == etree._ElementUnicodeResult:
|
html_block += element
|
||||||
html_block += str(element)
|
|
||||||
else:
|
else:
|
||||||
|
# Return the HTML which will get parsed as text
|
||||||
html_block += etree.tostring(element, pretty_print=True).decode('utf-8')
|
html_block += etree.tostring(element, pretty_print=True).decode('utf-8')
|
||||||
|
|
||||||
return html_block
|
return html_block
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@@ -362,6 +362,7 @@ class model(dict):
|
|||||||
# @todo bump static cache of the last timestamp so we dont need to examine the file to set a proper ''viewed'' status
|
# @todo bump static cache of the last timestamp so we dont need to examine the file to set a proper ''viewed'' status
|
||||||
return snapshot_fname
|
return snapshot_fname
|
||||||
|
|
||||||
|
@property
|
||||||
@property
|
@property
|
||||||
def has_empty_checktime(self):
|
def has_empty_checktime(self):
|
||||||
# using all() + dictionary comprehension
|
# using all() + dictionary comprehension
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
class difference_detection_processor():
|
class difference_detection_processor():
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class perform_site_check(difference_detection_processor):
|
|||||||
include_filters_from_tags = self.datastore.get_tag_overrides_for_watch(uuid=uuid, attr='include_filters')
|
include_filters_from_tags = self.datastore.get_tag_overrides_for_watch(uuid=uuid, attr='include_filters')
|
||||||
|
|
||||||
# 1845 - remove duplicated filters in both group and watch include filter
|
# 1845 - remove duplicated filters in both group and watch include filter
|
||||||
include_filters_rule = list({*watch.get('include_filters', []), *include_filters_from_tags})
|
include_filters_rule = list(dict.fromkeys(watch.get('include_filters', []) + include_filters_from_tags))
|
||||||
|
|
||||||
subtractive_selectors = [*self.datastore.get_tag_overrides_for_watch(uuid=uuid, attr='subtractive_selectors'),
|
subtractive_selectors = [*self.datastore.get_tag_overrides_for_watch(uuid=uuid, attr='subtractive_selectors'),
|
||||||
*watch.get("subtractive_selectors", []),
|
*watch.get("subtractive_selectors", []),
|
||||||
|
|||||||
@@ -160,6 +160,12 @@ $(document).ready(function () {
|
|||||||
e.offsetX > item.left * y_scale && e.offsetX < item.left * y_scale + item.width * y_scale
|
e.offsetX > item.left * y_scale && e.offsetX < item.left * y_scale + item.width * y_scale
|
||||||
|
|
||||||
) {
|
) {
|
||||||
|
// Ignore really large ones, because we are scraping 'div' also from xpath_element_scraper but
|
||||||
|
// that div or whatever could be some wrapper and would generally make you select the whole page
|
||||||
|
if (item.width > 800 && item.height > 400) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// There could be many elements here, record them all and then we'll find out which is the most 'useful'
|
// There could be many elements here, record them all and then we'll find out which is the most 'useful'
|
||||||
// (input, textarea, button, A etc)
|
// (input, textarea, button, A etc)
|
||||||
if (item.width < xpath_data['browser_width']) {
|
if (item.width < xpath_data['browser_width']) {
|
||||||
|
|||||||
@@ -28,15 +28,11 @@ $(document).ready(function() {
|
|||||||
notification_format: $('#notification_format').val(),
|
notification_format: $('#notification_format').val(),
|
||||||
notification_title: $('#notification_title').val(),
|
notification_title: $('#notification_title').val(),
|
||||||
notification_urls: $('.notification-urls').val(),
|
notification_urls: $('.notification-urls').val(),
|
||||||
|
tags: $('#tags').val(),
|
||||||
window_url: window.location.href,
|
window_url: window.location.href,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!data['notification_urls'].length) {
|
|
||||||
alert("Notification URL list is empty, cannot send test.")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
url: notification_base_url,
|
url: notification_base_url,
|
||||||
@@ -49,7 +45,7 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
}).done(function(data){
|
}).done(function(data){
|
||||||
console.log(data);
|
console.log(data);
|
||||||
alert('Sent');
|
alert(data);
|
||||||
}).fail(function(data){
|
}).fail(function(data){
|
||||||
console.log(data);
|
console.log(data);
|
||||||
alert('There was an error communicating with the server.');
|
alert('There was an error communicating with the server.');
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
--color-last-checked: #bbb;
|
--color-last-checked: #bbb;
|
||||||
--color-text-footer: #444;
|
--color-text-footer: #444;
|
||||||
--color-border-watch-table-cell: #eee;
|
--color-border-watch-table-cell: #eee;
|
||||||
--color-text-watch-tag-list: #e70069;
|
--color-text-watch-tag-list: rgba(231, 0, 105, 0.4);
|
||||||
--color-background-new-watch-form: rgba(0, 0, 0, 0.05);
|
--color-background-new-watch-form: rgba(0, 0, 0, 0.05);
|
||||||
--color-background-new-watch-input: var(--color-white);
|
--color-background-new-watch-input: var(--color-white);
|
||||||
--color-text-new-watch-input: var(--color-text);
|
--color-text-new-watch-input: var(--color-text);
|
||||||
@@ -111,7 +111,7 @@ html[data-darkmode="true"] {
|
|||||||
--color-background-input: var(--color-grey-350);
|
--color-background-input: var(--color-grey-350);
|
||||||
--color-text-input-description: var(--color-grey-600);
|
--color-text-input-description: var(--color-grey-600);
|
||||||
--color-text-input-placeholder: var(--color-grey-600);
|
--color-text-input-placeholder: var(--color-grey-600);
|
||||||
--color-text-watch-tag-list: #fa3e92;
|
--color-text-watch-tag-list: rgba(250, 62, 146, 0.4);
|
||||||
--color-background-code: var(--color-grey-200);
|
--color-background-code: var(--color-grey-200);
|
||||||
--color-background-tab: rgba(0, 0, 0, 0.2);
|
--color-background-tab: rgba(0, 0, 0, 0.2);
|
||||||
--color-background-tab-hover: rgba(0, 0, 0, 0.5);
|
--color-background-tab-hover: rgba(0, 0, 0, 0.5);
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
--color-text-footer: #444;
|
--color-text-footer: #444;
|
||||||
--color-border-watch-table-cell: #eee;
|
--color-border-watch-table-cell: #eee;
|
||||||
|
|
||||||
--color-text-watch-tag-list: #e70069;
|
--color-text-watch-tag-list: rgba(231, 0, 105, 0.4);
|
||||||
--color-background-new-watch-form: rgba(0, 0, 0, 0.05);
|
--color-background-new-watch-form: rgba(0, 0, 0, 0.05);
|
||||||
--color-background-new-watch-input: var(--color-white);
|
--color-background-new-watch-input: var(--color-white);
|
||||||
--color-text-new-watch-input: var(--color-text);
|
--color-text-new-watch-input: var(--color-text);
|
||||||
@@ -127,7 +127,7 @@ html[data-darkmode="true"] {
|
|||||||
--color-background-input: var(--color-grey-350);
|
--color-background-input: var(--color-grey-350);
|
||||||
--color-text-input-description: var(--color-grey-600);
|
--color-text-input-description: var(--color-grey-600);
|
||||||
--color-text-input-placeholder: var(--color-grey-600);
|
--color-text-input-placeholder: var(--color-grey-600);
|
||||||
--color-text-watch-tag-list: #fa3e92;
|
--color-text-watch-tag-list: rgba(250, 62, 146, 0.4);
|
||||||
--color-background-code: var(--color-grey-200);
|
--color-background-code: var(--color-grey-200);
|
||||||
|
|
||||||
--color-background-tab: rgba(0, 0, 0, 0.2);
|
--color-background-tab: rgba(0, 0, 0, 0.2);
|
||||||
|
|||||||
@@ -187,8 +187,11 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.watch-tag-list {
|
.watch-tag-list {
|
||||||
color: var(--color-text-watch-tag-list);
|
color: var(--color-white);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
background: var(--color-text-watch-tag-list);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 2px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
@@ -1096,3 +1099,16 @@ ul {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#chrome-extension-link {
|
||||||
|
img {
|
||||||
|
height: 21px;
|
||||||
|
padding: 2px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding: 9px;
|
||||||
|
border: 1px solid var(--color-grey-800);
|
||||||
|
border-radius: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ ul#requests-extra_browsers {
|
|||||||
--color-last-checked: #bbb;
|
--color-last-checked: #bbb;
|
||||||
--color-text-footer: #444;
|
--color-text-footer: #444;
|
||||||
--color-border-watch-table-cell: #eee;
|
--color-border-watch-table-cell: #eee;
|
||||||
--color-text-watch-tag-list: #e70069;
|
--color-text-watch-tag-list: rgba(231, 0, 105, 0.4);
|
||||||
--color-background-new-watch-form: rgba(0, 0, 0, 0.05);
|
--color-background-new-watch-form: rgba(0, 0, 0, 0.05);
|
||||||
--color-background-new-watch-input: var(--color-white);
|
--color-background-new-watch-input: var(--color-white);
|
||||||
--color-text-new-watch-input: var(--color-text);
|
--color-text-new-watch-input: var(--color-text);
|
||||||
@@ -327,7 +327,7 @@ html[data-darkmode="true"] {
|
|||||||
--color-background-input: var(--color-grey-350);
|
--color-background-input: var(--color-grey-350);
|
||||||
--color-text-input-description: var(--color-grey-600);
|
--color-text-input-description: var(--color-grey-600);
|
||||||
--color-text-input-placeholder: var(--color-grey-600);
|
--color-text-input-placeholder: var(--color-grey-600);
|
||||||
--color-text-watch-tag-list: #fa3e92;
|
--color-text-watch-tag-list: rgba(250, 62, 146, 0.4);
|
||||||
--color-background-code: var(--color-grey-200);
|
--color-background-code: var(--color-grey-200);
|
||||||
--color-background-tab: rgba(0, 0, 0, 0.2);
|
--color-background-tab: rgba(0, 0, 0, 0.2);
|
||||||
--color-background-tab-hover: rgba(0, 0, 0, 0.5);
|
--color-background-tab-hover: rgba(0, 0, 0, 0.5);
|
||||||
@@ -532,8 +532,11 @@ code {
|
|||||||
margin: 0 3px 0 5px; }
|
margin: 0 3px 0 5px; }
|
||||||
|
|
||||||
.watch-tag-list {
|
.watch-tag-list {
|
||||||
color: var(--color-text-watch-tag-list);
|
color: var(--color-white);
|
||||||
white-space: nowrap; }
|
white-space: nowrap;
|
||||||
|
background: var(--color-text-watch-tag-list);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 2px 5px; }
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
@@ -1180,3 +1183,13 @@ ul {
|
|||||||
.restock-label.not-in-stock {
|
.restock-label.not-in-stock {
|
||||||
background-color: var(--color-background-button-cancel);
|
background-color: var(--color-background-button-cancel);
|
||||||
color: #777; }
|
color: #777; }
|
||||||
|
|
||||||
|
#chrome-extension-link {
|
||||||
|
padding: 9px;
|
||||||
|
border: 1px solid var(--color-grey-800);
|
||||||
|
border-radius: 10px;
|
||||||
|
vertical-align: middle; }
|
||||||
|
#chrome-extension-link img {
|
||||||
|
height: 21px;
|
||||||
|
padding: 2px;
|
||||||
|
vertical-align: middle; }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
flash
|
flash
|
||||||
@@ -657,7 +657,10 @@ class ChangeDetectionStore:
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
def tag_exists_by_name(self, tag_name):
|
def tag_exists_by_name(self, tag_name):
|
||||||
return any(v.get('title', '').lower() == tag_name.lower() for k, v in self.__data['settings']['application']['tags'].items())
|
# Check if any tag dictionary has a 'title' attribute matching the provided tag_name
|
||||||
|
tags = self.__data['settings']['application']['tags'].values()
|
||||||
|
return next((v for v in tags if v.get('title', '').lower() == tag_name.lower()),
|
||||||
|
None)
|
||||||
|
|
||||||
def get_updates_available(self):
|
def get_updates_available(self):
|
||||||
import inspect
|
import inspect
|
||||||
|
|||||||
23
changedetectionio/strtobool.py
Normal file
23
changedetectionio/strtobool.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Because strtobool was removed in python 3.12 distutils
|
||||||
|
|
||||||
|
_MAP = {
|
||||||
|
'y': True,
|
||||||
|
'yes': True,
|
||||||
|
't': True,
|
||||||
|
'true': True,
|
||||||
|
'on': True,
|
||||||
|
'1': True,
|
||||||
|
'n': False,
|
||||||
|
'no': False,
|
||||||
|
'f': False,
|
||||||
|
'false': False,
|
||||||
|
'off': False,
|
||||||
|
'0': False
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def strtobool(value):
|
||||||
|
try:
|
||||||
|
return _MAP[str(value).lower()]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError('"{}" is not a valid bool value'.format(value))
|
||||||
@@ -147,7 +147,19 @@
|
|||||||
<section class="content">
|
<section class="content">
|
||||||
<div id="overlay">
|
<div id="overlay">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<strong>changedetection.io needs your support!</strong><br>
|
<h4>Try our Chrome extension</h4>
|
||||||
|
<p>
|
||||||
|
<a id="chrome-extension-link"
|
||||||
|
title="Try our new Chrome Extension!"
|
||||||
|
href="https://chromewebstore.google.com/detail/changedetectionio-website/kefcfmgmlhmankjmnbijimhofdjekbop">
|
||||||
|
<img src="{{url_for('static_content', group='images', filename='Google-Chrome-icon.png')}}">
|
||||||
|
Chrome Webstore
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Easily add the current web-page from your browser directly into your changedetection.io tool, more great features coming soon!
|
||||||
|
|
||||||
|
<h4>Changedetection.io needs your support!</h4>
|
||||||
<p>
|
<p>
|
||||||
You can help us by supporting changedetection.io on these platforms;
|
You can help us by supporting changedetection.io on these platforms;
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
<script>
|
<script>
|
||||||
const browser_steps_available_screenshots=JSON.parse('{{ watch.get_browsersteps_available_screenshots|tojson }}');
|
const browser_steps_available_screenshots=JSON.parse('{{ watch.get_browsersteps_available_screenshots|tojson }}');
|
||||||
const browser_steps_config=JSON.parse('{{ browser_steps_config|tojson }}');
|
const browser_steps_config=JSON.parse('{{ browser_steps_config|tojson }}');
|
||||||
const browser_steps_fetch_screenshot_image_url="{{url_for('browser_steps.browser_steps_fetch_screenshot_image', uuid=uuid)}}";
|
<!-- Should be _external so that firefox and others load it more reliably -->
|
||||||
|
const browser_steps_fetch_screenshot_image_url="{{url_for('browser_steps.browser_steps_fetch_screenshot_image', uuid=uuid, _external=True)}}";
|
||||||
const browser_steps_last_error_step={{ watch.browser_steps_last_error_step|tojson }};
|
const browser_steps_last_error_step={{ watch.browser_steps_last_error_step|tojson }};
|
||||||
const browser_steps_start_url="{{url_for('browser_steps.browsersteps_start_session', uuid=uuid)}}";
|
const browser_steps_start_url="{{url_for('browser_steps.browsersteps_start_session', uuid=uuid)}}";
|
||||||
const browser_steps_sync_url="{{url_for('browser_steps.browsersteps_ui_update', uuid=uuid)}}";
|
const browser_steps_sync_url="{{url_for('browser_steps.browsersteps_ui_update', uuid=uuid)}}";
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
<script src="{{url_for('static_content', group='js', filename='browser-steps.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='browser-steps.js')}}" defer></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% set has_tag_filters_extra="WARNING: Watch has tag/groups set with special filters\n" if has_special_tag_options else '' %}
|
||||||
<script src="{{url_for('static_content', group='js', filename='recheck-proxy.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='recheck-proxy.js')}}" defer></script>
|
||||||
|
|
||||||
<div class="edit-form monospaced-textarea">
|
<div class="edit-form monospaced-textarea">
|
||||||
@@ -280,7 +282,7 @@ User-Agent: wonderbra 1.0") }}
|
|||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{% set field = render_field(form.include_filters,
|
{% set field = render_field(form.include_filters,
|
||||||
rows=5,
|
rows=5,
|
||||||
placeholder="#example
|
placeholder=has_tag_filters_extra+"#example
|
||||||
xpath://body/div/span[contains(@class, 'example-class')]",
|
xpath://body/div/span[contains(@class, 'example-class')]",
|
||||||
class="m-d")
|
class="m-d")
|
||||||
%}
|
%}
|
||||||
@@ -316,13 +318,14 @@ xpath://body/div/span[contains(@class, 'example-class')]",
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<fieldset class="pure-control-group">
|
<fieldset class="pure-control-group">
|
||||||
{{ render_field(form.subtractive_selectors, rows=5, placeholder="header
|
{{ render_field(form.subtractive_selectors, rows=5, placeholder=has_tag_filters_extra+"header
|
||||||
footer
|
footer
|
||||||
nav
|
nav
|
||||||
.stockticker") }}
|
.stockticker") }}
|
||||||
<span class="pure-form-message-inline">
|
<span class="pure-form-message-inline">
|
||||||
<ul>
|
<ul>
|
||||||
<li> Remove HTML element(s) by CSS selector before text conversion. </li>
|
<li> Remove HTML element(s) by CSS selector before text conversion. </li>
|
||||||
|
<li> Don't paste HTML here, use only CSS selectors </li>
|
||||||
<li> Add multiple elements or CSS selectors per line to ignore multiple parts of the HTML. </li>
|
<li> Add multiple elements or CSS selectors per line to ignore multiple parts of the HTML. </li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
@@ -436,7 +439,7 @@ Unavailable") }}
|
|||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{% if visualselector_enabled %}
|
{% if visualselector_enabled %}
|
||||||
<span class="pure-form-message-inline">
|
<span class="pure-form-message-inline">
|
||||||
The Visual Selector tool lets you select the <i>text</i> elements that will be used for the change detection ‐ after the <i>Browser Steps</i> has completed.<br><br>
|
The Visual Selector tool lets you select the <i>text</i> elements that will be used for the change detection ‐ after the <i>Browser Steps</i> has completed, this tool is a helper to manage filters in the "CSS/JSONPath/JQ/XPath Filters" box of the <a href="#filters-and-triggers">Filters & Triggers</a> tab.
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div id="selector-header">
|
<div id="selector-header">
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
<option value="" style="color: #aaa"> -- none --</option>
|
<option value="" style="color: #aaa"> -- none --</option>
|
||||||
<option value="url">URL</option>
|
<option value="url">URL</option>
|
||||||
<option value="title">Title</option>
|
<option value="title">Title</option>
|
||||||
<option value="include_filter">CSS/xPath filter</option>
|
<option value="include_filters">CSS/xPath filter</option>
|
||||||
<option value="tag">Group / Tag name(s)</option>
|
<option value="tag">Group / Tag name(s)</option>
|
||||||
<option value="interval_minutes">Recheck time (minutes)</option>
|
<option value="interval_minutes">Recheck time (minutes)</option>
|
||||||
</select></td>
|
</select></td>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
|
{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
|
||||||
{% from '_common_fields.jinja' import render_common_settings_form %}
|
{% from '_common_fields.jinja' import render_common_settings_form %}
|
||||||
<script>
|
<script>
|
||||||
const notification_base_url="{{url_for('ajax_callback_send_notification_test', watch_uuid=uuid)}}";
|
const notification_base_url="{{url_for('ajax_callback_send_notification_test', mode="global-settings")}}";
|
||||||
{% if emailprefix %}
|
{% if emailprefix %}
|
||||||
const email_notification_prefix=JSON.parse('{{emailprefix|tojson}}');
|
const email_notification_prefix=JSON.parse('{{emailprefix|tojson}}');
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -168,12 +168,12 @@ nav
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane-inner" id="api">
|
<div class="tab-pane-inner" id="api">
|
||||||
|
<h4>API Access</h4>
|
||||||
<p>Drive your changedetection.io via API, More about <a href="https://github.com/dgtlmoon/changedetection.io/wiki/API-Reference">API access here</a></p>
|
<p>Drive your changedetection.io via API, More about <a href="https://github.com/dgtlmoon/changedetection.io/wiki/API-Reference">API access here</a></p>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{{ render_checkbox_field(form.application.form.api_access_token_enabled) }}
|
{{ render_checkbox_field(form.application.form.api_access_token_enabled) }}
|
||||||
<div class="pure-form-message-inline">Restrict API access limit by using <code>x-api-key</code> header</div><br>
|
<div class="pure-form-message-inline">Restrict API access limit by using <code>x-api-key</code> header - required for the Chrome Extension to work</div><br>
|
||||||
<div class="pure-form-message-inline"><br>API Key <span id="api-key">{{api_key}}</span>
|
<div class="pure-form-message-inline"><br>API Key <span id="api-key">{{api_key}}</span>
|
||||||
<span style="display:none;" id="api-key-copy" >copy</span>
|
<span style="display:none;" id="api-key-copy" >copy</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,6 +181,20 @@ nav
|
|||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<a href="{{url_for('settings_reset_api_key')}}" class="pure-button button-small button-cancel">Regenerate API key</a>
|
<a href="{{url_for('settings_reset_api_key')}}" class="pure-button button-small button-cancel">Regenerate API key</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<h4>Chrome Extension</h4>
|
||||||
|
<p>Easily add any web-page to your changedetection.io installation from within Chrome.</p>
|
||||||
|
<strong>Step 1</strong> Install the extension, <strong>Step 2</strong> Navigate to this page,
|
||||||
|
<strong>Step 3</strong> Open the extension from the toolbar and click "<i>Sync API Access</i>"
|
||||||
|
<p>
|
||||||
|
<a id="chrome-extension-link"
|
||||||
|
title="Try our new Chrome Extension!"
|
||||||
|
href="https://chromewebstore.google.com/detail/changedetectionio-website/kefcfmgmlhmankjmnbijimhofdjekbop">
|
||||||
|
<img src="{{ url_for('static_content', group='images', filename='Google-Chrome-icon.png') }}">
|
||||||
|
Chrome Webstore
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane-inner" id="proxies">
|
<div class="tab-pane-inner" id="proxies">
|
||||||
<div id="recommended-proxy">
|
<div id="recommended-proxy">
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
<button class="pure-button button-secondary button-xsmall" name="op" value="assign-tag" id="checkbox-assign-tag">Tag</button>
|
<button class="pure-button button-secondary button-xsmall" name="op" value="assign-tag" id="checkbox-assign-tag">Tag</button>
|
||||||
<button class="pure-button button-secondary button-xsmall" name="op" value="mark-viewed">Mark viewed</button>
|
<button class="pure-button button-secondary button-xsmall" name="op" value="mark-viewed">Mark viewed</button>
|
||||||
<button class="pure-button button-secondary button-xsmall" name="op" value="notification-default">Use default notification</button>
|
<button class="pure-button button-secondary button-xsmall" name="op" value="notification-default">Use default notification</button>
|
||||||
|
<button class="pure-button button-secondary button-xsmall" name="op" value="clear-errors">Clear errors</button>
|
||||||
<button class="pure-button button-secondary button-xsmall" style="background: #dd4242;" name="op" value="clear-history">Clear/reset history</button>
|
<button class="pure-button button-secondary button-xsmall" style="background: #dd4242;" name="op" value="clear-history">Clear/reset history</button>
|
||||||
<button class="pure-button button-secondary button-xsmall" style="background: #dd4242;" name="op" value="delete">Delete</button>
|
<button class="pure-button button-secondary button-xsmall" style="background: #dd4242;" name="op" value="delete">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import os
|
||||||
|
from flask import url_for
|
||||||
|
from ..util import live_server_setup, wait_for_all_checks, extract_UUID_from_client
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_custom_js(client, live_server):
|
||||||
|
|
||||||
|
live_server_setup(live_server)
|
||||||
|
assert os.getenv('PLAYWRIGHT_DRIVER_URL'), "Needs PLAYWRIGHT_DRIVER_URL set for this test"
|
||||||
|
|
||||||
|
test_url = url_for('test_interactive_html_endpoint', _external=True)
|
||||||
|
test_url = test_url.replace('localhost.localdomain', 'cdio')
|
||||||
|
test_url = test_url.replace('localhost', 'cdio')
|
||||||
|
|
||||||
|
res = client.post(
|
||||||
|
url_for("form_quick_watch_add"),
|
||||||
|
data={"url": test_url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert b"Watch added in Paused state, saving will unpause" in res.data
|
||||||
|
|
||||||
|
res = client.post(
|
||||||
|
url_for("edit_page", uuid="first", unpause_on_save=1),
|
||||||
|
data={
|
||||||
|
"url": test_url,
|
||||||
|
"tags": "",
|
||||||
|
'fetch_backend': "html_webdriver",
|
||||||
|
'webdriver_js_execute_code': 'document.querySelector("button[name=test-button]").click();',
|
||||||
|
'headers': "testheader: yes\buser-agent: MyCustomAgent",
|
||||||
|
},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"unpaused" in res.data
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
|
uuid = extract_UUID_from_client(client)
|
||||||
|
assert live_server.app.config['DATASTORE'].data['watching'][uuid].history_n >= 1, "Watch history had atleast 1 (everything fetched OK)"
|
||||||
|
|
||||||
|
assert b"This text should be removed" not in res.data
|
||||||
|
|
||||||
|
# Check HTML conversion detected and workd
|
||||||
|
res = client.get(
|
||||||
|
url_for("preview_page", uuid=uuid),
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"This text should be removed" not in res.data
|
||||||
|
assert b"I smell JavaScript because the button was pressed" in res.data
|
||||||
|
|
||||||
|
assert b"testheader: yes" in res.data
|
||||||
|
assert b"user-agent: mycustomagent" in res.data
|
||||||
|
|
||||||
|
client.get(
|
||||||
|
url_for("form_delete", uuid="all"),
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
@@ -95,7 +95,7 @@ def test_restock_detection(client, live_server):
|
|||||||
|
|
||||||
# We should have a notification
|
# We should have a notification
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
assert os.path.isfile("test-datastore/notification.txt")
|
assert os.path.isfile("test-datastore/notification.txt"), "Notification received"
|
||||||
os.unlink("test-datastore/notification.txt")
|
os.unlink("test-datastore/notification.txt")
|
||||||
|
|
||||||
# Default behaviour is to only fire notification when it goes OUT OF STOCK -> IN STOCK
|
# Default behaviour is to only fire notification when it goes OUT OF STOCK -> IN STOCK
|
||||||
@@ -103,4 +103,9 @@ def test_restock_detection(client, live_server):
|
|||||||
set_original_response()
|
set_original_response()
|
||||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
assert not os.path.isfile("test-datastore/notification.txt")
|
assert not os.path.isfile("test-datastore/notification.txt"), "No notification should have fired when it went OUT OF STOCK by default"
|
||||||
|
|
||||||
|
# BUT we should see that it correctly shows "not in stock"
|
||||||
|
res = client.get(url_for("index"))
|
||||||
|
assert b'not-in-stock' in res.data, "Correctly showing NOT IN STOCK in the list after it changed from IN STOCK"
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,12 @@ def test_setup_group_tag(client, live_server):
|
|||||||
assert b'Should be only this' in res.data
|
assert b'Should be only this' in res.data
|
||||||
assert b'And never this' not in res.data
|
assert b'And never this' not in res.data
|
||||||
|
|
||||||
|
res = client.get(
|
||||||
|
url_for("edit_page", uuid="first"),
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
# 2307 the UI notice should appear in the placeholder
|
||||||
|
assert b'WARNING: Watch has tag/groups set with special filters' in res.data
|
||||||
|
|
||||||
# RSS Group tag filter
|
# RSS Group tag filter
|
||||||
# An extra one that should be excluded
|
# An extra one that should be excluded
|
||||||
@@ -321,3 +327,154 @@ def test_clone_tag_on_quickwatchform_add(client, live_server):
|
|||||||
|
|
||||||
res = client.get(url_for("tags.delete_all"), follow_redirects=True)
|
res = client.get(url_for("tags.delete_all"), follow_redirects=True)
|
||||||
assert b'All tags deleted' in res.data
|
assert b'All tags deleted' in res.data
|
||||||
|
|
||||||
|
def test_order_of_filters_tag_filter_and_watch_filter(client, live_server):
|
||||||
|
|
||||||
|
# Add a tag with some config, import a tag and it should roughly work
|
||||||
|
res = client.post(
|
||||||
|
url_for("tags.form_tag_add"),
|
||||||
|
data={"name": "test-tag-keep-order"},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"Tag added" in res.data
|
||||||
|
assert b"test-tag-keep-order" in res.data
|
||||||
|
tag_filters = [
|
||||||
|
'#only-this', # duplicated filters
|
||||||
|
'#only-this',
|
||||||
|
'#only-this',
|
||||||
|
'#only-this',
|
||||||
|
]
|
||||||
|
|
||||||
|
res = client.post(
|
||||||
|
url_for("tags.form_tag_edit_submit", uuid="first"),
|
||||||
|
data={"name": "test-tag-keep-order",
|
||||||
|
"include_filters": '\n'.join(tag_filters) },
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"Updated" in res.data
|
||||||
|
tag_uuid = get_UUID_for_tag_name(client, name="test-tag-keep-order")
|
||||||
|
res = client.get(
|
||||||
|
url_for("tags.form_tag_edit", uuid="first")
|
||||||
|
)
|
||||||
|
assert b"#only-this" in res.data
|
||||||
|
|
||||||
|
|
||||||
|
d = """<html>
|
||||||
|
<body>
|
||||||
|
Some initial text<br>
|
||||||
|
<p id="only-this">And 1 this</p>
|
||||||
|
<br>
|
||||||
|
<p id="not-this">And 2 this</p>
|
||||||
|
<p id="">And 3 this</p><!--/html/body/p[3]/-->
|
||||||
|
<p id="">And 4 this</p><!--/html/body/p[4]/-->
|
||||||
|
<p id="">And 5 this</p><!--/html/body/p[5]/-->
|
||||||
|
<p id="">And 6 this</p><!--/html/body/p[6]/-->
|
||||||
|
<p id="">And 7 this</p><!--/html/body/p[7]/-->
|
||||||
|
<p id="">And 8 this</p><!--/html/body/p[8]/-->
|
||||||
|
<p id="">And 9 this</p><!--/html/body/p[9]/-->
|
||||||
|
<p id="">And 10 this</p><!--/html/body/p[10]/-->
|
||||||
|
<p id="">And 11 this</p><!--/html/body/p[11]/-->
|
||||||
|
<p id="">And 12 this</p><!--/html/body/p[12]/-->
|
||||||
|
<p id="">And 13 this</p><!--/html/body/p[13]/-->
|
||||||
|
<p id="">And 14 this</p><!--/html/body/p[14]/-->
|
||||||
|
<p id="not-this">And 15 this</p><!--/html/body/p[15]/-->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open("test-datastore/endpoint-content.txt", "w") as f:
|
||||||
|
f.write(d)
|
||||||
|
|
||||||
|
test_url = url_for('test_endpoint', _external=True)
|
||||||
|
res = client.post(
|
||||||
|
url_for("import_page"),
|
||||||
|
data={"urls": test_url},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"1 Imported" in res.data
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
|
filters = [
|
||||||
|
'/html/body/p[3]',
|
||||||
|
'/html/body/p[4]',
|
||||||
|
'/html/body/p[5]',
|
||||||
|
'/html/body/p[6]',
|
||||||
|
'/html/body/p[7]',
|
||||||
|
'/html/body/p[8]',
|
||||||
|
'/html/body/p[9]',
|
||||||
|
'/html/body/p[10]',
|
||||||
|
'/html/body/p[11]',
|
||||||
|
'/html/body/p[12]',
|
||||||
|
'/html/body/p[13]', # duplicated tags
|
||||||
|
'/html/body/p[13]',
|
||||||
|
'/html/body/p[13]',
|
||||||
|
'/html/body/p[13]',
|
||||||
|
'/html/body/p[13]',
|
||||||
|
'/html/body/p[14]',
|
||||||
|
]
|
||||||
|
|
||||||
|
res = client.post(
|
||||||
|
url_for("edit_page", uuid="first"),
|
||||||
|
data={"include_filters": '\n'.join(filters),
|
||||||
|
"url": test_url,
|
||||||
|
"tags": "test-tag-keep-order",
|
||||||
|
"headers": "",
|
||||||
|
'fetch_backend': "html_requests"},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"Updated watch." in res.data
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
|
res = client.get(
|
||||||
|
url_for("preview_page", uuid="first"),
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert b"And 1 this" in res.data # test-tag-keep-order
|
||||||
|
|
||||||
|
a_tag_filter_check = b'And 1 this' #'#only-this' of tag_filters
|
||||||
|
# check there is no duplication of tag_filters
|
||||||
|
assert res.data.count(a_tag_filter_check) == 1, f"duplicated filters didn't removed {res.data.count(a_tag_filter_check)} of {a_tag_filter_check} in {res.data=}"
|
||||||
|
|
||||||
|
a_filter_check = b"And 13 this" # '/html/body/p[13]'
|
||||||
|
# check there is no duplication of filters
|
||||||
|
assert res.data.count(a_filter_check) == 1, f"duplicated filters didn't removed. {res.data.count(a_filter_check)} of {a_filter_check} in {res.data=}"
|
||||||
|
|
||||||
|
a_filter_check_not_include = b"And 2 this" # '/html/body/p[2]'
|
||||||
|
assert a_filter_check_not_include not in res.data
|
||||||
|
|
||||||
|
checklist = [
|
||||||
|
b"And 3 this",
|
||||||
|
b"And 4 this",
|
||||||
|
b"And 5 this",
|
||||||
|
b"And 6 this",
|
||||||
|
b"And 7 this",
|
||||||
|
b"And 8 this",
|
||||||
|
b"And 9 this",
|
||||||
|
b"And 10 this",
|
||||||
|
b"And 11 this",
|
||||||
|
b"And 12 this",
|
||||||
|
b"And 13 this",
|
||||||
|
b"And 14 this",
|
||||||
|
b"And 1 this", # result of filter from tag.
|
||||||
|
]
|
||||||
|
# check whether everything a user requested is there
|
||||||
|
for test in checklist:
|
||||||
|
assert test in res.data
|
||||||
|
|
||||||
|
# check whether everything a user requested is in order of filters.
|
||||||
|
n = 0
|
||||||
|
for test in checklist:
|
||||||
|
t_index = res.data[n:].find(test)
|
||||||
|
# if the text is not searched, return -1.
|
||||||
|
assert t_index >= 0, f"""failed because {test=} not in {res.data[n:]=}
|
||||||
|
#####################
|
||||||
|
Looks like some feature changed the order of result of filters.
|
||||||
|
#####################
|
||||||
|
the {test} appeared before. {test in res.data[:n]=}
|
||||||
|
{res.data[:n]=}
|
||||||
|
"""
|
||||||
|
n += t_index + len(test)
|
||||||
|
|
||||||
|
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
||||||
|
assert b'Deleted' in res.data
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ def test_fetch_pdf(client, live_server):
|
|||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
|
|
||||||
assert b'PDF-1.5' not in res.data
|
# PDF header should not be there (it was converted to text)
|
||||||
|
assert b'PDF' not in res.data[:10]
|
||||||
assert b'hello world' in res.data
|
assert b'hello world' in res.data
|
||||||
|
|
||||||
# So we know if the file changes in other ways
|
# So we know if the file changes in other ways
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/python3
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
@@ -255,6 +255,69 @@ def test_xpath23_prefix_validation(client, live_server):
|
|||||||
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
||||||
assert b'Deleted' in res.data
|
assert b'Deleted' in res.data
|
||||||
|
|
||||||
|
def test_xpath1_lxml(client, live_server):
|
||||||
|
#live_server_setup(live_server)
|
||||||
|
|
||||||
|
d = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>rpilocator.com</title>
|
||||||
|
<link>https://rpilocator.com</link>
|
||||||
|
<description>Find Raspberry Pi Computers in Stock</description>
|
||||||
|
<lastBuildDate>Thu, 19 May 2022 23:27:30 GMT</lastBuildDate>
|
||||||
|
<image>
|
||||||
|
<url>https://rpilocator.com/favicon.png</url>
|
||||||
|
<title>rpilocator.com</title>
|
||||||
|
<link>https://rpilocator.com/</link>
|
||||||
|
<width>32</width>
|
||||||
|
<height>32</height>
|
||||||
|
</image>
|
||||||
|
<item>
|
||||||
|
<title>Stock Alert (UK): RPi CM4</title>
|
||||||
|
<foo>something else unrelated</foo>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Stock Alert (UK): Big monitorěěěě</title>
|
||||||
|
<foo>something else unrelated</foo>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>'''.encode('utf-8')
|
||||||
|
|
||||||
|
with open("test-datastore/endpoint-content.txt", "wb") as f:
|
||||||
|
f.write(d)
|
||||||
|
|
||||||
|
|
||||||
|
test_url = url_for('test_endpoint', _external=True)
|
||||||
|
res = client.post(
|
||||||
|
url_for("import_page"),
|
||||||
|
data={"urls": test_url},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"1 Imported" in res.data
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
|
res = client.post(
|
||||||
|
url_for("edit_page", uuid="first"),
|
||||||
|
data={"include_filters": "xpath1://title/text()", "url": test_url, "tags": "", "headers": "",
|
||||||
|
'fetch_backend': "html_requests"},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
##### #2312
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
res = client.get(url_for("index"))
|
||||||
|
assert b'_ElementStringResult' not in res.data # tested with 5.1.1 when it was removed and 5.1.0
|
||||||
|
assert b'Exception' not in res.data
|
||||||
|
res = client.get(
|
||||||
|
url_for("preview_page", uuid="first"),
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert b"rpilocator.com" in res.data # in selector
|
||||||
|
assert "Stock Alert (UK): Big monitorěěěě".encode('utf-8') in res.data # not in selector
|
||||||
|
|
||||||
|
#####
|
||||||
|
|
||||||
|
|
||||||
def test_xpath1_validation(client, live_server):
|
def test_xpath1_validation(client, live_server):
|
||||||
# Add our URL to the import page
|
# Add our URL to the import page
|
||||||
|
|||||||
@@ -242,5 +242,28 @@ def live_server_setup(live_server):
|
|||||||
resp.headers['Content-Type'] = 'application/pdf'
|
resp.headers['Content-Type'] = 'application/pdf'
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@live_server.app.route('/test-interactive-html-endpoint')
|
||||||
|
def test_interactive_html_endpoint():
|
||||||
|
header_text=""
|
||||||
|
for k,v in request.headers.items():
|
||||||
|
header_text += f"{k}: {v}<br>"
|
||||||
|
|
||||||
|
resp = make_response(f"""
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
Primitive JS check for <pre>changedetectionio/tests/visualselector/test_fetch_data.py</pre>
|
||||||
|
<p id="remove">This text should be removed</p>
|
||||||
|
<form onsubmit="event.preventDefault();">
|
||||||
|
<!-- obfuscated text so that we dont accidentally get a false positive due to conversion of the source :) --->
|
||||||
|
<button name="test-button" onclick="getElementById('remove').remove();getElementById('some-content').innerHTML = atob('SSBzbWVsbCBKYXZhU2NyaXB0IGJlY2F1c2UgdGhlIGJ1dHRvbiB3YXMgcHJlc3NlZCE=')">Click here</button>
|
||||||
|
<div id=some-content></div>
|
||||||
|
<pre>
|
||||||
|
{header_text.lower()}
|
||||||
|
</pre>
|
||||||
|
</body>
|
||||||
|
</html>""", 200)
|
||||||
|
resp.headers['Content-Type'] = 'text/html'
|
||||||
|
return resp
|
||||||
|
|
||||||
live_server.start()
|
live_server.start()
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,19 @@ from ..util import live_server_setup, wait_for_all_checks, extract_UUID_from_cli
|
|||||||
def test_setup(client, live_server):
|
def test_setup(client, live_server):
|
||||||
live_server_setup(live_server)
|
live_server_setup(live_server)
|
||||||
|
|
||||||
|
|
||||||
# Add a site in paused mode, add an invalid filter, we should still have visual selector data ready
|
# Add a site in paused mode, add an invalid filter, we should still have visual selector data ready
|
||||||
def test_visual_selector_content_ready(client, live_server):
|
def test_visual_selector_content_ready(client, live_server):
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
assert os.getenv('PLAYWRIGHT_DRIVER_URL'), "Needs PLAYWRIGHT_DRIVER_URL set for this test"
|
assert os.getenv('PLAYWRIGHT_DRIVER_URL'), "Needs PLAYWRIGHT_DRIVER_URL set for this test"
|
||||||
|
|
||||||
# Add our URL to the import page, because the docker container (playwright/selenium) wont be able to connect to our usual test url
|
# Add our URL to the import page, because the docker container (playwright/selenium) wont be able to connect to our usual test url
|
||||||
test_url = "https://changedetection.io/ci-test/test-runjs.html"
|
test_url = url_for('test_interactive_html_endpoint', _external=True)
|
||||||
|
test_url = test_url.replace('localhost.localdomain', 'cdio')
|
||||||
|
test_url = test_url.replace('localhost', 'cdio')
|
||||||
|
|
||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("form_quick_watch_add"),
|
url_for("form_quick_watch_add"),
|
||||||
@@ -23,28 +27,31 @@ def test_visual_selector_content_ready(client, live_server):
|
|||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
assert b"Watch added in Paused state, saving will unpause" in res.data
|
assert b"Watch added in Paused state, saving will unpause" in res.data
|
||||||
|
uuid = extract_UUID_from_client(client)
|
||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("edit_page", uuid="first", unpause_on_save=1),
|
url_for("edit_page", uuid=uuid, unpause_on_save=1),
|
||||||
data={
|
data={
|
||||||
"url": test_url,
|
"url": test_url,
|
||||||
"tags": "",
|
"tags": "",
|
||||||
"headers": "",
|
# For now, cookies doesnt work in headers because it must be a full cookiejar object
|
||||||
'fetch_backend': "html_webdriver",
|
'headers': "testheader: yes\buser-agent: MyCustomAgent",
|
||||||
'webdriver_js_execute_code': 'document.querySelector("button[name=test-button]").click();'
|
'fetch_backend': "html_webdriver",
|
||||||
},
|
},
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
assert b"unpaused" in res.data
|
assert b"unpaused" in res.data
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
uuid = extract_UUID_from_client(client)
|
|
||||||
|
|
||||||
# Check the JS execute code before extract worked
|
|
||||||
|
assert live_server.app.config['DATASTORE'].data['watching'][uuid].history_n >= 1, "Watch history had atleast 1 (everything fetched OK)"
|
||||||
|
|
||||||
res = client.get(
|
res = client.get(
|
||||||
url_for("preview_page", uuid="first"),
|
url_for("preview_page", uuid=uuid),
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
assert b'I smell JavaScript' in res.data
|
assert b"testheader: yes" in res.data
|
||||||
|
assert b"user-agent: mycustomagent" in res.data
|
||||||
|
|
||||||
|
|
||||||
assert os.path.isfile(os.path.join('test-datastore', uuid, 'last-screenshot.png')), "last-screenshot.png should exist"
|
assert os.path.isfile(os.path.join('test-datastore', uuid, 'last-screenshot.png')), "last-screenshot.png should exist"
|
||||||
assert os.path.isfile(os.path.join('test-datastore', uuid, 'elements.json')), "xpath elements.json data should exist"
|
assert os.path.isfile(os.path.join('test-datastore', uuid, 'elements.json')), "xpath elements.json data should exist"
|
||||||
@@ -74,30 +81,33 @@ def test_visual_selector_content_ready(client, live_server):
|
|||||||
|
|
||||||
def test_basic_browserstep(client, live_server):
|
def test_basic_browserstep(client, live_server):
|
||||||
|
|
||||||
assert os.getenv('PLAYWRIGHT_DRIVER_URL'), "Needs PLAYWRIGHT_DRIVER_URL set for this test"
|
|
||||||
#live_server_setup(live_server)
|
#live_server_setup(live_server)
|
||||||
|
assert os.getenv('PLAYWRIGHT_DRIVER_URL'), "Needs PLAYWRIGHT_DRIVER_URL set for this test"
|
||||||
|
|
||||||
# Add our URL to the import page, because the docker container (playwright/selenium) wont be able to connect to our usual test url
|
test_url = url_for('test_interactive_html_endpoint', _external=True)
|
||||||
test_url = "https://changedetection.io/ci-test/test-runjs.html"
|
test_url = test_url.replace('localhost.localdomain', 'cdio')
|
||||||
|
test_url = test_url.replace('localhost', 'cdio')
|
||||||
|
|
||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("form_quick_watch_add"),
|
url_for("form_quick_watch_add"),
|
||||||
data={"url": test_url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'},
|
data={"url": test_url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'},
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
|
|
||||||
assert b"Watch added in Paused state, saving will unpause" in res.data
|
assert b"Watch added in Paused state, saving will unpause" in res.data
|
||||||
|
|
||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("edit_page", uuid="first", unpause_on_save=1),
|
url_for("edit_page", uuid="first", unpause_on_save=1),
|
||||||
data={
|
data={
|
||||||
"url": test_url,
|
"url": test_url,
|
||||||
"tags": "",
|
"tags": "",
|
||||||
"headers": "",
|
'fetch_backend': "html_webdriver",
|
||||||
'fetch_backend': "html_webdriver",
|
'browser_steps-0-operation': 'Goto site',
|
||||||
'browser_steps-0-operation': 'Goto site',
|
'browser_steps-1-operation': 'Click element',
|
||||||
'browser_steps-1-operation': 'Click element',
|
'browser_steps-1-selector': 'button[name=test-button]',
|
||||||
'browser_steps-1-selector': 'button[name=test-button]',
|
'browser_steps-1-optional_value': '',
|
||||||
'browser_steps-1-optional_value': ''
|
# For now, cookies doesnt work in headers because it must be a full cookiejar object
|
||||||
|
'headers': "testheader: yes\buser-agent: MyCustomAgent",
|
||||||
},
|
},
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
@@ -105,6 +115,9 @@ def test_basic_browserstep(client, live_server):
|
|||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
uuid = extract_UUID_from_client(client)
|
uuid = extract_UUID_from_client(client)
|
||||||
|
assert live_server.app.config['DATASTORE'].data['watching'][uuid].history_n >= 1, "Watch history had atleast 1 (everything fetched OK)"
|
||||||
|
|
||||||
|
assert b"This text should be removed" not in res.data
|
||||||
|
|
||||||
# Check HTML conversion detected and workd
|
# Check HTML conversion detected and workd
|
||||||
res = client.get(
|
res = client.get(
|
||||||
@@ -114,13 +127,19 @@ def test_basic_browserstep(client, live_server):
|
|||||||
assert b"This text should be removed" not in res.data
|
assert b"This text should be removed" not in res.data
|
||||||
assert b"I smell JavaScript because the button was pressed" in res.data
|
assert b"I smell JavaScript because the button was pressed" in res.data
|
||||||
|
|
||||||
|
assert b"testheader: yes" in res.data
|
||||||
|
assert b"user-agent: mycustomagent" in res.data
|
||||||
|
|
||||||
|
four_o_four_url = url_for('test_endpoint', status_code=404, _external=True)
|
||||||
|
four_o_four_url = four_o_four_url.replace('localhost.localdomain', 'cdio')
|
||||||
|
four_o_four_url = four_o_four_url.replace('localhost', 'cdio')
|
||||||
|
|
||||||
# now test for 404 errors
|
# now test for 404 errors
|
||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("edit_page", uuid=uuid, unpause_on_save=1),
|
url_for("edit_page", uuid=uuid, unpause_on_save=1),
|
||||||
data={
|
data={
|
||||||
"url": "https://changedetection.io/404",
|
"url": four_o_four_url,
|
||||||
"tags": "",
|
"tags": "",
|
||||||
"headers": "",
|
|
||||||
'fetch_backend': "html_webdriver",
|
'fetch_backend': "html_webdriver",
|
||||||
'browser_steps-0-operation': 'Goto site',
|
'browser_steps-0-operation': 'Goto site',
|
||||||
'browser_steps-1-operation': 'Click element',
|
'browser_steps-1-operation': 'Click element',
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ class update_worker(threading.Thread):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Exception reached processing watch UUID: {uuid}")
|
logger.error(f"Exception reached processing watch UUID: {uuid}")
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
self.datastore.update_watch(uuid=uuid, update_obj={'last_error': str(e)})
|
self.datastore.update_watch(uuid=uuid, update_obj={'last_error': "Exception: " + str(e)})
|
||||||
# Other serious error
|
# Other serious error
|
||||||
process_changedetection_results = False
|
process_changedetection_results = False
|
||||||
# import traceback
|
# import traceback
|
||||||
|
|||||||
BIN
docs/chrome-extension-screenshot.png
Normal file
BIN
docs/chrome-extension-screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
@@ -9,6 +9,7 @@ flask-login>=0.6.3
|
|||||||
flask-paginate
|
flask-paginate
|
||||||
flask_expects_json~=1.7
|
flask_expects_json~=1.7
|
||||||
flask_restful
|
flask_restful
|
||||||
|
flask_cors # For the Chrome extension to operate
|
||||||
flask_wtf~=1.2
|
flask_wtf~=1.2
|
||||||
flask~=2.3
|
flask~=2.3
|
||||||
inscriptis~=2.2
|
inscriptis~=2.2
|
||||||
@@ -35,10 +36,12 @@ dnspython==2.3.0 # related to eventlet fixes
|
|||||||
# jq not available on Windows so must be installed manually
|
# jq not available on Windows so must be installed manually
|
||||||
|
|
||||||
# Notification library
|
# Notification library
|
||||||
apprise~=1.7.1
|
apprise~=1.7.4
|
||||||
|
|
||||||
# apprise mqtt https://github.com/dgtlmoon/changedetection.io/issues/315
|
# apprise mqtt https://github.com/dgtlmoon/changedetection.io/issues/315
|
||||||
paho-mqtt
|
# and 2.0.0 https://github.com/dgtlmoon/changedetection.io/issues/2241 not yet compatible
|
||||||
|
# use v1.x due to https://github.com/eclipse/paho.mqtt.python/issues/814
|
||||||
|
paho-mqtt>=1.6.1,<2.0.0
|
||||||
|
|
||||||
# This mainly affects some ARM builds, which unlike the other builds ignores "ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1"
|
# This mainly affects some ARM builds, which unlike the other builds ignores "ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1"
|
||||||
# so without this pinning, the newer versions on ARM will forcefully try to build rust, which results in "rust compiler not found"
|
# so without this pinning, the newer versions on ARM will forcefully try to build rust, which results in "rust compiler not found"
|
||||||
@@ -49,7 +52,7 @@ cryptography~=3.4
|
|||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
|
|
||||||
# XPath filtering, lxml is required by bs4 anyway, but put it here to be safe.
|
# XPath filtering, lxml is required by bs4 anyway, but put it here to be safe.
|
||||||
lxml
|
lxml >=4.8.0,<6
|
||||||
|
|
||||||
# XPath 2.0-3.1 support - 4.2.0 broke something?
|
# XPath 2.0-3.1 support - 4.2.0 broke something?
|
||||||
elementpath==4.1.5
|
elementpath==4.1.5
|
||||||
@@ -72,7 +75,7 @@ pillow
|
|||||||
# playwright is installed at Dockerfile build time because it's not available on all platforms
|
# playwright is installed at Dockerfile build time because it's not available on all platforms
|
||||||
|
|
||||||
# experimental release
|
# experimental release
|
||||||
pyppeteer-ng==2.0.0rc2
|
pyppeteer-ng==2.0.0rc5
|
||||||
|
|
||||||
# Include pytest, so if theres a support issue we can ask them to run these tests on their setup
|
# Include pytest, so if theres a support issue we can ask them to run these tests on their setup
|
||||||
pytest ~=7.2
|
pytest ~=7.2
|
||||||
|
|||||||
Reference in New Issue
Block a user