mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-04-15 23:48:05 +00:00
Compare commits
6 Commits
0.54.9
...
text-filte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
430d2130b3 | ||
|
|
7e58fed0ab | ||
|
|
4be295b613 | ||
|
|
fcba83724a | ||
|
|
2a09f21722 | ||
|
|
ac9f220147 |
@@ -2,7 +2,7 @@
|
||||
|
||||
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
||||
# Semver means never use .01, or 00. Should be .1.
|
||||
__version__ = '0.54.9'
|
||||
__version__ = '0.54.8'
|
||||
|
||||
from changedetectionio.strtobool import strtobool
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
@@ -38,39 +38,26 @@
|
||||
if (a.size !== b.size) {
|
||||
return b.size - a.size;
|
||||
}
|
||||
|
||||
|
||||
// Second priority: apple-touch-icon over regular icon
|
||||
const isAppleA = /apple-touch-icon/.test(a.rel);
|
||||
const isAppleB = /apple-touch-icon/.test(b.rel);
|
||||
if (isAppleA && !isAppleB) return -1;
|
||||
if (!isAppleA && isAppleB) return 1;
|
||||
|
||||
|
||||
// Third priority: icons with no size attribute (fallback icons) last
|
||||
const hasNoSizeA = !a.hasSizes;
|
||||
const hasNoSizeB = !b.hasSizes;
|
||||
if (hasNoSizeA && !hasNoSizeB) return 1;
|
||||
if (!hasNoSizeA && hasNoSizeB) return -1;
|
||||
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
const timeoutMs = 2000;
|
||||
// 1 MB — matches the server-side limit in bump_favicon()
|
||||
const MAX_BYTES = 1 * 1024 * 1024;
|
||||
|
||||
for (const icon of icons) {
|
||||
try {
|
||||
// Inline data URI — no network fetch needed, data is already here
|
||||
if (icon.href.startsWith('data:')) {
|
||||
const match = icon.href.match(/^data:([^;]+);base64,([A-Za-z0-9+/=]+)$/);
|
||||
if (!match) continue;
|
||||
const mime_type = match[1];
|
||||
const base64 = match[2];
|
||||
// Rough size check: base64 is ~4/3 the binary size
|
||||
if (base64.length * 0.75 > MAX_BYTES) continue;
|
||||
return { url: icon.href, mime_type, base64 };
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
@@ -87,15 +74,12 @@
|
||||
|
||||
const blob = await resp.blob();
|
||||
|
||||
if (blob.size > MAX_BYTES) continue;
|
||||
|
||||
// Convert blob to base64
|
||||
const reader = new FileReader();
|
||||
return await new Promise(resolve => {
|
||||
reader.onloadend = () => {
|
||||
resolve({
|
||||
url: icon.href,
|
||||
mime_type: blob.type,
|
||||
base64: reader.result.split(",")[1]
|
||||
});
|
||||
};
|
||||
@@ -114,3 +98,4 @@
|
||||
// Auto-execute and return result for page.evaluate()
|
||||
return await window.getFaviconAsBlob();
|
||||
})();
|
||||
|
||||
|
||||
@@ -798,50 +798,24 @@ class model(EntityPersistenceMixin, watch_base):
|
||||
# Also in the case that the file didnt exist
|
||||
return True
|
||||
|
||||
def bump_favicon(self, url, favicon_base_64: str, mime_type: str = None) -> None:
|
||||
def bump_favicon(self, url, favicon_base_64: str) -> None:
|
||||
from urllib.parse import urlparse
|
||||
import base64
|
||||
import binascii
|
||||
import re
|
||||
decoded = None
|
||||
|
||||
MAX_FAVICON_BYTES = 1 * 1024 * 1024 # 1 MB
|
||||
|
||||
MIME_TO_EXT = {
|
||||
'image/png': 'png',
|
||||
'image/x-icon': 'ico',
|
||||
'image/vnd.microsoft.icon': 'ico',
|
||||
'image/jpeg': 'jpg',
|
||||
'image/gif': 'gif',
|
||||
'image/svg+xml': 'svg',
|
||||
'image/webp': 'webp',
|
||||
'image/bmp': 'bmp',
|
||||
}
|
||||
|
||||
extension = None
|
||||
|
||||
# If the caller already resolved the MIME type (e.g. from blob.type or a data URI),
|
||||
# use that directly — it's more reliable than guessing from a URL path.
|
||||
if mime_type:
|
||||
extension = MIME_TO_EXT.get(mime_type.lower().split(';')[0].strip(), None)
|
||||
|
||||
# Fall back to extracting extension from URL path, unless it's a data URI.
|
||||
if not extension and url and not url.startswith('data:'):
|
||||
if url:
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
filename = os.path.basename(parsed.path)
|
||||
(_base, ext) = filename.lower().strip().rsplit('.', 1)
|
||||
extension = ext
|
||||
(base, extension) = filename.lower().strip().rsplit('.', 1)
|
||||
except ValueError:
|
||||
logger.warning(f"UUID: {self.get('uuid')} Cant work out file extension from '{url}', defaulting to ico")
|
||||
|
||||
# Handle data URIs: extract MIME type from the URI itself when not already known
|
||||
if not extension and url and url.startswith('data:'):
|
||||
m = re.match(r'^data:([^;]+);base64,', url)
|
||||
if m:
|
||||
extension = MIME_TO_EXT.get(m.group(1).lower(), None)
|
||||
|
||||
if not extension:
|
||||
extension = 'ico'
|
||||
logger.error(f"UUID: {self.get('uuid')} Cant work out file extension from '{url}'")
|
||||
return None
|
||||
else:
|
||||
# Assume favicon.ico
|
||||
base = "favicon"
|
||||
extension = "ico"
|
||||
|
||||
fname = os.path.join(self.data_dir, f"favicon.{extension}")
|
||||
|
||||
@@ -850,27 +824,22 @@ class model(EntityPersistenceMixin, watch_base):
|
||||
decoded = base64.b64decode(favicon_base_64, validate=True)
|
||||
except (binascii.Error, ValueError) as e:
|
||||
logger.warning(f"UUID: {self.get('uuid')} FavIcon save data (Base64) corrupt? {str(e)}")
|
||||
return None
|
||||
else:
|
||||
if decoded:
|
||||
try:
|
||||
with open(fname, 'wb') as f:
|
||||
f.write(decoded)
|
||||
|
||||
if len(decoded) > MAX_FAVICON_BYTES:
|
||||
logger.warning(f"UUID: {self.get('uuid')} Favicon too large ({len(decoded)} bytes), skipping")
|
||||
return None
|
||||
# Invalidate module-level favicon filename cache for this watch
|
||||
_FAVICON_FILENAME_CACHE.pop(self.data_dir, None)
|
||||
|
||||
try:
|
||||
with open(fname, 'wb') as f:
|
||||
f.write(decoded)
|
||||
# A signal that could trigger the socket server to update the browser also
|
||||
watch_check_update = signal('watch_favicon_bump')
|
||||
if watch_check_update:
|
||||
watch_check_update.send(watch_uuid=self.get('uuid'))
|
||||
|
||||
# Invalidate module-level favicon filename cache for this watch
|
||||
_FAVICON_FILENAME_CACHE.pop(self.data_dir, None)
|
||||
|
||||
# A signal that could trigger the socket server to update the browser also
|
||||
watch_check_update = signal('watch_favicon_bump')
|
||||
if watch_check_update:
|
||||
watch_check_update.send(watch_uuid=self.get('uuid'))
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"UUID: {self.get('uuid')} error saving FavIcon to {fname} - {str(e)}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"UUID: {self.get('uuid')} error saving FavIcon to {fname} - {str(e)}")
|
||||
|
||||
# @todo - Store some checksum and only write when its different
|
||||
logger.debug(f"UUID: {self.get('uuid')} updated favicon to at {fname}")
|
||||
|
||||
@@ -338,7 +338,6 @@ class watch_base(dict):
|
||||
# These are set by processors/workers and should not trigger edited flag
|
||||
additional_system_fields = {
|
||||
'last_check_status', # Set by processors
|
||||
'last_filter_config_hash', # Set by text_json_diff processor, internal skip-cache
|
||||
'restock', # Set by restock processor
|
||||
'last_viewed', # Set by mark_all_viewed endpoint
|
||||
}
|
||||
|
||||
@@ -259,12 +259,9 @@ def apply_service_tweaks(url, n_body, n_title, requested_output_format):
|
||||
elif (url.startswith('discord://') or url.startswith('https://discordapp.com/api/webhooks')
|
||||
or url.startswith('https://discord.com/api'))\
|
||||
and 'html' in requested_output_format:
|
||||
# Discord doesn't render HTML — convert markup to plain text equivalents.
|
||||
# is injected upstream to preserve double-spaces for HTML email clients;
|
||||
# Discord displays it as the literal string " " so strip it here.
|
||||
# Discord doesn't support HTML, replace <br> with newlines
|
||||
n_body = n_body.strip().replace('<br>', '\n')
|
||||
n_body = n_body.replace('</br>', '\n')
|
||||
n_body = n_body.replace(' ', ' ')
|
||||
n_body = newline_re.sub('\n', n_body)
|
||||
|
||||
# Don't replace placeholders or truncate here - let the custom Discord plugin handle it
|
||||
|
||||
@@ -97,6 +97,7 @@ class difference_detection_processor():
|
||||
logger.warning(f"Failed to read checksum file for {self.watch_uuid}: {e}")
|
||||
self.last_raw_content_checksum = None
|
||||
|
||||
|
||||
async def validate_iana_url(self):
|
||||
"""Pre-flight SSRF check — runs DNS lookup in executor to avoid blocking the event loop.
|
||||
Covers all fetchers (requests, playwright, puppeteer, plugins) since every fetch goes
|
||||
|
||||
@@ -105,30 +105,6 @@ class FilterConfig:
|
||||
def text_should_not_be_present(self):
|
||||
return self._get_merged_rules('text_should_not_be_present')
|
||||
|
||||
def get_filter_config_hash(self):
|
||||
"""
|
||||
Stable hash of the effective filter configuration.
|
||||
|
||||
Used by the skip-logic in run_changedetection() so that any change to
|
||||
global settings, tag overrides, or watch filters automatically invalidates
|
||||
the raw-content-unchanged shortcut — without needing scattered
|
||||
clear_all_last_checksums() calls at every settings mutation site.
|
||||
"""
|
||||
app = self.datastore.data['settings']['application']
|
||||
config = {
|
||||
'extract_lines_containing': sorted(self.extract_lines_containing),
|
||||
'extract_text': sorted(self.extract_text),
|
||||
'ignore_text': sorted(self.ignore_text),
|
||||
'include_filters': sorted(self.include_filters),
|
||||
'subtractive_selectors': sorted(self.subtractive_selectors),
|
||||
'text_should_not_be_present': sorted(self.text_should_not_be_present),
|
||||
'trigger_text': sorted(self.trigger_text),
|
||||
# Global processing flags not captured by the filter lists above
|
||||
'ignore_whitespace': app.get('ignore_whitespace', False),
|
||||
'strip_ignored_lines': app.get('strip_ignored_lines', False),
|
||||
}
|
||||
return hashlib.md5(json.dumps(config, sort_keys=True).encode()).hexdigest()
|
||||
|
||||
@property
|
||||
def has_include_filters(self):
|
||||
return bool(self.include_filters) and bool(self.include_filters[0].strip())
|
||||
@@ -416,26 +392,19 @@ class perform_site_check(difference_detection_processor):
|
||||
raise Exception("Watch no longer exists.")
|
||||
|
||||
current_raw_document_checksum = self.get_raw_document_checksum()
|
||||
|
||||
# Build filter config up front so we can hash it for the skip check.
|
||||
filter_config = FilterConfig(watch, self.datastore)
|
||||
current_filter_config_hash = filter_config.get_filter_config_hash()
|
||||
|
||||
# Skip only when ALL of these hold:
|
||||
# 1. raw HTML is unchanged
|
||||
# 2. watch config was not edited (was_edited covers per-watch field changes)
|
||||
# 3. effective filter config is unchanged (covers global/tag setting changes that
|
||||
# bypass was_edited — e.g. global_ignore_text, global_subtractive_selectors)
|
||||
# last_filter_config_hash being False means first run or upgrade: don't skip.
|
||||
# Skip processing only if BOTH conditions are true:
|
||||
# 1. HTML content unchanged (checksum matches last saved checksum)
|
||||
# 2. Watch configuration was not edited (including trigger_text, filters, etc.)
|
||||
# The was_edited flag handles all watch configuration changes, so we don't need
|
||||
# separate checks for trigger_text or other processing rules.
|
||||
if (not force_reprocess and
|
||||
not watch.was_edited and
|
||||
self.last_raw_content_checksum and
|
||||
self.last_raw_content_checksum == current_raw_document_checksum and
|
||||
watch.get('last_filter_config_hash') and
|
||||
watch.get('last_filter_config_hash') == current_filter_config_hash):
|
||||
self.last_raw_content_checksum == current_raw_document_checksum):
|
||||
raise checksumFromPreviousCheckWasTheSame()
|
||||
|
||||
# Initialize remaining components
|
||||
# Initialize components
|
||||
filter_config = FilterConfig(watch, self.datastore)
|
||||
content_processor = ContentProcessor(self.fetcher, watch, filter_config, self.datastore)
|
||||
transformer = ContentTransformer()
|
||||
rule_engine = RuleEngine()
|
||||
@@ -456,7 +425,6 @@ class perform_site_check(difference_detection_processor):
|
||||
|
||||
# Save the raw content checksum to file (processor implementation detail, not watch config)
|
||||
self.update_last_raw_content_checksum(current_raw_document_checksum)
|
||||
update_obj['last_filter_config_hash'] = current_filter_config_hash
|
||||
|
||||
# === CONTENT PREPROCESSING ===
|
||||
# Avoid creating unnecessary intermediate string copies by reassigning only when needed
|
||||
@@ -587,8 +555,8 @@ class perform_site_check(difference_detection_processor):
|
||||
# === BLOCKING RULES EVALUATION ===
|
||||
blocked = False
|
||||
|
||||
# Check trigger_text - use text_for_checksuming so ignore_text can suppress trigger_text
|
||||
if rule_engine.evaluate_trigger_text(text_for_checksuming, filter_config.trigger_text):
|
||||
# Check trigger_text
|
||||
if rule_engine.evaluate_trigger_text(stripped_text, filter_config.trigger_text):
|
||||
blocked = True
|
||||
|
||||
# Check text_should_not_be_present
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
function checkDiscordHtmlWarning() {
|
||||
var urls = $('textarea.notification-urls').val() || '';
|
||||
var format = $('select.notification-format').val() || '';
|
||||
var isDiscord = /discord:\/\/|https:\/\/discord(?:app)?\.com\/api/i.test(urls);
|
||||
var isHtml = format === 'html' || format === 'htmlcolor';
|
||||
if (isDiscord && isHtml) {
|
||||
$('#discord-html-format-warning').show();
|
||||
} else {
|
||||
$('#discord-html-format-warning').hide();
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
$('textarea.notification-urls, select.notification-format').on('change input', checkDiscordHtmlWarning);
|
||||
checkDiscordHtmlWarning();
|
||||
|
||||
$('#add-email-helper').click(function (e) {
|
||||
e.preventDefault();
|
||||
email = prompt("Destination email");
|
||||
|
||||
@@ -195,10 +195,6 @@
|
||||
<div class="">
|
||||
{{ render_field(form.notification_format , class="notification-format") }}
|
||||
<span class="pure-form-message-inline">{{ _('Format for all notifications') }}</span>
|
||||
<div id="discord-html-format-warning" class="inline-warning" style="display: none; margin-top: 6px;">
|
||||
<img class="inline-warning-icon" src="{{url_for('static_content', group='images', filename='notice.svg')}}" alt="{{ _('Note') }}" title="{{ _('Note') }}">
|
||||
{{ _('Discord does not render HTML — switch to') }} <strong>{{ _('Plain Text') }}</strong> {{ _('format to avoid') }} <code>&nbsp;</code> {{ _('and other HTML entities appearing literally in your notifications.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -10,7 +10,7 @@ xpath://body/div/span[contains(@class, 'example-class')]",
|
||||
<span class="pure-form-message-inline"><strong>Note!: //text() function does not work where the <element> contains <![CDATA[]]></strong></span><br>
|
||||
{% endif %}
|
||||
<span class="pure-form-message-inline">One CSS, xPath 1 & 2, JSON Path/JQ selector per line, <i>any</i> rules that matches will be used.<br>
|
||||
<span data-target="#advanced-help-selectors" class="toggle-show pure-button button-tag button-xsmall">{{ _('Show advanced help and tips') }}</span><br>
|
||||
<span data-target="#advanced-help-selectors" class="toggle-show pure-button button-tag button-xsmall">Show advanced help and tips</span><br>
|
||||
<ul id="advanced-help-selectors" style="display: none;">
|
||||
<li>CSS - Limit text to this CSS rule, only text matching this CSS rule is included.</li>
|
||||
<li>JSON - Limit text to this JSON rule, using either <a href="https://pypi.org/project/jsonpath-ng/" target="new">JSONPath</a> or <a href="https://stedolan.github.io/jq/" target="new">jq</a> (if installed).
|
||||
@@ -47,9 +47,9 @@ nav
|
||||
//*[contains(text(), 'Advertisement')]") }}
|
||||
<span class="pure-form-message-inline">
|
||||
<ul>
|
||||
<li> {{ _('Remove HTML element(s) by CSS and XPath selectors before text conversion.') }} </li>
|
||||
<li> {{ _("Don't paste HTML here, use only CSS and XPath selectors") }} </li>
|
||||
<li> {{ _('Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML.') }} </li>
|
||||
<li> Remove HTML element(s) by CSS and XPath selectors before text conversion. </li>
|
||||
<li> Don't paste HTML here, use only CSS and XPath selectors </li>
|
||||
<li> Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML. </li>
|
||||
</ul>
|
||||
</span>
|
||||
</fieldset>
|
||||
|
||||
@@ -381,14 +381,14 @@ def test_extract_lines_containing_with_ignore_text(client, live_server, measure_
|
||||
f.write(initial_data)
|
||||
|
||||
test_url = url_for('test_endpoint', _external=True)
|
||||
uuid = client.application.config.get('DATASTORE').add_watch(url=test_url, extras={'paused': True})
|
||||
uuid = client.application.config.get('DATASTORE').add_watch(url=test_url)
|
||||
|
||||
# Set filters BEFORE the first check so the baseline is always filtered+ignored.
|
||||
# (Setting them after an initial unfiltered check creates a race: the forced recheck
|
||||
# that updates previous_md5 must complete before the next content write, which is
|
||||
# timing-sensitive and fails intermittently on slower systems / Python 3.14.)
|
||||
res = client.post(
|
||||
url_for("ui.ui_edit.edit_page", uuid=uuid, unpause_on_save=1),
|
||||
url_for("ui.ui_edit.edit_page", uuid=uuid),
|
||||
data={
|
||||
'extract_lines_containing': 'celsius',
|
||||
'ignore_text': 'Feels like',
|
||||
@@ -400,12 +400,13 @@ def test_extract_lines_containing_with_ignore_text(client, live_server, measure_
|
||||
},
|
||||
follow_redirects=True
|
||||
)
|
||||
assert b"unpaused" in res.data
|
||||
assert b"Updated watch." in res.data
|
||||
|
||||
# First check — establishes filtered+ignored baseline. previous_md5 was False so
|
||||
# a change is always detected here; mark_all_viewed clears it before we assert.
|
||||
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
||||
wait_for_all_checks(client)
|
||||
client.get(url_for("ui.mark_all_viewed"), follow_redirects=True)
|
||||
|
||||
# Sanity: preview should only show celsius lines
|
||||
res = client.get(url_for("ui.ui_preview.preview_page", uuid=uuid), follow_redirects=True)
|
||||
@@ -426,10 +427,10 @@ def test_extract_lines_containing_with_ignore_text(client, live_server, measure_
|
||||
wait_for_all_checks(client)
|
||||
|
||||
res = client.get(url_for("watchlist.index"))
|
||||
assert b'has-unread-changes' not in res.data, "Changing an ignored line should not trigger a change notification"
|
||||
assert b'has-unread-changes' not in res.data, \
|
||||
"Changing an ignored line should not trigger a change notification"
|
||||
|
||||
client.get(url_for("ui.mark_all_viewed"), follow_redirects=True)
|
||||
time.sleep(1)
|
||||
|
||||
# Change the non-ignored celsius line — SHOULD trigger
|
||||
triggered_data = """<html><body>
|
||||
@@ -445,7 +446,8 @@ def test_extract_lines_containing_with_ignore_text(client, live_server, measure_
|
||||
wait_for_all_checks(client)
|
||||
|
||||
res = client.get(url_for("watchlist.index"))
|
||||
assert b'has-unread-changes' in res.data, "Changing a non-ignored line should trigger a change notification"
|
||||
assert b'has-unread-changes' in res.data, \
|
||||
"Changing a non-ignored line should trigger a change notification"
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
@@ -50,85 +50,6 @@ def test_favicon(client, live_server, measure_memory_usage, datastore_path):
|
||||
res = client.get(url_for('static_content', group='js', filename='../styles/styles.css'))
|
||||
assert res.status_code != 200
|
||||
|
||||
def test_favicon_inline_data_uri(client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
bump_favicon() must handle a data URI as the url parameter.
|
||||
Previously this logged "Cant work out file extension from 'data:image/png;base64,...'" and bailed.
|
||||
The mime_type from the data URI should be used to pick the correct extension.
|
||||
"""
|
||||
import base64
|
||||
import os
|
||||
|
||||
# 1x1 transparent PNG (minimal valid PNG bytes)
|
||||
PNG_BYTES = (
|
||||
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01'
|
||||
b'\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x01'
|
||||
b'\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82'
|
||||
)
|
||||
png_b64 = base64.b64encode(PNG_BYTES).decode()
|
||||
data_uri = f"data:image/png;base64,{png_b64}"
|
||||
|
||||
uuid = client.application.config.get('DATASTORE').add_watch(url='https://localhost')
|
||||
watch = live_server.app.config['DATASTORE'].data['watching'][uuid]
|
||||
|
||||
# Should NOT raise / bail — must save as favicon.png
|
||||
watch.bump_favicon(url=data_uri, favicon_base_64=png_b64, mime_type='image/png')
|
||||
|
||||
favicon_fname = watch.get_favicon_filename()
|
||||
assert favicon_fname is not None, "Favicon should have been saved"
|
||||
assert favicon_fname.endswith('.png'), f"Expected .png extension, got: {favicon_fname}"
|
||||
|
||||
full_path = os.path.join(watch.data_dir, favicon_fname)
|
||||
assert os.path.getsize(full_path) == len(PNG_BYTES)
|
||||
|
||||
# Also verify it's served correctly via the static route
|
||||
res = client.get(url_for('static_content', group='favicon', filename=uuid))
|
||||
assert res.status_code == 200
|
||||
assert res.data == PNG_BYTES
|
||||
|
||||
|
||||
def test_favicon_mime_type_overrides_url_extension(client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
mime_type parameter takes precedence over the URL path extension.
|
||||
A URL ending in .ico but with mime_type='image/png' should save as .png.
|
||||
"""
|
||||
import base64
|
||||
import os
|
||||
|
||||
PNG_BYTES = (
|
||||
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01'
|
||||
b'\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x01'
|
||||
b'\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82'
|
||||
)
|
||||
png_b64 = base64.b64encode(PNG_BYTES).decode()
|
||||
|
||||
uuid = client.application.config.get('DATASTORE').add_watch(url='https://localhost')
|
||||
watch = live_server.app.config['DATASTORE'].data['watching'][uuid]
|
||||
|
||||
watch.bump_favicon(url='https://example.com/favicon.ico', favicon_base_64=png_b64, mime_type='image/png')
|
||||
|
||||
favicon_fname = watch.get_favicon_filename()
|
||||
assert favicon_fname is not None
|
||||
assert favicon_fname.endswith('.png'), f"mime_type should override URL extension, got: {favicon_fname}"
|
||||
|
||||
|
||||
def test_favicon_oversized_rejected(client, live_server, measure_memory_usage, datastore_path):
|
||||
"""Favicons larger than 1 MB must be silently dropped."""
|
||||
import base64
|
||||
import os
|
||||
|
||||
oversized = b'\x00' * (1 * 1024 * 1024 + 1)
|
||||
oversized_b64 = base64.b64encode(oversized).decode()
|
||||
|
||||
uuid = client.application.config.get('DATASTORE').add_watch(url='https://localhost')
|
||||
watch = live_server.app.config['DATASTORE'].data['watching'][uuid]
|
||||
|
||||
result = watch.bump_favicon(url='https://example.com/big.png', favicon_base_64=oversized_b64, mime_type='image/png')
|
||||
|
||||
assert result is None, "bump_favicon should return None for oversized favicon"
|
||||
assert watch.get_favicon_filename() is None, "No favicon file should have been written"
|
||||
|
||||
|
||||
def test_bad_access(client, live_server, measure_memory_usage, datastore_path):
|
||||
|
||||
res = client.post(
|
||||
|
||||
@@ -70,10 +70,6 @@ def test_trigger_functionality(client, live_server, measure_memory_usage, datast
|
||||
uuid = client.application.config.get('DATASTORE').add_watch(url=test_url)
|
||||
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
||||
|
||||
|
||||
# And set the trigger text as 'ignore text', it should then not trigger
|
||||
live_server.app.config['DATASTORE'].data['settings']['application']['global_ignore_text'] = [trigger_text]
|
||||
|
||||
# Trigger a check
|
||||
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
||||
|
||||
@@ -126,16 +122,6 @@ def test_trigger_functionality(client, live_server, measure_memory_usage, datast
|
||||
# Now set the content which contains the trigger text
|
||||
set_modified_with_trigger_text_response(datastore_path=datastore_path)
|
||||
|
||||
# There is a "ignore text" set of the change that should be also the trigger, it should not trigger
|
||||
# because the ignore text should be stripped from the response, therefor, the trigger should not fire
|
||||
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
||||
wait_for_all_checks(client)
|
||||
res = client.get(url_for("watchlist.index"))
|
||||
assert b'has-unread-changes' not in res.data
|
||||
|
||||
|
||||
live_server.app.config['DATASTORE'].data['settings']['application']['global_ignore_text'] = []
|
||||
# check that the trigger fired once we stopped ignore it
|
||||
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
||||
wait_for_all_checks(client)
|
||||
res = client.get(url_for("watchlist.index"))
|
||||
|
||||
Binary file not shown.
@@ -78,7 +78,7 @@ msgstr "ファイルは .zip バックアップファイルでなければなり
|
||||
#: changedetectionio/blueprint/backups/restore.py
|
||||
#, python-format
|
||||
msgid "Backup file is too large (max %(mb)s MB)"
|
||||
msgstr "バックアップファイルが大きすぎます(最大 %(mb)s MB)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/backups/restore.py
|
||||
msgid "Invalid or corrupted zip file"
|
||||
@@ -137,7 +137,7 @@ msgstr "注意:これはメインアプリケーションの設定を上書き
|
||||
#: changedetectionio/blueprint/backups/templates/backup_restore.html
|
||||
#, python-format
|
||||
msgid "Max upload size: %(upload)s MB, Max decompressed size: %(decomp)s MB"
|
||||
msgstr "最大アップロードサイズ: %(upload)s MB、最大解凍サイズ: %(decomp)s MB"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/backups/templates/backup_restore.html
|
||||
msgid "Include all groups found in backup?"
|
||||
@@ -215,7 +215,7 @@ msgstr ".XLSX & Wachete"
|
||||
|
||||
#: changedetectionio/blueprint/imports/templates/import.html
|
||||
msgid "Backup Restore"
|
||||
msgstr "バックアップ復元"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/imports/templates/import.html
|
||||
msgid "Restoring changedetection.io backups is in the"
|
||||
@@ -433,11 +433,10 @@ msgid "After this many consecutive times that the CSS/xPath filter is missing, s
|
||||
msgstr "CSS/XPath フィルタがこの回数連続して見つからない場合、通知を送信"
|
||||
|
||||
# 訳注: "Set to [N] to disable" → 「[N]に設定すると無効になります」
|
||||
# 述語の訳を後半にまとめた
|
||||
# 前半は日本語では不要なため、空白1文字で非表示にする(空文字は英語にフォールバックするため)
|
||||
# 前半の断片に訳を置くと語順が崩れるため、後半にまとめた
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Set to"
|
||||
msgstr " "
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "to disable"
|
||||
@@ -473,7 +472,7 @@ msgstr "すべてのウォッチのデフォルトプロキシを選択"
|
||||
|
||||
# 訳注: "Base URL used for the {{base_url}} token in notification links."
|
||||
# → 「通知リンクの {{base_url}} トークンに使用するベースURL。」
|
||||
# 修飾語と被修飾語の訳を入れ替えた
|
||||
# 英語と語順が逆になるため、前後の断片で訳を入れ替えた
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Base URL used for the"
|
||||
msgstr "通知リンクの"
|
||||
@@ -522,7 +521,7 @@ msgstr "ページが完全にレンダリングされるのを待つのに問題
|
||||
|
||||
# 訳注: "This will wait [n] seconds before extracting the text."
|
||||
# → 「テキスト抽出前に [n] 秒間待機します。」
|
||||
# 時間条件と述語の訳を入れ替えた
|
||||
# 前半に文脈、後半に述語を置くよう訳を分担した
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "This will wait"
|
||||
msgstr "テキスト抽出前に"
|
||||
@@ -573,7 +572,7 @@ msgstr "ブラウザが検出されるすべての方法"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Connect using Bright Data proxies, find out more here."
|
||||
msgstr "Bright Data プロキシを使用して接続します。詳細はこちら。"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/blueprint/ui/templates/diff.html
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html changedetectionio/templates/_common_fields.html
|
||||
@@ -606,7 +605,7 @@ msgstr "テキスト変換前に CSS および XPath セレクターで HTML 要
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr "ここにHTMLを貼り付けないでください。CSSとXPathセレクターのみを使用してください。"
|
||||
msgstr "ここにHTMLを貼り付けないでください。CSSとXPathセレクターのみを使用してください"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
@@ -774,7 +773,7 @@ msgstr "ヒント"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "\"Residential\" and \"Mobile\" proxy type can be more successful than \"Data Center\" for blocked websites."
|
||||
msgstr "ブロックされたWebサイトには「Residential」や「Mobile」プロキシタイプが「Data Center」より効果的な場合があります。"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "\"Name\" will be used for selecting the proxy in the Watch Edit settings"
|
||||
@@ -847,38 +846,35 @@ msgstr "フィルタとトリガー"
|
||||
msgid ""
|
||||
"Automatically applies this tag to any watch whose URL matches. Supports wildcards: <code>*example.com*</code> or "
|
||||
"plain substring: <code>github.com/myorg</code>"
|
||||
msgstr "URLが一致するウォッチにこのタグを自動適用します。ワイルドカード: <code>*example.com*</code> または部分文字列: <code>github.com/myorg</code> に対応"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Currently matching watches"
|
||||
msgstr "現在マッチしているウォッチ"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Tag colour"
|
||||
msgstr "タグの色"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Custom colour"
|
||||
msgstr "カスタム色"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "Leave unchecked to use the auto-generated colour based on the tag name."
|
||||
msgstr "タグ名に基づく自動生成色を使用する場合はチェックを外してください。"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "These settings are"
|
||||
msgstr "これらの設定は"
|
||||
|
||||
# 訳注: "These settings are [added] to any existing watch configurations."
|
||||
# → 「これらの設定は既存のすべてのウォッチ設定に[追加]されます。」
|
||||
# 目的語と述語の訳を入れ替えた
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "added"
|
||||
msgstr "既存のすべてのウォッチ設定に"
|
||||
msgstr "追加されます"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
msgid "to any existing watch configurations."
|
||||
msgstr "追加されます。"
|
||||
msgstr "(既存のすべてのウォッチ設定に)"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Text filtering"
|
||||
@@ -900,12 +896,9 @@ msgstr "注意!"
|
||||
msgid "Lookout!"
|
||||
msgstr "注意!"
|
||||
|
||||
# 訳注: "There are" + <a>"system-wide notification URLs enabled"</a> + ", " + ...
|
||||
# → 「 件のシステム全体の通知URLが有効化されています、...」
|
||||
# 前半は日本語では不要なため、空白1文字で非表示にする(空文字は英語にフォールバックするため)
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "There are"
|
||||
msgstr " "
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "system-wide notification URLs enabled"
|
||||
@@ -1167,12 +1160,9 @@ msgstr "プレビューを表示できません - 取得/チェックが完了
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr "これにより、すべてのウォッチのバージョン履歴(スナップショット)が削除されますが、URLのリストは保持されます!"
|
||||
|
||||
# 訳注: "You may like to use the [BACKUP] link first."
|
||||
# → 「先に[バックアップ]リンクをご利用ください。」
|
||||
# 副詞と述語の訳を入れ替えた
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "You may like to use the"
|
||||
msgstr "先に"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "BACKUP"
|
||||
@@ -1180,7 +1170,7 @@ msgstr "バックアップ"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "link first."
|
||||
msgstr "リンクをご利用ください。"
|
||||
msgstr "リンクを先にご利用ください。"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "Confirmation text"
|
||||
@@ -1308,7 +1298,7 @@ msgstr "最新のリクエストからの現在のエラースクリーンショ
|
||||
|
||||
# 訳注: "Pro-tip: You can enable [option] from settings."
|
||||
# → 「プロのヒント:設定から [option] を有効にできます。」
|
||||
# 修飾語と述語の訳を入れ替えた
|
||||
# "from settings" を前半に移し、述語を後半にまとめた
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html
|
||||
msgid "Pro-tip: You can enable"
|
||||
msgstr "プロのヒント:設定から"
|
||||
@@ -1387,7 +1377,7 @@ msgstr "メイン一覧ページで使用される組織タグ/グループ名"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Also automatically applied by URL pattern:"
|
||||
msgstr "URLパターンによる自動適用:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "Automatically uses the page title if found, you can also use your own title/description here"
|
||||
@@ -1563,7 +1553,7 @@ msgstr "追加"
|
||||
|
||||
# 訳注: "So it's always better to select [X] when you're interested in new content."
|
||||
# → 「そのため、新しいコンテンツに興味がある場合は [X] を選択することをおすすめします。」
|
||||
# 条件節と述語の訳を入れ替えた
|
||||
# 英語と語順が逆になるため、条件節を前半に、述語を後半に移した
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "So it's always better to select"
|
||||
msgstr "そのため、新しいコンテンツに興味がある場合は"
|
||||
@@ -2256,7 +2246,7 @@ msgstr "要素を削除"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Extract lines containing"
|
||||
msgstr "指定テキストを含む行を抽出"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Extract text"
|
||||
@@ -2296,7 +2286,7 @@ msgstr "テキストをアルファベット順にソート"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Strip ignored lines"
|
||||
msgstr "無視する行を除外"
|
||||
msgstr "無視行を除去"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Trim whitespace before and after text"
|
||||
@@ -2758,7 +2748,7 @@ msgstr "changedetection.io が生成したプレビューページのURL。"
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
#, python-format
|
||||
msgid "Date/time of the change, accepts format=, change_datetime(format='%A')', default is '%Y-%m-%d %H:%M:%S %Z'"
|
||||
msgstr "変更の日時。format= を受け付けます(例: change_datetime(format='%A'))。デフォルトは '%Y-%m-%d %H:%M:%S %Z'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The URL of the diff output for the watch."
|
||||
@@ -2768,16 +2758,13 @@ msgstr "ウォッチの差分出力のURL。"
|
||||
msgid "The diff output - only changes, additions, and removals"
|
||||
msgstr "差分出力 - 変更、追加、削除のみ"
|
||||
|
||||
# 訳注: "All diff variants accept [codes] args, e.g. [examples]"
|
||||
# → 「すべての差分バリアントは [codes] 引数を受け付けます。例: [examples]」
|
||||
# 述語の訳を後半にまとめた
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "All diff variants accept"
|
||||
msgstr "すべての差分バリアントは"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "args, e.g."
|
||||
msgstr "引数を受け付けます。例:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The diff output - only changes, additions, and removals —"
|
||||
@@ -2819,13 +2806,13 @@ msgstr "差分出力 - 統一フォーマットのパッチ"
|
||||
msgid ""
|
||||
"Only the changed words/values from the previous version — e.g. the old price. Best when a single value changes per "
|
||||
"line; multiple changed fragments are joined by newline."
|
||||
msgstr "前バージョンから変更された単語/値のみ — 例: 旧価格。1行あたり1つの値が変わる場合に最適。複数の変更箇所は改行で結合されます。"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid ""
|
||||
"Only the changed words/values from the new version — e.g. the new price. Best when a single value changes per line; "
|
||||
"multiple changed fragments are joined by newline."
|
||||
msgstr "新バージョンの変更された単語/値のみ — 例: 新価格。1行あたり1つの値が変わる場合に最適。複数の変更箇所は改行で結合されます。"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The current snapshot text contents value, useful when combined with JSON or CSS filters"
|
||||
@@ -2837,7 +2824,7 @@ msgstr "フィルタからトリガーを発動させたテキスト"
|
||||
|
||||
# 訳注: "Warning: Contents of [token1] and [token2] depend on how the difference algorithm perceives the change."
|
||||
# → 「警告: [token1] および [token2] の内容は、差分アルゴリズムが変更をどのように認識するかによって異なります。」
|
||||
# 主語の訳を後半にまとめた
|
||||
# 述語「の内容は〜異なります」を後半の断片にまとめた
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Warning: Contents of"
|
||||
msgstr "警告:"
|
||||
@@ -2870,15 +2857,9 @@ msgstr "ほぼすべてのサービスへの通知に対応!"
|
||||
msgid "Please read the notification services wiki here for important configuration notes"
|
||||
msgstr "重要な設定に関するメモについては、通知サービスのWikiをこちらでお読みください"
|
||||
|
||||
# 訳注: 2箇所で使用される共通フラグメント
|
||||
# (1) _common_fields.html: "Use" + <a>AppRise Notification URLs</a> + "for notification to just about any service!"
|
||||
# → 「 AppRise 通知URL ほぼすべてのサービスへの通知に対応!」
|
||||
# (2) text-options.html: "Use" + <code>//(?aiLmsux))</code> + "type flags (more" + <a>information here</a> + ")"
|
||||
# → 「 //(?aiLmsux)) タイプフラグを使用できます(詳細 はこちら)」
|
||||
# 前半は日本語では不要なため、空白1文字で非表示にする(空文字は英語にフォールバックするため)
|
||||
#: changedetectionio/templates/_common_fields.html changedetectionio/templates/edit/text-options.html
|
||||
msgid "Use"
|
||||
msgstr " "
|
||||
msgstr "使用:"
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Show advanced help and tips"
|
||||
@@ -2890,7 +2871,7 @@ msgstr "(または"
|
||||
|
||||
# 訳注: "[service] only supports a maximum [2,000 characters] of notification text, including the title."
|
||||
# → 「[service] がサポートする通知テキストは最大 [2,000文字] です(タイトルを含む)。」
|
||||
# 助詞を補い、目的語と述語の訳を入れ替えた
|
||||
# "only supports a maximum" に主語の助詞「が」を付けて文を成立させた
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "only supports a maximum"
|
||||
msgstr "がサポートする通知テキストは最大"
|
||||
@@ -2929,11 +2910,10 @@ msgstr "詳細なヘルプはこちら"
|
||||
|
||||
# 訳注: "Accepts the {{token}} placeholders listed below"
|
||||
# → 「{{token}} 以下のプレースホルダーを受け付けます」
|
||||
# 述語の訳を後半にまとめた
|
||||
# 前半は日本語では不要なため、空白1文字で非表示にする(空文字は英語にフォールバックするため)
|
||||
# 前半の断片に訳を置くと語順が崩れるため、後半にまとめた
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "Accepts the"
|
||||
msgstr " "
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "placeholders listed below"
|
||||
@@ -3033,7 +3013,7 @@ msgstr "playwright 環境変数を有効にする"
|
||||
|
||||
# 訳注: "and uncomment the [code] in the [filename] file"
|
||||
# → 「そして [code] のコメントを [filename] ファイル内で解除してください」
|
||||
# 目的語と述語の訳を入れ替えた
|
||||
# 3つの断片に訳を分散させて自然な語順にした
|
||||
#: changedetectionio/templates/_helpers.html
|
||||
msgid "and uncomment the"
|
||||
msgstr "そして"
|
||||
@@ -3138,11 +3118,9 @@ msgstr "検索"
|
||||
msgid "URL or Title"
|
||||
msgstr "URLまたはタイトル"
|
||||
|
||||
# 訳注: "URL or Title in 'タグ名'" → 「URLまたはタイトル - 'タグ名'」
|
||||
# 前置詞 "in" を区切り文字に置き換えた
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "in"
|
||||
msgstr "-"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Enter search term..."
|
||||
@@ -3196,26 +3174,23 @@ msgstr "ここのすべての行が存在しない必要があります(各行
|
||||
|
||||
#: changedetectionio/templates/edit/text-options.html
|
||||
msgid "Keep only lines that contain any of these words or phrases (plain text, case-insensitive)"
|
||||
msgstr "これらの単語やフレーズのいずれかを含む行のみを保持(プレーンテキスト、大文字小文字区別なし)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/edit/text-options.html
|
||||
msgid "One entry per line — any line in the page text that contains a match is kept"
|
||||
msgstr "1行に1エントリ — ページテキスト内でマッチを含む行が保持されます"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/edit/text-options.html
|
||||
msgid "Simpler alternative to regex — use this when you just want lines about a specific topic"
|
||||
msgstr "正規表現のより簡単な代替手段 — 特定のトピックに関する行だけが必要な場合に使用"
|
||||
msgstr ""
|
||||
|
||||
# 訳注: "Example: enter [celsius] to keep only lines mentioning temperature readings"
|
||||
# → 「例: [celsius] と入力すると、温度に関する行のみが保持されます」
|
||||
# 述語の訳を後半にまとめた
|
||||
#: changedetectionio/templates/edit/text-options.html
|
||||
msgid "Example: enter"
|
||||
msgstr "例:"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/edit/text-options.html
|
||||
msgid "to keep only lines mentioning temperature readings"
|
||||
msgstr "と入力すると、温度に関する行のみが保持されます"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/edit/text-options.html
|
||||
msgid "Extracts text in the final output (line by line) after other filters using regular expressions or string match:"
|
||||
@@ -3231,7 +3206,7 @@ msgstr "行頭の空白を考慮することを忘れないでください"
|
||||
|
||||
#: changedetectionio/templates/edit/text-options.html
|
||||
msgid "type flags (more"
|
||||
msgstr "タイプフラグを使用できます(詳細"
|
||||
msgstr "タイプフラグ(詳細"
|
||||
|
||||
#: changedetectionio/templates/edit/text-options.html
|
||||
msgid "information here"
|
||||
|
||||
@@ -484,8 +484,7 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
|
||||
# Store favicon if necessary
|
||||
if update_handler.fetcher.favicon_blob and update_handler.fetcher.favicon_blob.get('base64'):
|
||||
watch.bump_favicon(url=update_handler.fetcher.favicon_blob.get('url'),
|
||||
favicon_base_64=update_handler.fetcher.favicon_blob.get('base64'),
|
||||
mime_type=update_handler.fetcher.favicon_blob.get('mime_type')
|
||||
favicon_base_64=update_handler.fetcher.favicon_blob.get('base64')
|
||||
)
|
||||
|
||||
datastore.update_watch(uuid=uuid, update_obj=final_updates)
|
||||
|
||||
Reference in New Issue
Block a user