This commit is contained in:
dgtlmoon
2025-11-24 13:17:34 +01:00
parent 652f939d6d
commit 91c2995cd3
4 changed files with 108 additions and 1 deletions

View File

@@ -187,6 +187,10 @@ def main():
logger.critical(str(e)) logger.critical(str(e))
return return
# Inject datastore into plugins that need access to settings
from changedetectionio.pluggy_interface import inject_datastore_into_plugins
inject_datastore_into_plugins(datastore)
if default_url: if default_url:
datastore.add_watch(url = default_url) datastore.add_watch(url = default_url)

View File

@@ -205,7 +205,7 @@ document.addEventListener('DOMContentLoaded', function() {
{%- if watch.get('restock') and watch['restock']['price'] != None -%} {%- if watch.get('restock') and watch['restock']['price'] != None -%}
{%- if watch['restock']['price'] != None -%} {%- if watch['restock']['price'] != None -%}
<span class="restock-label price" title="Price"> <span class="restock-label price" title="Price">
{{ watch['restock']['price']|format_number_locale }} {{ watch['restock']['currency'] }} {{ watch['restock']['price']|format_number_locale if watch['restock'].get('price') else '' }} {{ watch['restock'].get('currency','') }}
</span> </span>
{%- endif -%} {%- endif -%}
{%- elif not watch.has_restock_info -%} {%- elif not watch.has_restock_info -%}

View File

@@ -2,6 +2,7 @@ import pluggy
import os import os
import importlib import importlib
import sys import sys
from loguru import logger
# Global plugin namespace for changedetection.io # Global plugin namespace for changedetection.io
PLUGIN_NAMESPACE = "changedetectionio" PLUGIN_NAMESPACE = "changedetectionio"
@@ -59,6 +60,31 @@ class ChangeDetectionSpec:
""" """
pass pass
@hookspec
def get_itemprop_availability_override(self, content, fetcher_name, fetcher_instance, url):
"""Provide custom implementation of get_itemprop_availability for a specific fetcher.
This hook allows plugins to provide their own product availability detection
when their fetcher is being used. This is called as a fallback when the built-in
method doesn't find good data.
Args:
content: The HTML/text content to parse
fetcher_name: The name of the fetcher being used (e.g., 'html_js_zyte')
fetcher_instance: The fetcher instance that generated the content
url: The URL being watched/checked
Returns:
dict or None: Dictionary with availability data:
{
'price': float or None,
'availability': str or None, # e.g., 'in stock', 'out of stock'
'currency': str or None, # e.g., 'USD', 'EUR'
}
Or None if this plugin doesn't handle this fetcher or couldn't extract data
"""
pass
# Set up Plugin Manager # Set up Plugin Manager
plugin_manager = pluggy.PluginManager(PLUGIN_NAMESPACE) plugin_manager = pluggy.PluginManager(PLUGIN_NAMESPACE)
@@ -99,6 +125,22 @@ load_plugins_from_directories()
# Discover installed plugins from external packages (if any) # Discover installed plugins from external packages (if any)
plugin_manager.load_setuptools_entrypoints(PLUGIN_NAMESPACE) plugin_manager.load_setuptools_entrypoints(PLUGIN_NAMESPACE)
# Function to inject datastore into plugins that need it
def inject_datastore_into_plugins(datastore):
"""Inject the global datastore into plugins that need access to settings.
This should be called after plugins are loaded and datastore is initialized.
Args:
datastore: The global ChangeDetectionStore instance
"""
for plugin_name, plugin_obj in plugin_manager.list_name_plugin():
# Check if plugin has datastore attribute and it's not set
if hasattr(plugin_obj, 'datastore'):
if plugin_obj.datastore is None:
plugin_obj.datastore = datastore
logger.debug(f"Injected datastore into plugin: {plugin_name}")
# Function to register built-in fetchers - called later from content_fetchers/__init__.py # Function to register built-in fetchers - called later from content_fetchers/__init__.py
def register_builtin_fetchers(): def register_builtin_fetchers():
"""Register built-in content fetchers as internal plugins """Register built-in content fetchers as internal plugins
@@ -156,3 +198,35 @@ def collect_fetcher_status_icons(fetcher_name):
return result return result
return None return None
def get_itemprop_availability_from_plugin(content, fetcher_name, fetcher_instance, url):
"""Get itemprop availability data from plugins as a fallback.
This is called when the built-in get_itemprop_availability doesn't find good data.
Args:
content: The HTML/text content to parse
fetcher_name: The name of the fetcher being used (e.g., 'html_js_zyte')
fetcher_instance: The fetcher instance that generated the content
url: The URL being watched (watch.link - includes Jinja2 evaluation)
Returns:
dict or None: Availability data dictionary from first matching plugin, or None
"""
# Get availability data from plugins
results = plugin_manager.hook.get_itemprop_availability_override(
content=content,
fetcher_name=fetcher_name,
fetcher_instance=fetcher_instance,
url=url
)
# Return first non-None result with actual data
if results:
for result in results:
if result and isinstance(result, dict):
# Check if the result has any meaningful data
if result.get('price') is not None or result.get('availability'):
return result
return None

View File

@@ -187,6 +187,8 @@ class perform_site_check(difference_detection_processor):
itemprop_availability = {} itemprop_availability = {}
# Try built-in extraction first, this will scan metadata in the HTML
try: try:
itemprop_availability = get_itemprop_availability(self.fetcher.content) itemprop_availability = get_itemprop_availability(self.fetcher.content)
except MoreThanOnePriceFound as e: except MoreThanOnePriceFound as e:
@@ -198,6 +200,33 @@ class perform_site_check(difference_detection_processor):
xpath_data=self.fetcher.xpath_data xpath_data=self.fetcher.xpath_data
) )
# If built-in extraction didn't get both price AND availability, try plugin override
# Only check plugin if this watch is using a fetcher that might provide better data
has_price = itemprop_availability.get('price') is not None
has_availability = itemprop_availability.get('availability') is not None
# @TODO !!! some setting like "Use as fallback" or "always use", "t
if not (has_price and has_availability) or True:
from changedetectionio.pluggy_interface import get_itemprop_availability_from_plugin
fetcher_name = watch.get('fetch_backend', 'html_requests')
# Only try plugin override if not using system default (which might be anything)
if fetcher_name and fetcher_name != 'system':
logger.debug("Calling extra plugins for getting item price/availability")
plugin_availability = get_itemprop_availability_from_plugin(self.fetcher.content, fetcher_name, self.fetcher, watch.link)
if plugin_availability:
# Plugin provided better data, use it
plugin_has_price = plugin_availability.get('price') is not None
plugin_has_availability = plugin_availability.get('availability') is not None
# Only use plugin data if it's actually better than what we have
if plugin_has_price or plugin_has_availability:
itemprop_availability = plugin_availability
logger.info(f"Using plugin-provided availability data for fetcher '{fetcher_name}' (built-in had price={has_price}, availability={has_availability}; plugin has price={plugin_has_price}, availability={plugin_has_availability})")
if not plugin_availability:
logger.debug("No item price/availability from plugins")
# Something valid in get_itemprop_availability() by scraping metadata ? # Something valid in get_itemprop_availability() by scraping metadata ?
if itemprop_availability.get('price') or itemprop_availability.get('availability'): if itemprop_availability.get('price') or itemprop_availability.get('availability'):
# Store for other usage # Store for other usage