Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions changedetectionio/model/Watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@

# Module-level favicon filename cache: data_dir → basename (or None)
# Keyed by data_dir so it survives Watch object recreation, deepcopy, and concurrent requests.
# Invalidated explicitly in bump_favicon() when a new favicon is saved.
# Invalidated explicitly when the favicon is saved or the watch history is cleared.
_FAVICON_FILENAME_CACHE: dict = {}

minimum_seconds_recheck_time = int(os.getenv('MINIMUM_SECONDS_RECHECK_TIME', 3))
Expand Down Expand Up @@ -334,6 +334,8 @@ def clear_watch(self):
continue
os.unlink(item)

_FAVICON_FILENAME_CACHE.pop(self.data_dir, None)

# Force the attr to recalculate
bump = self.history

Expand Down Expand Up @@ -896,7 +898,7 @@ def get_favicon_filename(self) -> str | None:

Uses a module-level cache keyed by data_dir to survive Watch object recreation,
deepcopy (which drops instance attrs), and concurrent request races.
Invalidated by bump_favicon() when a new favicon is saved.
Invalidated when a favicon is saved or the watch history is cleared.

Returns:
str: Basename of the favicon file, or None if not found.
Expand Down
30 changes: 30 additions & 0 deletions changedetectionio/tests/unit/test_watch_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import unittest
import os
import pickle
import tempfile
from copy import deepcopy

from changedetectionio.model import Watch, Tag
Expand Down Expand Up @@ -249,6 +250,35 @@ def test_watch_copy_performance(self):
self.assertLess(elapsed, 0.5,
f"Deepcopy too slow ({elapsed:.3f}s for 10 copies) - might be copying datastore")


class TestFaviconFilenameCache(unittest.TestCase):

def test_clear_watch_invalidates_cached_favicon_filename(self):
from changedetectionio.model.Watch import _FAVICON_FILENAME_CACHE

mock_datastore = {'settings': {'application': {}}, 'watching': {}}

with tempfile.TemporaryDirectory() as datastore_path:
watch = Watch.model(
datastore_path=datastore_path,
__datastore=mock_datastore,
default={'url': 'https://example.com'}
)
watch.ensure_data_dir_exists()
self.addCleanup(_FAVICON_FILENAME_CACHE.pop, watch.data_dir, None)

favicon_path = os.path.join(watch.data_dir, 'favicon.ico')
with open(favicon_path, 'wb') as favicon_file:
favicon_file.write(b'favicon')

self.assertEqual(watch.get_favicon_filename(), 'favicon.ico')

watch.clear_watch()

self.assertFalse(os.path.exists(favicon_path))
self.assertIsNone(watch.get_favicon_filename())


class TestLLMDiffSummaryCache(unittest.TestCase):
"""Tests for get_llm_diff_summary / save_llm_diff_summary — version-pair + prompt-hash caching."""

Expand Down
Loading