From 8329eb28a3810915f905011852fd4c49f64e7ec8 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Tue, 13 Jan 2026 13:37:54 +0100 Subject: [PATCH] Fix: Action.com availability false positives and Amazon login detection optimization. Cleaned up diagnostic scripts. --- amazon.fr_dump.html | 19490 ------------- analyze_bm_prices.py | 82 - analyze_bm_v2.py | 50 - analyze_carrefour_prices.py | 59 - analyze_carrefour_product.py | 70 - analyze_centrakor.py | 69 - analyze_centrakor_detailed.py | 58 - analyze_gifi.py | 24 - analyze_gifi_price.py | 69 - analyze_gifi_search_prices.py | 71 - analyze_missing_sites.py | 93 - app/services/scheduler_service.py | 13 +- app/services/tracking_scraper_service.py | 143 +- check_bs4.py | 2 - check_import.py | 12 - check_method.py | 10 - check_proxies.py | 52 - check_syntax.py | 7 - deep_centrakor_analysis.py | 56 - diagnose_selectors.py | 71 - dump_gifi.py | 56 - dump_gifi_v2.py | 76 - dump_html.py | 88 - filter_proxies.py | 103 - find_working_proxies.py | 118 - gifi_dump.html | 0 gifi_full.html | 18821 ------------ inspect_carrefour.py | 33 - inspect_lafoirfouille.py | 53 - inspect_lafoirfouille_images.py | 25 - inspect_lincroyable.py | 33 - inspect_local_playwright.py | 47 - inspect_sites.py | 47 - lincroyable_dump.html | 1876 -- lincroyable_local.png | Bin 48779 -> 0 bytes lincroyable_local_dump.html | 1600 - list_missing_sites.py | 62 - reproduce_gifi_scrape.py | 67 - reset_admin.py | 31 - run_cleanup.py | 53 - stokomani.fr_dump.html | 18100 ------------ stokomani.fr_failed_verification.html | 18100 ------------ stokomani_raw.html | 32156 --------------------- task.md | 26 +- test_amazon_scraper.py | 187 - test_carrefour_api.py | 42 - test_carrefour_prices.py | 40 - test_carrefour_search.py | 26 - test_centrakor_filter.py | 69 - test_centrakor_images.py | 42 - test_gifi_final.py | 34 - test_gifi_prices.py | 38 - test_gifi_prices_final.py | 41 - test_gifi_search.py | 26 - test_soup.py | 29 - verify_catalog_fix.py | 43 - verify_search_price.py | 45 - verify_standalone.py | 108 - 58 files changed, 100 insertions(+), 112742 deletions(-) delete mode 100644 amazon.fr_dump.html delete mode 100644 analyze_bm_prices.py delete mode 100644 analyze_bm_v2.py delete mode 100644 analyze_carrefour_prices.py delete mode 100644 analyze_carrefour_product.py delete mode 100644 analyze_centrakor.py delete mode 100644 analyze_centrakor_detailed.py delete mode 100644 analyze_gifi.py delete mode 100644 analyze_gifi_price.py delete mode 100644 analyze_gifi_search_prices.py delete mode 100644 analyze_missing_sites.py delete mode 100644 check_bs4.py delete mode 100644 check_import.py delete mode 100644 check_method.py delete mode 100644 check_proxies.py delete mode 100644 check_syntax.py delete mode 100644 deep_centrakor_analysis.py delete mode 100644 diagnose_selectors.py delete mode 100644 dump_gifi.py delete mode 100644 dump_gifi_v2.py delete mode 100644 dump_html.py delete mode 100644 filter_proxies.py delete mode 100644 find_working_proxies.py delete mode 100644 gifi_dump.html delete mode 100644 gifi_full.html delete mode 100644 inspect_carrefour.py delete mode 100644 inspect_lafoirfouille.py delete mode 100644 inspect_lafoirfouille_images.py delete mode 100644 inspect_lincroyable.py delete mode 100644 inspect_local_playwright.py delete mode 100644 inspect_sites.py delete mode 100644 lincroyable_dump.html delete mode 100644 lincroyable_local.png delete mode 100644 lincroyable_local_dump.html delete mode 100644 list_missing_sites.py delete mode 100644 reproduce_gifi_scrape.py delete mode 100644 reset_admin.py delete mode 100644 run_cleanup.py delete mode 100644 stokomani.fr_dump.html delete mode 100644 stokomani.fr_failed_verification.html delete mode 100644 stokomani_raw.html delete mode 100755 test_amazon_scraper.py delete mode 100644 test_carrefour_api.py delete mode 100644 test_carrefour_prices.py delete mode 100644 test_carrefour_search.py delete mode 100644 test_centrakor_filter.py delete mode 100644 test_centrakor_images.py delete mode 100644 test_gifi_final.py delete mode 100644 test_gifi_prices.py delete mode 100644 test_gifi_prices_final.py delete mode 100644 test_gifi_search.py delete mode 100644 test_soup.py delete mode 100644 verify_catalog_fix.py delete mode 100644 verify_search_price.py delete mode 100644 verify_standalone.py diff --git a/amazon.fr_dump.html b/amazon.fr_dump.html deleted file mode 100644 index dab68f3..0000000 --- a/amazon.fr_dump.html +++ /dev/null @@ -1,19490 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Amazon.fr : chaise - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
\ No newline at end of file diff --git a/analyze_bm_prices.py b/analyze_bm_prices.py deleted file mode 100644 index e1778ce..0000000 --- a/analyze_bm_prices.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -Analyze B&M product page for price extraction -""" -import asyncio -import sys -import re -sys.path.insert(0, '/app') - -from playwright.async_api import async_playwright -from bs4 import BeautifulSoup - -async def main(): - playwright = await async_playwright().start() - browser = await playwright.chromium.connect_over_cdp("ws://browserless:3000") - - context = await browser.new_context( - viewport={"width": 1920, "height": 1080}, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - ) - - page = await context.new_page() - - # URL from the screenshot - url = "https://www.bmstores.fr/products/chaise-haute-pliante-bois-492966" - print(f"Loading: {url}\n") - await page.goto(url, wait_until="networkidle") - - content = await page.content() - soup = BeautifulSoup(content, "html.parser") - - # Find all elements with € symbol - price_els = soup.find_all(string=re.compile('€')) - print(f"Found {len(price_els)} elements with '€'\n") - - prices_found = {} - for el in price_els[:30]: - text = el.strip() - if text and len(text) < 100: - parent = el.find_parent() - if parent: - parent_class = parent.get('class', []) - parent_class_str = ' '.join(parent_class) if isinstance(parent_class, list) else str(parent_class) - - # Extract price value - price_match = re.search(r'(\d+[.,]\d+)\s*€', text) - if price_match: - price_val = price_match.group(1) - key = f"{price_val}€ in .{parent_class_str[:50]}" - if key not in prices_found: - prices_found[key] = text - - print("Prices found:") - for key, text in prices_found.items(): - print(f" {key}: '{text}'") - - # Try common price selectors - print("\nTrying specific selectors:") - selectors = [ - ".price", - "[class*='price']", - "[data-price]", - ".product-price", - "span[class*='price']" - ] - - for selector in selectors: - try: - els = soup.select(selector) - if els: - for el in els[:2]: - text = el.get_text(strip=True) - if '€' in text: - print(f" {selector}: {text}") - except: - pass - - await context.close() - await browser.close() - await playwright.stop() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/analyze_bm_v2.py b/analyze_bm_v2.py deleted file mode 100644 index a7d9f74..0000000 --- a/analyze_bm_v2.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Analyze B&M with longer wait and playwright evaluation -""" -import asyncio -import sys -sys.path.insert(0, '/app') - -from playwright.async_api import async_playwright - -async def main(): - playwright = await async_playwright().start() - browser = await playwright.chromium.connect_over_cdp("ws://browserless:3000") - - context = await browser.new_context( - viewport={"width": 1920, "height": 1080}, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - ) - - page = await context.new_page() - - url = "https://www.bmstores.fr/products/chaise-haute-pliante-bois-492966" - print(f"Loading: {url}\n") - await page.goto(url, wait_until="networkidle") - - # Wait extra time for JS - await page.wait_for_timeout(5000) - - # Look for elements containing price - print("Searching for price elements...\n") - - # Try to find any text with € - price_els = await page.query_selector_all("*:has-text('€')") - print(f"Found {len(price_els)} elements with €\n") - - for i, el in enumerate(price_els[:10]): - text = await el.inner_text() - tag = await el.evaluate("el => el.tagName") - classes = await el.evaluate("el => el.className") - print(f"{i+1}. <{tag} class='{classes}'> {text[:100]}") - - # Screenshot for debugging - await page.screenshot(path="/app/debug_dumps/bm_screenshot.png", full_page=True) - print("\nScreenshot saved to /app/debug_dumps/bm_screenshot.png") - - await context.close() - await browser.close() - await playwright.stop() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/analyze_carrefour_prices.py b/analyze_carrefour_prices.py deleted file mode 100644 index c7aa92d..0000000 --- a/analyze_carrefour_prices.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Analyze Carrefour price extraction -""" -import asyncio -import sys -sys.path.insert(0, '/app') - -from playwright.async_api import async_playwright -from bs4 import BeautifulSoup - -async def main(): - playwright = await async_playwright().start() - browser = await playwright.chromium.connect_over_cdp("ws://browserless:3000") - - context = await browser.new_context( - viewport={"width": 1920, "height": 1080}, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - ) - - page = await context.new_page() - - print("Loading Carrefour search...") - await page.goto("https://www.carrefour.fr/s?q=chaise", wait_until="networkidle") - - content = await page.content() - soup = BeautifulSoup(content, "html.parser") - - products = soup.select("article.product-list-card-plp-grid-new") - print(f"Found {len(products)} products\n") - - for i, product in enumerate(products[:3]): - print(f"=== Product {i+1} ===") - - # Title - title_el = product.select_one("h3, h2, a") - title = title_el.get_text(strip=True) if title_el else "N/A" - print(f"Title: {title[:60]}") - - # Find all text with € symbol - import re - product_html = str(product) - prices = re.findall(r'(\d+[.,]\d+)\s*€', product_html) - print(f"Prices found in HTML: {prices}") - - # Look for price elements - price_els = product.find_all(string=re.compile('€')) - if price_els: - print(f"Elements with €:") - for el in price_els[:3]: - print(f" - {el.strip()[:50]}") - - print() - - await context.close() - await browser.close() - await playwright.stop() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/analyze_carrefour_product.py b/analyze_carrefour_product.py deleted file mode 100644 index 96d18b3..0000000 --- a/analyze_carrefour_product.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Analyze a Carrefour product page for price selectors -""" -import asyncio -import sys -sys.path.insert(0, '/app') - -from playwright.async_api import async_playwright -from bs4 import BeautifulSoup -import re - -async def main(): - playwright = await async_playwright().start() - browser = await playwright.chromium.connect_over_cdp("ws://browserless:3000") - - context = await browser.new_context( - viewport={"width": 1920, "height": 1080}, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - ) - - page = await context.new_page() - - # Visit a Carrefour product page (from the screenshot) - url = "https://www.carrefour.fr/p/chaise-pliante-44x45-7x79-cm-gris-carrefour-home-3245390032010" - print(f"Loading: {url}") - await page.goto(url, wait_until="networkidle") - - content = await page.content() - soup = BeautifulSoup(content, "html.parser") - - # Find all elements with € symbol - price_els = soup.find_all(string=re.compile('€')) - print(f"\nFound {len(price_els)} elements with '€'") - - prices_found = set() - for el in price_els[:20]: - text = el.strip() - if text and len(text) < 50: - prices_found.add(text) - parent = el.find_parent() - print(f" '{text}' in <{parent.name} class='{parent.get('class')}'>") - - # Try common price selectors - selectors = [ - ".product-price", - "[class*='price']", - ".price", - "span.price", - "div.price", - "[data-price]" - ] - - print("\nTrying specific selectors:") - for selector in selectors: - try: - els = soup.select(selector) - if els: - for el in els[:2]: - text = el.get_text(strip=True) - if '€' in text: - print(f" {selector}: {text}") - except: - pass - - await context.close() - await browser.close() - await playwright.stop() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/analyze_centrakor.py b/analyze_centrakor.py deleted file mode 100644 index ea34efb..0000000 --- a/analyze_centrakor.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Analyze Centrakor HTML structure for image selectors -""" -import asyncio -import sys -import re -sys.path.insert(0, '/app') - -from playwright.async_api import async_playwright -from bs4 import BeautifulSoup - -async def main(): - print("Connecting to browserless...") - playwright = await async_playwright().start() - browser = await playwright.chromium.connect_over_cdp("ws://browserless:3000") - - context = await browser.new_context( - viewport={"width": 1920, "height": 1080}, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - ) - - page = await context.new_page() - - print("Loading Centrakor search...") - await page.goto("https://www.centrakor.com/search/chaise", wait_until="networkidle") - - content = await page.content() - - soup = BeautifulSoup(content, "html.parser") - - # Try to find product containers - selectors = [ - "div.product-item", - "div.product-card", - "article", - "div[class*='product']", - "li[class*='product']" - ] - - for selector in selectors: - products = soup.select(selector) - if products: - print(f"\n✓ Found {len(products)} products with selector: {selector}") - - # Analyze first product - first = products[0] - print(f"\nFirst product HTML snippet:") - print(str(first)[:500]) - print("\n...") - - # Find all images - images = first.find_all('img') - print(f"\nFound {len(images)} images in first product:") - for i, img in enumerate(images): - print(f"\n Image {i+1}:") - print(f" Class: {img.get('class')}") - print(f" Src: {img.get('src', '')[:80]}") - print(f" Data-src: {img.get('data-src', '')[:80]}") - print(f" Alt: {img.get('alt', '')[:50]}") - - break - - await context.close() - await browser.close() - await playwright.stop() - print("\nDone") - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/analyze_centrakor_detailed.py b/analyze_centrakor_detailed.py deleted file mode 100644 index 0d205b5..0000000 --- a/analyze_centrakor_detailed.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -Detailed analysis of Centrakor image structure -""" -import asyncio -import sys -sys.path.insert(0, '/app') - -from playwright.async_api import async_playwright -from bs4 import BeautifulSoup - -async def main(): - playwright = await async_playwright().start() - browser = await playwright.chromium.connect_over_cdp("ws://browserless:3000") - - context = await browser.new_context( - viewport={"width": 1920, "height": 1080}, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - ) - - page = await context.new_page() - - await page.goto("https://www.centrakor.com/search/chaise", wait_until="networkidle") - content = await page.content() - - soup = BeautifulSoup(content, "html.parser") - products = soup.select("div.product-item") - - print(f"Analyzing {min(5, len(products))} products:\n") - - for i, product in enumerate(products[:5]): - print(f"=== Product {i+1} ===") - - # Title - title_el = product.select_one("a.product-item__name") - title = title_el.get_text(strip=True) if title_el else "N/A" - print(f"Title: {title}") - - # All images - images = product.find_all('img') - print(f"Found {len(images)} img tags") - - for j, img in enumerate(images): - print(f"\n Image {j+1}:") - print(f" tag: {img.name}") - print(f" class: {img.get('class')}") - for attr in ['src', 'data-src', 'data-lazy-src', 'srcset', 'data-srcset']: - val = img.get(attr) - if val: - print(f" {attr}: {val[:80]}") - - print() - - await context.close() - await browser.close() - await playwright.stop() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/analyze_gifi.py b/analyze_gifi.py deleted file mode 100644 index 64bf2b0..0000000 --- a/analyze_gifi.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -Analyze Gifi HTML to find correct selectors -""" -with open("/app/debug_dumps/gifi_full.html", "r", encoding="utf-8") as f: - html = f.read() - -# Find product-related divs -import re -matches = re.findall(r']*class="[^"]*"[^>]*>.*?', html[:50000], re.DOTALL) - -print(f"Total HTML size: {len(html)} bytes") - -# Search for price patterns -price_patterns = re.findall(r'50[.,]00\s*€', html[:50000]) -print(f"\nFound {len(price_patterns)} instances of '50,00 €'") - -# Find all class names containing specific keywords -for keyword in ['product', 'article', 'item', 'card']: - classes = re.findall(rf'class="([^"]*{keyword}[^"]*)"', html[:100000], re.IGNORECASE) - unique_classes = set(classes) - if unique_classes: - print(f"\nClasses containing '{keyword}':") - for cls in sorted(unique_classes): - print(f" - {cls}") diff --git a/analyze_gifi_price.py b/analyze_gifi_price.py deleted file mode 100644 index 2c9c2a5..0000000 --- a/analyze_gifi_price.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Analyze a Gifi product page to understand price structure -""" -import asyncio -import sys -sys.path.insert(0, '/app') - -from playwright.async_api import async_playwright - -async def main(): - print("Connecting to browserless...") - playwright = await async_playwright().start() - browser = await playwright.chromium.connect_over_cdp("ws://browserless:3000") - - context = await browser.new_context( - viewport={"width": 1920, "height": 1080}, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - ) - - page = await context.new_page() - - # Visit a Gifi product page - url = "https://www.gifi.fr/meuble-et-deco/linge-de-maison/coussin-plaid-et-tapis/housse-de-chaise-canape-ou-fauteuil/housse-de-chaise-uni-blanc/000000000000410028.html" - print(f"Loading: {url}") - await page.goto(url, wait_until="networkidle") - - # Get page title - title = await page.title() - print(f"Title: {title}") - - # Find all elements with price-like text - price_els = await page.query_selector_all("*:has-text('€')") - print(f"\nFound {len(price_els)} elements with '€'") - - # Get first 10 price elements - for i, el in enumerate(price_els[:10]): - text = await el.inner_text() - tag = await el.evaluate("el => el.tagName") - classes = await el.evaluate("el => el.className") - print(f"{i+1}. <{tag} class='{classes}'> {text[:50]}") - - # Try specific selectors - selectors = [ - ".price", - ".product-price", - "[class*='price']", - "[data-price]", - "span.price", - "div.price" - ] - - print("\nTrying specific selectors:") - for selector in selectors: - try: - els = await page.query_selector_all(selector) - if els: - for el in els[:2]: - text = await el.inner_text() - print(f" {selector}: {text}") - except: - pass - - await context.close() - await browser.close() - await playwright.stop() - print("\nDone") - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/analyze_gifi_search_prices.py b/analyze_gifi_search_prices.py deleted file mode 100644 index c2018a9..0000000 --- a/analyze_gifi_search_prices.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Test extracting price from Gifi search page HTML -""" -import asyncio -import sys -import re -sys.path.insert(0, '/app') - -from playwright.async_api import async_playwright -from bs4 import BeautifulSoup - -async def main(): - print("Connecting to browserless...") - playwright = await async_playwright().start() - browser = await playwright.chromium.connect_over_cdp("ws://browserless:3000") - - context = await browser.new_context( - viewport={"width": 1920, "height": 1080}, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - ) - - page = await context.new_page() - - print("Loading Gifi search...") - await page.goto("https://www.gifi.fr/resultat-recherche?q=chaise", wait_until="networkidle") - - content = await page.content() - - soup = BeautifulSoup(content, "html.parser") - products = soup.select("div.product-tile") - - print(f"Found {len(products)} products\n") - - for i, product in enumerate(products[:3]): - print(f"\\n=== Product {i+1} ===") - - # Get title - title_el = product.select_one("div.pdp-link a") - title = title_el.get_text(strip=True) if title_el else "N/A" - print(f"Title: {title}") - - # Try to find price in product HTML - product_html = product.prettify() - - # Look for price patterns - price_patterns = [ - r'(\d+)[,.](\d+)\s*€', # 19,99 € or 19.99 € - r'€\s*(\d+)[,.](\d+)', # € 19,99 - r'(\d+)€(\d+)', # 19€99 - r'"price"\s*:\s*"?(\d+\.?\d*)"?', # JSON price - ] - - for pattern in price_patterns: - matches = re.findall(pattern, product_html) - if matches: - print(f"Pattern '{pattern}': {matches[:3]}") - - # Find all text with € - euro_texts = product.find_all(string=re.compile('€')) - if euro_texts: - print(f"Texts with €:") - for text in euro_texts[:5]: - print(f" - {text.strip()[:80]}") - - await context.close() - await browser.close() - await playwright.stop() - print("\nDone") - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/analyze_missing_sites.py b/analyze_missing_sites.py deleted file mode 100644 index fa7eca5..0000000 --- a/analyze_missing_sites.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -Script pour analyser les sélecteurs CSS des sites manquants -""" -import asyncio -from playwright.async_api import async_playwright - -async def analyze_site(url: str, site_name: str): - print(f"\n=== Analyse de {site_name} ===") - print(f"URL: {url}") - - async with async_playwright() as p: - browser = await p.chromium.launch(headless=False) - page = await browser.new_page() - - try: - await page.goto(url, wait_until="networkidle", timeout=30000) - await page.wait_for_timeout(3000) - - # Try cookie banners - cookie_selectors = [ - "button:has-text('Accepter')", - "button:has-text('Tout accepter')", - "#didomi-notice-agree-button", - ".didomi-continue-without-agreeing" - ] - for selector in cookie_selectors: - try: - await page.click(selector, timeout=2000) - print(f"✓ Cookie banner fermé: {selector}") - break - except: - pass - - await page.wait_for_timeout(2000) - - # Save HTML - html = await page.content() - filename = f"{site_name.lower().replace(' ', '_')}_search.html" - with open(filename, "w", encoding="utf-8") as f: - f.write(html) - print(f"✓ HTML sauvegardé: {filename}") - - # Take screenshot - screenshot_path = f"{site_name.lower().replace(' ', '_')}_search.png" - await page.screenshot(path=screenshot_path, full_page=True) - print(f"✓ Screenshot: {screenshot_path}") - - # Test common product link selectors - test_selectors = [ - "a.product-link", - "a.product-name", - "a[href*='/produit']", - "a[href*='/product']", - "a[href*='/p/']", - ".product-title a", - ".product-item a", - "article a", - "a.product", - "a[itemprop='url']", - ] - - print("\n--- Test de sélecteurs ---") - for selector in test_selectors: - try: - elements = await page.query_selector_all(selector) - if elements: - print(f"✓ {selector}: {len(elements)} éléments trouvés") - # Get first few hrefs - for i, elem in enumerate(elements[:3]): - href = await elem.get_attribute("href") - text = await elem.inner_text() - print(f" [{i+1}] {text[:50]} -> {href}") - except Exception as e: - pass - - except Exception as e: - print(f"❌ Erreur: {e}") - finally: - await browser.close() - -async def main(): - sites = [ - ("https://bmstores.fr/module/ambjolisearch/jolisearch?s=chaise", "BM"), - ("https://www.centrakor.com/recherche?controller=search&s=chaise", "Centrakor"), - ("https://www.lincroyable.fr/recherche?query=chaise", "L'Incroyable"), - ] - - for url, name in sites: - await analyze_site(url, name) - await asyncio.sleep(2) - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/app/services/scheduler_service.py b/app/services/scheduler_service.py index 459f7fe..e2808a3 100644 --- a/app/services/scheduler_service.py +++ b/app/services/scheduler_service.py @@ -50,6 +50,10 @@ def _normalize_title(title: str) -> str: for pattern in patterns: title = re.sub(pattern, "", title) + # Separate numbers from letters (handles cases like 'Blanc40' or '100pièces') + title = re.sub(r"([a-zA-Z])(\d)", r"\1 \2", title) + title = re.sub(r"(\d)([a-zA-Z])", r"\1 \2", title) + # Remove extra spaces and punctuation that might differ between sites title = re.sub(r"[^\w\s]", " ", title) title = " ".join(title.split()) @@ -323,7 +327,14 @@ async def process_item_check(item_id: int): html_lower = html_content.lower() if html_content else "" if any(term in final_url_lower for term in unavailable_terms) or any( - term in html_lower for term in unavailable_terms + term in html_lower + for term in [ + # Use more specific terms for HTML to avoid false positives in random text or meta + "malheureusement, ce produit est actuellement indisponible", + ">produit indisponible<", + ">ce produit est indisponible<", + "product-unavailable-message", + ] ): logger.warning(f"Product unavailable confirmed for item {item_id} on Action.com") await loop.run_in_executor( diff --git a/app/services/tracking_scraper_service.py b/app/services/tracking_scraper_service.py index f6bab4c..f7f9de9 100644 --- a/app/services/tracking_scraper_service.py +++ b/app/services/tracking_scraper_service.py @@ -31,7 +31,6 @@ POPUP_SELECTORS = [ "input[value='Continuer les achats']", "input[value='Continue shopping']", "input[value='Continue shopping']", - "form:has-text('Continuer les achats') input[type='submit']", "[aria-labelledby='continue-shopping-label']", "#sp-cc-accept", @@ -53,7 +52,6 @@ POPUP_SELECTORS = [ ] - # Random User-Agents to alternate fingerprint AMAZON_USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", @@ -63,6 +61,7 @@ AMAZON_USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0", ] + @dataclass class ScrapeConfig: """Configuration for scraping parameters.""" @@ -174,12 +173,13 @@ class ScraperService: try: # Random delay to simulate human lead-in import random + await asyncio.sleep(random.uniform(0.5, 2.0)) - + await ScraperService._navigate_and_wait(page, url, timeout) final_url = page.url page_title = await page.title() - + # Humanize: scroll a bit and back await page.mouse.move(random.randint(100, 500), random.randint(100, 500)) await page.evaluate("window.scrollBy(0, 100)") @@ -198,16 +198,18 @@ class ScraperService: logger.info("Opening store selector...") await store_btn.first.click(timeout=2000) await page.wait_for_timeout(1000) - + # Type Nancy in the search input search_input = page.locator("#search_mag") if await search_input.count() > 0: await search_input.fill("Nancy") await page.keyboard.press("Enter") await page.wait_for_timeout(1500) - + # Select the first Nancy store (Nancy Essey or Nancy Centre) - select_btn = page.locator(".shop-list .btn-select-shop, button:has-text('Choisir ce magasin')") + select_btn = page.locator( + ".shop-list .btn-select-shop, button:has-text('Choisir ce magasin')" + ) if await select_btn.count() > 0: logger.info("Selecting Nancy store...") await select_btn.first.click() @@ -230,7 +232,7 @@ class ScraperService: content_data = await page.content() else: content_data = await ScraperService._extract_text(page, config.text_length) - + screenshot_path = await ScraperService._take_screenshot(page, url, item_id) return screenshot_path, content_data, final_url, page_title @@ -244,10 +246,7 @@ class ScraperService: @staticmethod async def _scrape_amazon_specific( - url: str, - item_id: int | None, - config: ScrapeConfig, - return_html: bool + url: str, item_id: int | None, config: ScrapeConfig, return_html: bool ) -> tuple[str | None, str, str, str]: """ Specialized scraping flow for Amazon to avoid bot detection and ensure good screenshots. @@ -258,15 +257,16 @@ class ScraperService: 4. Wait for main image to be visible """ logger.info(f"🛒 Starting specialized Amazon scrape for: {url}") - + # Determine base domain from urllib.parse import urlparse + parsed = urlparse(url) base_domain = f"{parsed.scheme}://{parsed.netloc}" - + context = await ScraperService._create_context(ScraperService._browser, url) page = await context.new_page() - + try: # 1. Warm-up: Visit Homepage to get cookies/session try: @@ -280,7 +280,7 @@ class ScraperService: # 2. Navigate to Product logger.info(f"➡️ Navigating to product page: {url}") await page.goto(url, wait_until="domcontentloaded", timeout=60000) - + final_url = page.url try: page_title = await page.title() @@ -289,22 +289,28 @@ class ScraperService: # 3. Check for Hard Redirects (URL-based) if "/ap/signin" in page.url: - logger.error("🚫 Amazon Login Redirect detected (URL)!") - return None, "LOGIN_REQUIRED", final_url, page_title + logger.error("🚫 Amazon Login Redirect detected (URL)!") + return None, "LOGIN_REQUIRED", final_url, page_title # 4. Handle Popups & Location Selectors await ScraperService._handle_popups(page) - + # Dismiss "Change Address" or specific Amazon location modals if any try: - await page.evaluate("document.getElementById('nav-main')?.classList.remove('nav-progressive-attribute')") - except: pass + await page.evaluate( + "document.getElementById('nav-main')?.classList.remove('nav-progressive-attribute')" + ) + except: + pass # 5. Check for Bot Detection / CAPTCHA / Login (Content-based) # We do this AFTER popup removal because sometimes "Identifiez-vous" is in a dismissible modal content_check = await page.content() - - if "Type the characters you see in this image" in content_check or "Saisissez les caractères que vous voyez" in content_check: + + if ( + "Type the characters you see in this image" in content_check + or "Saisissez les caractères que vous voyez" in content_check + ): logger.error("🚫 Amazon CAPTCHA detected!") # Attempt refresh once logger.info("Retrying with refresh...") @@ -313,7 +319,7 @@ class ScraperService: # Re-check content_check = await page.content() if "Type the characters you see in this image" in content_check: - return None, "CAPTCHA_DETECTED", final_url, page_title + return None, "CAPTCHA_DETECTED", final_url, page_title if "Identifiez-vous" in content_check or "ap_signin" in content_check: # Double check: is it a modal we missed? Try one last specific closure @@ -323,40 +329,46 @@ class ScraperService: logger.info("Found login modal close button, clicking...") await close_btn.first.click() await page.wait_for_timeout(1000) - content_check = await page.content() # Refresh content - except: pass + content_check = await page.content() # Refresh content + except: + pass if "Identifiez-vous" in content_check or "ap_signin" in content_check: - # Retry once for Login wall too - logger.info("⚠️ Amazon Login/Auth detected. Retrying with refresh...") - await page.reload() - await asyncio.sleep(3) - content_check = await page.content() + # Final Check: Do we have a product title? + # If we have a title, it's just a popup we missed/hid. Proceed. + # If NO title, it's a hard redirect/gate. Fail. + try: + title_check = page.locator("#productTitle, #title") + if await title_check.count() > 0 and await title_check.first.is_visible(): + logger.info("⚠️ Login prompt detected but Product Title found. Ignoring/Hiding modal...") + # Attempt to brute-force remove the modal overlay again just in case + await page.evaluate( + "() => document.querySelectorAll('.a-popover-modal, .a-modal-scroller').forEach(e => e.remove())" + ) + else: + # No title found, now we can try to reload or fail + logger.info("⚠️ Amazon Login/Auth detected and No Title found. Retrying with refresh...") + await page.reload() + await asyncio.sleep(3) + content_check = await page.content() - if "Identifiez-vous" in content_check or "ap_signin" in content_check: - # Final Check: Do we have a product title? - # If we have a title, it's just a popup we missed/hid. Proceed. - # If NO title, it's a hard redirect/gate. Fail. - try: - title_check = page.locator("#productTitle, #title") - if await title_check.count() > 0: - logger.info("⚠️ Login prompt detected but Product Title found. Ignoring/Hiding modal...") - # Attempt to brute-force remove the modal overlay again just in case - await page.evaluate("document.querySelectorAll('.a-popover-modal, .a-modal-scroller').forEach(e => e.remove())") - else: - logger.error("🚫 Amazon Login Prompt detected (Blocking)!") - return None, "LOGIN_REQUIRED", final_url, page_title - except: - logger.error("🚫 Amazon Login Prompt detected (Error Checking Title)!") - return None, "LOGIN_REQUIRED", final_url, page_title + if "Identifiez-vous" in content_check or "ap_signin" in content_check: + # Re-check title after reload + if await title_check.count() > 0 and await title_check.first.is_visible(): + logger.info("⚠️ Title appeared after refresh despite login prompt.") + else: + logger.error("🚫 Amazon Login Prompt detected (Blocking)!") + return None, "LOGIN_REQUIRED", final_url, page_title + except Exception as e: + logger.error(f"🚫 Amazon Login Prompt detected (Error Checking Title: {e})") + return None, "LOGIN_REQUIRED", final_url, page_title # 6. Wait for Main Image (Critical for screenshot) logger.info("🖼️ Waiting for product image...") try: # Main image container on desktop await page.wait_for_selector( - "#imgTagWrapperId, #landingImage, #main-image-container, .imgTagWrapper", - timeout=10000 + "#imgTagWrapperId, #landingImage, #main-image-container, .imgTagWrapper", timeout=10000 ) except Exception as e: logger.warning(f"Could not find main image container: {e}") @@ -373,10 +385,10 @@ class ScraperService: content_data = await page.content() else: content_data = await ScraperService._extract_text(page, config.text_length) - + # 8. Screenshot screenshot_path = await ScraperService._take_screenshot(page, url, item_id) - + return screenshot_path, content_data, final_url, page_title except Exception as e: @@ -396,14 +408,15 @@ class ScraperService: """Create context with advanced stealth and headers (specifically for Amazon)""" # Determine base domain for referer from urllib.parse import urlparse + parsed = urlparse(url) base_domain = f"{parsed.scheme}://{parsed.netloc}/" if "amazon" in url: - ua = random.choice(AMAZON_USER_AGENTS) - logger.info(f"🎭 Using rotated User-Agent: {ua}") + ua = random.choice(AMAZON_USER_AGENTS) + logger.info(f"🎭 Using rotated User-Agent: {ua}") else: - ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" + ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" context = await browser.new_context( viewport={"width": 1920, "height": 1080}, @@ -422,8 +435,8 @@ class ScraperService: "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Upgrade-Insecure-Requests": "1", - "Referer": base_domain if "amazon" in url else "https://www.google.com/" - } + "Referer": base_domain if "amazon" in url else "https://www.google.com/", + }, ) # Advanced Stealth mode @@ -475,10 +488,10 @@ class ScraperService: async def _handle_popups(page: Page): """Aggressive multi-pass popup and overlay removal""" logger.info("Starting aggressive popup removal...") - + # 1. Multi-pass clicking (some popups appear after others are closed) for i in range(2): - logger.debug(f"Popup removal pass {i+1}") + logger.debug(f"Popup removal pass {i + 1}") for selector in POPUP_SELECTORS: try: locators = page.locator(selector) @@ -492,12 +505,13 @@ class ScraperService: await page.wait_for_timeout(500) except Exception: pass - + # Hammer Escape key try: await page.keyboard.press("Escape") await page.wait_for_timeout(200) - except: pass + except: + pass # 2. Javascript cleanup (Hide pesky overlays and CMPs that won't close) logger.info("Injecting CSS/JS cleanup for persistent overlays...") @@ -540,7 +554,7 @@ class ScraperService: document.documentElement.style.overflow = 'auto'; } """) - + # Wait for any transitions await page.wait_for_timeout(1000) @@ -582,7 +596,7 @@ class ScraperService: try: logger.info(f"Extracting text (limit: {text_length} chars)...") - + # Smart extraction: remove noise (nav, footer, scripts) before getting text clean_text = await page.evaluate(""" () => { @@ -604,7 +618,7 @@ class ScraperService: return clone.innerText; } """) - + # Fallback if cleaning removed everything (unlikely but safe) if not clean_text or len(clean_text) < 100: logger.warning("Cleaned text too short, falling back to full body text") @@ -612,7 +626,8 @@ class ScraperService: # Collapse whitespace import re - clean_text = re.sub(r'\s+', ' ', clean_text).strip() + + clean_text = re.sub(r"\s+", " ", clean_text).strip() page_text = clean_text[:text_length] logger.info(f"Extracted {len(page_text)} chars") diff --git a/check_bs4.py b/check_bs4.py deleted file mode 100644 index cc10bf9..0000000 --- a/check_bs4.py +++ /dev/null @@ -1,2 +0,0 @@ -from app.services.improved_search_service import BeautifulSoup -print("BeautifulSoup imported successfully") diff --git a/check_import.py b/check_import.py deleted file mode 100644 index 8adb705..0000000 --- a/check_import.py +++ /dev/null @@ -1,12 +0,0 @@ -import sys -import os -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "."))) -import app.core.search_config -print(f"Search Config File: {app.core.search_config.__file__}") -from app.core.search_config import SITE_CONFIGS -print(f"Keys: {list(SITE_CONFIGS.keys())}") - -with open(app.core.search_config.__file__, 'r') as f: - content = f.read() - print(f"File content length: {len(content)}") - print(f"Contains stokomani.fr: {'stokomani.fr' in content}") diff --git a/check_method.py b/check_method.py deleted file mode 100644 index aebd9e5..0000000 --- a/check_method.py +++ /dev/null @@ -1,10 +0,0 @@ -import sys -from app.services.improved_search_service import ImprovedSearchService - -print("Attributes of ImprovedSearchService:") -attrs = dir(ImprovedSearchService) -if 'search_site_generator' in attrs: - print("SUCCESS: search_site_generator found") -else: - print("FAILURE: search_site_generator NOT found") - print("Available attributes:", [a for a in attrs if not a.startswith('__')]) diff --git a/check_proxies.py b/check_proxies.py deleted file mode 100644 index 43ce566..0000000 --- a/check_proxies.py +++ /dev/null @@ -1,52 +0,0 @@ -import asyncio -import os -import sys - -# Add app to path -sys.path.insert(0, os.getcwd()) - -from playwright.async_api import async_playwright -from app.core.search_config import get_amazon_proxies - - -async def check_proxy(proxy, semaphore): - async with semaphore: - proxy_url = proxy["server"] - print(f"Testing {proxy_url}...") - - async with async_playwright() as p: - # Connect to browserless or launch local - # Using launch local for simpler testing without ws dependency if possible - # But the app uses browserless. Let's try launch first. - try: - browser = await p.chromium.launch(headless=True, proxy=proxy) - page = await browser.new_page() - try: - # amazon.fr might block, use httpbin for connectivity check - await page.goto("http://httpbin.org/ip", timeout=15000) - content = await page.content() - print(f"✅ {proxy_url}: Success") - await browser.close() - return True - except Exception as e: - print(f"❌ {proxy_url}: Failed - {str(e)[:100]}") - await browser.close() - return False - except Exception as e: - print(f"❌ {proxy_url}: Launch Failed - {str(e)[:100]}") - return False - - -async def main(): - proxies = get_amazon_proxies() - print(f"Checking {len(proxies)} proxies...") - - semaphore = asyncio.Semaphore(3) # Limit concurrency - results = await asyncio.gather(*[check_proxy(p, semaphore) for p in proxies]) - - working = sum(results) - print(f"\nSummary: {working}/{len(proxies)} working.") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/check_syntax.py b/check_syntax.py deleted file mode 100644 index 4cc2eb0..0000000 --- a/check_syntax.py +++ /dev/null @@ -1,7 +0,0 @@ -import sys -try: - from app.services import improved_search_service - print("Import successful") -except Exception as e: - print(f"Import failed: {e}") - sys.exit(1) diff --git a/deep_centrakor_analysis.py b/deep_centrakor_analysis.py deleted file mode 100644 index 2be63d0..0000000 --- a/deep_centrakor_analysis.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Deep analysis - compare products WITH images vs WITHOUT images -""" -import asyncio -import sys -sys.path.insert(0, '/app') - -from playwright.async_api import async_playwright -from bs4 import BeautifulSoup - -async def main(): - playwright = await async_playwright().start() - browser = await playwright.chromium.connect_over_cdp("ws://browserless:3000") - - context = await browser.new_context( - viewport={"width": 1920, "height": 1080}, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - ) - - page = await context.new_page() - - await page.goto("https://www.centrakor.com/search/chaise", wait_until="networkidle") - content = await page.content() - - soup = BeautifulSoup(content, "html.parser") - products = soup.select("div.product-item") - - print(f"Analyzing {len(products)} products for image patterns\n") - - for i, product in enumerate(products[:10]): - # Get title - title_el = product.select_one("a.product-item__name") - title = title_el.get_text(strip=True) if title_el else f"Product {i+1}" - - # Get ALL images - all_imgs = product.select("img.responsive-image__actual") - - print(f"\n=== {i+1}. {title[:50]} ===") - print(f"Found {len(all_imgs)} images") - - for j, img in enumerate(all_imgs): - src = img.get('src', '') - print(f" Image {j+1}: {src if src else '(no src)'}") - if not src: - # Check other attributes - for attr in ['data-src', 'data-lazy-src', 'srcset']: - val = img.get(attr) - if val: - print(f" {attr}: {val[:80]}") - - await context.close() - await browser.close() - await playwright.stop() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/diagnose_selectors.py b/diagnose_selectors.py deleted file mode 100644 index 362b255..0000000 --- a/diagnose_selectors.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Quick diagnostic script to analyze actual HTML structure of search pages -""" -import asyncio -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent)) - -from app.services.search_service import NewSearchService -from app.core.search_config import SITE_CONFIGS - -async def diagnose_site(site_key: str, query: str = "chaise"): - """Run a search and show detailed diagnostics""" - config = SITE_CONFIGS.get(site_key) - if not config: - print(f"❌ Site '{site_key}' not found in config") - return - - print(f"\n{'='*80}") - print(f"🔍 Diagnosing: {config['name']} ({site_key})") - print(f"{'='*80}") - print(f"Search URL: {config['search_url'].format(query=query)}") - print(f"Product Selector: {config['product_selector']}") - print(f"Image Selector: {config.get('product_image_selector', 'NONE')}") - - try: - results = await NewSearchService.search_site(site_key, query) - - print(f"\n📊 Results: {len(results)} products found") - - if len(results) == 0: - print("⚠️ NO RESULTS - Check if product_selector is correct") - else: - print("\n✅ Sample Results:") - for i, result in enumerate(results[:3], 1): - print(f"\n {i}. {result.title[:60]}") - print(f" URL: {result.url[:80]}") - print(f" Image: {result.image_url[:80] if result.image_url else '❌ NONE'}") - print(f" Price: {result.price}€" if result.price else " Price: ❌ NONE") - - # Count images - with_images = sum(1 for r in results if r.image_url) - print(f"\n📈 Images: {with_images}/{len(results)} ({with_images/len(results)*100:.0f}%)") - - with_prices = sum(1 for r in results if r.price) - print(f"💰 Prices: {with_prices}/{len(results)} ({with_prices/len(results)*100:.0f}%)") - - except Exception as e: - print(f"❌ ERROR: {e}") - import traceback - traceback.print_exc() - -async def main(): - """Diagnose problematic sites""" - sites_to_check = [ - "e-leclerc.com", - "auchan.fr", - "carrefour.fr", - "stokomani.fr", - "centrakor.com", - "cdiscount.com", - "lincroyable.fr" - ] - - for site_key in sites_to_check: - await diagnose_site(site_key) - print("\n" + "="*80 + "\n") - await asyncio.sleep(1) # Rate limiting - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/dump_gifi.py b/dump_gifi.py deleted file mode 100644 index bcb8c31..0000000 --- a/dump_gifi.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Script to dump Gifi HTML and analyze structure -""" -import asyncio -import sys -import os -sys.path.insert(0, '/app') - -from playwright.async_api import async_playwright - -async def main(): - print("Connecting to browserless...") - playwright = await async_playwright().start() - browser = await playwright.chromium.connect_over_cdp("ws://browserless:3000") - - context = await browser.new_context( - viewport={"width": 1920, "height": 1080}, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - ) - - page = await context.new_page() - - print("Loading Gifi search page...") - await page.goto("https://www.gifi.fr/resultat-recherche?q=chaise", wait_until="domcontentloaded") - - # Wait for products - try: - await page.wait_for_selector("article.product-miniature", timeout=10000) - except: - pass - - # Save HTML - content = await page.content() - os.makedirs("/app/debug_dumps", exist_ok=True) - with open("/app/debug_dumps/gifi_full.html", "w", encoding="utf-8") as f: - f.write(content) - - print(f"HTML saved ({len(content)} bytes)") - - # Extract first product structure - products = await page.query_selector_all("article.product-miniature") - print(f"Found {len(products)} products") - - if products: - first_html = await products[0].evaluate("el => el.outerHTML") - with open("/app/debug_dumps/gifi_first_product.html", "w", encoding="utf-8") as f: - f.write(first_html) - print(f"First product HTML saved") - - await context.close() - await browser.close() - await playwright.stop() - print("Done") - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/dump_gifi_v2.py b/dump_gifi_v2.py deleted file mode 100644 index 2594580..0000000 --- a/dump_gifi_v2.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Dump Gifi with longer wait for JavaScript -""" -import asyncio -import sys -import os -sys.path.insert(0, '/app') - -from playwright.async_api import async_playwright - -async def main(): - print("Connecting to browserless...") - playwright = await async_playwright().start() - browser = await playwright.chromium.connect_over_cdp("ws://browserless:3000") - - context = await browser.new_context( - viewport={"width": 1920, "height": 1080}, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - ) - - page = await context.new_page() - - print("Loading Gifi search page...") - await page.goto("https://www.gifi.fr/resultat-recherche?q=chaise", wait_until="networkidle") - - # Wait for ANY content - print("Waiting for content...") - await page.wait_for_timeout(5000) - - # Save HTML - content = await page.content() - os.makedirs("/app/debug_dumps", exist_ok=True) - with open("/app/debug_dumps/gifi_with_wait.html", "w", encoding="utf-8") as f: - f.write(content) - - print(f"HTML saved ({len(content)} bytes)") - - # Find any elements with price - price_els = await page.query_selector_all("*:has-text('€')") - print(f"Elements with € symbol: {len(price_els)}") - - # Find all divs/articles - all_divs = await page.query_selector_all("div, article, li") - print(f"Total divs/articles/li: {len(all_divs)}") - - # Screenshot - await page.screenshot(path="/app/debug_dumps/gifi_screenshot.png", full_page=True) - print("Screenshot saved") - - # Get all classes - all_classes = await page.evaluate("""() => { - const elements = document.querySelectorAll('*'); - const classes = new Set(); - elements.forEach(el => { - if (el.className && typeof el.className === 'string') { - el.className.split(' ').forEach(cls => { - if (cls && (cls.includes('product') || cls.includes('item') || cls.includes('card'))) { - classes.add(cls); - } - }); - } - }); - return Array.from(classes); - }""") - - print(f"\\nProduct-related classes found:") - for cls in all_classes: - print(f" - {cls}") - - await context.close() - await browser.close() - await playwright.stop() - print("Done") - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/dump_html.py b/dump_html.py deleted file mode 100644 index 3c3742f..0000000 --- a/dump_html.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -Quick HTML Dumper - Saves raw HTML from search pages for manual analysis -""" -import asyncio -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent)) - -from app.services.browserless_service import browserless_service -from app.core.search_config import SITE_CONFIGS - -async def dump_search_html(site_key: str, query: str = "chaise"): - """Download and save raw HTML for manual inspection""" - config = SITE_CONFIGS.get(site_key) - if not config: - print(f"❌ Site '{site_key}' not found") - return - - print(f"\n🔍 Dumping HTML for: {config['name']}") - - # Ensure browser is initialized - await browserless_service.initialize() - - try: - search_url = config["search_url"].format(query=query) - print(f" URL: {search_url}") - - # Override wait_selector for La Foir'Fouille - wait_selector = config.get("wait_selector") - if site_key == "lafoirfouille.fr": - wait_selector = ".sf-grid-vignet" - print(f" ⚠️ Overriding wait_selector to: {wait_selector}") - - html_content, screenshot_path = await browserless_service.get_page_content( - search_url, - wait_selector=wait_selector, - use_proxy=config.get("requires_proxy", False) - ) - - if not html_content: - print(" ❌ No HTML content returned") - return - - filename = f"dump_{site_key.replace('.', '_')}.html" - with open(filename, "w", encoding="utf-8") as f: - f.write(html_content) - - print(f" ✅ Saved to: {filename} ({len(html_content)} bytes)") - - # Quick analysis - from bs4 import BeautifulSoup - soup = BeautifulSoup(html_content, "html.parser") - - # Try current selector - current_selector = config.get("product_selector") - matches = soup.select(current_selector) - print(f" 📊 Current selector '{current_selector}' matches: {len(matches)}") - - # Try image selector - if "product_image_selector" in config: - img_selector = config["product_image_selector"] - img_matches = soup.select(img_selector) - print(f" 🖼️ Current image selector '{img_selector}' matches: {len(img_matches)}") - - except Exception as e: - print(f" ❌ Error during dump: {e}") - - finally: - # We don't close the browser here to allow reuse if needed, - # but main() will shut it down. - pass - -async def main(): - sites = [ - "stokomani.fr" - ] - - for site_key in sites: - try: - await dump_search_html(site_key) - except Exception as e: - print(f"❌ Error: {e}") - await asyncio.sleep(1) - - await browserless_service.shutdown() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/filter_proxies.py b/filter_proxies.py deleted file mode 100644 index c63526d..0000000 --- a/filter_proxies.py +++ /dev/null @@ -1,103 +0,0 @@ -import json -import urllib.request -import urllib.error - -proxies_raw = """ -46.161.6.165:8080 -78.47.219.204:3128 -134.209.29.120:8080 -161.35.70.249:80 -134.209.29.120:80 -52.188.28.218:3128 -209.97.150.167:3128 -62.60.151.128:80 -68.235.35.171:3128 -209.97.150.167:80 -159.203.61.169:8080 -209.97.150.167:8080 -195.158.8.123:3128 -208.87.243.199:7878 -144.76.42.215:8118 -216.229.112.25:8080 -159.203.61.169:80 -103.3.246.71:3128 -138.68.60.8:80 -139.59.1.14:80 -8.243.68.11:8080 -41.223.119.156:3128 -34.96.238.40:8080 -59.6.25.118:3128 -129.150.39.251:8000 -162.240.154.26:3128 -35.152.252.253:8080 -144.125.164.158:8081 -47.81.14.7:3129 -144.125.164.222:8080 -175.99.220.171:80 -8.219.97.248:80 -144.125.164.158:8080 -164.68.110.241:8091 -144.125.164.222:8081 -140.238.184.182:3128 -139.59.1.14:3128 -8.212.160.196:8080 -164.68.110.241:9992 -173.212.246.157:3128 -47.236.130.95:3128 -103.147.246.18:8080 -128.199.202.122:80 -200.24.159.230:8080 -128.199.202.122:8080 -103.166.158.251:1111 -59.153.16.214:1120 -43.224.118.155:1121 -89.43.132.247:8080 -182.253.62.190:8080 -193.95.53.131:8077 -203.196.8.6:3128 -103.245.110.198:1452 -45.180.140.241:8080 -212.2.254.246:3128 -103.220.206.110:8585 -103.157.79.145:1080 -45.87.140.155:8080 -164.138.205.119:8080 -137.59.51.243:1120 -38.210.179.77:999 -27.147.163.188:40544 -194.87.77.22:80 -20.27.219.85:8080 -49.254.245.70:15648 -115.144.173.67:15648 -""" - -proxy_list = [p.strip() for p in proxies_raw.strip().split("\n") if p.strip()] -ips = [p.split(":")[0] for p in proxy_list] - -chunk_size = 100 -fr_proxies = [] - -for i in range(0, len(ips), chunk_size): - chunk = ips[i : i + chunk_size] - try: - req = urllib.request.Request("http://ip-api.com/batch", data=json.dumps(chunk).encode("utf-8")) - with urllib.request.urlopen(req) as response: - data = json.loads(response.read().decode("utf-8")) - - for idx, result in enumerate(data): - if result.get("countryCode") == "FR": - full_proxy = proxy_list[i + idx] - fr_proxies.append(full_proxy) - print(f"Found FR proxy: {full_proxy}") - except Exception as e: - print(f"Error querying batch: {e}") - -print(f"Total FR proxies found: {len(fr_proxies)}") - -if len(fr_proxies) > 0: - for p in fr_proxies: - print(f"PROXY:{p}") -else: - print("No French proxies found. Printing first 10 generic ones as backup:") - for p in proxy_list[:10]: - print(f"PROXY:{p}") diff --git a/find_working_proxies.py b/find_working_proxies.py deleted file mode 100644 index be4b28d..0000000 --- a/find_working_proxies.py +++ /dev/null @@ -1,118 +0,0 @@ -import urllib.request -import logging -import concurrent.futures - -# Setup logging -logging.basicConfig(level=logging.INFO, format="%(message)s") -logger = logging.getLogger("proxy_finder") - -# Target URL for verification -TARGET_URL = "https://www.amazon.fr" - - -def fetch_proxy_list(url): - try: - req = urllib.request.Request( - url, data=None, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"} - ) - with urllib.request.urlopen(req, timeout=10) as response: - if response.status == 200: - text = response.read().decode("utf-8") - proxies = [p.strip() for p in text.splitlines() if p.strip() and ":" in p] - logger.info(f"Fetched {len(proxies)} proxies from {url}") - return proxies - except Exception as e: - logger.error(f"Failed to fetch from {url}: {e}") - return [] - - -def check_proxy_fast(proxy): - try: - proxy_handler = urllib.request.ProxyHandler({"http": proxy, "https": proxy}) - opener = urllib.request.build_opener(proxy_handler) - opener.addheaders = [("User-Agent", "Mozilla/5.0")] - with opener.open(TARGET_URL, timeout=5) as response: - # 200, 403, 503 all mean the proxy is alive (amazon may block but proxy works) - if response.status in [200, 403, 503]: - return proxy - except: - pass - return None - - -def main(): - logger.info("Starting proxy finder (FAST MODE)...") - - sources = [ - "https://api.proxyscrape.com/v2/?request=getproxies&protocol=http&timeout=10000&country=all&ssl=all&anonymity=all", - "https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt", - "https://raw.githubusercontent.com/ShiftyTR/Proxy-List/master/http.txt", - "https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/http.txt", - ] - - # 1. Fetch all proxies - all_proxies = set() - for url in sources: - proxies = fetch_proxy_list(url) - if proxies: - all_proxies.update(proxies) - - # Add local raw proxies - local_raw = [ - "164.68.110.241:8091", - "164.68.110.241:9992", - "173.212.246.157:3128", - "142.111.48.253:7030", - "31.59.20.176:6754", - "23.95.150.145:6114", - "198.23.239.134:6540", - "107.172.163.27:6543", - "198.105.121.200:6462", - "64.137.96.74:6641", - "84.247.60.125:6095", - "216.10.27.159:6837", - "142.111.67.146:5611", - ] - all_proxies.update(local_raw) - - print(f"\nTesting {len(all_proxies)} unique proxies against {TARGET_URL}...") - - working_proxies = [] - - # Use ThreadPoolExecutor for speed - with concurrent.futures.ThreadPoolExecutor(max_workers=100) as executor: - future_to_proxy = {executor.submit(check_proxy_fast, p): p for p in all_proxies} - - count = 0 - total = len(all_proxies) - - for future in concurrent.futures.as_completed(future_to_proxy): - count += 1 - if count % 500 == 0: - print(f"Processed {count}/{total} - Found {len(working_proxies)} so far") - - res = future.result() - if res: - print(f"ALIVE: {res}") - working_proxies.append(res) - # Stop if we have enough - if len(working_proxies) >= 30: - print("Found 30 proxies, stopping.") - executor.shutdown(wait=False, cancel_futures=True) - break - - print("\n" + "=" * 50) - print(f"FOUND {len(working_proxies)} WORKING PROXIES") - print("=" * 50) - - # Format for python list - formatted_list = "[\n" + ",\n".join([f' "{p}"' for p in working_proxies]) + "\n]" - print(formatted_list) - - # Save to file - with open("working_proxies.txt", "w") as f: - f.write("\n".join(working_proxies)) - - -if __name__ == "__main__": - main() diff --git a/gifi_dump.html b/gifi_dump.html deleted file mode 100644 index e69de29..0000000 diff --git a/gifi_full.html b/gifi_full.html deleted file mode 100644 index 0455d34..0000000 --- a/gifi_full.html +++ /dev/null @@ -1,18821 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - chaise pas cher | GIFI - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - -
- - - gifi example - -
-
- - - gifi example - -
- - - - -
-
- - - -
- - - - - - - - - - - - - - - -
- - -
- - - - - - - - - -
-
-
-
- -
-
- - - - -

- - Résultats de recherche pour : - - - chaise - -

-
- - Nombre de produits trouvés : - - - 147 - -
- -
- - - - -
-
- -
-
- - - -
- -
-
- - - - - - - - -
-
-

- Filtrer -

- -
-
- -
- -
-
- -
-
- - - - - -
    - - - -
  • - -
  • - - - - - -
  • - -
  • - - - - - -
  • - -
  • - - - - - -
  • - -
  • - - - - - -
  • - -
  • - - - - - -
  • - -
  • - - - - - -
  • - -
  • - - - - - -
  • - -
  • - - - - - -
  • - -
  • - - - - - -
  • - -
  • - - - - - -
  • - -
  • - - - - - -
  • - -
  • - - - - - -
  • - -
  • - - - - - -
  • - -
  • - - - -
- - - - - -
-
- -
-
- -
-
- - - - - - - - -
    -
  • -
    -
    -
    -
    -
    - - -
    -
    -
    - - - - € - -
    -
    - - - - € - -
    -
    -
    - - - - - - -
  • -
- - -
-
- -
- - - -
- - -
- - - -
-
- - - - -
- - - - - - - - -
    - - -
- -
- - -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
- -
- -
- - -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 7,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 16,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 7,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 79,90 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 25,00 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 25,00 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 39,95 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 39,90 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 45,00 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 17,90 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 1,29 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 17,90 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 2,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 1,29 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 5,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 3,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 3,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 1,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 1,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 1,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 1,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 3,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 40,27 € - - - - - - - - - - - - - -
- -
- -
- - - - - - - Price reduced from - - - -
- - 79,99 € - - - - - - - - - - - - - -
- - - to - -
-
- -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 3,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 3,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 3,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 3,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 2,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 2,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 2,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- -
- - - - - - -
- - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - 2,99 € - - - - - - - - - - - - - -
- -
- -
- - -
- - -
- - - - - - - -
- - - -
- Vendu par GiFi -
- - - -
- - -
- - - - - - - - -
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
-
- - - 50% - - - - - En Bon d'achat - - -
-
-
- - - -
- - -
- -
- - -
- - - - - -
-
-
- -
-
-
- - -
- - - - - -
-
-
- - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/inspect_carrefour.py b/inspect_carrefour.py deleted file mode 100644 index 91a0b10..0000000 --- a/inspect_carrefour.py +++ /dev/null @@ -1,33 +0,0 @@ -from bs4 import BeautifulSoup - -with open("dump_carrefour_fr.html", "r", encoding="utf-8") as f: - html = f.read() - -soup = BeautifulSoup(html, "html.parser") -articles = soup.select("article.product-list-card-plp-grid-new") - -print(f"Found {len(articles)} articles") - -if articles: - first = articles[0] - print("\n--- First Article Structure ---") - print(first.prettify()[:1000]) # Print first 1000 chars - - # Check for link - link = first.select_one("a.product-card-click-wrapper") - if link: - print(f"\nLink found: {link.get('href')}") - print(f"Link classes: {link.get('class')}") - - # Check for image INSIDE link - img = link.select_one("img.product-card-image-new__content") - if img: - print(f"\n✅ Image found INSIDE link: {img.get('src')}") - else: - print(f"\n❌ Image NOT found inside link") - # Check if image is elsewhere in article - img_article = first.select_one("img.product-card-image-new__content") - if img_article: - print(f" But image exists in article: {img_article.get('src')}") - else: - print("\nNo link found with selector a.product-card-click-wrapper") diff --git a/inspect_lafoirfouille.py b/inspect_lafoirfouille.py deleted file mode 100644 index 55d43de..0000000 --- a/inspect_lafoirfouille.py +++ /dev/null @@ -1,53 +0,0 @@ -from bs4 import BeautifulSoup - -with open("dump_lafoirfouille_fr.html", "r", encoding="utf-8") as f: - html = f.read() - -soup = BeautifulSoup(html, "html.parser") - -# Try to find product containers -print("Searching for product containers...") -potential_selectors = [ - "div.product-miniature", - "article", - "div[class*='product']", - "div.product-card", - "div.item" -] - -for selector in potential_selectors: - matches = soup.select(selector) - print(f"Selector '{selector}' matches: {len(matches)}") - if len(matches) > 0 and len(matches) < 5: - # If few matches, print classes to see if it's a wrapper - print(f" Classes: {matches[0].get('class')}") - -# Print structure of first potential product -products = soup.select("div.product-miniature") -if not products: - products = soup.select("div[class*='product-item']") - -if products: - first = products[0] - print("\n--- First Product Structure ---") - print(first.prettify()[:1000]) - - link = first.find("a") - if link: - print(f"\nLink found: {link.get('href')}") - - img = first.find("img") - if img: - print(f"\nImage found: {img.get('src')}") -else: - print("\nNo obvious products found. Dumping generic structure...") - # Find any div with many children - divs = soup.find_all("div") - for div in divs: - if len(div.find_all("div", recursive=False)) > 10: - print(f"Found container with many children: {div.get('class')}") - # Print first child - child = div.find("div") - if child: - print(child.prettify()[:500]) - break diff --git a/inspect_lafoirfouille_images.py b/inspect_lafoirfouille_images.py deleted file mode 100644 index b3e1cb3..0000000 --- a/inspect_lafoirfouille_images.py +++ /dev/null @@ -1,25 +0,0 @@ -from bs4 import BeautifulSoup - -with open("dump_lafoirfouille_fr.html", "r", encoding="utf-8") as f: - html = f.read() - -soup = BeautifulSoup(html, "html.parser") -images = soup.select("img") - -print(f"Found {len(images)} images") - -for i, img in enumerate(images[:10]): - print(f"\n--- Image {i+1} ---") - print(f"Src: {img.get('src')}") - print(f"Classes: {img.get('class')}") - - parent = img.parent - print(f"Parent: {parent.name} (Classes: {parent.get('class')})") - - grandparent = parent.parent - if grandparent: - print(f"Grandparent: {grandparent.name} (Classes: {grandparent.get('class')})") - - greatgrandparent = grandparent.parent - if greatgrandparent: - print(f"Great Grandparent: {greatgrandparent.name} (Classes: {greatgrandparent.get('class')})") diff --git a/inspect_lincroyable.py b/inspect_lincroyable.py deleted file mode 100644 index 177ecd8..0000000 --- a/inspect_lincroyable.py +++ /dev/null @@ -1,33 +0,0 @@ -import asyncio -import logging -import sys -import os - -# Add project root to path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "."))) - -from app.services.browserless_service import browserless_service - -# Configure logging -logging.basicConfig(level=logging.INFO) - - -async def inspect(): - url = "https://www.lincroyable.fr/recherche-query=iphone/" - print(f"Inspecting {url}...") - - # Use host port for local debugging - os.environ["BROWSERLESS_URL"] = "ws://localhost:3012" - - await browserless_service.initialize() - try: - content, _ = await browserless_service.get_page_content(url, wait_selector="body") - with open("lincroyable_dump.html", "w", encoding="utf-8") as f: - f.write(content) - print("HTML dumped to lincroyable_dump.html") - finally: - await browserless_service.shutdown() - - -if __name__ == "__main__": - asyncio.run(inspect()) diff --git a/inspect_local_playwright.py b/inspect_local_playwright.py deleted file mode 100644 index ed34dc8..0000000 --- a/inspect_local_playwright.py +++ /dev/null @@ -1,47 +0,0 @@ -import asyncio -import logging -import os -from playwright.async_api import async_playwright - -# Configure logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - - -async def inspect_local(): - url = "https://www.lincroyable.fr/recherche-query=iphone/" - print(f"Inspecting {url} using local Playwright...") - - async with async_playwright() as p: - # Launch local browser (headless=True matches server environment usually, but we can try False to see) - browser = await p.chromium.launch(headless=True) - context = await browser.new_context( - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" - ) - page = await context.new_page() - - try: - logging.info(f"Navigating to {url}") - await page.goto(url, timeout=30000) - logging.info("Navigation successful") - - content = await page.content() - with open("lincroyable_local_dump.html", "w", encoding="utf-8") as f: - f.write(content) - print("HTML dumped to lincroyable_local_dump.html") - - await page.screenshot(path="lincroyable_local.png") - print("Screenshot saved to lincroyable_local.png") - - except Exception as e: - logging.error(f"Error: {e}") - try: - await page.screenshot(path="lincroyable_error.png") - except: - pass - finally: - await browser.close() - - -if __name__ == "__main__": - asyncio.run(inspect_local()) diff --git a/inspect_sites.py b/inspect_sites.py deleted file mode 100644 index 6b02d7d..0000000 --- a/inspect_sites.py +++ /dev/null @@ -1,47 +0,0 @@ -import asyncio -import logging -from app.services.browserless_service import BrowserlessService -from app.core.search_config import SITE_CONFIGS - -# Configure logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -async def dump_site(site_key, query="chaise"): - config = SITE_CONFIGS.get(site_key) - if not config: - logger.error(f"Site {site_key} not found in config") - return - - search_url = config["search_url"].format(query=query) - logger.info(f"Dumping {site_key} from {search_url}") - - try: - content, _ = await BrowserlessService.get_page_content( - search_url, - wait_selector=config.get("wait_selector"), - use_proxy=config.get("requires_proxy", False) - ) - - if content: - filename = f"/app/{site_key}_dump.html" - with open(filename, "w", encoding="utf-8") as f: - f.write(content) - logger.info(f"Successfully dumped to {filename}") - else: - logger.error(f"Failed to get content for {site_key}") - - except Exception as e: - logger.error(f"Error dumping {site_key}: {e}") - -async def main(): - await BrowserlessService.initialize() - try: - await dump_site("amazon.fr") - await dump_site("stokomani.fr") - # await dump_site("lincroyable.fr") # Already have this - finally: - await BrowserlessService.shutdown() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/lincroyable_dump.html b/lincroyable_dump.html deleted file mode 100644 index c13465b..0000000 --- a/lincroyable_dump.html +++ /dev/null @@ -1,1876 +0,0 @@ - - - - - - - - iphone - L'Incroyable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - - - - - - - - -
- -
-
-
-

Recherche

26 résultats pour votre recherche "iphone"

- -
-
- -
-
-
- - - -
-
- -
-
- -
-
-

coup de coeur

-

-
-
- Poêle - plusieurs gammes
+3
- -
- -
-

Poêle 'Metallic' induction

-

En aluminium
-

d28cm

-

14€99

- -
- -

Uniquement en magasin

-
-
-
-
-
-

coup de coeur

-

-
-
- Chaise Florence - plusieurs gammes
- -
- -
-

Chaise Florence

-

En polypropylène
-

44.5x42/48.5xh.79cm

-

29€99

- -
- -

Ajouter au panier

-
-
-
-
-
-

coup de coeur

-

-
-
- Bac de rangement - plusieurs gammes
- -
- -
-

Bac de rangement 'Estanca' 75L gris

-

Certifié IP67
-

64x47xh33.3cm

-

24€99

- -
- -

Ajouter au panier

-
-
-
-
-
-

coup de coeur

-

-
-
- Poêle - plusieurs gammes
+3
- -
- -
-

Poêle 'Metallic' induction

-

En aluminium
-

d30cm

-

16€99

- -
- -

Uniquement en magasin

-
-
-
- -
-
-

coup de coeur

-

-
-
- Chaise Florence - plusieurs gammes
- -
- -
-

Chaise Florence

-

En polypropylène
-

44.5x42/48.5xh.79cm

-

29€99

- -
- -

Ajouter au panier

-
-
-
-
-
-

coup de coeur

-

-
-
- Poêle - plusieurs gammes
+3
- -
- -
-

Poêle 'Metallic' induction

-

En aluminium
-

d26cm

-

13€99

- -
- -

Uniquement en magasin

-
-
-
- -
-
-

coup de coeur

-

-
-
- Chaise Florence - plusieurs gammes
- -
- -
-

Chaise Florence

-

En polypropylène
-

44.5x42/48.5xh.79cm

-

29€99

- -
- -

Ajouter au panier

-
-
-
-
-
-

coup de coeur

-

-
-
- Bac de rangement - plusieurs gammes
- -
- -
-

Bac de rangement 'Estanca' 50L gris

-

Certifié IP67
-

57.8x38.2xh31.8cm

-

17€99

- -
- -

Uniquement en magasin

-
-
-
-
-
-
-
- retour Top -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/lincroyable_local.png b/lincroyable_local.png deleted file mode 100644 index e470194f1e904a3088d2ff478b1781e2d85a6dac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48779 zcmdRW(`Z^CkOB?wQ&5>@{nzy_2x7$}-rmNnQg00Bku~$!`F_3#5rU@d_3B@tIN{ z1pxR5ASWrV?vZx5f~oi3bQ9w=)P#`0t&tr?0R4q34jQhc5@#<@@63oi^O48!>d1Nd zb)s8~?nz0jTif1Jet{-DL54*4XIMDK$Dd9gd#Fl3@UyTSR-O8rnYrs2{@DKd1L3YxE_)h?G zt;q42{R05)zpMcL&w!WzWdT|P0B`X{l~gQ{oi?@p)7JHy4L#kzia(gb$TCs zKkq@6m{B!LwG*U|LmsD7cL&hi+^h~QOLk>6ZD1NhtqEG1@&^MVF?Df}C8fd(Ie*$D z0)=h#lp4LegS>=tTPV|{_ z8`NkEoA|)Rhd)EgKkz=pC$}y%fj)hyeqB=6NrQF801yEz7MzA1wRLENXYA=(xXl+ci-#PE<~8(Jue@A%zd#CB$PX! z+9?y?0JaPq($=P@ew+CZkW}?Sf!=a$35D4$gKC1n1o*a>TmB2YssPm$g8ycoCcWVDQWth>-tXMSby(deQ7 zz)5yTL)w0+s&dGDw(2VM!R|gEo%|5E`?R>I+jE>#Zu{sXk(8)m|3O8l$d;A%b5@q|OJJj`~$Ldroz-*-#GFu-la14<+Y^`HF&CdGi&dXU31e za6*R!T=Ek?d$UmZId*sFiS6xCuJ}ri6cFe6J75~%#a;&vw_xKfv;KM-V;JSh@(?lO!)YPNh^MeD>&uPxx#f z74>Rw^kBN~22(euKIWyeR;@pG^uninx%bETV%=QiQkG!EPZ`3f00a^0zx#wF)7_U; zWM}k&^le+^TtBqS8;YCQo1j_R*w}oXcqXpste?!vE(7S1xTe4rH^|xy8;ifBhvlBg zYMj&^d@Lx_JyVKz-hz5=HLvuW98XM{9LRAKl$j_yW2zP2Umo-JPghwVKwf*>wJA^& z2=6ej)P@y32p$D1kuFZxAtNtYzE_-`metCSgkgNG_9zuR;uFU2S7A&;B^qOYl5Utw za#U;MmGOQ5A1=Q0Y2vn#ttq#cK5+{+Z_4@?^&DZ7uKn!2)oUe-e_(SycD26ZJ87=K zx+BxE^>;~7L%7VH(sur2s2j939;h*{j)I-pYPcfXKQ~Vp9(U8)BE;63m^f2;gQ`I(7~;Z5psgo0x?` zmZugs7bx4=o~N25COHM%i<8Su_r-j!?_ba~PWLo*DOkHxMvj#wOk#$WN*(?%o%+dJ z(#kwew$i@+EyRn0B0{faIf*=-$9(>H1vsT{a)Isd*}3KTbIPm)hSFexfO~PaDNYORQ^c6F9j$L|iqx)cVs{gC_d0cc&OZLAFs)9SXd*b2yV>7cjbU|)}Hn(J*`T6&CzyXnICx*){sDJ;?iSS zQRw`FMud6Iqw6ojWjy9SMuQ7skd16h@~cQljHy|Ii&;Vkr`Y2aR?+l^(v@oZ4TAH@2(;8O^OfDe6}#6Iwb$=088Y5h27SOKU;=w{k$`H*N6VWJ7^ zu}4iF{b+GfAhjvG^pI$_Bpbo#QZvnwNZ)H09f=mMb^KzXq?Y#^U*>hKB2qVB{PX)rd_f=Seb9 zAM%IsG=+d`xlxqg>ZUJ=U89c7WUQ^fj=M0jN1c`l3~T~&kJlr5FQVC&DU$RwPB?|X zrzD{@#zE)^Em@nQVNads3i;G#cQX;?k-Ml8gx;hCLxIR@^H{{*f~*t4TE4R0*3jm&`4BNbD)&mJeFkY|3&(WtBGX1h5r!~T}M zKTEHLuC}0Vjv8JVx$?9yRwF+p8Zm&`y(hN895(bii8xGgFV1q;q-}q;O>+yM$GSlY zWVZnWuaQj=nLJJ^DY#7sea;OJjHsgognFB%>p0_4ZFIGJm20I+ntT{Dli{P0=In^9f%rSa(p1?UXEXj<0 z+^}fYwUzsMDpyM}DL=oDaGd)4vnuY~B)f%qJ2@Mp_sz{`H)qCk-y$jxXjX1%oe>ZA zp{+6zJYlv2SO<^5zL*V#rQNRg++hmpNtOP0oTGHJ*M0eoCsllOweFJC802uB`Rx(b zr182#K_Gh-uPNd1&Js*d4z?J?tMjWN;;qjVo2QWrwR7!f+TATBbiqTmriL0`S4%BT zJ-T`|J8AvuAPgu-e!|nTlkR)7%`1*d5=iNH(V7%pu$?O>8u0Xrt^?qe z{e$S^cSRHqHQ2D^+RJX~FVp2;R5SIpb@Vh$7`UwOAjM4A=>3cH;=`}va-s{T{g;tI z1a2J4aCK7K;&hy8+oiKM`=g+h-p?M9a-=+7CJK5!o2!D+Ix?QsF|wJQEaNxIj3pCV zUZBSe9|*c8taRNp4hX5v=X09(_>z%$6THjnp7JrgwE z8l5bvTAOhP>Miow&GS;f*5U7N4P=be_OSdk9+X~E8b?YUz4CVFS43Du6#qYUXD?)+ zxGF+vI?}%$Vi_{NCVFj4+8Ys^onn^|iYbnF_fe!>d)hpnE!4zt^C2f z+P%Mz){$cK13_oXP3k84&7g6gSySdHn(A9&J-OO zLg(SJ-rnsge684Y+Kf#q)~uFD3#DbdGIpa{Y>M`=U6oB@kWJTHb!+uHC`>HVIf7bY z{NZD0jgPD;+V^-f)O%33e>9(bm3O*jLK>aVaJ)p)X4Z&h8Hy>APd{%w1eM$#KXs)t z-$nJqz_28R+vXRX z!#Lau(j?bcDYbi!YoM@4o1nRbuU=+51oS&XGEmAAmIoIKQL4vF=$|Uxf}rsk>vioL z&03Cl4RuK>c1f{y>o0ljw!rH9lw$KG(xk|Tx9N@flIp3_LKn-8+ogJm(^^OAhkuGi ze>XB$@;LnjCnn^jD>bMI^;ruQmK6$R>efkN>?C!s$Q8P@H1BRq9sd9=4lpF<_4ae8 zs@~o+%xqd)uT3eNrZa)ugy;!YF9^#r#MqxFCRa#v+dV(&z9(pNSb0 zS686s^vV(D&q{)bX%hGv%(&p6k{U^oo#4XE+`XhvqlRxgUO#c8$o96e9hA@qqjVsPZz~+b3|BRC?-EFk3m!xg- zhxY=-4627`Se>qa&ezP!e*fg{2p&bB;G6~={_}opi_F+uA_>G+l;E`eDM28Kj{+=` z>R%}7%biiIWnJtx%ap5f_5v#nRU`8$I@8$u+ER`^@g(9?Km>V z_KEG-Xy|HyIY6>RK3$L^(CJJtI=QC|EvmavfJ{LvgD&p+^pP+mrLQMJiVn{1UL-j3 zUQK}gjjL+;bYZMi4K zrmDU5vv6RqHgrnt!AIgAtAwAggxQZFEM0%$6;uON{PV%$T$N*jneziQh9-XcTgV?b zc%$<$#PnBoLhgTS0VID%S=mqI+P>RU%Z)cqU#{?K#g0T>gP0T-%WIWVIZN9#M|`Bz zaLZ$c2}fR?9JWr8pRhI6A;I5Os{qklQ)_Q2W9}w|8XB-p)6In|wNfPLEy#(5LX_n% z4tTknxo@@I%O%zNh0Wd~o{_|glyrMan(v8^WQr>q_Od%fx>3()M*DfA+k^!j7W;O* zb2_DGB6bN{BO)PyVZkg&kGcl7Z+}?GmmrO%FIa1=a~-bDdvRh%zUcHPRtRLV(a0hy zst>$DvijXY9!QPD-k1KUzL-jOCn44|6X?Ad+)OXc!@}2?c<=p@LB)|%n69H@OR`JE z9Q$#DLvMR`Y&2BpJ)uEja}t5s7E1)AuuSh)QRv{t*bA_yZn2JLhWFhCVlc=fXappr zC&`tY)DLI!_5>%>g^T$xxNWMY_DhDXg+adL=;$kEDm?J9M3>M}XVPIwphcGC;2cGN zV^zRflTtkip%8T0t(J7WuHD^%ZkKwyTjtFLZyg>Nl{qu?Wd#x(EDcOljyX;u*+$ax zvE<~9UG_9jT>#TrZMQALct3bCaSTc!c^EI-b#!_1$1-p(rTjwhgbn6c?`Eo7Q*CbwumyKVf(KyuZOE^AUxxUt? ziJ51PltpmVqu54vo5EflluerMh1U7*R38e|BoyMe(5=7f9-*W+dtebcSSz zeFrIw6!?~{_zDWgnKRAnM3OaIoGf@I`N1yrd~|$Lk|Wj4w(~}OHWXBFE^?;@Z7!?Z zlzrV3tL#*K=00h*Uc-tuh&tUUoITC0`>Fwt#^%# zQ%&i>+OvYa{7cx%9UR>W-#R8--GZ!BGPD+pH8a2%AU@;R3`jTYevKmd0SyUSHgwV| z&x=5WxpM+U!O7`7%zNiNFy#|Bs1wONPWM^9pFK3f63L{CjcL3{%tG9)-n#nSDe z6E-=#E|=nTm(j)eH2ovK@W~cx&n7-ynp>ZkZwodFNqavgvZ~VvE7`wxPG;(~9XA`M zY(1Kl=%b5Re9%uP>TwjAy@^mbT zb$Y0J<||Wi2j<$w+>w_dY-(<17DTLSFTF3~=lziA=F+k>xIkD%7iCvgr@uKbB`w)> znYmMm5uz+!MOS3?U;Y$^=!B@NGw@se9@@tTU)6$LrO(|c z*!M_9?dS6CakRq3mTuwZeFZu9x^S`-MyrZf$$~<(cJ4m+$By=tuJFNL8?`j5x0^yN zfdkPGkL$Vl6vu5Sb60XU#PQYG=`Tnr`E-Bfis3d>Zzv4Q!v@>=j!_C|Td&8=Gu!L6 zKGdgIY=mNpD}0IS>n|+#JvkC->lOhI$4)s_*_u7p8KVk}&O!4qX`R=6pnzVg$7aI* zx&*YNQrm@TL@vAi@J&L2jHzafvehQmlz)rFE7lcRy(_mUaut%Qzkye6>+C}Pw(tAd ze1t@L=0_W!#ug)&->oIm32f2_!RY%E^p^0Su2t_lH<-XuCX;YN_rYb6^Hd%n{|N_% ziK8G;*V<&|O4o;))ZDC*7P_L?g}(8}+MVZq+vi@%rB32DW0pJR`JUR`O{aho0k8Gh zDZcEvEilz-y1Bp=Wp#aJ*yhRxbTK(vZY)$d;COjcRMN6BXI4-Sy0|W!6V@RHLS~tb zsV8`>IK-ncI{iQU);PCw3m(YR1RS$`|)7&>JlhO2P%+PNCq97mA}#4oVY)QZp) zyB~IkX^YI*{3P%q*G<)_yV|VB)xr9%VVbW0qIT~-{7C2r!QqLi8>;yN09jug|Wm;dc)4#ANHb8{@U%UcVjA@LBqIRg!Ma+ACs#mODw?0;N+}gOC~-!f}DlI;KUw^p2*fzR{79|KnNZ zK)p%&iy$^b^Ex`ubhwWBvfgBGujZ#4U80#j?SGRZ3p-g<-H&Yc)2@1 z@tWtezu5?0@~faeVYTBj=616j({=3wYU+{%CX&3@Qgp0Rwl)tJsB2gD?T9@Oc3d?$ z1vlJLhPvLedokxtL=14v_nMI%L)8{vsht|5yf9<7h`ul^u(>_pyE#2YRC}WB;XhnY ziy#}cFEr*Y4OR_-zhxljb4)tS-vi_~D|RZ{|XMzBhxm=CL#**dSaM@cO8ji+}ecK%wD zUxhr6({M8u1MijfMFd`q)-SZ8p;ou#C|#?4N*?!l)Mu#HmNTTul#Tm>4id)cPBsVi z@>ZIb_$tYENqeq*`H0Vq+R$Fl-{MyEdklE{lQ6@HkM7Fd_}%pWPkqq~r$75P(+k}z zN|MJFYxhx@)#oe+Tb8YCCKbQ*hS>C{VeeA>lpt%=UW{6aOK10H{9I|kv7EQ!O1zi0 z^oOQe-rrJ)@hK_enCq{Q63fK3P2K(ElAZ{Ux%XL>Mt9j=UR_>z&Udcu3WH3l5kRH0F&&kej4@waa%i6_oUWR`$A4=obxL7|_B z<|m`4FPWXZ4;Sm*l`J4sN78Jo_=7B~OkR%N-mBTB>m;}s_THO0GzJ6$gPrk=%kL;e~P@w@kh@BqAP-qt1eI6C`w88dI=38#^uamDNYmu zMGEf9r~$`gPy-bJAS(_zIRMa<&E?q*H3@Y-u8?Z$qqg3uz~t6c>M-F(R+X~&zx=O4 z5>lS~ukruB;`9#%h4O#q2HXq9|6eb1wN_Y6=9p*t)^&HzK|$Wf%K}I*zeVM^l(uiX zD+mk>WRuX;(wfeF|8L!GLfp^qX)um~xA~KxU{mGD=x8Gw7z{=S+%uvZ?t&j5ANBO~ zT!^cxs$&1$!fpWlQcFwAN&9n#ceC9})8D^_czAfI053EYBClLD3>d_wzjM& zfQiq;4s)eCc$Adw{?C4>fUM4dd_M&R-`WC$Q6s=Hoh~Ym*$7cte|e+xPKKDjsF+yh z=NqDT0jr_?n|q&VGshVK01|!Qt69}tX;x6v<=sx^{Gq5(SJ0aOGh&b-lNPz2*6}Ry zK+%UoZm&l#IK*7{G6!Vg4%6xvjGe%V2S}7ll=rq>PAN3uJ}=p~;}a6biCWIq#Yw0! zu&>Hq4zxdAm5-+KuQj+sHi8)7tqmJ#TsPi>J8cj=g{DOfl-qCU5$ zH*5X|8X8i7rXO}sU$RdTH)~>UhYRxt{;Kq>Fg-@ppvbG5q#x#zh{AbT;(A zScGgYJTt|%L)C)YLS8*Np~WYI+~w%4RkpJ~9@JhnJDzQM3xdd%{ZBAyXmVvJeO=DC z1}{(LaC4A?IeL}?6x7^z#95${sT}c>l$5lrv{d(v#PAQ^H3EG6m?%zCg_m(V$D24C zqRDu0*25m1#~=7Hu+Zt{dQQwA7RTPO$9T;J(C2dZ8#lrcDY}zLZ@dyAPFCa=W<#(^ zt*opXdK8{1|M`c*V&VgIzB3TRl7fE1_EpEkfeRJD{nHyf56Jl&{^KvKYUlD!@&0{Dn^VtV+*?*cGB-j8F`g=(=J=$f`ZkBT#r_YLhit#*z`5HoaTQu2 z3)mTpBd^VinYI2Df2&B%`n~c&MRdAG%;IEX%?me208}P~Zzoxt%~LQ-h4eOVl2cD0+efOJLHJn<(7!@>})d{|xO+0E^S`s{;16yN3NuPKvoj39CGnhxW0N3Y@?g3h## zD)wM;Zp*J`QBM7qRjp;Dy>B=Yj5Dm<#TrR0F0^DYU3YI<1Wtw5Pd%aNat%i%@0EByEiIl|z#5Y8*p`oD znqU7y1`^Zk>}<`OI2Tgy9?fI6no@XKR2oZDr?noB^%uD^@l^3 z#iMAqnS}$MBlQLmxf07xgmm?>fjkt2eor%qI<7`!KNLJ&m5CqdoQtRFE zX?zOlI=nCb{j`%MJl`ouf`(dITGE9lN;8oBBa3e^h0E^tEJSx$Bpy#Bc0do;&fTL9 zJ+<}g!|VA@N1VY4zHox&nKhR)B=^*|uk=U;Xx$&1>_{?5_P!5>GgN9F(dGF#Az|L_JR zCBH-zcMODFlj8^x+Q`J7y%Cu-4H4l8^_8nRd859RSr7htHHo(0Rdg*n@iM(XPF!#* zxX8JpIUr+dgHsj`hpUJoYUVnBLF4;1UAN2TO76(|31pP>3v~Y}F?0I_L3&OX>AAP5 zuM+zGhNLxml}#e*s6c^b=g^$|gZ(Olu0*tHwt)_j7yrVPNq3(v~8E=2DG?;YMw3-G#>57$ZJqx4eil`#4 z^L~?T&2N*BqfNy$S^HmX4?5|Y51uE*L%DdfVDnbDX4#EWN1Xyjc@imi2tjV=JK2uOBD;US7uc^z9WkrNE|*vTkqeA^^&gbt1s$?nw6?& znRTy+>5QOQTn^En@hx^;EdT}0Gktx1C#lUocU(u_*M~Z;NrH1o^jjh&SBjA@PVtD~ zw^JP5wn8|#COWAJc{2@U1UgNoNEtZI#-_}ZTZt~DDWz&wUN{SD6^7M_2=hM|@2iyX z*``Q%p?d$_f)dt)6BW2MN*27-o%w8+cnYhzozFIUcQU+>+n*8aIB63!P`$>ShmPLf zUZ=)1m+2qX#P3Fh@E3evRqr|QVl&joow&e;1hZR@*mKo2T4Pd&al|G1kA_ zR_c%%e|$Qlr-`oaOuXf}6@$Xf*$zD&&5=HRQebyk`xi zIZ_Cvnh<%pzP^s0m9$uC5?Wn-A%QEBx9HgU^Tj3C0`mTSkB{H$y3Qmfh9pB%ci8~T&JhHx=NA{bj8WbXNcv&j z8ftt5tKAt(rvcm$h$PxBH%!7{DH)j432N2z+~dL*m(waq2?=Gower=zEW4=7Xm46o zO-yo;CG?dUULhP(zQ{{?o7}0D9yppH&se=05P7~lBY+FkX z@M`Sp`l6<$cC~~AMKSHm+U8~a8;ZI+((1yhY?PRm9g#4ZO4CRrM(wMh^fDFUaC-`9lGS;evWyb0lqjwGMS@wPNQYF^6_aor~P z(HuZ=i6H=-3ifHvYfA4-I|`bkjvIMA*M9DpnT2Qw<`NPQsyof+h|Zc8_ce@|Yt5hgx_9m}vCde`%q6C1 z{1}&VP!|Y+HxU}zfsi?W1eunxKMxCF!)~`1$GnUA;c0H?@}2_tDnvZ>H8c)Epmgm(NSdXpWeRLIT;y=4+Z$}6TJgWD7@@3+(65Sj=T}Se*xHa zCbJ2uX1E7au8X_F^G@uG908&m#H1p^ZWXC<_(*?d9CAP1`rVKUJ} z1LJ`dVUj|-sLxfBn7vc|6A97p(Ec}GwohndZeOsRpZuRh&nErJdO)2M_5b#Qp8Oh7 zgp2Wz?GXa9C>iphFL`{?Ej;{2Rpf1kuopvQ6ht-DG$i|d9}HEkA@n`i z|KMe$`2l5D4Ox@A#A1B_`%KKkzI;Zazd=PMPbz_6o{Cuc*mAtgvw=2j6+3^!xWKAL zwu5FugQt9qWJhUX#|DAe6x%fop6k|Ybld@tn>5VZ`#3Vye;xLP-$}*Ji-MtA9A$jJQ9ivW`|RHRiwb8s!eIPq_UX4AL`uh&;k&Yr70S&93Z)F{gnQQ8Qa`)1$o29u z?*IzE8IavF(*yEk?>iK}L`3I;ITq$206>SYl)~TT^q`uW>9MJvPWg$61#LZjpq{FC zVGCd3*Dp;!j<>3NP3t2{WDT76&&wDJ-=q`6YcJ{#f=zkLFg%Pj0SNz%QR-u5!(9^Q z>~sAy(t3Hh`rbljv{L}k8C5VVe_KWQ4Ex&Es(q><7;gy&aJ3$CKUi zD|cku`%S_?BtP9k?`pA*g0fxImwOq5_la`PvI9x72Xb2AGs;^9#n*e*8_=+Fv-dB& zmi!TBJ;^^<->p-}K8l`$amM zAc9lAy?8XPlo@t!$#J;PQ0haq4w>gX!tq^Rp)~lvgPgS^WP&n7&)03l$W#RU5p`wx z0s8vn8)iz)j~Brl+-fFXvjWC77`~*gq6e|X6j+xEfqFJC7s8Q!^UGp>$C}yQWMJ?c zi-}gpk{m^9=CV>=4vq=}h&I-psGVli+|Pan&-7N!--%1T;ks$G4#iw^(}CZ_3AYc)XI{Rn_@Ze2 z-&#P=((JyMCgxs7Ct6t};m5@Jm=ZK+dQCN8X6%Z-lAkW>NIG=74%b8J`uA0hbKsUZJ}F~^w|s`uOCY1gEURoq+5U_I+v2NrG`>faZ^%BYIa(VYrt!Fe&4j5-_P$e4Vx@%+^K`MK}sZvx*v$Oy(-Cc}gXzteC-2`0+w^$Kps z{`p|d;c=9_iu&xpD`(;yw=)dYqc?nWYj-6YYI_mS4<|)GWBF303153S8i+|5{GkNC z5Zt#%@tEXk*Mdj=C7C{XJ6n6seq*pmJZo-+H+lJhfbCs&QTT$G)mRYYCn-|RHNyv) zy*@2A8(aSr@Ugg4g>&)jR%6Tj#0l{fSkO*+ixkIISVlZd9?3{a2RpeJrM7Eru@jS3 zV-55WyPu*fyU8xXi%YDw@-7ZbM|^}2r>Y{50Y%T~4r9t#5dpE)UGPTf!<%xyczMVq9h+*n1 zewd{e9+Id02>qfL34X(tPgoo0C*TqgMB3}}_w+5uzHXv-_2;FM)bQ01(X|4urAyMyr6+E&pi7= zrlq}C8#MYvN+5aR}c zkVF%0>>e2>xHfON{h{lJkbX>j+uceQT|HFTPgyr5@!*%V~N*Nvl^_rUP z+)Vjb3F?BOxKTuZ*bEAXAKe_UkoQ8${JzDRx5qDFt-3NUmF^BiKR8>45vVXtu%f_h z9i28<3O#D@>97sp-IIDblG&#K9#W!rpKXQ6pO}j{r)X%X&lTZdyw?uKp&pzcZk|PF z1z2o;r>UstVJ-gC>ZDeflEVY=#`$MbML-*}y7!U8dbTjrLuT_wS#SV_{$5yUM0$nd zH?1G$&(~?s$IVrD&lEZAnj*a61dU1RF8OD)UR5>cMXJjO_R+hDm^x6`%SBj-#ZDvh;=0W`$|~Wes!6AF>hagwM)QIhpw%wWuO+p zG?6~NhI%Si-#00i<8Q;NSYPIu>^;K1^5&^d72+q8G~b$`X2fj&J)>ztmS6??G&<%| zT@pO9@VBho7>SdVI9GXrleCJ}i$ZG|w&w4!XdWB9ox1ezW8f05H-l8i)`X!`sODaf zy2=mR>2th?!r|euh!Y>2S2apw<${!q0KB0TX{{nG1(|9 zC8E5pf0~B1{ozcXM4nimSp-GPejnU!hlkRm*S3_`mKGm>wdsSSY3Z)vaQggSNipxg z^;{_0{HSzj9(U0$6s!If_FiVq%WNbU@8vW**Tbj*Pv%<{w7X|bCDV;Fi*fV0=RVi> zkTZV|_mE~h=WGNDCBvw__=3VavGPPlR3cwn@b=g7kBy*?*b_K8hWoKu%@KH#^ zqC-L^@8s04NFVsO+u(!x+%a?4!VR{G4VM(}{ud3}$rsNTHyBIp#I>v^qJ(hc@fx7= z_|OEGj*zt6g=S*^A}^1SxfKih4b5fr$e*^m7>!*i@#^YDql~%~dj!T}Eq(y`0&9MH zYEU*SqU?2zU(TNUd^7jVo;#A{>D{Mm>pu8NH`H&6_6@^KfC{ z>f-p^D$?`6^ZJ{8SCL-i;4Rpto@%L?ndm(=b!3bwB)_pY@?Km>J9RS0@ZR`0H@6ng zw4JtKw$xw?IG9lj?K=n1s_c<15mh&;4B|Jt+xxf7UW80wHPu^a$o}(zQK=ODxH6^N zR;~c+e&WPs+eP}`HFYx#0=h);c4Rt6J0ya*;9{~7_yZ#QOV;;^qylCc1UfR4W zY3lC&bTeeAbugY7nt6L!`7eu^uLTwwbcu@0j&~>OXFS~u<0<=zW>WY}y2-h>T#OP! z@3nuGs}*Jpb$c>(MK`mlD3EK~KgE3}m7as!e+1jb`0-Bay8uXHd4=>R8nV8YTeK`( ztLc(0f?x0h+);FY9A8+>Jo=4xFWVBmD|)>1C*OIkYkTls!MQ}yhrFSaq~u^E^9{iW z_D5f%@n6+CSaw>^TO*Rm_3h#L&zZe*!Eh_R(luynf)v_pXWzh|Mb*$_k-g{oXE(6- zu!ym;Nb)_al6wu{ZcT{&kJz8%d_QsDCohkHq$<_U4W~W!Li;JgRp{I%8VC$d*3lMZ z_?39ZmHEy0qo`7&wS#lbU%OC;8opf9fn}{+eScw(us-ne<(G3X&{UsI6BW-4QJCLw zIq^_VPU!cTE^rs!Ob^ewidUteJc-isXA+hqw%demJF|QAg+wcnbCaC zGg(*nD4Jx{j6kB&^yRa3yzU1cWqEkGw3`m|bK89YOh}~JB$EMk7sCL0^hQ>?0&X;brK|fo+`Y~t zWV$NMuLd!@2&ZlDMYtX)r0GV9hRYAYnok3X6o}&aQ4LLTNDru?HM6r zD#Y#WM3ti9=z`^c^zNfs3&s3>xqOu(u(>XVaWcjx&Z;lDdQ!0!m#BN?xKu4goXjbL?f;^n8a!~3-v{#{W$mRe07BHT)QmJ-g2cRZRid=*!T zqlA&a(??S@skc!%jJP>QgDw_W=KWsUfhlS%P|Tdn&VH!z3y~!!9%Mw0TkibmzWkXZ zhf3VK-eZB*je6IBs%_g)r Pg)@FP>Rpb1jM&P%Ws3gmnVMSA^HNj`EC1` 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 Qibg||6qEY`5z+Vi9}{NAv;Y7A diff --git a/lincroyable_local_dump.html b/lincroyable_local_dump.html deleted file mode 100644 index 4aac0b5..0000000 --- a/lincroyable_local_dump.html +++ /dev/null @@ -1,1600 +0,0 @@ - - - - - - - - iphone - L'Incroyable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - - - - - - - - -
- -
-
-
-

Recherche

26 résultats pour votre recherche "iphone"

- -
-
- -
-
-
- - - -
-
- -
-
-
-
-
- retour Top -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/list_missing_sites.py b/list_missing_sites.py deleted file mode 100644 index 6644217..0000000 --- a/list_missing_sites.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -Script to list all sites in database and check which ones are missing from search_config.py -""" -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent)) - -from app.database import SessionLocal -from app.models import SearchSite -from app.core.search_config import SITE_CONFIGS - -def main(): - """List all sites in DB and identify missing configurations""" - db = SessionLocal() - - try: - # Get all sites from database - sites = db.query(SearchSite).order_by(SearchSite.name).all() - - print(f"\n{'='*80}") - print(f"Sites in Database: {len(sites)}") - print(f"Sites in SITE_CONFIGS: {len(SITE_CONFIGS)}") - print(f"{'='*80}\n") - - # Check each DB site - missing = [] - configured = [] - - for site in sites: - domain_clean = site.domain.replace("www.", "").lower() - - # Check if configured - is_configured = False - for key in SITE_CONFIGS.keys(): - if key in domain_clean or domain_clean in key: - configured.append((site.id, site.name, site.domain, key)) - is_configured = True - break - - if not is_configured: - missing.append((site.id, site.name, site.domain)) - - # Display results - if configured: - print("✅ CONFIGURED SITES:") - for sid, name, domain, key in configured: - print(f" [{sid:2d}] {name:20s} ({domain:25s}) → {key}") - - if missing: - print(f"\n❌ MISSING {len(missing)} SITES:") - for sid, name, domain in missing: - print(f" [{sid:2d}] {name:20s} ({domain})") - else: - print("\n✅ All sites are configured!") - - print(f"\n{'='*80}\n") - - finally: - db.close() - -if __name__ == "__main__": - main() diff --git a/reproduce_gifi_scrape.py b/reproduce_gifi_scrape.py deleted file mode 100644 index 360129e..0000000 --- a/reproduce_gifi_scrape.py +++ /dev/null @@ -1,67 +0,0 @@ -import asyncio -import logging -import sys -from playwright.async_api import async_playwright - -# Configure logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -URL = "https://www.gifi.fr/meuble-et-deco/decoration/bougie-et-senteur/diffuseur-et-senteur/encens-nag-champa-15-g/000000000000540823.html" - -async def reproduce_scrape(): - logger.info("Starting reproduction script...") - async with async_playwright() as p: - # Launch browser (headless=True by default which is what we want for reproduction usually) - # But for debugging blocking, sometimes headless=False helps. Let's start with True (default) - browser = await p.chromium.launch(headless=True) - context = await browser.new_context( - viewport={"width": 1920, "height": 1080}, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" - ) - - page = await context.new_page() - - logger.info(f"Navigating to {URL}...") - try: - await page.goto(URL, wait_until="domcontentloaded", timeout=60000) - logger.info("Page loaded.") - - # Wait a bit for dynamic content - await page.wait_for_timeout(5000) - - # Extract title - title = await page.title() - logger.info(f"Page Title: {title}") - - # Extract body text - content = await page.content() - body_text = await page.inner_text("body") - - logger.info(f"Content Length: {len(content)}") - logger.info(f"Body Text Length: {len(body_text)}") - - # Check for price - if "€" in body_text: - logger.info("Found '€' in body text.") - else: - logger.warning("'€' NOT found in body text.") - - # specific check for likely price - import re - prices = re.findall(r'\d+[,\.]\d{2}\s*€', body_text) - logger.info(f"Prices found in text: {prices}") - - # Save content for review - with open("gifi_reproduction.html", "w", encoding="utf-8") as f: - f.write(content) - logger.info("Saved gifi_reproduction.html") - - except Exception as e: - logger.error(f"Error during navigation/scraping: {e}") - - finally: - await browser.close() - -if __name__ == "__main__": - asyncio.run(reproduce_scrape()) diff --git a/reset_admin.py b/reset_admin.py deleted file mode 100644 index c99f940..0000000 --- a/reset_admin.py +++ /dev/null @@ -1,31 +0,0 @@ -from app.database import SessionLocal -from app.services import auth_service -from app import models - -def reset_admin(): - db = SessionLocal() - try: - user = auth_service.get_user_by_username(db, "admin") - if user: - print("Found admin user. Resetting password...") - auth_service.update_password(db, user, "admin") - print("Password reset to 'admin'") - - # Verify - print("Verifying login...") - auth_user = auth_service.authenticate_user(db, "admin", "admin") - if auth_user: - print("SUCCESS: Login verified!") - else: - print("ERROR: Login failed after reset!") - else: - print("Admin user not found. Creating...") - auth_service.create_user(db, "admin", "admin", is_admin=True) - print("Admin user created with password 'admin'") - except Exception as e: - print(f"Error: {e}") - finally: - db.close() - -if __name__ == "__main__": - reset_admin() diff --git a/run_cleanup.py b/run_cleanup.py deleted file mode 100644 index 9aaed22..0000000 --- a/run_cleanup.py +++ /dev/null @@ -1,53 +0,0 @@ -import sys -import os -import logging -from sqlalchemy.orm import Session - -# Add project root to path -sys.path.append(os.getcwd()) - -from app.database import SessionLocal, engine -from app.models import Catalogue - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - - -def cleanup_catalogs(): - db = SessionLocal() - try: - logger.info("Starting cleanup...") - deleted_count = 0 - - # 1. Delete catalogs with 0 pages - bad_catalogs = db.query(Catalogue).filter(Catalogue.nombre_pages == 0).all() - for cat in bad_catalogs: - logger.info(f"Deleting empty catalog: {cat.titre}") - db.delete(cat) - deleted_count += 1 - - # 2. Delete catalogs with iconic/bad images - all_catalogs = db.query(Catalogue).all() - for cat in all_catalogs: - if not cat.image_couverture_url: - continue - - if any( - x in cat.image_couverture_url.lower() - for x in ["icon", "logo", "loader", "facebook", "twitter", "assets/img"] - ): - logger.info(f"Deleting catalog with bad image: {cat.titre} ({cat.image_couverture_url})") - db.delete(cat) - deleted_count += 1 - - db.commit() - logger.info(f"Cleanup complete. Deleted {deleted_count} catalogs.") - - except Exception as e: - logger.error(f"Error during cleanup: {e}") - finally: - db.close() - - -if __name__ == "__main__": - cleanup_catalogs() diff --git a/stokomani.fr_dump.html b/stokomani.fr_dump.html deleted file mode 100644 index 837301c..0000000 --- a/stokomani.fr_dump.html +++ /dev/null @@ -1,18100 +0,0 @@ - - - - - - - - - Recherche : 31 résultats trouvés pour « chaise » – Stokomani - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Passer au contenu -
- -
- -
-
- - -
-
-
- - -
-
-
- -
- - -
-
-
- -
-
- -
-
-
-
- -
-
-
- - - -
- - -
-
- - - - - - - -
-
- - - - -
- - -
-
- -
-
- - - - - - - - - - - - - - - -
- - -
- - - - - - - -
- - -
- -
\ No newline at end of file diff --git a/stokomani.fr_failed_verification.html b/stokomani.fr_failed_verification.html deleted file mode 100644 index c7dbbe9..0000000 --- a/stokomani.fr_failed_verification.html +++ /dev/null @@ -1,18100 +0,0 @@ - - - - - - - - - Recherche : 31 résultats trouvés pour « chaise » – Stokomani - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Passer au contenu -
- -
- -
-
- - -
-
-
- - -
-
-
- -
- - -
-
-
- -
-
- -
-
-
-
- -
-
-
- - - -
- - -
-
- - - - - - - -
-
- - - - -
- - -
-
- -
-
- - - - - - - - - - - - - - - -
- - -
- - - - - - - -
- - -
- -
\ No newline at end of file diff --git a/stokomani_raw.html b/stokomani_raw.html deleted file mode 100644 index 596b11f..0000000 --- a/stokomani_raw.html +++ /dev/null @@ -1,32156 +0,0 @@ - - - - - - - - - - - - Recherche : 31 résultats trouvés pour « chaise » – Stokomani - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Passer au contenu -
- -
- -
-
- - -
-
-
- - -
-
-
- -
- - -
-
-
- -
-
- -
-
-
-
- -
-
-
- - - -
- - -
-
- - - - - - - -
-
- - - - -
- - -
-
- -
-
- - - - - - - - - - - - - - - -
- - - - - - - -
- - -
- - -
- diff --git a/task.md b/task.md index d6628b5..a2f60bb 100644 --- a/task.md +++ b/task.md @@ -1,29 +1,21 @@ -# Investigation et Correction du Statut "Produit Retiré" +# Nettoyage et Commit du Projet ## Contexte -Le système de suivi des prix marquait incorrectement certains produits comme "retirés" alors qu'ils étaient toujours disponibles, souvent à cause de blocages (bot detection) ou de changements mineurs de titre. +Après avoir corrigé les problèmes de disponibilité d'Action.com et Amazon, il est nécessaire de nettoyer le répertoire racine des scripts de diagnostic et fichiers temporaires avant de committer les changements. ## Focus Actuel -Finalisation et vérification. +Identification et suppression des fichiers inutiles, suivi du commit et push. ## Master Plan -- [x] Analyser la logique de comparaison de titres dans `app/services/scheduler_service.py` -- [x] Vérifier si les scrapers extraient correctement les titres lors des mises à jour -- [x] Identifier les cas limites (edge cases) où la similarité de titre échoue -- [x] Corriger la logique pour éviter les faux positifs d'indisponibilité -- [x] Vérifier la correction avec un exemple concret (test_logic.py) -- [x] Implémenter le reset automatique de disponibilité si un prix est trouvé +- [ ] Identifier les fichiers non essentiels à l'application +- [ ] Supprimer les fichiers de diagnostic et scripts temporaires +- [ ] Vérifier que l'application fonctionne toujours (build/syntaxe) +- [ ] Committer les changements vers le dépôt Git +- [ ] Pusher les changements ## Log de Progression -- [x] Investigation terminée : identification des faux positifs dus aux titres de blocage (Cloudflare, etc.) et aux placeholders ("Loading"). -- [x] Logique de normalisation renforcée. -- [x] Détection des bots ajoutée pour tous les sites. -- [x] Auto-reset de `is_available` implémenté dans `_update_db_result`. -- [x] Correction déployée dans `scheduler_service.py`. -- [x] Affinage de la détection de bot pour Stokomani/L'Incroyable (détection conditionnelle au titre). -- [x] Support des versions "V2" et matching par mots pour les noms courts sur Amazon. -- [x] Sécurisation du flux Action.com (check d'indisponibilité déplacé après le titre). +- [ ] Planification du nettoyage commencée. diff --git a/test_amazon_scraper.py b/test_amazon_scraper.py deleted file mode 100755 index 9a008b1..0000000 --- a/test_amazon_scraper.py +++ /dev/null @@ -1,187 +0,0 @@ -#!/usr/bin/env python3 -""" -Script de test pour le scraper Amazon France -Teste le système anti-détection et l'extraction des produits -""" - -import asyncio -import logging -import sys -from pathlib import Path - -# Ajouter le répertoire app au path -sys.path.insert(0, str(Path(__file__).parent)) - -from app.services.amazon_scraper_service import amazon_scraper_service, AmazonScraperService -from app.core.search_config import AMAZON_PROXY_LIST_RAW, USER_AGENT_DATA - -# Define missing variable for test compatibility -AMAZON_USER_AGENTS = USER_AGENT_DATA -USER_AGENT_POOL = USER_AGENT_DATA - - -# Configuration du logging -logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") - -logger = logging.getLogger(__name__) - - -# Monkey patch _connect_browser to use local launch for testing -async def _connect_browser_local(p): - logger.info("Launching local browser (headless)...") - return await p.chromium.launch(headless=True) - - -AmazonScraperService._connect_browser = _connect_browser_local - - -async def test_basic_search(): - """Test basique de recherche""" - logger.info("=" * 80) - logger.info("TEST 1: Recherche basique - 'aspirateur'") - logger.info("=" * 80) - - products = await amazon_scraper_service.scrape_search("aspirateur", max_results=5) - - if not products: - logger.error("❌ Aucun produit trouvé - possibilité de détection ou problème réseau") - return False - - logger.info(f"✅ {len(products)} produits trouvés") - - for idx, product in enumerate(products, 1): - logger.info(f"\n{idx}. {product.title[:60]}...") - logger.info( - f" 💰 Prix: {product.price}€" + (f" (était {product.original_price}€)" if product.original_price else "") - ) - logger.info(f" ⭐ Note: {product.rating}/5" if product.rating else " ⭐ Pas de note") - logger.info(f" 📦 {'En stock' if product.in_stock else 'Indisponible'}") - logger.info(f" {'🚚 Prime' if product.prime else '📮 Standard'}") - logger.info(f" {'📢 Sponsorisé' if product.sponsored else '🔍 Organique'}") - - return True - - -async def test_multiple_queries(): - """Test avec plusieurs requêtes différentes""" - logger.info("\n" + "=" * 80) - logger.info("TEST 2: Requêtes multiples") - logger.info("=" * 80) - - queries = ["clavier", "souris", "casque"] - results = {} - - for query in queries: - logger.info(f"\n🔍 Recherche: '{query}'") - products = await amazon_scraper_service.scrape_search(query, max_results=3) - results[query] = len(products) - logger.info(f" ✅ {len(products)} produits trouvés") - - # Délai entre requêtes pour respecter les bonnes pratiques - await asyncio.sleep(3) - - logger.info("\n📊 Résumé:") - for query, count in results.items(): - logger.info(f" • {query}: {count} produits") - - total = sum(results.values()) - if total > 0: - logger.info(f"\n✅ Total: {total} produits extraits") - return True - else: - logger.error("\n❌ Aucun produit extrait - problème possible") - return False - - -async def test_anti_detection(): - """Test du système anti-détection""" - logger.info("\n" + "=" * 80) - logger.info("TEST 3: Vérification anti-détection") - logger.info("=" * 80) - - # Updated to just check if we can run - logger.info("Skipping specific proxy/agent checks for this service as it handles them internally") - - logger.info(f"✓ {len(AMAZON_PROXY_LIST_RAW)} proxies disponibles") - logger.info(f"✓ {len(USER_AGENT_POOL)} User-Agents standards") - logger.info(f"✓ {len(AMAZON_USER_AGENTS)} User-Agents Amazon spécifiques") - - # Test proxy - # Test proxy - import random - - proxy = random.choice(AMAZON_PROXY_LIST_RAW) if AMAZON_PROXY_LIST_RAW else None - if proxy: - # Extract just the IP for logging (hide credentials) - proxy_parts = proxy.split("@") - proxy_server = proxy_parts[1] if len(proxy_parts) > 1 else proxy - logger.info(f"✓ Proxy test: {proxy_server}") - else: - logger.warning("⚠️ Pas de proxy configuré") - - # Test d'une recherche simple - logger.info("\n🧪 Test de recherche avec anti-détection...") - products = await amazon_scraper_service.scrape_search("livre", max_results=3) - - if products: - logger.info(f"✅ Anti-détection fonctionnel - {len(products)} produits extraits") - return True - else: - logger.error("❌ Échec - possibilité de blocage") - return False - - -async def run_all_tests(): - """Lance tous les tests""" - logger.info("\n" + "=" * 80) - logger.info("🚀 DÉMARRAGE DES TESTS DU SCRAPER AMAZON FRANCE") - logger.info("=" * 80) - - tests = [ - ("Recherche basique", test_basic_search), - ("Requêtes multiples", test_multiple_queries), - ("Anti-détection", test_anti_detection), - ] - - results = {} - - for test_name, test_func in tests: - try: - logger.info(f"\n▶️ Exécution: {test_name}") - success = await test_func() - results[test_name] = "✅ PASS" if success else "❌ FAIL" - except Exception as e: - logger.error(f"❌ Erreur dans {test_name}: {e}", exc_info=True) - results[test_name] = "❌ ERROR" - - # Résumé final - logger.info("\n" + "=" * 80) - logger.info("📊 RÉSUMÉ DES TESTS") - logger.info("=" * 80) - - for test_name, result in results.items(): - logger.info(f"{result} - {test_name}") - - passed = sum(1 for r in results.values() if "PASS" in r) - total = len(results) - - logger.info(f"\n🎯 Score: {passed}/{total} tests réussis") - - if passed == total: - logger.info("✅ TOUS LES TESTS ONT RÉUSSI!") - return True - else: - logger.warning("⚠️ Certains tests ont échoué") - return False - - -if __name__ == "__main__": - try: - success = asyncio.run(run_all_tests()) - sys.exit(0 if success else 1) - except KeyboardInterrupt: - logger.info("\n⏸️ Tests interrompus par l'utilisateur") - sys.exit(130) - except Exception as e: - logger.error(f"❌ Erreur fatale: {e}", exc_info=True) - sys.exit(1) diff --git a/test_carrefour_api.py b/test_carrefour_api.py deleted file mode 100644 index 9b53484..0000000 --- a/test_carrefour_api.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Test Carrefour via API endpoint (production-like) -""" -import requests -import json - -url = "http://localhost:8555/api/search" -params = { - "q": "chaise", - "sites": "7", # Carrefour site ID - "max_results": 10 -} - -print(f"Testing: {url}") -print(f"Params: {params}\n") - -response = requests.get(url, params=params, stream=True) - -print(f"Status: {response.status_code}") -print(f"Headers: {dict(response.headers)}\n") - -count = 0 -for line in response.iter_lines(): - if line: - try: - # Each line should be JSON - data = json.loads(line.decode('utf-8')) - count += 1 - - title = data.get('title', 'N/A')[:60] - price = data.get('price', 'N/A') - image = "✅" if data.get('image_url') else "❌" - - print(f"{count}. [{image}] {title} - {price}€") - - if count >= 10: - break - except json.JSONDecodeError as e: - print(f"JSON Error: {e}") - print(f"Line: {line[:100]}") - -print(f"\nTotal: {count} results") diff --git a/test_carrefour_prices.py b/test_carrefour_prices.py deleted file mode 100644 index 9347cf8..0000000 --- a/test_carrefour_prices.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Test if current Carrefour price extraction works -""" -import asyncio -import sys -sys.path.insert(0, '/app') - -from app.services.improved_search_service import ImprovedSearchService - -async def main(): - print("Initializing browser...") - await ImprovedSearchService.initialize() - - print("Searching Carrefour for 'chaise'...\n") - results = [] - count = 0 - async for result in ImprovedSearchService.search_site_generator("carrefour.fr", "chaise"): - results.append(result) - count += 1 - price_status = f"{result.price}€" if result.price else "N/A" - print(f"{count}. {result.title[:55]} - {price_status}") - if count >= 10: - break - - print(f"\n==> Got {len(results)} results") - - # Count prices - with_prices = sum(1 for r in results if r.price) - print(f"Products with prices: {with_prices}/{len(results)}") - - if with_prices == len(results): - print("✅ SUCCESS: All products have prices!") - else: - print(f"⚠️ WARNING: {len(results) - with_prices} products missing prices") - - print("\nShutting down...") - await ImprovedSearchService.shutdown() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/test_carrefour_search.py b/test_carrefour_search.py deleted file mode 100644 index def6b68..0000000 --- a/test_carrefour_search.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Test script to trigger Carrefour search and generate HTML dump -""" -import asyncio -import sys -sys.path.insert(0, '/app') - -from app.services.improved_search_service import ImprovedSearchService - -async def main(): - print("Initializing browser...") - await ImprovedSearchService.initialize() - - print("Searching Carrefour for 'chaise'...") - results = [] - async for result in ImprovedSearchService.search_site_generator("carrefour.fr", "chaise"): - results.append(result) - print(f"Found: {result.title}") - - print(f"\nTotal results: {len(results)}") - - print("Shutting down...") - await ImprovedSearchService.shutdown() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/test_centrakor_filter.py b/test_centrakor_filter.py deleted file mode 100644 index cd4ceb5..0000000 --- a/test_centrakor_filter.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Test detailed logging for Centrakor image extraction -""" -import asyncio -import sys -sys.path.insert(0, '/app') - -from playwright.async_api import async_playwright -from bs4 import BeautifulSoup -import re - -async def main(): - playwright = await async_playwright().start() - browser = await playwright.chromium.connect_over_cdp("ws://browserless:3000") - - context = await browser.new_context( - viewport={"width": 1920, "height": 1080}, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - ) - - page = await context.new_page() - - await page.goto("https://www.centrakor.com/search/chaise", wait_until="networkidle") - content = await page.content() - - soup = BeautifulSoup(content, "html.parser") - products = soup.select("div.product-item") - - print(f"Testing first product:\n") - - first = products[0] - - # Get all images - img_els = first.select("img.responsive-image__actual") - print(f"Found {len(img_els)} images with selector") - - for i, img_el in enumerate(img_els): - print(f"\n=== Image {i+1} ===") - candidate_url = img_el.get("src") - print(f"URL: {candidate_url}") - - # Test filters - if any(keyword in candidate_url.lower() for keyword in ['picto', 'icon', 'logo', 'badge']): - print(" ❌ Filtered: Contains picto/icon/logo/badge keyword") - continue - - width_match = re.search(r'width=(\d+)', candidate_url) - height_match = re.search(r'height=(\d+)', candidate_url) - print(f" Width match: {width_match.group(1) if width_match else None}") - print(f" Height match: {height_match.group(1) if height_match else None}") - - if width_match and height_match: - width = int(width_match.group(1)) - height = int(height_match.group(1)) - print(f" Dimensions: {width}x{height}") - if width < 100 and height < 100: - print(f" ❌ Filtered: Too small ({width}x{height})") - continue - else: - print(f" ✅ PASS: Large enough ({width}x{height})") - else: - print(" ✅ PASS: No dimensions in URL") - - await context.close() - await browser.close() - await playwright.stop() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/test_centrakor_images.py b/test_centrakor_images.py deleted file mode 100644 index e8eb106..0000000 --- a/test_centrakor_images.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Test Centrakor image extraction -""" -import asyncio -import sys -sys.path.insert(0, '/app') - -from app.services.improved_search_service import ImprovedSearchService - -async def main(): - print("Initializing browser...") - await ImprovedSearchService.initialize() - - print("Searching Centrakor for 'chaise'...\n") - results = [] - count = 0 - async for result in ImprovedSearchService.search_site_generator("centrakor.com", "chaise"): - results.append(result) - count += 1 - has_image = "✅" if result.image_url else "❌" - print(f"{count}. {has_image} {result.title[:55]} - {result.price}€") - if result.image_url: - print(f" Image: {result.image_url[:70]}...") - if count >= 10: - break - - print(f"\n==> Got {len(results)} results") - - # Count images - with_images = sum(1 for r in results if r.image_url) - print(f"Products with images: {with_images}/{len(results)}") - - if with_images == len(results): - print("✅ SUCCESS: All products have images!") - else: - print(f"⚠️ WARNING: {len(results) - with_images} products missing images") - - print("\nShutting down...") - await ImprovedSearchService.shutdown() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/test_gifi_final.py b/test_gifi_final.py deleted file mode 100644 index a969b43..0000000 --- a/test_gifi_final.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -Final test of Gifi with new selectors -""" -import asyncio -import sys -sys.path.insert(0, '/app') - -from app.services.improved_search_service import ImprovedSearchService - -async def main(): - print("Initializing browser...") - await ImprovedSearchService.initialize() - - print("Searching Gifi for 'chaise'...") - results = [] - async for result in ImprovedSearchService.search_site_generator("gifi.fr", "chaise"): - results.append(result) - print(f"✓ {result.title[:50]} - {result.price}€") - - print(f"\n==> Total: {len(results)} results") - - if results: - print("\nFirst 3 products:") - for i, r in enumerate(results[:3]): - print(f"{i+1}. Title: {r.title}") - print(f" Price: {r.price}€") - print(f" URL: {r.url[:80]}...") - print() - - print("Shutting down...") - await ImprovedSearchService.shutdown() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/test_gifi_prices.py b/test_gifi_prices.py deleted file mode 100644 index ec8232c..0000000 --- a/test_gifi_prices.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -Test Gifi price extraction -""" -import asyncio -import sys -sys.path.insert(0, '/app') - -from app.services.improved_search_service import ImprovedSearchService - -async def main(): - print("Initializing browser...") - await ImprovedSearchService.initialize() - - print("Searching Gifi for 'chaise'...") - results = [] - count = 0 - async for result in ImprovedSearchService.search_site_generator("gifi.fr", "chaise"): - results.append(result) - count += 1 - print(f"{count}. {result.title[:60]} - Price: {result.price}€") - if count >= 5: # Only test first 5 - break - - print(f"\n==> Got {len(results)} results") - - # Check if all prices are the same - prices = [r.price for r in results if r.price] - if prices: - unique_prices = set(prices) - print(f"Unique prices: {unique_prices}") - if len(unique_prices) == 1: - print("⚠️ WARNING: All prices are the same!") - - print("Shutting down...") - await ImprovedSearchService.shutdown() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/test_gifi_prices_final.py b/test_gifi_prices_final.py deleted file mode 100644 index e5be347..0000000 --- a/test_gifi_prices_final.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -Test fixed Gifi price extraction -""" -import asyncio -import sys -sys.path.insert(0, '/app') - -from app.services.improved_search_service import ImprovedSearchService - -async def main(): - print("Initializing browser...") - await ImprovedSearchService.initialize() - - print("Searching Gifi for 'chaise'...\n") - results = [] - count = 0 - async for result in ImprovedSearchService.search_site_generator("gifi.fr", "chaise"): - results.append(result) - count += 1 - print(f"{count}. {result.title[:60]} - {result.price}€") - if count >= 10: - break - - print(f"\n==> Got {len(results)} results") - - # Check price diversity - prices = [r.price for r in results if r.price] - if prices: - unique_prices = set(prices) - print(f"Unique prices: {sorted(unique_prices)}") - print(f"Price range: {min(prices)}€ - {max(prices)}€") - if len(unique_prices) > 1: - print("✅ SUCCESS: Multiple different prices found!") - else: - print("⚠️ WARNING: All prices are the same") - - print("\nShutting down...") - await ImprovedSearchService.shutdown() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/test_gifi_search.py b/test_gifi_search.py deleted file mode 100644 index f8e8a37..0000000 --- a/test_gifi_search.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Test script to analyze Gifi HTML structure -""" -import asyncio -import sys -sys.path.insert(0, '/app') - -from app.services.improved_search_service import ImprovedSearchService - -async def main(): - print("Initializing browser...") - await ImprovedSearchService.initialize() - - print("Searching Gifi for 'chaise'...") - results = [] - async for result in ImprovedSearchService.search_site_generator("gifi.fr", "chaise"): - results.append(result) - print(f"Found: {result.title} - Price: {result.price}") - - print(f"\nTotal results: {len(results)}") - - print("Shutting down...") - await ImprovedSearchService.shutdown() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/test_soup.py b/test_soup.py deleted file mode 100644 index 3df7bef..0000000 --- a/test_soup.py +++ /dev/null @@ -1,29 +0,0 @@ -from bs4 import BeautifulSoup -import sys - -try: - with open("/app/debug_dumps/stokomani.fr_failed_verification.html", "r", encoding="utf-8") as f: - content = f.read() - - print(f"Read {len(content)} bytes") - - soup = BeautifulSoup(content, "html.parser") - print("Soup created") - - selector = "div.product-card" - items = soup.select(selector) - print(f"Found {len(items)} items with selector '{selector}'") - - if items: - item = items[0] - print("First item classes:", item.get("class")) - - title_selector = "h3.product-card__title a" - title_el = item.select_one(title_selector) - if title_el: - print("Title found:", title_el.get_text(strip=True)) - else: - print(f"Title NOT found with '{title_selector}'") - -except Exception as e: - print(f"Error: {e}") diff --git a/verify_catalog_fix.py b/verify_catalog_fix.py deleted file mode 100644 index abc2f5e..0000000 --- a/verify_catalog_fix.py +++ /dev/null @@ -1,43 +0,0 @@ -import asyncio -import logging -import sys -import os - -# Identify workspace root -sys.path.append(os.getcwd()) - -from app.services.cataloguemate_scraper import scrape_catalog_pages - -# Setup logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - - -async def verify_scraper(): - # Gifi catalog URL from the browser session - url = "https://www.cataloguemate.fr/gifi/catalogue-du-mardi-16122025-61964/" - - print(f"Verifying scraper on: {url}") - - pages = await scrape_catalog_pages(url) - - print(f"Found {len(pages)} pages.") - - if not pages: - print("FAIL: No pages found.") - return - - # check first page image - first_img = pages[0]["image_url"] - print(f"Page 1 Image: {first_img}") - - if "thumbor" in first_img or "leafletscdns" in first_img: - print("SUCCESS: Image is a Thumbor/Leaflet URL.") - elif "icon" in first_img or "logo" in first_img: - print("FAIL: Image appears to be an icon/logo.") - else: - print(f"WARNING: Image URL is: {first_img}") - - -if __name__ == "__main__": - asyncio.run(verify_scraper()) diff --git a/verify_search_price.py b/verify_search_price.py deleted file mode 100644 index 00bd5ca..0000000 --- a/verify_search_price.py +++ /dev/null @@ -1,45 +0,0 @@ -import asyncio -import logging -import sys -import os - -# Identify workspace root -sys.path.append(os.getcwd()) - -# Mock Env Vars -os.environ["DATABASE_URL"] = "postgresql://user:password@localhost:5432/pricewatch" -os.environ["BROWSERLESS_URL"] = "ws://localhost:3012" # Ignored by patch, but good for completeness - -from app.services.improved_search_service import ImprovedSearchService -from playwright.async_api import async_playwright - -# Setup logging -logging.basicConfig(level=logging.DEBUG) # DEBUG level to see price extraction logic -logger = logging.getLogger(__name__) - - -# Monkey patch _connect_browser to use local launch -async def _connect_browser_local(p): - logger.info("Launching local browser (headless)...") - return await p.chromium.launch(headless=True) - - -ImprovedSearchService._connect_browser = _connect_browser_local - - -async def test_search(): - await ImprovedSearchService.initialize() - - query = "nintendo switch" - target_site = "gifi.fr" - - print(f"Searching for: {query} on {target_site}") - - async for result in ImprovedSearchService.search_site_generator(target_site, query): - print(f"[{result.source}] {result.title}\n -> Price: {result.price}€\n -> URL: {result.url}") - - await ImprovedSearchService.shutdown() - - -if __name__ == "__main__": - asyncio.run(test_search()) diff --git a/verify_standalone.py b/verify_standalone.py deleted file mode 100644 index 072636d..0000000 --- a/verify_standalone.py +++ /dev/null @@ -1,108 +0,0 @@ -import asyncio -import logging -import re -import sys - -# Mock logger -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - - -# Fallback implementation of _fetch_with_fallback for standalone test -async def _fetch_with_fallback(url): - # We need to install httpx for this to work - try: - import httpx - - headers = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" - } - async with httpx.AsyncClient(verify=False, timeout=30.0) as client: - response = await client.get(url, headers=headers) - return response.text - except ImportError: - print("Please pip install httpx strict") - return "" - - -async def scrape_catalog_pages_standalone(catalog_url: str): - from bs4 import BeautifulSoup - - print(f"Scraping: {catalog_url}") - html_content = await _fetch_with_fallback(catalog_url) - - if not html_content: - print("Failed to fetch content") - return [] - - soup = BeautifulSoup(html_content, "html.parser") - - # --- COPIED LOGIC FROM cataloguemate_scraper.py --- - main_image_url = None - max_area = 0 - - # Strategy 1: Look for specific container/class identified in browser inspection - candidates = soup.select(".letaky-grid-preview img") - - # Strategy 2: Fallback to all images if specific container not found - if not candidates: - candidates = soup.find_all("img") - - print(f"Found {len(candidates)} candidates") - - for img in candidates: - # Check multiple attributes for the real image URL - src = img.get("src") or img.get("data-src") or img.get("data-original") - - if not src: - continue - - # Skip common UI elements - refined list - if any( - x in src.lower() - for x in [ - "logo", - "icon", - "facebook", - "twitter", - "instagram", - "loader", - "spinner", - "market", - "googleplay", - "appstore", - ] - ): - continue - - # Strong Signal: URL contains 'thumbor' or 'leafletscdns' (host for catalog images) - is_thumbor = "thumbor" in src.lower() or "leafletscdns" in src.lower() - - # Calculate area if dimensions exist - width = img.get("width") - height = img.get("height") - area = 0 - if width and height: - try: - area = int(width) * int(height) - except: - pass - - if is_thumbor: - if area > max_area or (area == 0 and max_area == 0): - max_area = area - main_image_url = src - print(f"Match (Thumbor): {src}") - elif area > 50000: - if area > max_area: - max_area = area - main_image_url = src - print(f"Match (Size): {src}") - - return main_image_url - - -if __name__ == "__main__": - url = "https://www.cataloguemate.fr/gifi/catalogue-du-mardi-16122025-61964/" - result = asyncio.run(scrape_catalog_pages_standalone(url)) - print(f"FINAL RESULT: {result}")