From 420dd3144ac1a09ee3be3e2bdb0eb26b19a21e39 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sat, 29 Nov 2025 17:10:20 +0100 Subject: [PATCH] feat: add `NewSearchService` to orchestrate multi-site e-commerce searches and scrape item details. --- app/core/search_config.py | 17 --------- app/services/browserless_service.py | 53 ++++++++++++++++++++++------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/app/core/search_config.py b/app/core/search_config.py index cbec7d6..edda33c 100644 --- a/app/core/search_config.py +++ b/app/core/search_config.py @@ -105,23 +105,6 @@ SITE_CONFIGS = { "search_url": "https://www.auchan.fr/recherche?text={query}", "product_selector": "article a[href]", "wait_selector": "article", - "category": "Grande Surface", - "requires_proxy": False, - }, - "centrakor.com": { - "name": "Centrakor", - "search_url": "https://www.centrakor.com/catalogsearch/result/?q={query}", - "product_selector": ".product-item a.product-item-link, .product-item-info a", - "wait_selector": ".products-grid, .product-items", - "category": "Déco & Maison", - "requires_proxy": False, - "pre_search_selector": "button.storelocator-search__button--go", - }, - "lincroyable.fr": { - "name": "L'Incroyable", - "search_url": "https://www.lincroyable.fr/recherche-query={query}/?", - "product_selector": "a[href^='/p']", - "wait_selector": ".products, .product-miniature", "category": "Déco & Maison", "requires_proxy": False, }, diff --git a/app/services/browserless_service.py b/app/services/browserless_service.py index 9e6baf7..e9b3723 100644 --- a/app/services/browserless_service.py +++ b/app/services/browserless_service.py @@ -162,21 +162,48 @@ class BrowserlessService: # Retry logic could be handled here or by caller, # for now we return what we have, caller decides - # Handle pre-search interaction if configured (e.g. Centrakor store selection) - # We can't easily pass the config here without changing the signature, - # but we can check if the URL matches a site with pre_search_selector + # Handle pre-search interaction from app.core.search_config import SITE_CONFIGS for config in SITE_CONFIGS.values(): - if config.get("pre_search_selector") and config["search_url"].split("/")[2] in url: - try: - selector = config["pre_search_selector"] - logger.info(f"Attempting pre-search interaction: {selector}") - if await page.locator(selector).is_visible(timeout=5000): - await page.click(selector) - logger.info("Clicked pre-search selector") - await asyncio.sleep(2) # Wait for transition - except Exception as e: - logger.warning(f"Pre-search interaction failed: {e}") + if config["search_url"].split("/")[2] in url: + # Handle complex interaction list + if config.get("pre_search_interaction"): + try: + logger.info(f"Executing pre-search interaction sequence for {config['name']}") + for step in config["pre_search_interaction"]: + step_type = step.get("type") + selector = step.get("selector") + + if step_type == "input": + logger.info(f"Inputting text into {selector}") + await page.fill(selector, step["value"]) + + elif step_type == "click": + logger.info(f"Clicking {selector}") + # Use force=True to bypass potential overlays + await page.click(selector, timeout=5000) + + elif step_type == "wait": + seconds = step.get("seconds", 1) + logger.info(f"Waiting {seconds}s") + await asyncio.sleep(seconds) + + logger.info("Pre-search sequence completed") + await asyncio.sleep(2) # Final stabilization wait + except Exception as e: + logger.warning(f"Pre-search sequence failed: {e}") + + # Handle simple selector (legacy support) + elif config.get("pre_search_selector"): + try: + selector = config["pre_search_selector"] + logger.info(f"Attempting pre-search interaction: {selector}") + if await page.locator(selector).is_visible(timeout=5000): + await page.click(selector) + logger.info("Clicked pre-search selector") + await asyncio.sleep(2) # Wait for transition + except Exception as e: + logger.warning(f"Pre-search interaction failed: {e}") break await self.handle_popups(page)