mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2025-12-14 12:06:55 +00:00
WIP
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 -%}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -155,4 +197,36 @@ def collect_fetcher_status_icons(fetcher_name):
|
|||||||
if result and isinstance(result, dict):
|
if result and isinstance(result, dict):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
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
|
return None
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user