diff --git a/changedetectionio/processors/restock_diff/__init__.py b/changedetectionio/processors/restock_diff/__init__.py index 9b659ec9..745864cc 100644 --- a/changedetectionio/processors/restock_diff/__init__.py +++ b/changedetectionio/processors/restock_diff/__init__.py @@ -45,7 +45,7 @@ class Restock(dict): 'in_stock': None, 'price': None, 'currency': None, - 'original_price': None + 'last_price': None # Price recorded at the most recent check (was misleadingly named 'original_price') } # Initialize the dictionary with default values @@ -59,8 +59,8 @@ class Restock(dict): raise ValueError("Only one positional argument of type 'dict' is allowed") def __setitem__(self, key, value): - # Custom logic to handle setting price and original_price - if key == 'price' or key == 'original_price': + # Custom logic to handle setting price and last_price + if key == 'price' or key == 'last_price': if isinstance(value, str): value = self.parse_currency(raw_value=value) @@ -89,7 +89,8 @@ class Watch(BaseWatch): def extra_notification_token_values(self): values = super().extra_notification_token_values() - values['restock'] = self.get('restock', {}) + # Copy so the derived 'previous_price' token added below doesn't mutate the stored restock object + values['restock'] = dict(self.get('restock', {})) values['restock']['previous_price'] = None if self.history_n >= 2: @@ -109,7 +110,7 @@ class Watch(BaseWatch): values.append(('restock.price', "Price detected")) values.append(('restock.in_stock', "In stock status")) - values.append(('restock.original_price', "Original price at first check")) + values.append(('restock.last_price', "Price at the previous check")) values.append(('restock.previous_price', "Previous price in history")) return values diff --git a/changedetectionio/processors/restock_diff/forms.py b/changedetectionio/processors/restock_diff/forms.py index 3597ef53..ec16477b 100644 --- a/changedetectionio/processors/restock_diff/forms.py +++ b/changedetectionio/processors/restock_diff/forms.py @@ -22,7 +22,7 @@ class RestockSettingsForm(Form): render_kw={"placeholder": _l("No limit"), "size": "10"}) price_change_max = FloatField(_l('Above price to trigger notification'), [validators.Optional()], render_kw={"placeholder": _l("No limit"), "size": "10"}) - price_change_threshold_percent = FloatField(_l('Threshold in %% for price changes since the original price'), validators=[ + price_change_threshold_percent = FloatField(_l('Threshold (%) for price changes since the previous check'), validators=[ validators.Optional(), validators.NumberRange(min=0, max=100, message=_l("Should be between 0 and 100")), @@ -73,8 +73,8 @@ class processor_settings_form(processor_text_json_diff_form):
diff --git a/changedetectionio/processors/restock_diff/processor.py b/changedetectionio/processors/restock_diff/processor.py index ecf6b132..321f1c8c 100644 --- a/changedetectionio/processors/restock_diff/processor.py +++ b/changedetectionio/processors/restock_diff/processor.py @@ -564,10 +564,15 @@ class perform_site_check(difference_detection_processor): # Main detection method fetched_md5 = None - # store original price if not set - if itemprop_availability and itemprop_availability.get('price') and not itemprop_availability.get('original_price'): - itemprop_availability['original_price'] = itemprop_availability.get('price') - update_obj['restock']["original_price"] = itemprop_availability.get('price') + # Record this check's price as 'last_price'. The freshly scraped itemprop never carries + # last_price, so this is (re)set on every check - i.e. last_price always holds the price + # from the most recent check, and at comparison time below it is the PREVIOUS check's price. + if itemprop_availability and itemprop_availability.get('price') and not itemprop_availability.get('last_price'): + itemprop_availability['last_price'] = itemprop_availability.get('price') + update_obj['restock']["last_price"] = itemprop_availability.get('price') + logger.debug( + f"{watch.get('uuid')} Updating price - setting 'last_price' to '{itemprop_availability.get('price')}' " + f"(previously stored 'last_price' was '{(watch.get('restock') or {}).get('last_price')}'). ") if not self.fetcher.instock_data and not itemprop_availability.get('availability') and not itemprop_availability.get('price'): raise ProcessorException( @@ -617,9 +622,13 @@ class perform_site_check(difference_detection_processor): if restock_settings.get('follow_price_changes') and watch.get('restock') and update_obj.get('restock') and update_obj['restock'].get('price'): price = float(update_obj['restock'].get('price')) - # Default to current price if no previous price found - if watch['restock'].get('original_price'): - previous_price = float(watch['restock'].get('original_price')) + # Compare against last_price (the price from the previous check) + if watch['restock'].get('last_price'): + previous_price = float(watch['restock'].get('last_price')) + logger.debug( + f"{watch.get('uuid')} Comparing NEW price '{price}' against stored 'last_price' '{previous_price}' " + f"(watch's stored current price was '{(watch.get('restock') or {}).get('price')}') -> " + f"price {'CHANGED' if price != previous_price else 'unchanged'}") # It was different, but negate it further down if price != previous_price: changed_detected = True @@ -642,11 +651,14 @@ class perform_site_check(difference_detection_processor): else: logger.trace(f"{watch.get('uuid')} {price} is between {min_limit} and {max_limit}, continuing normal comparison") - # Price comparison by % - if watch['restock'].get('original_price') and changed_detected and restock_settings.get('price_change_threshold_percent'): - previous_price = float(watch['restock'].get('original_price')) + # Price comparison by % - against last_price (the previous check's price) + if watch['restock'].get('last_price') and changed_detected and restock_settings.get('price_change_threshold_percent'): + previous_price = float(watch['restock'].get('last_price')) pc = float(restock_settings.get('price_change_threshold_percent')) change = abs((price - previous_price) / previous_price * 100) + logger.debug( + f"{watch.get('uuid')} % threshold check - comparing NEW price '{price}' against stored " + f"'last_price' '{previous_price}' = {change:.3f}% change (threshold {pc}%)") if change and change <= pc: logger.debug(f"{watch.get('uuid')} Override change-detected to FALSE because % threshold ({pc}%) was {change:.3f}%") changed_detected = False diff --git a/changedetectionio/store/updates.py b/changedetectionio/store/updates.py index f81c4967..5cd9b006 100644 --- a/changedetectionio/store/updates.py +++ b/changedetectionio/store/updates.py @@ -825,3 +825,25 @@ class DatastoreUpdatesMixin: self.data['settings']['application']['llm'] = llm logger.info("update_32: cleaned up obsolete max_tokens_per_check / renamed max_tokens_cumulative") + def update_33(self): + """Rename restock 'original_price' -> 'last_price'. + + The field was named 'original_price' but never held the first-seen price: it was + re-stamped with the current price on every check (the freshly scraped itemprop never + carries it, so the "set if not present" guard was always true). So it always held the + price from the most recent check - i.e. the previous check's price at comparison time. + Renamed so the stored field name matches what it actually contains. Idempotent. + """ + migrated = 0 + for uuid, watch in self.data['watching'].items(): + restock = watch.get('restock') + if isinstance(restock, dict) and 'original_price' in restock: + # last_price may already exist as the model default (None) after rehydration, so + # only copy the old value across when last_price is still empty; then drop the old key. + if not restock.get('last_price'): + restock['last_price'] = restock.get('original_price') + del restock['original_price'] + migrated += 1 + if migrated: + logger.info(f"update_33: renamed restock.original_price -> restock.last_price on {migrated} watch(es)") + diff --git a/changedetectionio/tests/test_restock_itemprop.py b/changedetectionio/tests/test_restock_itemprop.py index d1b260b1..3c16533b 100644 --- a/changedetectionio/tests/test_restock_itemprop.py +++ b/changedetectionio/tests/test_restock_itemprop.py @@ -298,6 +298,27 @@ def test_itemprop_percent_threshold(client, live_server, measure_memory_usage, d assert b'1,950.45' in res.data or b'1950.45' in res.data #depending on locale assert b'has-unread-changes' not in res.data + # PROOF that the threshold is measured "since the PREVIOUS check" and NOT "since the first check": + # a slow upward creep where every single step is below the 5% threshold versus the *previous* + # check, but the total drift from where the creep started (1950.45) ends up ABOVE 5%. + # 1950.45 -> 2000.00 = +2.54% vs previous (below 5%) + # 2000.00 -> 2050.00 = +2.50% vs previous (below 5%) + # 2050.00 -> 2100.00 = +2.44% vs previous (below 5%) + # 1950.45 -> 2100.00 = +7.67% in total (ABOVE 5%) + # Under "since previous check" NONE of these trigger (each step is sub-threshold). + # Under "since first check" the accumulated drift would cross 5% and trigger here - so the + # final assertion below would fail. We deliberately never mark_all_viewed during the creep, + # so any single trigger would leave has-unread-changes set. + for creep_price in ['2000.00', '2050.00', '2100.00']: + set_original_response(props_markup=instock_props[0], price=creep_price, datastore_path=datastore_path) + client.get(url_for("ui.form_watch_checknow")) + wait_for_all_checks(client) + + res = client.get(url_for("watchlist.index")) + assert b'2,100.00' in res.data or b'2100.00' in res.data #depending on locale + # +7.67% total drift since the creep started, yet still unread-free -> comparison is vs PREVIOUS check + assert b'has-unread-changes' not in res.data + # Re #2600 - Switch the mode to normal type and back, and see if the values stick.. ################################################################################### @@ -349,9 +370,9 @@ def test_change_with_notification_values(client, live_server, measure_memory_usa # Should see new tokens register res = client.get(url_for("settings.settings_page")) - assert b'{{restock.original_price}}' in res.data + assert b'{{restock.last_price}}' in res.data assert b'{{restock.previous_price}}' in res.data - assert b'Original price at first check' in res.data + assert b'Price at the previous check' in res.data ##################### # Set this up for when we remove the notification from the watch, it should fallback with these details diff --git a/changedetectionio/translations/UK-translations.txt b/changedetectionio/translations/UK-translations.txt index 6071c554..221ca0d3 100644 --- a/changedetectionio/translations/UK-translations.txt +++ b/changedetectionio/translations/UK-translations.txt @@ -2436,7 +2436,7 @@ msgstr "Ціна вище для спрацювання сповіщення" #: changedetectionio/processors/restock_diff/forms.py #, python-format -msgid "Threshold in %% for price changes since the original price" +msgid "Threshold in % for price changes since the original price" msgstr "Поріг у %% для зміни ціни від початкової" #: changedetectionio/processors/restock_diff/forms.py diff --git a/changedetectionio/translations/cs/LC_MESSAGES/messages.mo b/changedetectionio/translations/cs/LC_MESSAGES/messages.mo index 157c21d0..733197de 100644 Binary files a/changedetectionio/translations/cs/LC_MESSAGES/messages.mo and b/changedetectionio/translations/cs/LC_MESSAGES/messages.mo differ diff --git a/changedetectionio/translations/cs/LC_MESSAGES/messages.po b/changedetectionio/translations/cs/LC_MESSAGES/messages.po index aa73dc55..c8e267c2 100644 --- a/changedetectionio/translations/cs/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/cs/LC_MESSAGES/messages.po @@ -3339,9 +3339,8 @@ msgid "Above price to trigger notification" msgstr "Vyšší cena pro spuštění upozornění" #: changedetectionio/processors/restock_diff/forms.py -#, python-format -msgid "Threshold in %% for price changes since the original price" -msgstr "Prahová hodnota v %% pro změny ceny od původní ceny" +msgid "Threshold (%) for price changes since the previous check" +msgstr "Prahová hodnota (%) pro změny ceny od předchozí kontroly" #: changedetectionio/processors/restock_diff/forms.py msgid "Should be between 0 and 100" diff --git a/changedetectionio/translations/de/LC_MESSAGES/messages.mo b/changedetectionio/translations/de/LC_MESSAGES/messages.mo index 58bd50e6..d462e8be 100644 Binary files a/changedetectionio/translations/de/LC_MESSAGES/messages.mo and b/changedetectionio/translations/de/LC_MESSAGES/messages.mo differ diff --git a/changedetectionio/translations/de/LC_MESSAGES/messages.po b/changedetectionio/translations/de/LC_MESSAGES/messages.po index 7e5f39a1..e231d1cc 100644 --- a/changedetectionio/translations/de/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/de/LC_MESSAGES/messages.po @@ -3391,9 +3391,8 @@ msgid "Above price to trigger notification" msgstr "Über dem Preis, um eine Benachrichtigung auszulösen" #: changedetectionio/processors/restock_diff/forms.py -#, python-format -msgid "Threshold in %% for price changes since the original price" -msgstr "Schwellenwert in %% für Preisänderungen seit dem ursprünglichen Preis" +msgid "Threshold (%) for price changes since the previous check" +msgstr "Schwellenwert (%) für Preisänderungen seit der vorherigen Prüfung" #: changedetectionio/processors/restock_diff/forms.py msgid "Should be between 0 and 100" diff --git a/changedetectionio/translations/en_GB/LC_MESSAGES/messages.po b/changedetectionio/translations/en_GB/LC_MESSAGES/messages.po index cabc108f..10497650 100644 --- a/changedetectionio/translations/en_GB/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/en_GB/LC_MESSAGES/messages.po @@ -3331,8 +3331,7 @@ msgid "Above price to trigger notification" msgstr "" #: changedetectionio/processors/restock_diff/forms.py -#, python-format -msgid "Threshold in %% for price changes since the original price" +msgid "Threshold (%) for price changes since the previous check" msgstr "" #: changedetectionio/processors/restock_diff/forms.py diff --git a/changedetectionio/translations/en_US/LC_MESSAGES/messages.po b/changedetectionio/translations/en_US/LC_MESSAGES/messages.po index b0e37da1..85b8cd50 100644 --- a/changedetectionio/translations/en_US/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/en_US/LC_MESSAGES/messages.po @@ -3331,8 +3331,7 @@ msgid "Above price to trigger notification" msgstr "" #: changedetectionio/processors/restock_diff/forms.py -#, python-format -msgid "Threshold in %% for price changes since the original price" +msgid "Threshold (%) for price changes since the previous check" msgstr "" #: changedetectionio/processors/restock_diff/forms.py diff --git a/changedetectionio/translations/es/LC_MESSAGES/messages.mo b/changedetectionio/translations/es/LC_MESSAGES/messages.mo index ee0d37c8..5d8e640c 100644 Binary files a/changedetectionio/translations/es/LC_MESSAGES/messages.mo and b/changedetectionio/translations/es/LC_MESSAGES/messages.mo differ diff --git a/changedetectionio/translations/es/LC_MESSAGES/messages.po b/changedetectionio/translations/es/LC_MESSAGES/messages.po index 119ba06f..a2cdcda5 100644 --- a/changedetectionio/translations/es/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/es/LC_MESSAGES/messages.po @@ -3404,9 +3404,8 @@ msgid "Above price to trigger notification" msgstr "Precio superior para activar la notificación" #: changedetectionio/processors/restock_diff/forms.py -#, python-format -msgid "Threshold in %% for price changes since the original price" -msgstr "Umbral en %% fo cambios de precio desde el precio original" +msgid "Threshold (%) for price changes since the previous check" +msgstr "Umbral (%) para cambios de precio desde la comprobación anterior" #: changedetectionio/processors/restock_diff/forms.py msgid "Should be between 0 and 100" diff --git a/changedetectionio/translations/fr/LC_MESSAGES/messages.mo b/changedetectionio/translations/fr/LC_MESSAGES/messages.mo index 72799afc..ad14b966 100644 Binary files a/changedetectionio/translations/fr/LC_MESSAGES/messages.mo and b/changedetectionio/translations/fr/LC_MESSAGES/messages.mo differ diff --git a/changedetectionio/translations/fr/LC_MESSAGES/messages.po b/changedetectionio/translations/fr/LC_MESSAGES/messages.po index d6443725..35393a15 100644 --- a/changedetectionio/translations/fr/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/fr/LC_MESSAGES/messages.po @@ -3344,9 +3344,8 @@ msgid "Above price to trigger notification" msgstr "Au-dessus du prix pour déclencher une notification" #: changedetectionio/processors/restock_diff/forms.py -#, python-format -msgid "Threshold in %% for price changes since the original price" -msgstr "Seuil en %% pour les changements de prix depuis le prix initial" +msgid "Threshold (%) for price changes since the previous check" +msgstr "Seuil (%) pour les changements de prix depuis la vérification précédente" #: changedetectionio/processors/restock_diff/forms.py msgid "Should be between 0 and 100" diff --git a/changedetectionio/translations/it/LC_MESSAGES/messages.mo b/changedetectionio/translations/it/LC_MESSAGES/messages.mo index b85b7493..313ec459 100644 Binary files a/changedetectionio/translations/it/LC_MESSAGES/messages.mo and b/changedetectionio/translations/it/LC_MESSAGES/messages.mo differ diff --git a/changedetectionio/translations/it/LC_MESSAGES/messages.po b/changedetectionio/translations/it/LC_MESSAGES/messages.po index 006fd328..00e2d4d3 100644 --- a/changedetectionio/translations/it/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/it/LC_MESSAGES/messages.po @@ -3333,9 +3333,8 @@ msgid "Above price to trigger notification" msgstr "Prezzo massimo per attivare notifica" #: changedetectionio/processors/restock_diff/forms.py -#, python-format -msgid "Threshold in %% for price changes since the original price" -msgstr "Soglia in %% per modifiche prezzo dal prezzo originale" +msgid "Threshold (%) for price changes since the previous check" +msgstr "Soglia (%) per le variazioni di prezzo dal controllo precedente" #: changedetectionio/processors/restock_diff/forms.py msgid "Should be between 0 and 100" diff --git a/changedetectionio/translations/ja/LC_MESSAGES/messages.mo b/changedetectionio/translations/ja/LC_MESSAGES/messages.mo index 31ab7564..2445dce9 100644 Binary files a/changedetectionio/translations/ja/LC_MESSAGES/messages.mo and b/changedetectionio/translations/ja/LC_MESSAGES/messages.mo differ diff --git a/changedetectionio/translations/ja/LC_MESSAGES/messages.po b/changedetectionio/translations/ja/LC_MESSAGES/messages.po index 2d4095a3..56557a51 100644 --- a/changedetectionio/translations/ja/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/ja/LC_MESSAGES/messages.po @@ -3350,9 +3350,8 @@ msgid "Above price to trigger notification" msgstr "通知をトリガーする上限価格" #: changedetectionio/processors/restock_diff/forms.py -#, python-format -msgid "Threshold in %% for price changes since the original price" -msgstr "元の価格からの価格変動率のしきい値(%%)" +msgid "Threshold (%) for price changes since the previous check" +msgstr "前回のチェックからの価格変動率のしきい値(%)" #: changedetectionio/processors/restock_diff/forms.py msgid "Should be between 0 and 100" diff --git a/changedetectionio/translations/ko/LC_MESSAGES/messages.mo b/changedetectionio/translations/ko/LC_MESSAGES/messages.mo index 5f8fe1a9..d758a8ee 100644 Binary files a/changedetectionio/translations/ko/LC_MESSAGES/messages.mo and b/changedetectionio/translations/ko/LC_MESSAGES/messages.mo differ diff --git a/changedetectionio/translations/ko/LC_MESSAGES/messages.po b/changedetectionio/translations/ko/LC_MESSAGES/messages.po index 5b71946c..aa92d5bf 100644 --- a/changedetectionio/translations/ko/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/ko/LC_MESSAGES/messages.po @@ -3341,9 +3341,8 @@ msgid "Above price to trigger notification" msgstr "다음 가격 초과이면 알림 트리거" #: changedetectionio/processors/restock_diff/forms.py -#, python-format -msgid "Threshold in %% for price changes since the original price" -msgstr "원래 가격 대비 가격 변동 기준(%%)" +msgid "Threshold (%) for price changes since the previous check" +msgstr "이전 확인 대비 가격 변동 기준(%)" #: changedetectionio/processors/restock_diff/forms.py msgid "Should be between 0 and 100" diff --git a/changedetectionio/translations/messages.pot b/changedetectionio/translations/messages.pot index 33f8f849..8f02fcae 100644 --- a/changedetectionio/translations/messages.pot +++ b/changedetectionio/translations/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: changedetection.io 0.55.7\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-06-03 12:07+0200\n" +"POT-Creation-Date: 2026-06-18 11:28+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME