mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2025-11-25 10:53:23 +00:00
Compare commits
4 Commits
dynamic-ur
...
windows-di
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f16aa1b658 | ||
|
|
6d02110473 | ||
|
|
c782a7e5c8 | ||
|
|
d4b241720a |
9
.github/workflows/test-container-build.yml
vendored
9
.github/workflows/test-container-build.yml
vendored
@@ -1,21 +1,12 @@
|
|||||||
name: ChangeDetection.io Container Build Test
|
name: ChangeDetection.io Container Build Test
|
||||||
|
|
||||||
# Triggers the workflow on push or pull request events
|
# Triggers the workflow on push or pull request events
|
||||||
|
|
||||||
# This line doesnt work, even tho it is the documented one
|
|
||||||
#on: [push, pull_request]
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- requirements.txt
|
- requirements.txt
|
||||||
- Dockerfile
|
- Dockerfile
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- requirements.txt
|
|
||||||
- Dockerfile
|
|
||||||
|
|
||||||
# Changes to requirements.txt packages and Dockerfile may or may not always be compatible with arm etc, so worth testing
|
# Changes to requirements.txt packages and Dockerfile may or may not always be compatible with arm etc, so worth testing
|
||||||
# @todo: some kind of path filter for requirements.txt and Dockerfile
|
# @todo: some kind of path filter for requirements.txt and Dockerfile
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Otherwise, it's always best to PR into the `dev` branch.
|
|||||||
|
|
||||||
Please be sure that all new functionality has a matching test!
|
Please be sure that all new functionality has a matching test!
|
||||||
|
|
||||||
Use `pytest` to validate/test, you can run the existing tests as `pytest tests/test_notification.py` for example
|
Use `pytest` to validate/test, you can run the existing tests as `pytest tests/test_notifications.py` for example
|
||||||
|
|
||||||
```
|
```
|
||||||
pip3 install -r requirements-dev
|
pip3 install -r requirements-dev
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ EXPOSE 5000
|
|||||||
|
|
||||||
# The actual flask app
|
# The actual flask app
|
||||||
COPY changedetectionio /app/changedetectionio
|
COPY changedetectionio /app/changedetectionio
|
||||||
|
|
||||||
# The eventlet server wrapper
|
# The eventlet server wrapper
|
||||||
COPY changedetection.py /app/changedetection.py
|
COPY changedetection.py /app/changedetection.py
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from flask_wtf import CSRFProtect
|
|||||||
from changedetectionio import html_tools
|
from changedetectionio import html_tools
|
||||||
from changedetectionio.api import api_v1
|
from changedetectionio.api import api_v1
|
||||||
|
|
||||||
__version__ = '0.39.20.4'
|
__version__ = '0.39.20.3'
|
||||||
|
|
||||||
datastore = None
|
datastore = None
|
||||||
|
|
||||||
@@ -194,9 +194,6 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
watch_api.add_resource(api_v1.Watch, '/api/v1/watch/<string:uuid>',
|
watch_api.add_resource(api_v1.Watch, '/api/v1/watch/<string:uuid>',
|
||||||
resource_class_kwargs={'datastore': datastore, 'update_q': update_q})
|
resource_class_kwargs={'datastore': datastore, 'update_q': update_q})
|
||||||
|
|
||||||
watch_api.add_resource(api_v1.SystemInfo, '/api/v1/systeminfo',
|
|
||||||
resource_class_kwargs={'datastore': datastore, 'update_q': update_q})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -122,37 +122,3 @@ class CreateWatch(Resource):
|
|||||||
return {'status': "OK"}, 200
|
return {'status': "OK"}, 200
|
||||||
|
|
||||||
return list, 200
|
return list, 200
|
||||||
|
|
||||||
class SystemInfo(Resource):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
# datastore is a black box dependency
|
|
||||||
self.datastore = kwargs['datastore']
|
|
||||||
self.update_q = kwargs['update_q']
|
|
||||||
|
|
||||||
@auth.check_token
|
|
||||||
def get(self):
|
|
||||||
import time
|
|
||||||
overdue_watches = []
|
|
||||||
|
|
||||||
# Check all watches and report which have not been checked but should have been
|
|
||||||
|
|
||||||
for uuid, watch in self.datastore.data.get('watching', {}).items():
|
|
||||||
# see if now - last_checked is greater than the time that should have been
|
|
||||||
# this is not super accurate (maybe they just edited it) but better than nothing
|
|
||||||
t = watch.threshold_seconds()
|
|
||||||
if not t:
|
|
||||||
# Use the system wide default
|
|
||||||
t = self.datastore.threshold_seconds
|
|
||||||
|
|
||||||
time_since_check = time.time() - watch.get('last_checked')
|
|
||||||
|
|
||||||
# Allow 5 minutes of grace time before we decide it's overdue
|
|
||||||
if time_since_check - (5 * 60) > t:
|
|
||||||
overdue_watches.append(uuid)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'queue_size': self.update_q.qsize(),
|
|
||||||
'overdue_watches': overdue_watches,
|
|
||||||
'uptime': round(time.time() - self.datastore.start_time, 2),
|
|
||||||
'watch_count': len(self.datastore.data.get('watching', {}))
|
|
||||||
}, 200
|
|
||||||
|
|||||||
@@ -102,14 +102,6 @@ def main():
|
|||||||
has_password=datastore.data['settings']['application']['password'] != False
|
has_password=datastore.data['settings']['application']['password'] != False
|
||||||
)
|
)
|
||||||
|
|
||||||
# Monitored websites will not receive a Referer header
|
|
||||||
# when a user clicks on an outgoing link.
|
|
||||||
@app.after_request
|
|
||||||
def hide_referrer(response):
|
|
||||||
if os.getenv("HIDE_REFERER", False):
|
|
||||||
response.headers["Referrer-Policy"] = "no-referrer"
|
|
||||||
return response
|
|
||||||
|
|
||||||
# Proxy sub-directory support
|
# Proxy sub-directory support
|
||||||
# Set environment var USE_X_SETTINGS=1 on this script
|
# Set environment var USE_X_SETTINGS=1 on this script
|
||||||
# And then in your proxy_pass settings
|
# And then in your proxy_pass settings
|
||||||
|
|||||||
@@ -65,9 +65,7 @@ class perform_site_check():
|
|||||||
request_headers['Accept-Encoding'] = request_headers['Accept-Encoding'].replace(', br', '')
|
request_headers['Accept-Encoding'] = request_headers['Accept-Encoding'].replace(', br', '')
|
||||||
|
|
||||||
timeout = self.datastore.data['settings']['requests'].get('timeout')
|
timeout = self.datastore.data['settings']['requests'].get('timeout')
|
||||||
|
url = watch.get('url')
|
||||||
url = watch.link
|
|
||||||
|
|
||||||
request_body = self.datastore.data['watching'][uuid].get('body')
|
request_body = self.datastore.data['watching'][uuid].get('body')
|
||||||
request_method = self.datastore.data['watching'][uuid].get('method')
|
request_method = self.datastore.data['watching'][uuid].get('method')
|
||||||
ignore_status_codes = self.datastore.data['watching'][uuid].get('ignore_status_codes', False)
|
ignore_status_codes = self.datastore.data['watching'][uuid].get('ignore_status_codes', False)
|
||||||
|
|||||||
@@ -87,16 +87,6 @@ class model(dict):
|
|||||||
print ("> Creating data dir {}".format(target_path))
|
print ("> Creating data dir {}".format(target_path))
|
||||||
os.mkdir(target_path)
|
os.mkdir(target_path)
|
||||||
|
|
||||||
@property
|
|
||||||
def link(self):
|
|
||||||
url = self.get('url', '')
|
|
||||||
if '{%' in url or '{{' in url:
|
|
||||||
from jinja2 import Environment
|
|
||||||
# Jinja2 available in URLs along with https://pypi.org/project/jinja2-time/
|
|
||||||
jinja2_env = Environment(extensions=['jinja2_time.TimeExtension'])
|
|
||||||
return str(jinja2_env.from_string(url).render())
|
|
||||||
return url
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def label(self):
|
def label(self):
|
||||||
# Used for sorting
|
# Used for sorting
|
||||||
@@ -128,10 +118,7 @@ class model(dict):
|
|||||||
if os.path.isfile(fname):
|
if os.path.isfile(fname):
|
||||||
logging.debug("Reading history index " + str(time.time()))
|
logging.debug("Reading history index " + str(time.time()))
|
||||||
with open(fname, "r") as f:
|
with open(fname, "r") as f:
|
||||||
for i in f.readlines():
|
tmp_history = dict(i.strip().split(',', 2) for i in f.readlines())
|
||||||
if ',' in i:
|
|
||||||
k, v = i.strip().split(',', 2)
|
|
||||||
tmp_history[k] = v
|
|
||||||
|
|
||||||
if len(tmp_history):
|
if len(tmp_history):
|
||||||
self.__newest_history_key = list(tmp_history.keys())[-1]
|
self.__newest_history_key = list(tmp_history.keys())[-1]
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
# exit when any command fails
|
# exit when any command fails
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
|
||||||
|
|
||||||
find tests/test_*py -type f|while read test_name
|
find tests/test_*py -type f|while read test_name
|
||||||
do
|
do
|
||||||
echo "TEST RUNNING $test_name"
|
echo "TEST RUNNING $test_name"
|
||||||
@@ -47,9 +45,7 @@ docker kill $$-test_selenium
|
|||||||
|
|
||||||
echo "TESTING WEBDRIVER FETCH > PLAYWRIGHT/BROWSERLESS..."
|
echo "TESTING WEBDRIVER FETCH > PLAYWRIGHT/BROWSERLESS..."
|
||||||
# Not all platforms support playwright (not ARM/rPI), so it's not packaged in requirements.txt
|
# Not all platforms support playwright (not ARM/rPI), so it's not packaged in requirements.txt
|
||||||
PLAYWRIGHT_VERSION=$(grep -i -E "RUN pip install.+" "$SCRIPT_DIR/../Dockerfile" | grep --only-matching -i -E "playwright[=><~+]+[0-9\.]+")
|
pip3 install playwright~=1.24
|
||||||
echo "using $PLAYWRIGHT_VERSION"
|
|
||||||
pip3 install "$PLAYWRIGHT_VERSION"
|
|
||||||
docker run -d --name $$-test_browserless -e "DEFAULT_LAUNCH_ARGS=[\"--window-size=1920,1080\"]" --rm -p 3000:3000 --shm-size="2g" browserless/chrome:1.53-chrome-stable
|
docker run -d --name $$-test_browserless -e "DEFAULT_LAUNCH_ARGS=[\"--window-size=1920,1080\"]" --rm -p 3000:3000 --shm-size="2g" browserless/chrome:1.53-chrome-stable
|
||||||
# takes a while to spin up
|
# takes a while to spin up
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ class ChangeDetectionStore:
|
|||||||
def __init__(self, datastore_path="/datastore", include_default_watches=True, version_tag="0.0.0"):
|
def __init__(self, datastore_path="/datastore", include_default_watches=True, version_tag="0.0.0"):
|
||||||
# Should only be active for docker
|
# Should only be active for docker
|
||||||
# logging.basicConfig(filename='/dev/stdout', level=logging.INFO)
|
# logging.basicConfig(filename='/dev/stdout', level=logging.INFO)
|
||||||
self.__data = App.model()
|
self.needs_write = False
|
||||||
self.datastore_path = datastore_path
|
self.datastore_path = datastore_path
|
||||||
self.json_store_path = "{}/url-watches.json".format(self.datastore_path)
|
self.json_store_path = "{}/url-watches.json".format(self.datastore_path)
|
||||||
self.needs_write = False
|
|
||||||
self.proxy_list = None
|
self.proxy_list = None
|
||||||
self.start_time = time.time()
|
|
||||||
self.stop_thread = False
|
self.stop_thread = False
|
||||||
|
|
||||||
|
self.__data = App.model()
|
||||||
|
|
||||||
# Base definition for all watchers
|
# Base definition for all watchers
|
||||||
# deepcopy part of #569 - not sure why its needed exactly
|
# deepcopy part of #569 - not sure why its needed exactly
|
||||||
self.generic_definition = deepcopy(Watch.model(datastore_path = datastore_path, default={}))
|
self.generic_definition = deepcopy(Watch.model(datastore_path = datastore_path, default={}))
|
||||||
|
|||||||
@@ -40,8 +40,7 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{{ render_field(form.url, placeholder="https://...", required=true, class="m-d") }}
|
{{ render_field(form.url, placeholder="https://...", required=true, class="m-d") }}
|
||||||
<span class="pure-form-message-inline">Some sites use JavaScript to create the content, for this you should <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Fetching-pages-with-WebDriver">use the Chrome/WebDriver Fetcher</a></span><br/>
|
<span class="pure-form-message-inline">Some sites use JavaScript to create the content, for this you should <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Fetching-pages-with-WebDriver">use the Chrome/WebDriver Fetcher</a></span>
|
||||||
<span class="pure-form-message-inline">You can use variables in the URL, perfect for inserting the current date and other logic, <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Handling-variables-in-the-watched-URL">help and examples here</a></span><br/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{{ render_field(form.title, class="m-d") }}
|
{{ render_field(form.title, class="m-d") }}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
<a class="state-{{'on' if watch.notification_muted}}" href="{{url_for('index', op='mute', uuid=watch.uuid, tag=active_tag)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="Mute notifications" title="Mute notifications"/></a>
|
<a class="state-{{'on' if watch.notification_muted}}" href="{{url_for('index', op='mute', uuid=watch.uuid, tag=active_tag)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="Mute notifications" title="Mute notifications"/></a>
|
||||||
</td>
|
</td>
|
||||||
<td class="title-col inline">{{watch.title if watch.title is not none and watch.title|length > 0 else watch.url}}
|
<td class="title-col inline">{{watch.title if watch.title is not none and watch.title|length > 0 else watch.url}}
|
||||||
<a class="external" target="_blank" rel="noopener" href="{{ watch.link.replace('source:','') }}"></a>
|
<a class="external" target="_blank" rel="noopener" href="{{ watch.url.replace('source:','') }}"></a>
|
||||||
<a href="{{url_for('form_share_put_watch', uuid=watch.uuid)}}"><img style="height: 1em;display:inline-block;" src="{{url_for('static_content', group='images', filename='spread.svg')}}" /></a>
|
<a href="{{url_for('form_share_put_watch', uuid=watch.uuid)}}"><img style="height: 1em;display:inline-block;" src="{{url_for('static_content', group='images', filename='spread.svg')}}" /></a>
|
||||||
|
|
||||||
{%if watch.fetch_backend == "html_webdriver" %}<img style="height: 1em; display:inline-block;" src="{{url_for('static_content', group='images', filename='Google-Chrome-icon.png')}}" />{% endif %}
|
{%if watch.fetch_backend == "html_webdriver" %}<img style="height: 1em; display:inline-block;" src="{{url_for('static_content', group='images', filename='Google-Chrome-icon.png')}}" />{% endif %}
|
||||||
|
|||||||
@@ -147,16 +147,6 @@ def test_api_simple(client, live_server):
|
|||||||
# @todo how to handle None/default global values?
|
# @todo how to handle None/default global values?
|
||||||
assert watch['history_n'] == 2, "Found replacement history section, which is in its own API"
|
assert watch['history_n'] == 2, "Found replacement history section, which is in its own API"
|
||||||
|
|
||||||
# basic systeminfo check
|
|
||||||
res = client.get(
|
|
||||||
url_for("systeminfo"),
|
|
||||||
headers={'x-api-key': api_key},
|
|
||||||
)
|
|
||||||
info = json.loads(res.data)
|
|
||||||
assert info.get('watch_count') == 1
|
|
||||||
assert info.get('uptime') > 0.5
|
|
||||||
|
|
||||||
|
|
||||||
# Finally delete the watch
|
# Finally delete the watch
|
||||||
res = client.delete(
|
res = client.delete(
|
||||||
url_for("watch", uuid=watch_uuid),
|
url_for("watch", uuid=watch_uuid),
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
|
|
||||||
import time
|
|
||||||
from flask import url_for
|
|
||||||
from .util import live_server_setup
|
|
||||||
|
|
||||||
|
|
||||||
# If there was only a change in the whitespacing, then we shouldnt have a change detected
|
|
||||||
def test_jinja2_in_url_query(client, live_server):
|
|
||||||
live_server_setup(live_server)
|
|
||||||
|
|
||||||
# Give the endpoint time to spin up
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# Add our URL to the import page
|
|
||||||
test_url = url_for('test_return_query', _external=True)
|
|
||||||
|
|
||||||
# because url_for() will URL-encode the var, but we dont here
|
|
||||||
full_url = "{}?{}".format(test_url,
|
|
||||||
"date={% now 'Europe/Berlin', '%Y' %}.{% now 'Europe/Berlin', '%m' %}.{% now 'Europe/Berlin', '%d' %}", )
|
|
||||||
res = client.post(
|
|
||||||
url_for("form_quick_watch_add"),
|
|
||||||
data={"url": full_url, "tag": "test"},
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
assert b"Watch added" in res.data
|
|
||||||
time.sleep(3)
|
|
||||||
# It should report nothing found (no new 'unviewed' class)
|
|
||||||
res = client.get(
|
|
||||||
url_for("preview_page", uuid="first"),
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
assert b'date=2' in res.data
|
|
||||||
@@ -159,10 +159,5 @@ def live_server_setup(live_server):
|
|||||||
ret = " ".join([auth.username, auth.password, auth.type])
|
ret = " ".join([auth.username, auth.password, auth.type])
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
# Just return some GET var
|
|
||||||
@live_server.app.route('/test-return-query', methods=['GET'])
|
|
||||||
def test_return_query():
|
|
||||||
return request.query_string
|
|
||||||
|
|
||||||
live_server.start()
|
live_server.start()
|
||||||
|
|
||||||
|
|||||||
@@ -45,9 +45,6 @@ services:
|
|||||||
# Respect proxy_pass type settings, `proxy_set_header Host "localhost";` and `proxy_set_header X-Forwarded-Prefix /app;`
|
# Respect proxy_pass type settings, `proxy_set_header Host "localhost";` and `proxy_set_header X-Forwarded-Prefix /app;`
|
||||||
# More here https://github.com/dgtlmoon/changedetection.io/wiki/Running-changedetection.io-behind-a-reverse-proxy-sub-directory
|
# More here https://github.com/dgtlmoon/changedetection.io/wiki/Running-changedetection.io-behind-a-reverse-proxy-sub-directory
|
||||||
# - USE_X_SETTINGS=1
|
# - USE_X_SETTINGS=1
|
||||||
#
|
|
||||||
# Hides the `Referer` header so that monitored websites can't see the changedetection.io hostname.
|
|
||||||
# - HIDE_REFERER=true
|
|
||||||
|
|
||||||
# Comment out ports: when using behind a reverse proxy , enable networks: etc.
|
# Comment out ports: when using behind a reverse proxy , enable networks: etc.
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
flask ~= 2.0
|
flask~= 2.0
|
||||||
flask_wtf
|
flask_wtf
|
||||||
eventlet >= 0.31.0
|
eventlet>=0.31.0
|
||||||
validators
|
validators
|
||||||
timeago ~= 1.0
|
timeago ~=1.0
|
||||||
inscriptis ~= 2.2
|
inscriptis ~= 2.2
|
||||||
feedgen ~= 0.9
|
feedgen ~= 0.9
|
||||||
flask-login ~= 0.5
|
flask-login ~= 0.5
|
||||||
@@ -46,9 +46,4 @@ selenium ~= 4.1.0
|
|||||||
# need to revisit flask login versions
|
# need to revisit flask login versions
|
||||||
werkzeug ~= 2.0.0
|
werkzeug ~= 2.0.0
|
||||||
|
|
||||||
# Templating, so far just in the URLs but in the future can be for the notifications also
|
|
||||||
jinja2 ~= 3.1
|
|
||||||
jinja2-time
|
|
||||||
|
|
||||||
# playwright is installed at Dockerfile build time because it's not available on all platforms
|
# playwright is installed at Dockerfile build time because it's not available on all platforms
|
||||||
|
|
||||||
|
|||||||
49
x
Normal file
49
x
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
diff --git a/changedetectionio/__init__.py b/changedetectionio/__init__.py
|
||||||
|
index c745dd3e..19873cce 100644
|
||||||
|
--- a/changedetectionio/__init__.py
|
||||||
|
+++ b/changedetectionio/__init__.py
|
||||||
|
@@ -819,8 +819,8 @@ def changedetection_app(config=None, datastore_o=None):
|
||||||
|
# Read as binary and force decode as UTF-8
|
||||||
|
# Windows may fail decode in python if we just use 'r' mode (chardet decode exception)
|
||||||
|
try:
|
||||||
|
- with open(newest_file, 'rb') as f:
|
||||||
|
- newest_version_file_contents = f.read().decode('utf-8')
|
||||||
|
+ with open(newest_file, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
|
+ newest_version_file_contents = f.read()
|
||||||
|
except Exception as e:
|
||||||
|
newest_version_file_contents = "Unable to read {}.\n".format(newest_file)
|
||||||
|
|
||||||
|
@@ -832,8 +832,8 @@ def changedetection_app(config=None, datastore_o=None):
|
||||||
|
previous_file = history[dates[-2]]
|
||||||
|
|
||||||
|
try:
|
||||||
|
- with open(previous_file, 'rb') as f:
|
||||||
|
- previous_version_file_contents = f.read().decode('utf-8')
|
||||||
|
+ with open(previous_file, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
|
+ previous_version_file_contents = f.read()
|
||||||
|
except Exception as e:
|
||||||
|
previous_version_file_contents = "Unable to read {}.\n".format(previous_file)
|
||||||
|
|
||||||
|
@@ -909,7 +909,7 @@ def changedetection_app(config=None, datastore_o=None):
|
||||||
|
timestamp = list(watch.history.keys())[-1]
|
||||||
|
filename = watch.history[timestamp]
|
||||||
|
try:
|
||||||
|
- with open(filename, 'r') as f:
|
||||||
|
+ with open(filename, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
|
tmp = f.readlines()
|
||||||
|
|
||||||
|
# Get what needs to be highlighted
|
||||||
|
diff --git a/changedetectionio/model/Watch.py b/changedetectionio/model/Watch.py
|
||||||
|
index 9a87ad71..566eb88e 100644
|
||||||
|
--- a/changedetectionio/model/Watch.py
|
||||||
|
+++ b/changedetectionio/model/Watch.py
|
||||||
|
@@ -158,7 +158,8 @@ class model(dict):
|
||||||
|
|
||||||
|
logging.debug("Saving history text {}".format(snapshot_fname))
|
||||||
|
|
||||||
|
- # in /diff/ we are going to assume for now that it's UTF-8 when reading
|
||||||
|
+ # in /diff/ and /preview/ we are going to assume for now that it's UTF-8 when reading
|
||||||
|
+ # most sites are utf-8 and some are even broken utf-8
|
||||||
|
with open(snapshot_fname, 'wb') as f:
|
||||||
|
f.write(contents)
|
||||||
|
f.close()
|
||||||
Reference in New Issue
Block a user