mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2025-11-11 03:56:26 +00:00
Compare commits
35 Commits
loguru-twe
...
more-statu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62bbf78d08 | ||
|
|
c5a4e0aaa3 | ||
|
|
5119efe4fb | ||
|
|
78a2dceb81 | ||
|
|
72c7645f60 | ||
|
|
e09eb47fb7 | ||
|
|
616c0b3f65 | ||
|
|
c90b27823a | ||
|
|
3b16b19a94 | ||
|
|
4ee9fa79e1 | ||
|
|
4b49759113 | ||
|
|
e9a9790cb0 | ||
|
|
593660e2f6 | ||
|
|
7d96b4ba83 | ||
|
|
fca40e4d5b | ||
|
|
66e2dfcead | ||
|
|
bce7eb68fb | ||
|
|
93c0385119 | ||
|
|
e17f3be739 | ||
|
|
3a9f79b756 | ||
|
|
1f5670253e | ||
|
|
fe3cf5ffd2 | ||
|
|
d31a45d49a | ||
|
|
19ee65361d | ||
|
|
677082723c | ||
|
|
96793890f8 | ||
|
|
0439155127 | ||
|
|
29ca2521eb | ||
|
|
7d67ad057c | ||
|
|
2e88872b7e | ||
|
|
b30b718373 | ||
|
|
402f1e47e7 | ||
|
|
9510345e01 | ||
|
|
36085d8cf4 | ||
|
|
399cdf0fbf |
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
groups:
|
||||||
|
all:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -59,4 +59,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v3
|
||||||
|
|||||||
2
.github/workflows/containers.yml
vendored
2
.github/workflows/containers.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
|
|
||||||
|
|||||||
14
.github/workflows/pypi-release.yml
vendored
14
.github/workflows/pypi-release.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
- name: Install pypa/build
|
- name: Install pypa/build
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
- name: Build a binary wheel and a source tarball
|
- name: Build a binary wheel and a source tarball
|
||||||
run: python3 -m build
|
run: python3 -m build
|
||||||
- name: Store the distribution packages
|
- name: Store the distribution packages
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: python-package-distributions
|
name: python-package-distributions
|
||||||
path: dist/
|
path: dist/
|
||||||
@@ -34,18 +34,18 @@ jobs:
|
|||||||
- build
|
- build
|
||||||
steps:
|
steps:
|
||||||
- name: Download all the dists
|
- name: Download all the dists
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: python-package-distributions
|
name: python-package-distributions
|
||||||
path: dist/
|
path: dist/
|
||||||
- name: Test that the basic pip built package runs without error
|
- name: Test that the basic pip built package runs without error
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -ex
|
||||||
pip3 install dist/changedetection.io*.whl
|
pip3 install dist/changedetection.io*.whl
|
||||||
changedetection.io -d /tmp -p 10000 &
|
changedetection.io -d /tmp -p 10000 &
|
||||||
sleep 3
|
sleep 3
|
||||||
curl http://127.0.0.1:10000/static/styles/pure-min.css >/dev/null
|
curl --retry-connrefused --retry 6 http://127.0.0.1:10000/static/styles/pure-min.css >/dev/null
|
||||||
curl http://127.0.0.1:10000/ >/dev/null
|
curl --retry-connrefused --retry 6 http://127.0.0.1:10000/ >/dev/null
|
||||||
killall changedetection.io
|
killall changedetection.io
|
||||||
|
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download all the dists
|
- name: Download all the dists
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: python-package-distributions
|
name: python-package-distributions
|
||||||
path: dist/
|
path: dist/
|
||||||
|
|||||||
2
.github/workflows/test-container-build.yml
vendored
2
.github/workflows/test-container-build.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
|
|
||||||
|
|||||||
58
.github/workflows/test-only.yml
vendored
58
.github/workflows/test-only.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
|
|
||||||
# Mainly just for link/flake8
|
# Mainly just for link/flake8
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
|
|
||||||
@@ -27,13 +27,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
|
|
||||||
docker network create changedet-network
|
docker network create changedet-network
|
||||||
|
|
||||||
# Selenium+browserless
|
# Selenium and sockpuppetbrowser
|
||||||
docker run --network changedet-network -d --hostname selenium -p 4444:4444 --rm --shm-size="2g" selenium/standalone-chrome:4
|
docker run --network changedet-network -d --hostname selenium -p 4444:4444 --rm --shm-size="2g" selenium/standalone-chrome:4
|
||||||
docker run --network changedet-network -d --name browserless --hostname browserless -e "FUNCTION_BUILT_INS=[\"fs\",\"crypto\"]" -e "DEFAULT_LAUNCH_ARGS=[\"--window-size=1920,1080\"]" --rm -p 3000:3000 --shm-size="2g" browserless/chrome:1.60-chrome-stable
|
docker run --network changedet-network -d --cap-add=SYS_ADMIN --name sockpuppetbrowser --hostname sockpuppetbrowser --rm -p 3000:3000 dgtlmoon/sockpuppetbrowser:latest
|
||||||
|
|
||||||
# For accessing custom browser tests
|
# For accessing custom browser tests
|
||||||
docker run --network changedet-network -d --name browserless-custom-url --hostname browserless-custom-url -e "FUNCTION_BUILT_INS=[\"fs\",\"crypto\"]" -e "DEFAULT_LAUNCH_ARGS=[\"--window-size=1920,1080\"]" --rm --shm-size="2g" browserless/chrome:1.60-chrome-stable
|
docker run --network changedet-network -d --cap-add=SYS_ADMIN --name sockpuppetbrowser-custom-url --hostname sockpuppetbrowser-custom-url --rm dgtlmoon/sockpuppetbrowser:latest
|
||||||
|
|
||||||
- name: Build changedetection.io container for testing
|
- name: Build changedetection.io container for testing
|
||||||
run: |
|
run: |
|
||||||
@@ -47,7 +47,13 @@ jobs:
|
|||||||
# Debug SMTP server/echo message back server
|
# Debug SMTP server/echo message back server
|
||||||
docker run --network changedet-network -d -p 11025:11025 -p 11080:11080 --hostname mailserver test-changedetectionio bash -c 'python changedetectionio/tests/smtp/smtp-test-server.py'
|
docker run --network changedet-network -d -p 11025:11025 -p 11080:11080 --hostname mailserver test-changedetectionio bash -c 'python changedetectionio/tests/smtp/smtp-test-server.py'
|
||||||
|
|
||||||
- name: Test built container with pytest
|
- name: Show docker container state and other debug info
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
echo "Running processes in docker..."
|
||||||
|
docker ps
|
||||||
|
|
||||||
|
- name: Test built container with Pytest (generally as requests/plaintext fetching)
|
||||||
run: |
|
run: |
|
||||||
# Unit tests
|
# Unit tests
|
||||||
echo "run test with unittest"
|
echo "run test with unittest"
|
||||||
@@ -61,33 +67,35 @@ jobs:
|
|||||||
# append the docker option. e.g. '-e LOGGER_LEVEL=DEBUG'
|
# append the docker option. e.g. '-e LOGGER_LEVEL=DEBUG'
|
||||||
docker run --network changedet-network test-changedetectionio bash -c 'cd changedetectionio && ./run_basic_tests.sh'
|
docker run --network changedet-network test-changedetectionio bash -c 'cd changedetectionio && ./run_basic_tests.sh'
|
||||||
|
|
||||||
- name: Test built container selenium+browserless/playwright
|
- name: Specific tests in built container for Selenium
|
||||||
run: |
|
run: |
|
||||||
|
|
||||||
# Selenium fetch
|
# Selenium fetch
|
||||||
docker run --rm -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/fetchers/test_content.py && pytest tests/test_errorhandling.py'
|
docker run --rm -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/fetchers/test_content.py && pytest tests/test_errorhandling.py'
|
||||||
|
|
||||||
# Playwright/Browserless fetch
|
- name: Specific tests in built container for Playwright and SocketPuppetBrowser
|
||||||
docker run --rm -e "PLAYWRIGHT_DRIVER_URL=ws://browserless: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'
|
run: |
|
||||||
|
# Playwright via Sockpuppetbrowser fetch
|
||||||
# Settings headers playwright tests - Call back in from Browserless, check headers
|
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 --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://browserless:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
|
||||||
|
- name: Specific tests in built container for headers and requests checks with Playwright
|
||||||
|
run: |
|
||||||
|
# Settings headers playwright tests - Call back in from Sockpuppetbrowser, check headers
|
||||||
|
docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
||||||
|
|
||||||
|
- name: Specific tests in built container for headers and requests checks with Selenium
|
||||||
|
run: |
|
||||||
docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
||||||
docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "USE_EXPERIMENTAL_PUPPETEER_FETCH=yes" -e "PLAYWRIGHT_DRIVER_URL=ws://browserless:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
|
||||||
|
- name: Test built container restock detection via Playwright
|
||||||
# restock detection via playwright - added name=changedet here so that playwright/browserless can connect to it
|
run: |
|
||||||
docker run --rm --name "changedet" -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://browserless:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-port=5004 --live-server-host=0.0.0.0 tests/restock/test_restock.py'
|
# restock detection via playwright - added name=changedet here so that playwright and sockpuppetbrowser can connect to it
|
||||||
|
docker run --rm --name "changedet" -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-port=5004 --live-server-host=0.0.0.0 tests/restock/test_restock.py'
|
||||||
|
|
||||||
- name: Test SMTP notification mime types
|
- name: Test SMTP notification mime types
|
||||||
run: |
|
run: |
|
||||||
# SMTP content types - needs the 'Debug SMTP server/echo message back server' container from above
|
# SMTP content types - needs the 'Debug SMTP server/echo message back server' container from above
|
||||||
docker run --rm --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/smtp/test_notification_smtp.py'
|
docker run --rm --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/smtp/test_notification_smtp.py'
|
||||||
|
|
||||||
- name: Test with puppeteer fetcher and disk cache
|
|
||||||
run: |
|
|
||||||
docker run --rm -e "PUPPETEER_DISK_CACHE=/tmp/data/" -e "USE_EXPERIMENTAL_PUPPETEER_FETCH=yes" -e "PLAYWRIGHT_DRIVER_URL=ws://browserless: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'
|
|
||||||
# Browserless would have had -e "FUNCTION_BUILT_INS=[\"fs\",\"crypto\"]" added above
|
|
||||||
|
|
||||||
- name: Test proxy interaction
|
- name: Test proxy interaction
|
||||||
run: |
|
run: |
|
||||||
cd changedetectionio
|
cd changedetectionio
|
||||||
@@ -106,10 +114,10 @@ jobs:
|
|||||||
docker run --name test-changedetectionio -p 5556:5000 -d test-changedetectionio
|
docker run --name test-changedetectionio -p 5556:5000 -d test-changedetectionio
|
||||||
sleep 3
|
sleep 3
|
||||||
# Should return 0 (no error) when grep finds it
|
# Should return 0 (no error) when grep finds it
|
||||||
curl -s http://localhost:5556 |grep -q checkbox-uuid
|
curl --retry-connrefused --retry 6 -s http://localhost:5556 |grep -q checkbox-uuid
|
||||||
|
|
||||||
# and IPv6
|
# and IPv6
|
||||||
curl -s -g -6 "http://[::1]:5556"|grep -q checkbox-uuid
|
curl --retry-connrefused --retry 6 -s -g -6 "http://[::1]:5556"|grep -q checkbox-uuid
|
||||||
|
|
||||||
# Check whether TRACE log is enabled.
|
# Check whether TRACE log is enabled.
|
||||||
# Also, check whether TRACE is came from STDERR
|
# Also, check whether TRACE is came from STDERR
|
||||||
|
|||||||
@@ -273,3 +273,9 @@ I offer commercial support, this software is depended on by network security, ae
|
|||||||
## Third-party licenses
|
## Third-party licenses
|
||||||
|
|
||||||
changedetectionio.html_tools.elementpath_tostring: Copyright (c), 2018-2021, SISSA (Scuola Internazionale Superiore di Studi Avanzati), Licensed under [MIT license](https://github.com/sissaschool/elementpath/blob/master/LICENSE)
|
changedetectionio.html_tools.elementpath_tostring: Copyright (c), 2018-2021, SISSA (Scuola Internazionale Superiore di Studi Avanzati), Licensed under [MIT license](https://github.com/sissaschool/elementpath/blob/master/LICENSE)
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
Recognition of fantastic contributors to the project
|
||||||
|
|
||||||
|
- Constantin Hong https://github.com/Constantin1489
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
||||||
|
|
||||||
__version__ = '0.45.12'
|
__version__ = '0.45.14'
|
||||||
|
|
||||||
from distutils.util import strtobool
|
from distutils.util import strtobool
|
||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
@@ -60,7 +60,7 @@ def main():
|
|||||||
try:
|
try:
|
||||||
opts, args = getopt.getopt(sys.argv[1:], "6Ccsd:h:p:l:", "port")
|
opts, args = getopt.getopt(sys.argv[1:], "6Ccsd:h:p:l:", "port")
|
||||||
except getopt.GetoptError:
|
except getopt.GetoptError:
|
||||||
print('backend.py -s SSL enable -h [host] -p [port] -d [datastore path] -l [debug level]')
|
print('backend.py -s SSL enable -h [host] -p [port] -d [datastore path] -l [debug level - TRACE, DEBUG(default), INFO, SUCCESS, WARNING, ERROR, CRITICAL]')
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
create_datastore_dir = False
|
create_datastore_dir = False
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ class WatchHistory(Resource):
|
|||||||
|
|
||||||
# Get a list of available history for a watch by UUID
|
# Get a list of available history for a watch by UUID
|
||||||
# curl http://localhost:5000/api/v1/watch/<string:uuid>/history
|
# curl http://localhost:5000/api/v1/watch/<string:uuid>/history
|
||||||
|
@auth.check_token
|
||||||
def get(self, uuid):
|
def get(self, uuid):
|
||||||
"""
|
"""
|
||||||
@api {get} /api/v1/watch/<string:uuid>/history Get a list of all historical snapshots available for a watch
|
@api {get} /api/v1/watch/<string:uuid>/history Get a list of all historical snapshots available for a watch
|
||||||
|
|||||||
@@ -4,22 +4,13 @@
|
|||||||
# Why?
|
# Why?
|
||||||
# `browsersteps_playwright_browser_interface.chromium.connect_over_cdp()` will only run once without async()
|
# `browsersteps_playwright_browser_interface.chromium.connect_over_cdp()` will only run once without async()
|
||||||
# - this flask app is not async()
|
# - this flask app is not async()
|
||||||
# - browserless has a single timeout/keepalive which applies to the session made at .connect_over_cdp()
|
# - A single timeout/keepalive which applies to the session made at .connect_over_cdp()
|
||||||
#
|
#
|
||||||
# So it means that we must unfortunately for now just keep a single timer since .connect_over_cdp() was run
|
# So it means that we must unfortunately for now just keep a single timer since .connect_over_cdp() was run
|
||||||
# and know when that reaches timeout/keepalive :( when that time is up, restart the connection and tell the user
|
# and know when that reaches timeout/keepalive :( when that time is up, restart the connection and tell the user
|
||||||
# that their time is up, insert another coin. (reload)
|
# that their time is up, insert another coin. (reload)
|
||||||
#
|
#
|
||||||
# Bigger picture
|
|
||||||
# - It's horrible that we have this click+wait deal, some nice socket.io solution using something similar
|
|
||||||
# to what the browserless debug UI already gives us would be smarter..
|
|
||||||
#
|
#
|
||||||
# OR
|
|
||||||
# - Some API call that should be hacked into browserless or playwright that we can "/api/bump-keepalive/{session_id}/60"
|
|
||||||
# So we can tell it that we need more time (run this on each action)
|
|
||||||
#
|
|
||||||
# OR
|
|
||||||
# - use multiprocessing to bump this over to its own process and add some transport layer (queue/pipes)
|
|
||||||
|
|
||||||
from distutils.util import strtobool
|
from distutils.util import strtobool
|
||||||
from flask import Blueprint, request, make_response
|
from flask import Blueprint, request, make_response
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ class steppable_browser_interface():
|
|||||||
self.page.locator(selector, timeout=1000).uncheck(timeout=1000)
|
self.page.locator(selector, timeout=1000).uncheck(timeout=1000)
|
||||||
|
|
||||||
|
|
||||||
# Responsible for maintaining a live 'context' with browserless
|
# Responsible for maintaining a live 'context' with the chrome CDP
|
||||||
# @todo - how long do contexts live for anyway?
|
# @todo - how long do contexts live for anyway?
|
||||||
class browsersteps_live_ui(steppable_browser_interface):
|
class browsersteps_live_ui(steppable_browser_interface):
|
||||||
context = None
|
context = None
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q: PriorityQueue
|
|||||||
def accept(uuid):
|
def accept(uuid):
|
||||||
datastore.data['watching'][uuid]['track_ldjson_price_data'] = PRICE_DATA_TRACK_ACCEPT
|
datastore.data['watching'][uuid]['track_ldjson_price_data'] = PRICE_DATA_TRACK_ACCEPT
|
||||||
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}))
|
||||||
return redirect(url_for("form_watch_checknow", uuid=uuid))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@price_data_follower_blueprint.route("/<string:uuid>/reject", methods=['GET'])
|
@price_data_follower_blueprint.route("/<string:uuid>/reject", methods=['GET'])
|
||||||
|
|||||||
@@ -51,8 +51,9 @@ class BrowserStepsStepException(Exception):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# @todo - make base Exception class that announces via logger()
|
||||||
class PageUnloadable(Exception):
|
class PageUnloadable(Exception):
|
||||||
def __init__(self, status_code, url, message, screenshot=False):
|
def __init__(self, status_code=None, url='', message='', screenshot=False):
|
||||||
# Set this so we can use it in other parts of the app
|
# Set this so we can use it in other parts of the app
|
||||||
self.status_code = status_code
|
self.status_code = status_code
|
||||||
self.url = url
|
self.url = url
|
||||||
@@ -60,6 +61,10 @@ class PageUnloadable(Exception):
|
|||||||
self.message = message
|
self.message = message
|
||||||
return
|
return
|
||||||
|
|
||||||
|
class BrowserStepsInUnsupportedFetcher(Exception):
|
||||||
|
def __init__(self, url):
|
||||||
|
self.url = url
|
||||||
|
return
|
||||||
|
|
||||||
class EmptyReply(Exception):
|
class EmptyReply(Exception):
|
||||||
def __init__(self, status_code, url, screenshot=None):
|
def __init__(self, status_code, url, screenshot=None):
|
||||||
@@ -306,117 +311,6 @@ class base_html_playwright(Fetcher):
|
|||||||
with open(destination, 'w') as f:
|
with open(destination, 'w') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
def run_fetch_browserless_puppeteer(self,
|
|
||||||
url,
|
|
||||||
timeout,
|
|
||||||
request_headers,
|
|
||||||
request_body,
|
|
||||||
request_method,
|
|
||||||
ignore_status_codes=False,
|
|
||||||
current_include_filters=None,
|
|
||||||
is_binary=False):
|
|
||||||
|
|
||||||
from pkg_resources import resource_string
|
|
||||||
|
|
||||||
extra_wait_ms = (int(os.getenv("WEBDRIVER_DELAY_BEFORE_CONTENT_READY", 5)) + self.render_extract_delay) * 1000
|
|
||||||
|
|
||||||
self.xpath_element_js = self.xpath_element_js.replace('%ELEMENTS%', visualselector_xpath_selectors)
|
|
||||||
code = resource_string(__name__, "res/puppeteer_fetch.js").decode('utf-8')
|
|
||||||
# In the future inject this is a proper JS package
|
|
||||||
code = code.replace('%xpath_scrape_code%', self.xpath_element_js)
|
|
||||||
code = code.replace('%instock_scrape_code%', self.instock_data_js)
|
|
||||||
|
|
||||||
from requests.exceptions import ConnectTimeout, ReadTimeout
|
|
||||||
wait_browserless_seconds = 240
|
|
||||||
|
|
||||||
browserless_function_url = os.getenv('BROWSERLESS_FUNCTION_URL')
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
if not browserless_function_url:
|
|
||||||
# Convert/try to guess from PLAYWRIGHT_DRIVER_URL
|
|
||||||
o = urlparse(os.getenv('PLAYWRIGHT_DRIVER_URL'))
|
|
||||||
browserless_function_url = o._replace(scheme="http")._replace(path="function").geturl()
|
|
||||||
|
|
||||||
|
|
||||||
# Append proxy connect string
|
|
||||||
if self.proxy:
|
|
||||||
# Remove username/password if it exists in the URL or you will receive "ERR_NO_SUPPORTED_PROXIES" error
|
|
||||||
# Actual authentication handled by Puppeteer/node
|
|
||||||
o = urlparse(self.proxy.get('server'))
|
|
||||||
proxy_url = urllib.parse.quote(o._replace(netloc="{}:{}".format(o.hostname, o.port)).geturl())
|
|
||||||
browserless_function_url = f"{browserless_function_url}&--proxy-server={proxy_url}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
amp = '&' if '?' in browserless_function_url else '?'
|
|
||||||
response = requests.request(
|
|
||||||
method="POST",
|
|
||||||
json={
|
|
||||||
"code": code,
|
|
||||||
"context": {
|
|
||||||
# Very primitive disk cache - USE WITH EXTREME CAUTION
|
|
||||||
# Run browserless container with -e "FUNCTION_BUILT_INS=[\"fs\",\"crypto\"]"
|
|
||||||
'disk_cache_dir': os.getenv("PUPPETEER_DISK_CACHE", False), # or path to disk cache ending in /, ie /tmp/cache/
|
|
||||||
'execute_js': self.webdriver_js_execute_code,
|
|
||||||
'extra_wait_ms': extra_wait_ms,
|
|
||||||
'include_filters': current_include_filters,
|
|
||||||
'req_headers': request_headers,
|
|
||||||
'screenshot_quality': int(os.getenv("PLAYWRIGHT_SCREENSHOT_QUALITY", 72)),
|
|
||||||
'url': url,
|
|
||||||
'user_agent': {k.lower(): v for k, v in request_headers.items()}.get('user-agent', None),
|
|
||||||
'proxy_username': self.proxy.get('username', '') if self.proxy else False,
|
|
||||||
'proxy_password': self.proxy.get('password', '') if self.proxy and self.proxy.get('username') else False,
|
|
||||||
'no_cache_list': [
|
|
||||||
'twitter',
|
|
||||||
'.pdf'
|
|
||||||
],
|
|
||||||
# Could use https://github.com/easylist/easylist here, or install a plugin
|
|
||||||
'block_url_list': [
|
|
||||||
'adnxs.com',
|
|
||||||
'analytics.twitter.com',
|
|
||||||
'doubleclick.net',
|
|
||||||
'google-analytics.com',
|
|
||||||
'googletagmanager',
|
|
||||||
'trustpilot.com'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# @todo /function needs adding ws:// to http:// rebuild this
|
|
||||||
url=browserless_function_url+f"{amp}--disable-features=AudioServiceOutOfProcess&dumpio=true&--disable-remote-fonts",
|
|
||||||
timeout=wait_browserless_seconds)
|
|
||||||
|
|
||||||
except ReadTimeout:
|
|
||||||
raise PageUnloadable(url=url, status_code=None, message=f"No response from browserless in {wait_browserless_seconds}s")
|
|
||||||
except ConnectTimeout:
|
|
||||||
raise PageUnloadable(url=url, status_code=None, message=f"Timed out connecting to browserless, retrying..")
|
|
||||||
else:
|
|
||||||
# 200 Here means that the communication to browserless worked only, not the page state
|
|
||||||
if response.status_code == 200:
|
|
||||||
import base64
|
|
||||||
|
|
||||||
x = response.json()
|
|
||||||
if not x.get('screenshot'):
|
|
||||||
# https://github.com/puppeteer/puppeteer/blob/v1.0.0/docs/troubleshooting.md#tips
|
|
||||||
# https://github.com/puppeteer/puppeteer/issues/1834
|
|
||||||
# https://github.com/puppeteer/puppeteer/issues/1834#issuecomment-381047051
|
|
||||||
# Check your memory is shared and big enough
|
|
||||||
raise ScreenshotUnavailable(url=url, status_code=None)
|
|
||||||
|
|
||||||
if not x.get('content', '').strip():
|
|
||||||
raise EmptyReply(url=url, status_code=None)
|
|
||||||
|
|
||||||
if x.get('status_code', 200) != 200 and not ignore_status_codes:
|
|
||||||
raise Non200ErrorCodeReceived(url=url, status_code=x.get('status_code', 200), page_html=x['content'])
|
|
||||||
|
|
||||||
self.content = x.get('content')
|
|
||||||
self.headers = x.get('headers')
|
|
||||||
self.instock_data = x.get('instock_data')
|
|
||||||
self.screenshot = base64.b64decode(x.get('screenshot'))
|
|
||||||
self.status_code = x.get('status_code')
|
|
||||||
self.xpath_data = x.get('xpath_data')
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Some other error from browserless
|
|
||||||
raise PageUnloadable(url=url, status_code=None, message=response.content.decode('utf-8'))
|
|
||||||
|
|
||||||
def run(self,
|
def run(self,
|
||||||
url,
|
url,
|
||||||
timeout,
|
timeout,
|
||||||
@@ -428,21 +322,6 @@ class base_html_playwright(Fetcher):
|
|||||||
is_binary=False):
|
is_binary=False):
|
||||||
|
|
||||||
|
|
||||||
# For now, USE_EXPERIMENTAL_PUPPETEER_FETCH is not supported by watches with BrowserSteps (for now!)
|
|
||||||
# browser_connection_is_custom doesnt work with puppeteer style fetch (use playwright native too in this case)
|
|
||||||
if not self.browser_connection_is_custom and not self.browser_steps and os.getenv('USE_EXPERIMENTAL_PUPPETEER_FETCH'):
|
|
||||||
if strtobool(os.getenv('USE_EXPERIMENTAL_PUPPETEER_FETCH')):
|
|
||||||
# Temporary backup solution until we rewrite the playwright code
|
|
||||||
return self.run_fetch_browserless_puppeteer(
|
|
||||||
url,
|
|
||||||
timeout,
|
|
||||||
request_headers,
|
|
||||||
request_body,
|
|
||||||
request_method,
|
|
||||||
ignore_status_codes,
|
|
||||||
current_include_filters,
|
|
||||||
is_binary)
|
|
||||||
|
|
||||||
from playwright.sync_api import sync_playwright
|
from playwright.sync_api import sync_playwright
|
||||||
import playwright._impl._errors
|
import playwright._impl._errors
|
||||||
|
|
||||||
@@ -511,8 +390,15 @@ class base_html_playwright(Fetcher):
|
|||||||
extra_wait = int(os.getenv("WEBDRIVER_DELAY_BEFORE_CONTENT_READY", 5)) + self.render_extract_delay
|
extra_wait = int(os.getenv("WEBDRIVER_DELAY_BEFORE_CONTENT_READY", 5)) + self.render_extract_delay
|
||||||
self.page.wait_for_timeout(extra_wait * 1000)
|
self.page.wait_for_timeout(extra_wait * 1000)
|
||||||
|
|
||||||
|
try:
|
||||||
self.status_code = response.status
|
self.status_code = response.status
|
||||||
|
except Exception as e:
|
||||||
|
# https://github.com/dgtlmoon/changedetection.io/discussions/2122#discussioncomment-8241962
|
||||||
|
logger.critical(f"Response from the browser/Playwright did not have a status_code! Response follows.")
|
||||||
|
logger.critical(response)
|
||||||
|
context.close()
|
||||||
|
browser.close()
|
||||||
|
raise PageUnloadable(url=url, status_code=None, message=str(e))
|
||||||
|
|
||||||
if self.status_code != 200 and not ignore_status_codes:
|
if self.status_code != 200 and not ignore_status_codes:
|
||||||
|
|
||||||
@@ -552,16 +438,17 @@ class base_html_playwright(Fetcher):
|
|||||||
# which will significantly increase the IO size between the server and client, it's recommended to use the lowest
|
# which will significantly increase the IO size between the server and client, it's recommended to use the lowest
|
||||||
# acceptable screenshot quality here
|
# acceptable screenshot quality here
|
||||||
try:
|
try:
|
||||||
# The actual screenshot
|
# The actual screenshot - this always base64 and needs decoding! horrible! huge CPU usage
|
||||||
self.screenshot = self.page.screenshot(type='jpeg', full_page=True,
|
self.screenshot = self.page.screenshot(type='jpeg',
|
||||||
quality=int(os.getenv("PLAYWRIGHT_SCREENSHOT_QUALITY", 72)))
|
full_page=True,
|
||||||
|
quality=int(os.getenv("PLAYWRIGHT_SCREENSHOT_QUALITY", 72)),
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# It's likely the screenshot was too long/big and something crashed
|
||||||
|
raise ScreenshotUnavailable(url=url, status_code=self.status_code)
|
||||||
|
finally:
|
||||||
context.close()
|
context.close()
|
||||||
browser.close()
|
browser.close()
|
||||||
raise ScreenshotUnavailable(url=url, status_code=response.status_code)
|
|
||||||
|
|
||||||
context.close()
|
|
||||||
browser.close()
|
|
||||||
|
|
||||||
|
|
||||||
class base_html_webdriver(Fetcher):
|
class base_html_webdriver(Fetcher):
|
||||||
@@ -698,6 +585,9 @@ class html_requests(Fetcher):
|
|||||||
current_include_filters=None,
|
current_include_filters=None,
|
||||||
is_binary=False):
|
is_binary=False):
|
||||||
|
|
||||||
|
if self.browser_steps_get_valid_steps():
|
||||||
|
raise BrowserStepsInUnsupportedFetcher(url=url)
|
||||||
|
|
||||||
# Make requests use a more modern looking user-agent
|
# Make requests use a more modern looking user-agent
|
||||||
if not {k.lower(): v for k, v in request_headers.items()}.get('user-agent', None):
|
if not {k.lower(): v for k, v in request_headers.items()}.get('user-agent', None):
|
||||||
request_headers['User-Agent'] = os.getenv("DEFAULT_SETTINGS_HEADERS_USERAGENT",
|
request_headers['User-Agent'] = os.getenv("DEFAULT_SETTINGS_HEADERS_USERAGENT",
|
||||||
@@ -737,6 +627,8 @@ class html_requests(Fetcher):
|
|||||||
if encoding:
|
if encoding:
|
||||||
r.encoding = encoding
|
r.encoding = encoding
|
||||||
|
|
||||||
|
self.headers = r.headers
|
||||||
|
|
||||||
if not r.content or not len(r.content):
|
if not r.content or not len(r.content):
|
||||||
raise EmptyReply(url=url, status_code=r.status_code)
|
raise EmptyReply(url=url, status_code=r.status_code)
|
||||||
|
|
||||||
@@ -753,7 +645,7 @@ class html_requests(Fetcher):
|
|||||||
else:
|
else:
|
||||||
self.content = r.text
|
self.content = r.text
|
||||||
|
|
||||||
self.headers = r.headers
|
|
||||||
self.raw_content = r.content
|
self.raw_content = r.content
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from threading import Event
|
|||||||
import datetime
|
import datetime
|
||||||
import flask_login
|
import flask_login
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import pytz
|
import pytz
|
||||||
import queue
|
import queue
|
||||||
@@ -317,6 +316,9 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
|
|
||||||
@app.route("/rss", methods=['GET'])
|
@app.route("/rss", methods=['GET'])
|
||||||
def rss():
|
def rss():
|
||||||
|
from jinja2 import Environment, BaseLoader
|
||||||
|
jinja2_env = Environment(loader=BaseLoader)
|
||||||
|
now = time.time()
|
||||||
# Always requires token set
|
# Always requires token set
|
||||||
app_rss_token = datastore.data['settings']['application'].get('rss_access_token')
|
app_rss_token = datastore.data['settings']['application'].get('rss_access_token')
|
||||||
rss_url_token = request.args.get('token')
|
rss_url_token = request.args.get('token')
|
||||||
@@ -380,8 +382,12 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
include_equal=False,
|
include_equal=False,
|
||||||
line_feed_sep="<br>")
|
line_feed_sep="<br>")
|
||||||
|
|
||||||
fe.content(content="<html><body><h4>{}</h4>{}</body></html>".format(watch_title, html_diff),
|
# @todo Make this configurable and also consider html-colored markup
|
||||||
type='CDATA')
|
# @todo User could decide if <link> goes to the diff page, or to the watch link
|
||||||
|
rss_template = "<html><body>\n<h4><a href=\"{{watch_url}}\">{{watch_title}}</a></h4>\n<p>{{html_diff}}</p>\n</body></html>\n"
|
||||||
|
content = jinja2_env.from_string(rss_template).render(watch_title=watch_title, html_diff=html_diff, watch_url=watch.link)
|
||||||
|
|
||||||
|
fe.content(content=content, type='CDATA')
|
||||||
|
|
||||||
fe.guid(guid, permalink=False)
|
fe.guid(guid, permalink=False)
|
||||||
dt = datetime.datetime.fromtimestamp(int(watch.newest_history_key))
|
dt = datetime.datetime.fromtimestamp(int(watch.newest_history_key))
|
||||||
@@ -390,6 +396,7 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
|
|
||||||
response = make_response(fg.rss_str())
|
response = make_response(fg.rss_str())
|
||||||
response.headers.set('Content-Type', 'application/rss+xml;charset=utf-8')
|
response.headers.set('Content-Type', 'application/rss+xml;charset=utf-8')
|
||||||
|
logger.trace(f"RSS generated in {time.time() - now:.3f}s")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@app.route("/", methods=['GET'])
|
@app.route("/", methods=['GET'])
|
||||||
@@ -1603,7 +1610,7 @@ def notification_runner():
|
|||||||
n_object['notification_title'] = datastore.data['settings']['application'].get('notification_title')
|
n_object['notification_title'] = datastore.data['settings']['application'].get('notification_title')
|
||||||
|
|
||||||
if not n_object.get('notification_format') and datastore.data['settings']['application'].get('notification_format'):
|
if not n_object.get('notification_format') and datastore.data['settings']['application'].get('notification_format'):
|
||||||
n_object['notification_title'] = datastore.data['settings']['application'].get('notification_format')
|
n_object['notification_format'] = datastore.data['settings']['application'].get('notification_format')
|
||||||
|
|
||||||
sent_obj = notification.process_notification(n_object, datastore)
|
sent_obj = notification.process_notification(n_object, datastore)
|
||||||
|
|
||||||
|
|||||||
@@ -465,6 +465,7 @@ class watchForm(commonSettingsForm):
|
|||||||
method = SelectField('Request method', choices=valid_method, default=default_method)
|
method = SelectField('Request method', choices=valid_method, default=default_method)
|
||||||
ignore_status_codes = BooleanField('Ignore status codes (process non-2xx status codes as normal)', default=False)
|
ignore_status_codes = BooleanField('Ignore status codes (process non-2xx status codes as normal)', default=False)
|
||||||
check_unique_lines = BooleanField('Only trigger when unique lines appear', default=False)
|
check_unique_lines = BooleanField('Only trigger when unique lines appear', default=False)
|
||||||
|
sort_text_alphabetically = BooleanField('Sort text alphabetically', default=False)
|
||||||
|
|
||||||
filter_text_added = BooleanField('Added lines', default=True)
|
filter_text_added = BooleanField('Added lines', default=True)
|
||||||
filter_text_replaced = BooleanField('Replaced/changed lines', default=True)
|
filter_text_replaced = BooleanField('Replaced/changed lines', default=True)
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class import_url_list(Importer):
|
|||||||
|
|
||||||
# Flask wtform validators wont work with basic auth, use validators package
|
# Flask wtform validators wont work with basic auth, use validators package
|
||||||
# Up to 5000 per batch so we dont flood the server
|
# Up to 5000 per batch so we dont flood the server
|
||||||
# @todo validators.url failed on local hostnames (such as referring to ourself when using browserless)
|
# @todo validators.url will fail when you add your own IP etc
|
||||||
if len(url) and 'http' in url.lower() and good < 5000:
|
if len(url) and 'http' in url.lower() and good < 5000:
|
||||||
extras = None
|
extras = None
|
||||||
if processor:
|
if processor:
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ base_config = {
|
|||||||
'last_error': False,
|
'last_error': False,
|
||||||
'last_viewed': 0, # history key value of the last viewed via the [diff] link
|
'last_viewed': 0, # history key value of the last viewed via the [diff] link
|
||||||
'method': 'GET',
|
'method': 'GET',
|
||||||
|
'notification_alert_count': 0,
|
||||||
# Custom notification content
|
# Custom notification content
|
||||||
'notification_body': None,
|
'notification_body': None,
|
||||||
'notification_format': default_notification_format_for_watch,
|
'notification_format': default_notification_format_for_watch,
|
||||||
@@ -56,6 +57,8 @@ base_config = {
|
|||||||
'previous_md5': False,
|
'previous_md5': False,
|
||||||
'previous_md5_before_filters': False, # Used for skipping changedetection entirely
|
'previous_md5_before_filters': False, # Used for skipping changedetection entirely
|
||||||
'proxy': None, # Preferred proxy connection
|
'proxy': None, # Preferred proxy connection
|
||||||
|
'remote_server_reply': None, # From 'server' reply header
|
||||||
|
'sort_text_alphabetically': False,
|
||||||
'subtractive_selectors': [],
|
'subtractive_selectors': [],
|
||||||
'tag': '', # Old system of text name for a tag, to be removed
|
'tag': '', # Old system of text name for a tag, to be removed
|
||||||
'tags': [], # list of UUIDs to App.Tags
|
'tags': [], # list of UUIDs to App.Tags
|
||||||
@@ -211,7 +214,7 @@ class model(dict):
|
|||||||
# Read the history file as a dict
|
# Read the history file as a dict
|
||||||
fname = os.path.join(self.watch_data_dir, "history.txt")
|
fname = os.path.join(self.watch_data_dir, "history.txt")
|
||||||
if os.path.isfile(fname):
|
if os.path.isfile(fname):
|
||||||
logger.debug("Reading watch history index")
|
logger.debug(f"Reading watch history index for {self.get('uuid')}")
|
||||||
with open(fname, "r") as f:
|
with open(fname, "r") as f:
|
||||||
for i in f.readlines():
|
for i in f.readlines():
|
||||||
if ',' in i:
|
if ',' in i:
|
||||||
@@ -246,10 +249,10 @@ class model(dict):
|
|||||||
@property
|
@property
|
||||||
def has_browser_steps(self):
|
def has_browser_steps(self):
|
||||||
has_browser_steps = self.get('browser_steps') and list(filter(
|
has_browser_steps = self.get('browser_steps') and list(filter(
|
||||||
lambda s: (s['operation'] and len(s['operation']) and s['operation'] != 'Choose one' and s['operation'] != 'Goto site'),
|
lambda s: (s['operation'] and len(s['operation']) and s['operation'] != 'Choose one' and s['operation'] != 'Goto site'),
|
||||||
self.get('browser_steps')))
|
self.get('browser_steps')))
|
||||||
|
|
||||||
return has_browser_steps
|
return has_browser_steps
|
||||||
|
|
||||||
# Returns the newest key, but if theres only 1 record, then it's counted as not being new, so return 0.
|
# Returns the newest key, but if theres only 1 record, then it's counted as not being new, so return 0.
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -116,6 +116,9 @@ def apprise_custom_api_call_wrapper(body, title, notify_type, *args, **kwargs):
|
|||||||
|
|
||||||
def process_notification(n_object, datastore):
|
def process_notification(n_object, datastore):
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
if n_object.get('notification_timestamp'):
|
||||||
|
logger.trace(f"Time since queued {now-n_object['notification_timestamp']:.3f}s")
|
||||||
# Insert variables into the notification content
|
# Insert variables into the notification content
|
||||||
notification_parameters = create_notification_parameters(n_object, datastore)
|
notification_parameters = create_notification_parameters(n_object, datastore)
|
||||||
|
|
||||||
@@ -133,6 +136,8 @@ def process_notification(n_object, datastore):
|
|||||||
# Initially text or whatever
|
# Initially text or whatever
|
||||||
n_format = datastore.data['settings']['application'].get('notification_format', valid_notification_formats[default_notification_format])
|
n_format = datastore.data['settings']['application'].get('notification_format', valid_notification_formats[default_notification_format])
|
||||||
|
|
||||||
|
logger.trace(f"Complete notification body including Jinja and placeholders calculated in {time.time() - now:.3f}s")
|
||||||
|
|
||||||
# https://github.com/caronc/apprise/wiki/Development_LogCapture
|
# https://github.com/caronc/apprise/wiki/Development_LogCapture
|
||||||
# Anything higher than or equal to WARNING (which covers things like Connection errors)
|
# Anything higher than or equal to WARNING (which covers things like Connection errors)
|
||||||
# raise it as an exception
|
# raise it as an exception
|
||||||
@@ -147,6 +152,10 @@ def process_notification(n_object, datastore):
|
|||||||
with apprise.LogCapture(level=apprise.logging.DEBUG) as logs:
|
with apprise.LogCapture(level=apprise.logging.DEBUG) as logs:
|
||||||
for url in n_object['notification_urls']:
|
for url in n_object['notification_urls']:
|
||||||
url = url.strip()
|
url = url.strip()
|
||||||
|
if not url:
|
||||||
|
logger.warning(f"Process Notification: skipping empty notification URL.")
|
||||||
|
continue
|
||||||
|
|
||||||
logger.info(">> Process Notification: AppRise notifying {}".format(url))
|
logger.info(">> Process Notification: AppRise notifying {}".format(url))
|
||||||
url = jinja2_env.from_string(url).render(**notification_parameters)
|
url = jinja2_env.from_string(url).render(**notification_parameters)
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class difference_detection_processor():
|
|||||||
proxy_url = None
|
proxy_url = None
|
||||||
if preferred_proxy_id:
|
if preferred_proxy_id:
|
||||||
proxy_url = self.datastore.proxy_list.get(preferred_proxy_id).get('url')
|
proxy_url = self.datastore.proxy_list.get(preferred_proxy_id).get('url')
|
||||||
logger.debug(f"Using proxy Key: {preferred_proxy_id} as Proxy URL {proxy_url}")
|
logger.debug(f"Selected proxy key '{preferred_proxy_id}' as proxy URL '{proxy_url}' for {url}")
|
||||||
|
|
||||||
# Now call the fetcher (playwright/requests/etc) with arguments that only a fetcher would need.
|
# Now call the fetcher (playwright/requests/etc) with arguments that only a fetcher would need.
|
||||||
# When browser_connection_url is None, it method should default to working out whats the best defaults (os env vars etc)
|
# When browser_connection_url is None, it method should default to working out whats the best defaults (os env vars etc)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
|
||||||
import hashlib
|
|
||||||
import urllib3
|
|
||||||
from . import difference_detection_processor
|
from . import difference_detection_processor
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from loguru import logger
|
||||||
|
import hashlib
|
||||||
|
import urllib3
|
||||||
|
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
@@ -43,11 +44,13 @@ class perform_site_check(difference_detection_processor):
|
|||||||
fetched_md5 = hashlib.md5(self.fetcher.instock_data.encode('utf-8')).hexdigest()
|
fetched_md5 = hashlib.md5(self.fetcher.instock_data.encode('utf-8')).hexdigest()
|
||||||
# 'Possibly in stock' comes from stock-not-in-stock.js when no string found above the fold.
|
# 'Possibly in stock' comes from stock-not-in-stock.js when no string found above the fold.
|
||||||
update_obj["in_stock"] = True if self.fetcher.instock_data == 'Possibly in stock' else False
|
update_obj["in_stock"] = True if self.fetcher.instock_data == 'Possibly in stock' else False
|
||||||
|
logger.debug(f"Watch UUID {uuid} restock check returned '{self.fetcher.instock_data}' from JS scraper.")
|
||||||
else:
|
else:
|
||||||
raise UnableToExtractRestockData(status_code=self.fetcher.status_code)
|
raise UnableToExtractRestockData(status_code=self.fetcher.status_code)
|
||||||
|
|
||||||
# The main thing that all this at the moment comes down to :)
|
# The main thing that all this at the moment comes down to :)
|
||||||
changed_detected = False
|
changed_detected = False
|
||||||
|
logger.debug(f"Watch UUID {uuid} restock check - Previous MD5: {watch.get('previous_md5')}, Fetched MD5 {fetched_md5}")
|
||||||
|
|
||||||
if watch.get('previous_md5') and watch.get('previous_md5') != fetched_md5:
|
if watch.get('previous_md5') and watch.get('previous_md5') != fetched_md5:
|
||||||
# Yes if we only care about it going to instock, AND we are in stock
|
# Yes if we only care about it going to instock, AND we are in stock
|
||||||
@@ -60,5 +63,4 @@ class perform_site_check(difference_detection_processor):
|
|||||||
|
|
||||||
# Always record the new checksum
|
# Always record the new checksum
|
||||||
update_obj["previous_md5"] = fetched_md5
|
update_obj["previous_md5"] = fetched_md5
|
||||||
|
|
||||||
return changed_detected, update_obj, self.fetcher.instock_data.encode('utf-8').strip()
|
return changed_detected, update_obj, self.fetcher.instock_data.encode('utf-8').strip()
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import os
|
|||||||
import re
|
import re
|
||||||
import urllib3
|
import urllib3
|
||||||
|
|
||||||
|
from . import difference_detection_processor
|
||||||
|
from ..html_tools import PERL_STYLE_REGEX, cdata_in_document_to_text
|
||||||
from changedetectionio import content_fetcher, html_tools
|
from changedetectionio import content_fetcher, html_tools
|
||||||
from changedetectionio.blueprint.price_data_follower import PRICE_DATA_TRACK_ACCEPT, PRICE_DATA_TRACK_REJECT
|
from changedetectionio.blueprint.price_data_follower import PRICE_DATA_TRACK_ACCEPT, PRICE_DATA_TRACK_REJECT
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from . import difference_detection_processor
|
|
||||||
from ..html_tools import PERL_STYLE_REGEX, cdata_in_document_to_text
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
@@ -116,7 +116,9 @@ class perform_site_check(difference_detection_processor):
|
|||||||
# and then use getattr https://docs.python.org/3/reference/datamodel.html#object.__getitem__
|
# and then use getattr https://docs.python.org/3/reference/datamodel.html#object.__getitem__
|
||||||
# https://realpython.com/inherit-python-dict/ instead of doing it procedurely
|
# https://realpython.com/inherit-python-dict/ instead of doing it procedurely
|
||||||
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')
|
||||||
include_filters_rule = [*watch.get('include_filters', []), *include_filters_from_tags]
|
|
||||||
|
# 1845 - remove duplicated filters in both group and watch include filter
|
||||||
|
include_filters_rule = list({*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", []),
|
||||||
@@ -202,6 +204,12 @@ class perform_site_check(difference_detection_processor):
|
|||||||
is_rss=is_rss # #1874 activate the <title workaround hack
|
is_rss=is_rss # #1874 activate the <title workaround hack
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if watch.get('sort_text_alphabetically') and stripped_text_from_html:
|
||||||
|
# Note: Because a <p>something</p> will add an extra line feed to signify the paragraph gap
|
||||||
|
# we end up with 'Some text\n\n', sorting will add all those extra \n at the start, so we remove them here.
|
||||||
|
stripped_text_from_html = stripped_text_from_html.replace('\n\n', '\n')
|
||||||
|
stripped_text_from_html = '\n'.join( sorted(stripped_text_from_html.splitlines(), key=lambda x: x.lower() ))
|
||||||
|
|
||||||
# Re #340 - return the content before the 'ignore text' was applied
|
# Re #340 - return the content before the 'ignore text' was applied
|
||||||
text_content_before_ignored_filter = stripped_text_from_html.encode('utf-8')
|
text_content_before_ignored_filter = stripped_text_from_html.encode('utf-8')
|
||||||
|
|
||||||
@@ -335,6 +343,8 @@ class perform_site_check(difference_detection_processor):
|
|||||||
if not watch['title'] or not len(watch['title']):
|
if not watch['title'] or not len(watch['title']):
|
||||||
update_obj['title'] = html_tools.extract_element(find='title', html_content=self.fetcher.content)
|
update_obj['title'] = html_tools.extract_element(find='title', html_content=self.fetcher.content)
|
||||||
|
|
||||||
|
logger.debug(f"Watch UUID {uuid} content check - Previous MD5: {watch.get('previous_md5')}, Fetched MD5 {fetched_md5}")
|
||||||
|
|
||||||
if changed_detected:
|
if changed_detected:
|
||||||
if watch.get('check_unique_lines', False):
|
if watch.get('check_unique_lines', False):
|
||||||
has_unique_lines = watch.lines_contain_something_unique_compared_to_history(lines=stripped_text_from_html.splitlines())
|
has_unique_lines = watch.lines_contain_something_unique_compared_to_history(lines=stripped_text_from_html.splitlines())
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ module.exports = async ({page, context}) => {
|
|||||||
var xpath_data;
|
var xpath_data;
|
||||||
var instock_data;
|
var instock_data;
|
||||||
try {
|
try {
|
||||||
// Not sure the best way here, in the future this should be a new package added to npm then run in browserless
|
// Not sure the best way here, in the future this should be a new package added to npm then run in evaluatedCode
|
||||||
// (Once the old playwright is removed)
|
// (Once the old playwright is removed)
|
||||||
xpath_data = await page.evaluate((include_filters) => {%xpath_scrape_code%}, include_filters);
|
xpath_data = await page.evaluate((include_filters) => {%xpath_scrape_code%}, include_filters);
|
||||||
instock_data = await page.evaluate(() => {%instock_scrape_code%});
|
instock_data = await page.evaluate(() => {%instock_scrape_code%});
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
// Restock Detector
|
||||||
|
// (c) Leigh Morresi dgtlmoon@gmail.com
|
||||||
|
//
|
||||||
|
// Assumes the product is in stock to begin with, unless the following appears above the fold ;
|
||||||
|
// - outOfStockTexts appears above the fold (out of stock)
|
||||||
|
// - negateOutOfStockRegex (really is in stock)
|
||||||
|
|
||||||
function isItemInStock() {
|
function isItemInStock() {
|
||||||
// @todo Pass these in so the same list can be used in non-JS fetchers
|
// @todo Pass these in so the same list can be used in non-JS fetchers
|
||||||
const outOfStockTexts = [
|
const outOfStockTexts = [
|
||||||
@@ -29,6 +36,7 @@ function isItemInStock() {
|
|||||||
'nicht zur verfügung',
|
'nicht zur verfügung',
|
||||||
'niet beschikbaar',
|
'niet beschikbaar',
|
||||||
'niet leverbaar',
|
'niet leverbaar',
|
||||||
|
'niet op voorraad',
|
||||||
'no disponible temporalmente',
|
'no disponible temporalmente',
|
||||||
'no longer in stock',
|
'no longer in stock',
|
||||||
'no tickets available',
|
'no tickets available',
|
||||||
@@ -40,6 +48,7 @@ function isItemInStock() {
|
|||||||
'não estamos a aceitar encomendas',
|
'não estamos a aceitar encomendas',
|
||||||
'out of stock',
|
'out of stock',
|
||||||
'out-of-stock',
|
'out-of-stock',
|
||||||
|
'prodotto esaurito',
|
||||||
'produkt niedostępny',
|
'produkt niedostępny',
|
||||||
'sold out',
|
'sold out',
|
||||||
'sold-out',
|
'sold-out',
|
||||||
@@ -56,6 +65,7 @@ function isItemInStock() {
|
|||||||
'품절'
|
'품절'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
|
||||||
function getElementBaseText(element) {
|
function getElementBaseText(element) {
|
||||||
// .textContent can include text from children which may give the wrong results
|
// .textContent can include text from children which may give the wrong results
|
||||||
// scan only immediate TEXT_NODEs, which will be a child of the element
|
// scan only immediate TEXT_NODEs, which will be a child of the element
|
||||||
@@ -66,19 +76,13 @@ function isItemInStock() {
|
|||||||
return text.toLowerCase().trim();
|
return text.toLowerCase().trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
const negateOutOfStockRegexs = [
|
const negateOutOfStockRegex = new RegExp('([0-9] in stock|add to cart)', 'ig');
|
||||||
'[0-9] in stock'
|
|
||||||
]
|
|
||||||
var negateOutOfStockRegexs_r = [];
|
|
||||||
for (let i = 0; i < negateOutOfStockRegexs.length; i++) {
|
|
||||||
negateOutOfStockRegexs_r.push(new RegExp(negateOutOfStockRegexs[0], 'g'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The out-of-stock or in-stock-text is generally always above-the-fold
|
// The out-of-stock or in-stock-text is generally always above-the-fold
|
||||||
// and often below-the-fold is a list of related products that may or may not contain trigger text
|
// and often below-the-fold is a list of related products that may or may not contain trigger text
|
||||||
// so it's good to filter to just the 'above the fold' elements
|
// so it's good to filter to just the 'above the fold' elements
|
||||||
// and it should be atleast 100px from the top to ignore items in the toolbar, sometimes menu items like "Coming soon" exist
|
// and it should be atleast 100px from the top to ignore items in the toolbar, sometimes menu items like "Coming soon" exist
|
||||||
const elementsToScan = Array.from(document.getElementsByTagName('*')).filter(element => element.getBoundingClientRect().top + window.scrollY <= window.innerHeight && element.getBoundingClientRect().top + window.scrollY >= 100);
|
const elementsToScan = Array.from(document.getElementsByTagName('*')).filter(element => element.getBoundingClientRect().top + window.scrollY <= vh && element.getBoundingClientRect().top + window.scrollY >= 100);
|
||||||
|
|
||||||
var elementText = "";
|
var elementText = "";
|
||||||
|
|
||||||
@@ -94,10 +98,8 @@ function isItemInStock() {
|
|||||||
|
|
||||||
if (elementText.length) {
|
if (elementText.length) {
|
||||||
// try which ones could mean its in stock
|
// try which ones could mean its in stock
|
||||||
for (let i = 0; i < negateOutOfStockRegexs.length; i++) {
|
if (negateOutOfStockRegex.test(elementText)) {
|
||||||
if (negateOutOfStockRegexs_r[i].test(elementText)) {
|
return 'Possibly in stock';
|
||||||
return 'Possibly in stock';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,16 @@
|
|||||||
set -x
|
set -x
|
||||||
|
|
||||||
# A extra browser is configured, but we never chose to use it, so it should NOT show in the logs
|
# A extra browser is configured, but we never chose to use it, so it should NOT show in the logs
|
||||||
docker run --rm -e "PLAYWRIGHT_DRIVER_URL=ws://browserless:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/custom_browser_url/test_custom_browser_url.py::test_request_not_via_custom_browser_url'
|
docker run --rm -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/custom_browser_url/test_custom_browser_url.py::test_request_not_via_custom_browser_url'
|
||||||
docker logs browserless-custom-url &>log.txt
|
docker logs sockpuppetbrowser-custom-url &>log.txt
|
||||||
grep 'custom-browser-search-string=1' log.txt
|
grep 'custom-browser-search-string=1' log.txt
|
||||||
if [ $? -ne 1 ]
|
if [ $? -ne 1 ]
|
||||||
then
|
then
|
||||||
echo "Saw a request in 'browserless-custom-url' container with 'custom-browser-search-string=1' when I should not"
|
echo "Saw a request in 'sockpuppetbrowser-custom-url' container with 'custom-browser-search-string=1' when I should not"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker logs browserless &>log.txt
|
docker logs sockpuppetbrowser &>log.txt
|
||||||
grep 'custom-browser-search-string=1' log.txt
|
grep 'custom-browser-search-string=1' log.txt
|
||||||
if [ $? -ne 1 ]
|
if [ $? -ne 1 ]
|
||||||
then
|
then
|
||||||
@@ -24,16 +24,16 @@ then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Special connect string should appear in the custom-url container, but not in the 'default' one
|
# Special connect string should appear in the custom-url container, but not in the 'default' one
|
||||||
docker run --rm -e "PLAYWRIGHT_DRIVER_URL=ws://browserless:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/custom_browser_url/test_custom_browser_url.py::test_request_via_custom_browser_url'
|
docker run --rm -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/custom_browser_url/test_custom_browser_url.py::test_request_via_custom_browser_url'
|
||||||
docker logs browserless-custom-url &>log.txt
|
docker logs sockpuppetbrowser-custom-url &>log.txt
|
||||||
grep 'custom-browser-search-string=1' log.txt
|
grep 'custom-browser-search-string=1' log.txt
|
||||||
if [ $? -ne 0 ]
|
if [ $? -ne 0 ]
|
||||||
then
|
then
|
||||||
echo "Did not see request in 'browserless-custom-url' container with 'custom-browser-search-string=1' when I should"
|
echo "Did not see request in 'sockpuppetbrowser-custom-url' container with 'custom-browser-search-string=1' when I should"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker logs browserless &>log.txt
|
docker logs sockpuppetbrowser &>log.txt
|
||||||
grep 'custom-browser-search-string=1' log.txt
|
grep 'custom-browser-search-string=1' log.txt
|
||||||
if [ $? -ne 1 ]
|
if [ $? -ne 1 ]
|
||||||
then
|
then
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ docker run --network changedet-network \
|
|||||||
docker run --network changedet-network \
|
docker run --network changedet-network \
|
||||||
-e "SOCKSTEST=manual-playwright" \
|
-e "SOCKSTEST=manual-playwright" \
|
||||||
-v `pwd`/tests/proxy_socks5/proxies.json-example-noauth:/app/changedetectionio/test-datastore/proxies.json \
|
-v `pwd`/tests/proxy_socks5/proxies.json-example-noauth:/app/changedetectionio/test-datastore/proxies.json \
|
||||||
-e "PLAYWRIGHT_DRIVER_URL=ws://browserless:3000" \
|
-e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" \
|
||||||
--rm \
|
--rm \
|
||||||
test-changedetectionio \
|
test-changedetectionio \
|
||||||
bash -c 'cd changedetectionio && pytest tests/proxy_socks5/test_socks5_proxy_sources.py'
|
bash -c 'cd changedetectionio && pytest tests/proxy_socks5/test_socks5_proxy_sources.py'
|
||||||
|
|||||||
44
changedetectionio/static/images/steps.svg
Normal file
44
changedetectionio/static/images/steps.svg
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 19.966091 17.999964"
|
||||||
|
class="css-1oqmxjn"
|
||||||
|
version="1.1"
|
||||||
|
id="svg4"
|
||||||
|
sodipodi:docname="steps.svg"
|
||||||
|
width="19.966091"
|
||||||
|
height="17.999964"
|
||||||
|
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs8" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview6"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
showgrid="false"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
inkscape:zoom="8.6354167"
|
||||||
|
inkscape:cx="-1.3896261"
|
||||||
|
inkscape:cy="6.1375151"
|
||||||
|
inkscape:window-width="1280"
|
||||||
|
inkscape:window-height="667"
|
||||||
|
inkscape:window-x="2419"
|
||||||
|
inkscape:window-y="250"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="svg4" />
|
||||||
|
<path
|
||||||
|
d="m 16.95807,12.000003 c -0.7076,0.0019 -1.3917,0.2538 -1.9316,0.7113 -0.5398,0.4575 -0.9005,1.091 -1.0184,1.7887 H 5.60804 c -0.80847,0.0297 -1.60693,-0.1865 -2.29,-0.62 -0.26632,-0.1847 -0.48375,-0.4315 -0.63356,-0.7189 -0.14982,-0.2874 -0.22753,-0.607 -0.22644,-0.9311 -0.02843,-0.3931 0.03646,-0.7873 0.1894,-1.1505 0.15293,-0.3632 0.38957,-0.6851 0.6906,-0.9395 0.66628,-0.4559004 1.4637,-0.6807004 2.27,-0.6400004 h 8.35003 c 0.8515,-0.0223 1.6727,-0.3206 2.34,-0.85 0.3971,-0.3622 0.7076,-0.8091 0.9084,-1.3077 0.2008,-0.49857 0.2868,-1.03596 0.2516,-1.57229 0.0113,-0.47161 -0.0887,-0.93924 -0.292,-1.36493 -0.2033,-0.4257 -0.5041,-0.79745 -0.878,-1.08507 -0.7801,-0.55815 -1.7212,-0.84609 -2.68,-0.82 H 5.95804 c -0.12537,-0.7417 -0.5248,-1.40924 -1.11913,-1.87032996 -0.59434,-0.46108 -1.3402,-0.68207 -2.08979,-0.61917 -0.74958,0.06291 -1.44818,0.40512 -1.95736,0.95881 C 0.28259,1.5230126 0,2.2477926 0,3.0000126 c 0,0.75222 0.28259,1.47699 0.79176,2.03068 0.50918,0.55369 1.20778,0.8959 1.95736,0.95881 0.74959,0.0629 1.49545,-0.15808 2.08979,-0.61917 0.59433,-0.46109 0.99376,-1.12863 1.11913,-1.87032 h 7.70003 c 0.7353,-0.03061 1.4599,0.18397 2.06,0.61 0.2548,0.19335 0.4595,0.445 0.597,0.73385 0.1375,0.28884 0.2036,0.60644 0.193,0.92615 0.0316,0.38842 -0.0247,0.77898 -0.165,1.14258 -0.1402,0.36361 -0.3607,0.69091 -0.645,0.95741 -0.5713,0.4398 -1.2799,0.663 -2,0.63 H 5.69804 c -1.03259,-0.0462 -2.05065,0.2568 -2.89,0.86 -0.43755,0.3361 -0.78838,0.7720004 -1.02322,1.2712004 -0.23484,0.4993 -0.34688,1.0474 -0.32678,1.5988 -0.00726,0.484 0.10591,0.9622 0.32934,1.3916 0.22344,0.4295 0.55012,0.7966 0.95066,1.0684 0.85039,0.5592 1.85274,0.8421 2.87,0.81 h 8.40003 c 0.0954,0.5643 0.3502,1.0896 0.7343,1.5138 0.3842,0.4242 0.8817,0.7297 1.4338,0.8803 0.5521,0.1507 1.1358,0.1403 1.6822,-0.0299 0.5464,-0.1702 1.0328,-0.4932 1.4016,-0.9308 0.3688,-0.4376 0.6048,-0.9716 0.6801,-1.5389 0.0752,-0.5673 -0.0134,-1.1444 -0.2554,-1.663 -0.242,-0.5186 -0.6273,-0.9572 -1.1104,-1.264 -0.4831,-0.3068 -1.0439,-0.469 -1.6162,-0.4675 z m 0,5 c -0.3956,0 -0.7823,-0.1173 -1.1112,-0.3371 -0.3289,-0.2197 -0.5852,-0.5321 -0.7366,-0.8975 -0.1514,-0.3655 -0.191,-0.7676 -0.1138,-1.1556 0.0772,-0.3879 0.2677,-0.7443 0.5474,-1.024 0.2797,-0.2797 0.636,-0.4702 1.024,-0.5474 0.388,-0.0771 0.7901,-0.0375 1.1555,0.1138 0.3655,0.1514 0.6778,0.4078 0.8976,0.7367 0.2198,0.3289 0.3371,0.7155 0.3371,1.1111 0,0.5304 -0.2107,1.0391 -0.5858,1.4142 -0.3751,0.3751 -0.8838,0.5858 -1.4142,0.5858 z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#777777;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -10,7 +10,7 @@ $(document).ready(function () {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
var browsersteps_session_id;
|
var browsersteps_session_id;
|
||||||
var browserless_seconds_remaining = 0;
|
var browser_interface_seconds_remaining = 0;
|
||||||
var apply_buttons_disabled = false;
|
var apply_buttons_disabled = false;
|
||||||
var include_text_elements = $("#include_text_elements");
|
var include_text_elements = $("#include_text_elements");
|
||||||
var xpath_data = false;
|
var xpath_data = false;
|
||||||
@@ -49,7 +49,7 @@ $(document).ready(function () {
|
|||||||
$('#browsersteps-img').removeAttr('src');
|
$('#browsersteps-img').removeAttr('src');
|
||||||
$("#browsersteps-click-start").show();
|
$("#browsersteps-click-start").show();
|
||||||
$("#browsersteps-selector-wrapper .spinner").hide();
|
$("#browsersteps-selector-wrapper .spinner").hide();
|
||||||
browserless_seconds_remaining = 0;
|
browser_interface_seconds_remaining = 0;
|
||||||
browsersteps_session_id = false;
|
browsersteps_session_id = false;
|
||||||
apply_buttons_disabled = false;
|
apply_buttons_disabled = false;
|
||||||
ctx.clearRect(0, 0, c.width, c.height);
|
ctx.clearRect(0, 0, c.width, c.height);
|
||||||
@@ -61,12 +61,12 @@ $(document).ready(function () {
|
|||||||
$('#browser_steps >li:first-child').css('opacity', '0.5');
|
$('#browser_steps >li:first-child').css('opacity', '0.5');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show seconds remaining until playwright/browserless needs to restart the session
|
// Show seconds remaining until the browser interface needs to restart the session
|
||||||
// (See comment at the top of changedetectionio/blueprint/browser_steps/__init__.py )
|
// (See comment at the top of changedetectionio/blueprint/browser_steps/__init__.py )
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (browserless_seconds_remaining >= 1) {
|
if (browser_interface_seconds_remaining >= 1) {
|
||||||
document.getElementById('browserless-seconds-remaining').innerText = browserless_seconds_remaining + " seconds remaining in session";
|
document.getElementById('browser-seconds-remaining').innerText = browser_interface_seconds_remaining + " seconds remaining in session";
|
||||||
browserless_seconds_remaining -= 1;
|
browser_interface_seconds_remaining -= 1;
|
||||||
}
|
}
|
||||||
}, "1000")
|
}, "1000")
|
||||||
|
|
||||||
@@ -261,7 +261,7 @@ $(document).ready(function () {
|
|||||||
// This should trigger 'Goto site'
|
// This should trigger 'Goto site'
|
||||||
console.log("Got startup response, requesting Goto-Site (first) step fake click");
|
console.log("Got startup response, requesting Goto-Site (first) step fake click");
|
||||||
$('#browser_steps >li:first-child .apply').click();
|
$('#browser_steps >li:first-child .apply').click();
|
||||||
browserless_seconds_remaining = 500;
|
browser_interface_seconds_remaining = 500;
|
||||||
set_first_gotosite_disabled();
|
set_first_gotosite_disabled();
|
||||||
}).fail(function (data) {
|
}).fail(function (data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
|||||||
@@ -90,5 +90,10 @@ $(document).ready(function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$('#diff-form').on('submit', function (e) {
|
||||||
|
if ($('select[name=from_version]').val() === $('select[name=to_version]').val()) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Error - You are trying to compare the same version.');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ html[data-darkmode="true"] {
|
|||||||
html[data-darkmode="true"] .watch-table .title-col a[target="_blank"]::after,
|
html[data-darkmode="true"] .watch-table .title-col a[target="_blank"]::after,
|
||||||
html[data-darkmode="true"] .watch-table .current-diff-url::after {
|
html[data-darkmode="true"] .watch-table .current-diff-url::after {
|
||||||
filter: invert(0.5) hue-rotate(10deg) brightness(2); }
|
filter: invert(0.5) hue-rotate(10deg) brightness(2); }
|
||||||
|
html[data-darkmode="true"] .watch-table .status-browsersteps {
|
||||||
|
filter: invert(0.5) hue-rotate(10deg) brightness(1.5); }
|
||||||
html[data-darkmode="true"] .watch-table .watch-controls .state-off img {
|
html[data-darkmode="true"] .watch-table .watch-controls .state-off img {
|
||||||
opacity: 0.3; }
|
opacity: 0.3; }
|
||||||
html[data-darkmode="true"] .watch-table .watch-controls .state-on img {
|
html[data-darkmode="true"] .watch-table .watch-controls .state-on img {
|
||||||
|
|||||||
@@ -152,6 +152,10 @@ html[data-darkmode="true"] {
|
|||||||
filter: invert(.5) hue-rotate(10deg) brightness(2);
|
filter: invert(.5) hue-rotate(10deg) brightness(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-browsersteps {
|
||||||
|
filter: invert(.5) hue-rotate(10deg) brightness(1.5);
|
||||||
|
}
|
||||||
|
|
||||||
.watch-controls {
|
.watch-controls {
|
||||||
.state-off {
|
.state-off {
|
||||||
img {
|
img {
|
||||||
|
|||||||
@@ -342,6 +342,8 @@ html[data-darkmode="true"] {
|
|||||||
html[data-darkmode="true"] .watch-table .title-col a[target="_blank"]::after,
|
html[data-darkmode="true"] .watch-table .title-col a[target="_blank"]::after,
|
||||||
html[data-darkmode="true"] .watch-table .current-diff-url::after {
|
html[data-darkmode="true"] .watch-table .current-diff-url::after {
|
||||||
filter: invert(0.5) hue-rotate(10deg) brightness(2); }
|
filter: invert(0.5) hue-rotate(10deg) brightness(2); }
|
||||||
|
html[data-darkmode="true"] .watch-table .status-browsersteps {
|
||||||
|
filter: invert(0.5) hue-rotate(10deg) brightness(1.5); }
|
||||||
html[data-darkmode="true"] .watch-table .watch-controls .state-off img {
|
html[data-darkmode="true"] .watch-table .watch-controls .state-off img {
|
||||||
opacity: 0.3; }
|
opacity: 0.3; }
|
||||||
html[data-darkmode="true"] .watch-table .watch-controls .state-on img {
|
html[data-darkmode="true"] .watch-table .watch-controls .state-on img {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class ChangeDetectionStore:
|
|||||||
for uuid, watch in self.__data['watching'].items():
|
for uuid, watch in self.__data['watching'].items():
|
||||||
watch['uuid']=uuid
|
watch['uuid']=uuid
|
||||||
self.__data['watching'][uuid] = Watch.model(datastore_path=self.datastore_path, default=watch)
|
self.__data['watching'][uuid] = Watch.model(datastore_path=self.datastore_path, default=watch)
|
||||||
logger.debug(f"Watching: {uuid} {self.__data['watching'][uuid]['url']}")
|
logger.info(f"Watching: {uuid} {self.__data['watching'][uuid]['url']}")
|
||||||
|
|
||||||
# First time ran, Create the datastore.
|
# First time ran, Create the datastore.
|
||||||
except (FileNotFoundError):
|
except (FileNotFoundError):
|
||||||
@@ -255,6 +255,7 @@ class ChangeDetectionStore:
|
|||||||
'last_viewed': 0,
|
'last_viewed': 0,
|
||||||
'previous_md5': False,
|
'previous_md5': False,
|
||||||
'previous_md5_before_filters': False,
|
'previous_md5_before_filters': False,
|
||||||
|
'remote_server_reply': None,
|
||||||
'track_ldjson_price_data': None,
|
'track_ldjson_price_data': None,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -616,7 +617,7 @@ class ChangeDetectionStore:
|
|||||||
|
|
||||||
for uuid, tag in self.__data['settings']['application'].get('tags', {}).items():
|
for uuid, tag in self.__data['settings']['application'].get('tags', {}).items():
|
||||||
if n == tag.get('title', '').lower().strip():
|
if n == tag.get('title', '').lower().strip():
|
||||||
logger.error(f">>> Tag {name} already exists")
|
logger.warning(f"Tag '{name}' already exists, skipping creation.")
|
||||||
return uuid
|
return uuid
|
||||||
|
|
||||||
# Eventually almost everything todo with a watch will apply as a Tag
|
# Eventually almost everything todo with a watch will apply as a Tag
|
||||||
|
|||||||
@@ -115,6 +115,12 @@
|
|||||||
Warning: Contents of <code>{{ '{{diff}}' }}</code>, <code>{{ '{{diff_removed}}' }}</code>, and <code>{{ '{{diff_added}}' }}</code> depend on how the difference algorithm perceives the change. <br>
|
Warning: Contents of <code>{{ '{{diff}}' }}</code>, <code>{{ '{{diff_removed}}' }}</code>, and <code>{{ '{{diff_added}}' }}</code> depend on how the difference algorithm perceives the change. <br>
|
||||||
For example, an addition or removal could be perceived as a change in some cases. <a target="_new" href="https://github.com/dgtlmoon/changedetection.io/wiki/Using-the-%7B%7Bdiff%7D%7D,-%7B%7Bdiff_added%7D%7D,-and-%7B%7Bdiff_removed%7D%7D-notification-tokens">More Here</a> <br>
|
For example, an addition or removal could be perceived as a change in some cases. <a target="_new" href="https://github.com/dgtlmoon/changedetection.io/wiki/Using-the-%7B%7Bdiff%7D%7D,-%7B%7Bdiff_added%7D%7D,-and-%7B%7Bdiff_removed%7D%7D-notification-tokens">More Here</a> <br>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
For JSON payloads, use <strong>|tojson</strong> without quotes for automatic escaping, for example - <code>{ "name": {{ '{{ watch_title|tojson }}' }} }</code>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
URL encoding, use <strong>|urlencode</strong>, for example - <code>gets://hook-website.com/test.php?title={{ '{{ watch_title|urlencode }}' }}</code>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<script src="{{url_for('static_content', group='js', filename='diff-overview.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='diff-overview.js')}}" defer></script>
|
||||||
|
|
||||||
<div id="settings">
|
<div id="settings">
|
||||||
<form class="pure-form " action="" method="GET">
|
<form class="pure-form " action="" method="GET" id="diff-form">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{% if versions|length >= 1 %}
|
{% if versions|length >= 1 %}
|
||||||
<strong>Compare</strong>
|
<strong>Compare</strong>
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ User-Agent: wonderbra 1.0") }}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="browser-steps-fieldlist" style="padding-left: 1em; width: 350px; font-size: 80%;" >
|
<div id="browser-steps-fieldlist" style="padding-left: 1em; width: 350px; font-size: 80%;" >
|
||||||
<span id="browserless-seconds-remaining">Loading</span> <span style="font-size: 80%;"> (<a target=_new href="https://github.com/dgtlmoon/changedetection.io/pull/478/files#diff-1a79d924d1840c485238e66772391268a89c95b781d69091384cf1ea1ac146c9R4">?</a>) </span>
|
<span id="browser-seconds-remaining">Loading</span> <span style="font-size: 80%;"> (<a target=_new href="https://github.com/dgtlmoon/changedetection.io/pull/478/files#diff-1a79d924d1840c485238e66772391268a89c95b781d69091384cf1ea1ac146c9R4">?</a>) </span>
|
||||||
{{ render_field(form.browser_steps) }}
|
{{ render_field(form.browser_steps) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -339,6 +339,10 @@ nav
|
|||||||
<span class="pure-form-message-inline">When content is merely moved in a list, it will also trigger an <strong>addition</strong>, consider enabling <code><strong>Only trigger when unique lines appear</strong></code></span>
|
<span class="pure-form-message-inline">When content is merely moved in a list, it will also trigger an <strong>addition</strong>, consider enabling <code><strong>Only trigger when unique lines appear</strong></code></span>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="pure-control-group">
|
||||||
|
{{ render_checkbox_field(form.sort_text_alphabetically) }}
|
||||||
|
<span class="pure-form-message-inline">Helps reduce changes detected caused by sites shuffling lines around, combine with <i>check unique lines</i> below.</span>
|
||||||
|
</fieldset>
|
||||||
<fieldset class="pure-control-group">
|
<fieldset class="pure-control-group">
|
||||||
{{ render_checkbox_field(form.check_unique_lines) }}
|
{{ render_checkbox_field(form.check_unique_lines) }}
|
||||||
<span class="pure-form-message-inline">Good for websites that just move the content around, and you want to know when NEW content is added, compares new lines against all history for this watch.</span>
|
<span class="pure-form-message-inline">Good for websites that just move the content around, and you want to know when NEW content is added, compares new lines against all history for this watch.</span>
|
||||||
@@ -401,6 +405,7 @@ Unavailable") }}
|
|||||||
<li>Use <code>//(?aiLmsux))</code> type flags (more <a href="https://docs.python.org/3/library/re.html#index-15">information here</a>)<br></li>
|
<li>Use <code>//(?aiLmsux))</code> type flags (more <a href="https://docs.python.org/3/library/re.html#index-15">information here</a>)<br></li>
|
||||||
<li>Keyword example ‐ example <code>Out of stock</code></li>
|
<li>Keyword example ‐ example <code>Out of stock</code></li>
|
||||||
<li>Use groups to extract just that text ‐ example <code>/reports.+?(\d+)/i</code> returns a list of years only</li>
|
<li>Use groups to extract just that text ‐ example <code>/reports.+?(\d+)/i</code> returns a list of years only</li>
|
||||||
|
<li>Example - match lines containing a keyword <code>/.*icecream.*/</code></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>One line per regular-expression/string match</li>
|
<li>One line per regular-expression/string match</li>
|
||||||
@@ -482,6 +487,10 @@ Unavailable") }}
|
|||||||
<td>Last fetch time</td>
|
<td>Last fetch time</td>
|
||||||
<td>{{ watch.fetch_time }}s</td>
|
<td>{{ watch.fetch_time }}s</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Notification alert count</td>
|
||||||
|
<td>{{ watch.notification_alert_count }}</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -110,6 +110,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{%if watch.is_pdf %}<img class="status-icon" src="{{url_for('static_content', group='images', filename='pdf-icon.svg')}}" title="Converting PDF to text" >{% endif %}
|
{%if watch.is_pdf %}<img class="status-icon" src="{{url_for('static_content', group='images', filename='pdf-icon.svg')}}" title="Converting PDF to text" >{% endif %}
|
||||||
|
{% if watch.has_browser_steps %}<img class="status-icon status-browsersteps" src="{{url_for('static_content', group='images', filename='steps.svg')}}" title="Browser Steps is enabled" >{% endif %}
|
||||||
{% if watch.last_error is defined and watch.last_error != False %}
|
{% if watch.last_error is defined and watch.last_error != False %}
|
||||||
<div class="fetch-error">{{ watch.last_error }}
|
<div class="fetch-error">{{ watch.last_error }}
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ from ..util import live_server_setup, wait_for_all_checks
|
|||||||
def do_test(client, live_server, make_test_use_extra_browser=False):
|
def do_test(client, live_server, make_test_use_extra_browser=False):
|
||||||
|
|
||||||
# Grep for this string in the logs?
|
# Grep for this string in the logs?
|
||||||
test_url = f"https://changedetection.io/ci-test.html"
|
test_url = f"https://changedetection.io/ci-test.html?non-custom-default=true"
|
||||||
|
# "non-custom-default" should not appear in the custom browser connection
|
||||||
custom_browser_name = 'custom browser URL'
|
custom_browser_name = 'custom browser URL'
|
||||||
|
|
||||||
# needs to be set and something like 'ws://127.0.0.1:3000?stealth=1&--disable-web-security=true'
|
# needs to be set and something like 'ws://127.0.0.1:3000'
|
||||||
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"
|
||||||
|
|
||||||
#####################
|
#####################
|
||||||
@@ -19,9 +20,7 @@ def do_test(client, live_server, make_test_use_extra_browser=False):
|
|||||||
data={"application-empty_pages_are_a_change": "",
|
data={"application-empty_pages_are_a_change": "",
|
||||||
"requests-time_between_check-minutes": 180,
|
"requests-time_between_check-minutes": 180,
|
||||||
'application-fetch_backend': "html_webdriver",
|
'application-fetch_backend': "html_webdriver",
|
||||||
# browserless-custom-url is setup in .github/workflows/test-only.yml
|
'requests-extra_browsers-0-browser_connection_url': 'ws://sockpuppetbrowser-custom-url:3000',
|
||||||
# the test script run_custom_browser_url_test.sh will look for 'custom-browser-search-string' in the container logs
|
|
||||||
'requests-extra_browsers-0-browser_connection_url': 'ws://browserless-custom-url:3000?stealth=1&--disable-web-security=true&custom-browser-search-string=1',
|
|
||||||
'requests-extra_browsers-0-browser_name': custom_browser_name
|
'requests-extra_browsers-0-browser_name': custom_browser_name
|
||||||
},
|
},
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
@@ -51,7 +50,8 @@ def do_test(client, live_server, make_test_use_extra_browser=False):
|
|||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("edit_page", uuid="first"),
|
url_for("edit_page", uuid="first"),
|
||||||
data={
|
data={
|
||||||
"url": test_url,
|
# 'run_customer_browser_url_tests.sh' will search for this string to know if we hit the right browser container or not
|
||||||
|
"url": f"https://changedetection.io/ci-test.html?custom-browser-search-string=1",
|
||||||
"tags": "",
|
"tags": "",
|
||||||
"headers": "",
|
"headers": "",
|
||||||
'fetch_backend': f"extra_browser_{custom_browser_name}",
|
'fetch_backend': f"extra_browser_{custom_browser_name}",
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ def test_api_simple(client, live_server):
|
|||||||
# Loading the most recent snapshot should force viewed to become true
|
# Loading the most recent snapshot should force viewed to become true
|
||||||
client.get(url_for("diff_history_page", uuid="first"), follow_redirects=True)
|
client.get(url_for("diff_history_page", uuid="first"), follow_redirects=True)
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
# Fetch the whole watch again, viewed should be true
|
# Fetch the whole watch again, viewed should be true
|
||||||
res = client.get(
|
res = client.get(
|
||||||
url_for("watch", uuid=watch_uuid),
|
url_for("watch", uuid=watch_uuid),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks
|
from .util import set_original_response, live_server_setup, wait_for_all_checks
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
from urllib.request import urlopen
|
import io
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@@ -37,15 +37,10 @@ def test_backup(client, live_server):
|
|||||||
# Should be PK/ZIP stream
|
# Should be PK/ZIP stream
|
||||||
assert res.data.count(b'PK') >= 2
|
assert res.data.count(b'PK') >= 2
|
||||||
|
|
||||||
# ZipFile from buffer seems non-obvious, just save it instead
|
backup = ZipFile(io.BytesIO(res.data))
|
||||||
with open("download.zip", 'wb') as f:
|
l = backup.namelist()
|
||||||
f.write(res.data)
|
|
||||||
|
|
||||||
zip = ZipFile('download.zip')
|
|
||||||
l = zip.namelist()
|
|
||||||
uuid4hex = re.compile('^[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}.*txt', re.I)
|
uuid4hex = re.compile('^[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}.*txt', re.I)
|
||||||
newlist = list(filter(uuid4hex.match, l)) # Read Note below
|
newlist = list(filter(uuid4hex.match, l)) # Read Note below
|
||||||
|
|
||||||
# Should be two txt files in the archive (history and the snapshot)
|
# Should be two txt files in the archive (history and the snapshot)
|
||||||
assert len(newlist) == 2
|
assert len(newlist) == 2
|
||||||
|
|
||||||
|
|||||||
@@ -456,7 +456,7 @@ def test_ignore_json_order(client, live_server):
|
|||||||
|
|
||||||
def test_correct_header_detect(client, live_server):
|
def test_correct_header_detect(client, live_server):
|
||||||
# Like in https://github.com/dgtlmoon/changedetection.io/pull/1593
|
# Like in https://github.com/dgtlmoon/changedetection.io/pull/1593
|
||||||
# Specify extra html that JSON is sometimes wrapped in - when using Browserless/Puppeteer etc
|
# Specify extra html that JSON is sometimes wrapped in - when using SockpuppetBrowser / Puppeteer / Playwrightetc
|
||||||
with open("test-datastore/endpoint-content.txt", "w") as f:
|
with open("test-datastore/endpoint-content.txt", "w") as f:
|
||||||
f.write('<html><body>{"hello" : 123, "world": 123}')
|
f.write('<html><body>{"hello" : 123, "world": 123}')
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ def test_setup(live_server):
|
|||||||
# Hard to just add more live server URLs when one test is already running (I think)
|
# Hard to just add more live server URLs when one test is already running (I think)
|
||||||
# So we add our test here (was in a different file)
|
# So we add our test here (was in a different file)
|
||||||
def test_headers_in_request(client, live_server):
|
def test_headers_in_request(client, live_server):
|
||||||
#live_server_setup(live_server)
|
#ve_server_setup(live_server)
|
||||||
# Add our URL to the import page
|
# Add our URL to the import page
|
||||||
test_url = url_for('test_headers', _external=True)
|
test_url = url_for('test_headers', _external=True)
|
||||||
if os.getenv('PLAYWRIGHT_DRIVER_URL'):
|
if os.getenv('PLAYWRIGHT_DRIVER_URL'):
|
||||||
# Because its no longer calling back to localhost but from browserless, set in test-only.yml
|
# Because its no longer calling back to localhost but from the browser container, set in test-only.yml
|
||||||
test_url = test_url.replace('localhost', 'changedet')
|
test_url = test_url.replace('localhost', 'changedet')
|
||||||
|
|
||||||
# Add the test URL twice, we will check
|
# Add the test URL twice, we will check
|
||||||
@@ -70,16 +70,17 @@ def test_headers_in_request(client, live_server):
|
|||||||
|
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
# Re #137 - Examine the JSON index file, it should have only one set of headers entered
|
# Re #137 - It should have only one set of headers entered
|
||||||
watches_with_headers = 0
|
watches_with_headers = 0
|
||||||
with open('test-datastore/url-watches.json') as f:
|
for k, watch in client.application.config.get('DATASTORE').data.get('watching').items():
|
||||||
app_struct = json.load(f)
|
if (len(watch['headers'])):
|
||||||
for uuid in app_struct['watching']:
|
|
||||||
if (len(app_struct['watching'][uuid]['headers'])):
|
|
||||||
watches_with_headers += 1
|
watches_with_headers += 1
|
||||||
|
assert watches_with_headers == 1
|
||||||
|
|
||||||
|
# 'server' http header was automatically recorded
|
||||||
|
for k, watch in client.application.config.get('DATASTORE').data.get('watching').items():
|
||||||
|
assert 'custom' in watch.get('remote_server_reply') # added in util.py
|
||||||
|
|
||||||
# Should be only one with headers set
|
|
||||||
assert watches_with_headers==1
|
|
||||||
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
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ def test_body_in_request(client, live_server):
|
|||||||
# Add our URL to the import page
|
# Add our URL to the import page
|
||||||
test_url = url_for('test_body', _external=True)
|
test_url = url_for('test_body', _external=True)
|
||||||
if os.getenv('PLAYWRIGHT_DRIVER_URL'):
|
if os.getenv('PLAYWRIGHT_DRIVER_URL'):
|
||||||
# Because its no longer calling back to localhost but from browserless, set in test-only.yml
|
# Because its no longer calling back to localhost but from the browser container, set in test-only.yml
|
||||||
test_url = test_url.replace('localhost', 'cdio')
|
test_url = test_url.replace('localhost', 'cdio')
|
||||||
|
|
||||||
res = client.post(
|
res = client.post(
|
||||||
@@ -180,7 +181,7 @@ def test_method_in_request(client, live_server):
|
|||||||
# Add our URL to the import page
|
# Add our URL to the import page
|
||||||
test_url = url_for('test_method', _external=True)
|
test_url = url_for('test_method', _external=True)
|
||||||
if os.getenv('PLAYWRIGHT_DRIVER_URL'):
|
if os.getenv('PLAYWRIGHT_DRIVER_URL'):
|
||||||
# Because its no longer calling back to localhost but from browserless, set in test-only.yml
|
# Because its no longer calling back to localhost but from the browser container, set in test-only.yml
|
||||||
test_url = test_url.replace('localhost', 'cdio')
|
test_url = test_url.replace('localhost', 'cdio')
|
||||||
|
|
||||||
# Add the test URL twice, we will check
|
# Add the test URL twice, we will check
|
||||||
@@ -257,7 +258,7 @@ def test_headers_textfile_in_request(client, live_server):
|
|||||||
# Add our URL to the import page
|
# Add our URL to the import page
|
||||||
test_url = url_for('test_headers', _external=True)
|
test_url = url_for('test_headers', _external=True)
|
||||||
if os.getenv('PLAYWRIGHT_DRIVER_URL'):
|
if os.getenv('PLAYWRIGHT_DRIVER_URL'):
|
||||||
# Because its no longer calling back to localhost but from browserless, set in test-only.yml
|
# Because its no longer calling back to localhost but from the browser container, set in test-only.yml
|
||||||
test_url = test_url.replace('localhost', 'cdio')
|
test_url = test_url.replace('localhost', 'cdio')
|
||||||
|
|
||||||
print ("TEST URL IS ",test_url)
|
print ("TEST URL IS ",test_url)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
from .util import live_server_setup
|
from .util import live_server_setup, wait_for_all_checks
|
||||||
|
|
||||||
|
|
||||||
def set_original_ignore_response():
|
def set_original_ignore_response():
|
||||||
@@ -34,6 +34,23 @@ def set_modified_swapped_lines():
|
|||||||
with open("test-datastore/endpoint-content.txt", "w") as f:
|
with open("test-datastore/endpoint-content.txt", "w") as f:
|
||||||
f.write(test_return_data)
|
f.write(test_return_data)
|
||||||
|
|
||||||
|
def set_modified_swapped_lines_with_extra_text_for_sorting():
|
||||||
|
test_return_data = """<html>
|
||||||
|
<body>
|
||||||
|
<p> Which is across multiple lines</p>
|
||||||
|
<p>Some initial text</p>
|
||||||
|
<p> So let's see what happens.</p>
|
||||||
|
<p>Z last</p>
|
||||||
|
<p>0 numerical</p>
|
||||||
|
<p>A uppercase</p>
|
||||||
|
<p>a lowercase</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open("test-datastore/endpoint-content.txt", "w") as f:
|
||||||
|
f.write(test_return_data)
|
||||||
|
|
||||||
|
|
||||||
def set_modified_with_trigger_text_response():
|
def set_modified_with_trigger_text_response():
|
||||||
test_return_data = """<html>
|
test_return_data = """<html>
|
||||||
@@ -49,15 +66,14 @@ def set_modified_with_trigger_text_response():
|
|||||||
with open("test-datastore/endpoint-content.txt", "w") as f:
|
with open("test-datastore/endpoint-content.txt", "w") as f:
|
||||||
f.write(test_return_data)
|
f.write(test_return_data)
|
||||||
|
|
||||||
|
def test_setup(client, live_server):
|
||||||
def test_unique_lines_functionality(client, live_server):
|
|
||||||
live_server_setup(live_server)
|
live_server_setup(live_server)
|
||||||
|
|
||||||
sleep_time_for_fetch_thread = 3
|
def test_unique_lines_functionality(client, live_server):
|
||||||
|
#live_server_setup(live_server)
|
||||||
|
|
||||||
|
|
||||||
set_original_ignore_response()
|
set_original_ignore_response()
|
||||||
# Give the endpoint time to spin up
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# Add our URL to the import page
|
# Add our URL to the import page
|
||||||
test_url = url_for('test_endpoint', _external=True)
|
test_url = url_for('test_endpoint', _external=True)
|
||||||
@@ -67,7 +83,7 @@ def test_unique_lines_functionality(client, live_server):
|
|||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
assert b"1 Imported" in res.data
|
assert b"1 Imported" in res.data
|
||||||
time.sleep(sleep_time_for_fetch_thread)
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
# Add our URL to the import page
|
# Add our URL to the import page
|
||||||
res = client.post(
|
res = client.post(
|
||||||
@@ -83,12 +99,11 @@ def test_unique_lines_functionality(client, live_server):
|
|||||||
# Make a change
|
# Make a change
|
||||||
set_modified_swapped_lines()
|
set_modified_swapped_lines()
|
||||||
|
|
||||||
time.sleep(sleep_time_for_fetch_thread)
|
|
||||||
# Trigger a check
|
# Trigger a check
|
||||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||||
|
|
||||||
# Give the thread time to pick it up
|
# Give the thread time to pick it up
|
||||||
time.sleep(sleep_time_for_fetch_thread)
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
# It should report nothing found (no new 'unviewed' class)
|
# It should report nothing found (no new 'unviewed' class)
|
||||||
res = client.get(url_for("index"))
|
res = client.get(url_for("index"))
|
||||||
@@ -97,7 +112,57 @@ def test_unique_lines_functionality(client, live_server):
|
|||||||
# Now set the content which contains the new text and re-ordered existing text
|
# Now set the content which contains the new text and re-ordered existing text
|
||||||
set_modified_with_trigger_text_response()
|
set_modified_with_trigger_text_response()
|
||||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||||
time.sleep(sleep_time_for_fetch_thread)
|
wait_for_all_checks(client)
|
||||||
res = client.get(url_for("index"))
|
res = client.get(url_for("index"))
|
||||||
assert b'unviewed' in res.data
|
assert b'unviewed' in res.data
|
||||||
|
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
||||||
|
assert b'Deleted' in res.data
|
||||||
|
|
||||||
|
def test_sort_lines_functionality(client, live_server):
|
||||||
|
#live_server_setup(live_server)
|
||||||
|
|
||||||
|
set_modified_swapped_lines_with_extra_text_for_sorting()
|
||||||
|
|
||||||
|
# Add our URL to the import page
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Add our URL to the import page
|
||||||
|
res = client.post(
|
||||||
|
url_for("edit_page", uuid="first"),
|
||||||
|
data={"sort_text_alphabetically": "n",
|
||||||
|
"url": test_url,
|
||||||
|
"fetch_backend": "html_requests"},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"Updated watch." in res.data
|
||||||
|
|
||||||
|
|
||||||
|
# Trigger a check
|
||||||
|
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||||
|
|
||||||
|
# Give the thread time to pick it up
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
|
|
||||||
|
res = client.get(url_for("index"))
|
||||||
|
# Should be a change registered
|
||||||
|
assert b'unviewed' in res.data
|
||||||
|
|
||||||
|
res = client.get(
|
||||||
|
url_for("preview_page", uuid="first"),
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert res.data.find(b'0 numerical') < res.data.find(b'Z last')
|
||||||
|
assert res.data.find(b'A uppercase') < res.data.find(b'Z last')
|
||||||
|
assert res.data.find(b'Some initial text') < res.data.find(b'Which is across multiple lines')
|
||||||
|
|
||||||
|
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
||||||
|
assert b'Deleted' in res.data
|
||||||
@@ -175,12 +175,16 @@ def live_server_setup(live_server):
|
|||||||
@live_server.app.route('/test-headers')
|
@live_server.app.route('/test-headers')
|
||||||
def test_headers():
|
def test_headers():
|
||||||
|
|
||||||
output= []
|
output = []
|
||||||
|
|
||||||
for header in request.headers:
|
for header in request.headers:
|
||||||
output.append("{}:{}".format(str(header[0]),str(header[1]) ))
|
output.append("{}:{}".format(str(header[0]), str(header[1])))
|
||||||
|
|
||||||
return "\n".join(output)
|
content = "\n".join(output)
|
||||||
|
|
||||||
|
resp = make_response(content, 200)
|
||||||
|
resp.headers['server'] = 'custom'
|
||||||
|
return resp
|
||||||
|
|
||||||
# Just return the body in the request
|
# Just return the body in the request
|
||||||
@live_server.app.route('/test-body', methods=['POST', 'GET'])
|
@live_server.app.route('/test-body', methods=['POST', 'GET'])
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ class update_worker(threading.Thread):
|
|||||||
dates = []
|
dates = []
|
||||||
trigger_text = ''
|
trigger_text = ''
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
if watch:
|
if watch:
|
||||||
watch_history = watch.history
|
watch_history = watch.history
|
||||||
dates = list(watch_history.keys())
|
dates = list(watch_history.keys())
|
||||||
@@ -72,13 +74,14 @@ class update_worker(threading.Thread):
|
|||||||
'diff_full': diff.render_diff(prev_snapshot, current_snapshot, include_equal=True, line_feed_sep=line_feed_sep),
|
'diff_full': diff.render_diff(prev_snapshot, current_snapshot, include_equal=True, line_feed_sep=line_feed_sep),
|
||||||
'diff_patch': diff.render_diff(prev_snapshot, current_snapshot, line_feed_sep=line_feed_sep, patch_format=True),
|
'diff_patch': diff.render_diff(prev_snapshot, current_snapshot, line_feed_sep=line_feed_sep, patch_format=True),
|
||||||
'diff_removed': diff.render_diff(prev_snapshot, current_snapshot, include_added=False, line_feed_sep=line_feed_sep),
|
'diff_removed': diff.render_diff(prev_snapshot, current_snapshot, include_added=False, line_feed_sep=line_feed_sep),
|
||||||
|
'notification_timestamp': now,
|
||||||
'screenshot': watch.get_screenshot() if watch and watch.get('notification_screenshot') else None,
|
'screenshot': watch.get_screenshot() if watch and watch.get('notification_screenshot') else None,
|
||||||
'triggered_text': triggered_text,
|
'triggered_text': triggered_text,
|
||||||
'uuid': watch.get('uuid') if watch else None,
|
'uuid': watch.get('uuid') if watch else None,
|
||||||
'watch_url': watch.get('url') if watch else None,
|
'watch_url': watch.get('url') if watch else None,
|
||||||
})
|
})
|
||||||
|
logger.trace(f"Main rendered notification placeholders (diff_added etc) calculated in {time.time()-now:.3f}s")
|
||||||
logger.debug(">> SENDING NOTIFICATION")
|
logger.debug("Queued notification for sending")
|
||||||
notification_q.put(n_object)
|
notification_q.put(n_object)
|
||||||
|
|
||||||
# Prefer - Individual watch settings > Tag settings > Global settings (in that order)
|
# Prefer - Individual watch settings > Tag settings > Global settings (in that order)
|
||||||
@@ -147,6 +150,10 @@ class update_worker(threading.Thread):
|
|||||||
queued = False
|
queued = False
|
||||||
if n_object and n_object.get('notification_urls'):
|
if n_object and n_object.get('notification_urls'):
|
||||||
queued = True
|
queued = True
|
||||||
|
|
||||||
|
count = watch.get('notification_alert_count', 0) + 1
|
||||||
|
self.datastore.update_watch(uuid=watch_uuid, update_obj={'notification_alert_count': count})
|
||||||
|
|
||||||
self.queue_notification_for_watch(notification_q=self.notification_q, n_object=n_object, watch=watch)
|
self.queue_notification_for_watch(notification_q=self.notification_q, n_object=n_object, watch=watch)
|
||||||
|
|
||||||
return queued
|
return queued
|
||||||
@@ -220,7 +227,8 @@ class update_worker(threading.Thread):
|
|||||||
def run(self):
|
def run(self):
|
||||||
|
|
||||||
from .processors import text_json_diff, restock_diff
|
from .processors import text_json_diff, restock_diff
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
while not self.app.config.exit.is_set():
|
while not self.app.config.exit.is_set():
|
||||||
update_handler = None
|
update_handler = None
|
||||||
|
|
||||||
@@ -232,13 +240,12 @@ class update_worker(threading.Thread):
|
|||||||
else:
|
else:
|
||||||
uuid = queued_item_data.item.get('uuid')
|
uuid = queued_item_data.item.get('uuid')
|
||||||
self.current_uuid = uuid
|
self.current_uuid = uuid
|
||||||
|
|
||||||
if uuid in list(self.datastore.data['watching'].keys()) and self.datastore.data['watching'][uuid].get('url'):
|
if uuid in list(self.datastore.data['watching'].keys()) and self.datastore.data['watching'][uuid].get('url'):
|
||||||
changed_detected = False
|
changed_detected = False
|
||||||
contents = b''
|
contents = b''
|
||||||
process_changedetection_results = True
|
process_changedetection_results = True
|
||||||
update_obj = {}
|
update_obj = {}
|
||||||
logger.debug(f"> Processing UUID {uuid} "
|
logger.info(f"Processing watch UUID {uuid} "
|
||||||
f"Priority {queued_item_data.priority} "
|
f"Priority {queued_item_data.priority} "
|
||||||
f"URL {self.datastore.data['watching'][uuid]['url']}")
|
f"URL {self.datastore.data['watching'][uuid]['url']}")
|
||||||
now = time.time()
|
now = time.time()
|
||||||
@@ -280,7 +287,8 @@ class update_worker(threading.Thread):
|
|||||||
if not isinstance(contents, (bytes, bytearray)):
|
if not isinstance(contents, (bytes, bytearray)):
|
||||||
raise Exception("Error - returned data from the fetch handler SHOULD be bytes")
|
raise Exception("Error - returned data from the fetch handler SHOULD be bytes")
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
self.app.logger.error("File permission error updating", uuid, str(e))
|
logger.critical(f"File permission error updating file, watch: {uuid}")
|
||||||
|
logger.critical(str(e))
|
||||||
process_changedetection_results = False
|
process_changedetection_results = False
|
||||||
except content_fetcher.ReplyWithContentButNoText as e:
|
except content_fetcher.ReplyWithContentButNoText as e:
|
||||||
# Totally fine, it's by choice - just continue on, nothing more to care about
|
# Totally fine, it's by choice - just continue on, nothing more to care about
|
||||||
@@ -403,7 +411,7 @@ class update_worker(threading.Thread):
|
|||||||
'last_check_status': e.status_code})
|
'last_check_status': e.status_code})
|
||||||
process_changedetection_results = False
|
process_changedetection_results = False
|
||||||
except content_fetcher.ScreenshotUnavailable as e:
|
except content_fetcher.ScreenshotUnavailable as e:
|
||||||
err_text = "Screenshot unavailable, page did not render fully in the expected time - try increasing 'Wait seconds before extracting text'"
|
err_text = "Screenshot unavailable, page did not render fully in the expected time or page was too long - try increasing 'Wait seconds before extracting text'"
|
||||||
self.datastore.update_watch(uuid=uuid, update_obj={'last_error': err_text,
|
self.datastore.update_watch(uuid=uuid, update_obj={'last_error': err_text,
|
||||||
'last_check_status': e.status_code})
|
'last_check_status': e.status_code})
|
||||||
process_changedetection_results = False
|
process_changedetection_results = False
|
||||||
@@ -426,13 +434,21 @@ class update_worker(threading.Thread):
|
|||||||
'last_check_status': e.status_code,
|
'last_check_status': e.status_code,
|
||||||
'has_ldjson_price_data': None})
|
'has_ldjson_price_data': None})
|
||||||
process_changedetection_results = False
|
process_changedetection_results = False
|
||||||
|
except content_fetcher.BrowserStepsInUnsupportedFetcher as e:
|
||||||
|
err_text = "This watch has Browser Steps configured and so it cannot run with the 'Basic fast Plaintext/HTTP Client', either remove the Browser Steps or select a Chrome fetcher."
|
||||||
|
self.datastore.update_watch(uuid=uuid, update_obj={'last_error': err_text})
|
||||||
|
process_changedetection_results = False
|
||||||
|
logger.error(f"Exception (BrowserStepsInUnsupportedFetcher) reached processing watch UUID: {uuid}")
|
||||||
|
|
||||||
except UnableToExtractRestockData as e:
|
except UnableToExtractRestockData as e:
|
||||||
# Usually when fetcher.instock_data returns empty
|
# Usually when fetcher.instock_data returns empty
|
||||||
self.app.logger.error("Exception reached processing watch UUID: %s - %s", uuid, str(e))
|
logger.error(f"Exception (UnableToExtractRestockData) reached processing watch UUID: {uuid}")
|
||||||
|
logger.error(str(e))
|
||||||
self.datastore.update_watch(uuid=uuid, update_obj={'last_error': f"Unable to extract restock data for this page unfortunately. (Got code {e.status_code} from server)"})
|
self.datastore.update_watch(uuid=uuid, update_obj={'last_error': f"Unable to extract restock data for this page unfortunately. (Got code {e.status_code} from server)"})
|
||||||
process_changedetection_results = False
|
process_changedetection_results = False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.app.logger.error("Exception reached processing watch UUID: %s - %s", uuid, str(e))
|
logger.error(f"Exception reached processing watch UUID: {uuid}")
|
||||||
|
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': str(e)})
|
||||||
# Other serious error
|
# Other serious error
|
||||||
process_changedetection_results = False
|
process_changedetection_results = False
|
||||||
@@ -468,24 +484,33 @@ class update_worker(threading.Thread):
|
|||||||
|
|
||||||
# A change was detected
|
# A change was detected
|
||||||
if changed_detected:
|
if changed_detected:
|
||||||
logger.debug(f">> Change detected in UUID {uuid} - {watch['url']}")
|
|
||||||
|
|
||||||
# Notifications should only trigger on the second time (first time, we gather the initial snapshot)
|
# Notifications should only trigger on the second time (first time, we gather the initial snapshot)
|
||||||
if watch.history_n >= 2:
|
if watch.history_n >= 2:
|
||||||
|
logger.info(f"Change detected in UUID {uuid} - {watch['url']}")
|
||||||
if not self.datastore.data['watching'][uuid].get('notification_muted'):
|
if not self.datastore.data['watching'][uuid].get('notification_muted'):
|
||||||
self.send_content_changed_notification(watch_uuid=uuid)
|
self.send_content_changed_notification(watch_uuid=uuid)
|
||||||
|
else:
|
||||||
|
logger.info(f"Change triggered in UUID {uuid} due to first history saving (no notifications sent) - {watch['url']}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Catch everything possible here, so that if a worker crashes, we don't lose it until restart!
|
# Catch everything possible here, so that if a worker crashes, we don't lose it until restart!
|
||||||
logger.critical("!!!! Exception in update_worker !!!")
|
logger.critical("!!!! Exception in update_worker while processing process_changedetection_results !!!")
|
||||||
logger.critical(str(e))
|
logger.critical(str(e))
|
||||||
self.app.logger.error("Exception reached processing watch UUID: %s - %s", uuid, str(e))
|
|
||||||
self.datastore.update_watch(uuid=uuid, update_obj={'last_error': str(e)})
|
self.datastore.update_watch(uuid=uuid, update_obj={'last_error': str(e)})
|
||||||
|
|
||||||
if self.datastore.data['watching'].get(uuid):
|
if self.datastore.data['watching'].get(uuid):
|
||||||
# Always record that we atleast tried
|
# Always record that we atleast tried
|
||||||
count = self.datastore.data['watching'][uuid].get('check_count', 0) + 1
|
count = self.datastore.data['watching'][uuid].get('check_count', 0) + 1
|
||||||
|
|
||||||
|
# Record the 'server' header reply, can be used for actions in the future like cloudflare/akamai workarounds
|
||||||
|
try:
|
||||||
|
server_header = update_handler.fetcher.headers.get('server', '').strip().lower()[:255]
|
||||||
|
self.datastore.update_watch(uuid=uuid,
|
||||||
|
update_obj={'remote_server_reply': server_header}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
self.datastore.update_watch(uuid=uuid, update_obj={'fetch_time': round(time.time() - now, 3),
|
self.datastore.update_watch(uuid=uuid, update_obj={'fetch_time': round(time.time() - now, 3),
|
||||||
'last_checked': round(time.time()),
|
'last_checked': round(time.time()),
|
||||||
'check_count': count
|
'check_count': count
|
||||||
@@ -500,6 +525,7 @@ class update_worker(threading.Thread):
|
|||||||
|
|
||||||
self.current_uuid = None # Done
|
self.current_uuid = None # Done
|
||||||
self.q.task_done()
|
self.q.task_done()
|
||||||
|
logger.debug(f"Watch {uuid} done in {time.time()-now:.2f}s")
|
||||||
|
|
||||||
# Give the CPU time to interrupt
|
# Give the CPU time to interrupt
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ services:
|
|||||||
# https://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.common.proxy
|
# https://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.common.proxy
|
||||||
#
|
#
|
||||||
# Alternative Playwright URL, do not use "'s or 's!
|
# Alternative Playwright URL, do not use "'s or 's!
|
||||||
# - PLAYWRIGHT_DRIVER_URL=ws://playwright-chrome:3000/?stealth=1&--disable-web-security=true
|
# - PLAYWRIGHT_DRIVER_URL=ws://playwright-chrome:3000
|
||||||
#
|
#
|
||||||
# Playwright proxy settings playwright_proxy_server, playwright_proxy_bypass, playwright_proxy_username, playwright_proxy_password
|
# Playwright proxy settings playwright_proxy_server, playwright_proxy_bypass, playwright_proxy_username, playwright_proxy_password
|
||||||
#
|
#
|
||||||
@@ -71,30 +71,23 @@ services:
|
|||||||
# condition: service_started
|
# condition: service_started
|
||||||
|
|
||||||
# Used for fetching pages via Playwright+Chrome where you need Javascript support.
|
# Used for fetching pages via Playwright+Chrome where you need Javascript support.
|
||||||
# Note: Playwright/browserless not supported on ARM type devices (rPi etc)
|
|
||||||
# RECOMMENDED FOR FETCHING PAGES WITH CHROME
|
# RECOMMENDED FOR FETCHING PAGES WITH CHROME
|
||||||
# playwright-chrome:
|
# playwright-chrome:
|
||||||
# hostname: playwright-chrome
|
# hostname: playwright-chrome
|
||||||
# image: browserless/chrome:1.60-chrome-stable
|
# image: dgtlmoon/sockpuppetbrowser:latest
|
||||||
|
# cap_add:
|
||||||
|
# - SYS_ADMIN
|
||||||
|
## SYS_ADMIN might be too much, but it can be needed on your platform https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-puppeteer-on-gitlabci
|
||||||
# restart: unless-stopped
|
# restart: unless-stopped
|
||||||
# environment:
|
# environment:
|
||||||
# - SCREEN_WIDTH=1920
|
# - SCREEN_WIDTH=1920
|
||||||
# - SCREEN_HEIGHT=1024
|
# - SCREEN_HEIGHT=1024
|
||||||
# - SCREEN_DEPTH=16
|
# - SCREEN_DEPTH=16
|
||||||
# - ENABLE_DEBUGGER=false
|
# - MAX_CONCURRENT_CHROME_PROCESSES=10
|
||||||
# - PREBOOT_CHROME=true
|
|
||||||
# - CONNECTION_TIMEOUT=300000
|
|
||||||
# - MAX_CONCURRENT_SESSIONS=10
|
|
||||||
# - CHROME_REFRESH_TIME=600000
|
|
||||||
# - DEFAULT_BLOCK_ADS=true
|
|
||||||
# - DEFAULT_STEALTH=true
|
|
||||||
#
|
|
||||||
# Ignore HTTPS errors, like for self-signed certs
|
|
||||||
# - DEFAULT_IGNORE_HTTPS_ERRORS=true
|
|
||||||
#
|
|
||||||
|
|
||||||
# Used for fetching pages via Playwright+Chrome where you need Javascript support.
|
# Used for fetching pages via Playwright+Chrome where you need Javascript support.
|
||||||
# Note: works well but is deprecated, does not fetch full page screenshots (doesnt work with Visual Selector) and other issues
|
# Note: Works well but is deprecated, does not fetch full page screenshots (doesnt work with Visual Selector)
|
||||||
|
# Does not report status codes (200, 404, 403) and other issues
|
||||||
# browser-chrome:
|
# browser-chrome:
|
||||||
# hostname: browser-chrome
|
# hostname: browser-chrome
|
||||||
# image: selenium/standalone-chrome:4
|
# image: selenium/standalone-chrome:4
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ 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
|
||||||
|
|
||||||
# XPath 2.0-3.1 support
|
# XPath 2.0-3.1 support - 4.2.0 broke something?
|
||||||
elementpath
|
elementpath==4.1.5
|
||||||
|
|
||||||
selenium~=4.14.0
|
selenium~=4.14.0
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user