From 6877c2002e10dde8eb4c6aff78375d775a4bf5a8 Mon Sep 17 00:00:00 2001 From: Rev <162006joel@gmail.com> Date: Sat, 27 Dec 2025 15:55:07 +0800 Subject: [PATCH 1/8] Fix infinite notification loop and improve scraper robustness - Fixed bug where cart_status was never updated, causing spam.\n- Replaced hardcoded sleeps with WebDriverWait in scraperHelpers.py.\n- Pinned urllib3<2.0 for Selenium stability.\n- Made pygame optional to prevent crashes on systems without audio support. --- main.py | 23 +++++++++++++++++++---- requirements.txt | 4 +++- scraperHelpers.py | 25 +++++++++++++++++++------ 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/main.py b/main.py index a1e103b..b677278 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,12 @@ from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options -import pygame +try: + import pygame + PYGAME_AVAILABLE = True +except ImportError: + PYGAME_AVAILABLE = False + print("Pygame not properly installed. Audio alerts will be disabled.") from webdriver_manager.chrome import ChromeDriverManager from dotenv import load_dotenv import os @@ -18,7 +23,8 @@ sleep_min_seconds = config["sleep_min_seconds"] sleep_max_seconds = config["sleep_max_seconds"] -pygame.mixer.init() +if PYGAME_AVAILABLE: + pygame.mixer.init() cart_status = {item["url"]: False for item in urls_to_check} @@ -36,8 +42,14 @@ # This fcn is for notification sound def play_sound(sound_file): - pygame.mixer.music.load(sound_file) - pygame.mixer.music.play() + if not PYGAME_AVAILABLE: + print(f"🎵 (Audio disabled) Sound alert would play: {sound_file}") + return + try: + pygame.mixer.music.load(sound_file) + pygame.mixer.music.play() + except Exception as e: + print(f"Error playing sound: {e}") # This fcn is for sending messages def send_telegram_message(message): @@ -94,6 +106,7 @@ def send_telegram_message(message): print(f"ALERT: {message}") play_sound('Crystal.mp3') send_telegram_message(message) + cart_status[url] = True else: print(f"Checked {url} - no stock found for sizes {', '.join(sizes_to_check)}.") elif store == "bershka": @@ -103,6 +116,7 @@ def send_telegram_message(message): print(f"ALERT: {message}") play_sound('Crystal.mp3') send_telegram_message(message) + cart_status[url] = True else: print(f"Checked {url} - no stock found for sizes {', '.join(sizes_to_check)}.") elif store == "stradivarius": @@ -112,6 +126,7 @@ def send_telegram_message(message): print(f"ALERT: {message}") play_sound('Crystal.mp3') send_telegram_message(message) + cart_status[url] = True else: print(f"Checked {url} - no stock found for sizes {', '.join(sizes_to_check)}.") else: diff --git a/requirements.txt b/requirements.txt index df52e30..15bb0c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,6 @@ selenium==4.11.2 webdriver-manager requests certifi -urllib3 \ No newline at end of file +urllib3<2.0 + +python-dotenv \ No newline at end of file diff --git a/scraperHelpers.py b/scraperHelpers.py index f26b889..051db1d 100644 --- a/scraperHelpers.py +++ b/scraperHelpers.py @@ -16,9 +16,10 @@ def check_stock_zara(driver, sizes_to_check): try: # GitHub Actions için daha kısa timeout is_github_actions = os.getenv('GITHUB_ACTIONS') - timeout = 10 if is_github_actions else 60 + timeout = 10 if is_github_actions else 20 wait = WebDriverWait(driver, timeout) + # Close the cookie alert if it appears try: print("Checking for cookie alert...") @@ -31,6 +32,7 @@ def check_stock_zara(driver, sizes_to_check): # Wait for "Add to Cart" button to be clickable try: + print("Looking for 'Add to Cart' button...") add_to_cart_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button[data-qa-action='add-to-cart']"))) # Check if an overlay is blocking the button @@ -42,8 +44,11 @@ def check_stock_zara(driver, sizes_to_check): # Click "Add to Cart" using JavaScript to bypass any hidden overlays driver.execute_script("arguments[0].click();", add_to_cart_button) print("Clicked 'Add to Cart' button.") - except (TimeoutException, ElementClickInterceptedException) as e: - print(f"Failed to click 'Add to Cart' button: {e}") + except TimeoutException: + print("Add to cart button not found (Product might be OOS or removed).") + return None + except ElementClickInterceptedException as e: + print(f"Could not click 'Add to Cart' button: {e}") return None # Wait for the size selector to appear @@ -131,7 +136,12 @@ def check_stock_bershka(driver, sizes_to_check): wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "ul[data-qa-anchor='productDetailSize']"))) # Allow extra time for dynamic class updates to finish - time.sleep(3) # Give JS time to update classes (can be adjusted or replaced by smarter wait below) + # Allow extra time for dynamic class updates to finish + try: + wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, "button[data-qa-anchor='sizeListItem']"))) + except TimeoutException: + print("Size elements did not appear in time.") + return None size_buttons = driver.find_elements(By.CSS_SELECTOR, "button[data-qa-anchor='sizeListItem']") sizes_found = {size: False for size in sizes_to_check} @@ -181,7 +191,7 @@ def check_stock_stradivarius(driver, sizes_to_check): try: # GitHub Actions için optimize edilmiş timeout is_github_actions = os.getenv('GITHUB_ACTIONS') - timeout = 10 if is_github_actions else 60 + timeout = 10 if is_github_actions else 20 wait = WebDriverWait(driver, timeout) # Close the cookie alert if it appears @@ -226,7 +236,10 @@ def check_stock_stradivarius(driver, sizes_to_check): # Wait for the size selector to appear print("Waiting for sizes to appear...") - time.sleep(2) # Give some time for size selector to load + # Wait for the size selector to appear + print("Waiting for sizes to appear...") + # Replaced static sleep with dynamic wait logic below + # Try different size selector patterns for Stradivarius size_selectors = [ From 6c003c7e09851ddb98e6cf9be0eeb187b21f2763 Mon Sep 17 00:00:00 2001 From: Rev <162006joel@gmail.com> Date: Sat, 27 Dec 2025 16:14:33 +0800 Subject: [PATCH 2/8] Add Discord Webhook support - Added optional DISCORD_WEBHOOK_URL configuration.\n- Implemented send_discord_message function.\n- Integrated Discord notifications into the check loop alongside Telegram. --- env_example | 3 +++ main.py | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 env_example diff --git a/env_example b/env_example new file mode 100644 index 0000000..63d2657 --- /dev/null +++ b/env_example @@ -0,0 +1,3 @@ +BOT_API=your_token +CHAT_ID=your_id +DISCORD_WEBHOOK_URL=your_discord_webhook_url_here diff --git a/main.py b/main.py index b677278..1a8b378 100644 --- a/main.py +++ b/main.py @@ -32,13 +32,21 @@ load_dotenv() BOT_API = os.getenv("BOT_API") CHAT_ID = os.getenv("CHAT_ID") +DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL") # Foolproof for not having .env and bot installed: -if not BOT_API or not CHAT_ID: +TELEGRAM_ENABLED = False +if BOT_API and CHAT_ID: + TELEGRAM_ENABLED = True +else: print("BOT_API or CHAT_ID not found in .env file. Telegram messages will be disabled.") - TELEGRAM_ENABLED = False + +DISCORD_ENABLED = False +if DISCORD_WEBHOOK_URL: + DISCORD_ENABLED = True + print("Discord Webhook found. Discord messages will be enabled.") else: - TELEGRAM_ENABLED = True + print("DISCORD_WEBHOOK_URL not found in .env file. Discord messages will be disabled.") # This fcn is for notification sound def play_sound(sound_file): @@ -69,6 +77,24 @@ def send_telegram_message(message): except requests.exceptions.RequestException as e: print(f"Failed to send Telegram message: {e}") + except requests.exceptions.RequestException as e: + print(f"Failed to send Telegram message: {e}") + +# This fcn is for sending Discord messages +def send_discord_message(message): + if not DISCORD_ENABLED: + return + + payload = { + "content": message + } + try: + response = requests.post(DISCORD_WEBHOOK_URL, json=payload, timeout=10) + response.raise_for_status() + print("Discord message sent.") + except requests.exceptions.RequestException as e: + print(f"Failed to send Discord message: {e}") + while True: # Crate service & initialize chrome_options = Options() @@ -106,6 +132,7 @@ def send_telegram_message(message): print(f"ALERT: {message}") play_sound('Crystal.mp3') send_telegram_message(message) + send_discord_message(message) cart_status[url] = True else: print(f"Checked {url} - no stock found for sizes {', '.join(sizes_to_check)}.") @@ -116,6 +143,7 @@ def send_telegram_message(message): print(f"ALERT: {message}") play_sound('Crystal.mp3') send_telegram_message(message) + send_discord_message(message) cart_status[url] = True else: print(f"Checked {url} - no stock found for sizes {', '.join(sizes_to_check)}.") @@ -126,6 +154,7 @@ def send_telegram_message(message): print(f"ALERT: {message}") play_sound('Crystal.mp3') send_telegram_message(message) + send_discord_message(message) cart_status[url] = True else: print(f"Checked {url} - no stock found for sizes {', '.join(sizes_to_check)}.") From 4a710214ad4da1da658a214d55e3d0b5b884b9b6 Mon Sep 17 00:00:00 2001 From: Rev <162006joel@gmail.com> Date: Sat, 27 Dec 2025 16:20:57 +0800 Subject: [PATCH 3/8] Add multi-language support (English/Turkish) --- config.json | 38 +++----------------------------------- env_example | 1 + main.py | 48 ++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 42 insertions(+), 45 deletions(-) diff --git a/config.json b/config.json index 6372b41..c53cd5c 100644 --- a/config.json +++ b/config.json @@ -2,43 +2,11 @@ "urls": [ { "store": "zara", - "url": "https://www.zara.com/tr/en/contrast-puffer-jacket-p08073227.html?v1=431955923&utm_campaign=productShare&utm_medium=mobile_sharing_iOS&utm_source=red_social_movil", + "url": "https://www.zara.com/ph/en/3-pack-of-basic-medium-weight-t-shirts-p01887399.html?v1=502981451&v2=2458839", "sizes": [ - "S" + "L" ], - "person": "Gülsüm" - }, - { - "store": "zara", - "url": "https://www.zara.com/share/dokulu-midi-elbise-p04813323.html?v1=453988435&utm_campaign=productShare&utm_medium=mobile_sharing_iOS&utm_source=red_social_movil", - "sizes": [ - "S" - ], - "person": "Şule" - }, - { - "store": "zara", - "url": "https://www.zara.com/share/firfirli-halter-elbise-p01165301.html?v1=446346759&utm_campaign=productShare&utm_medium=mobile_sharing_iOS&utm_source=red_social_movil", - "sizes": [ - "S" - ], - "person": "Şule" - }, - { - "store": "zara", - "url": "https://www.zara.com/share/yelpaze-kupe-p01011032.html?v1=449637895&utm_campaign=productShare&utm_medium=mobile_sharing_iOS&utm_source=red_social_movil", - "sizes": [ - "S" - ], - "person": "Şule" - }, - { - "store": "zara", - "url": "https://www.zara.com/share/halter-yaka-cicekli-poplin-top-p03152086.html?v1=450245434&utm_campaign=productShare&utm_medium=mobile_sharing_iOS&utm_source=red_social_movil", - "sizes": [ - "S" - ], - "person": "Gülsüm" + "person": "Joel" } ], "sleep_min_seconds": 60, diff --git a/env_example b/env_example index 63d2657..8694402 100644 --- a/env_example +++ b/env_example @@ -1,3 +1,4 @@ BOT_API=your_token CHAT_ID=your_id DISCORD_WEBHOOK_URL=your_discord_webhook_url_here +LANGUAGE=en diff --git a/main.py b/main.py index 1a8b378..74cc9ed 100644 --- a/main.py +++ b/main.py @@ -32,7 +32,35 @@ load_dotenv() BOT_API = os.getenv("BOT_API") CHAT_ID = os.getenv("CHAT_ID") +BOT_API = os.getenv("BOT_API") +CHAT_ID = os.getenv("CHAT_ID") DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL") +LANGUAGE = os.getenv("LANGUAGE", "en") + +MESSAGES = { + "en": { + "stock_found": "🛍️ {size} size in stock!!!!\n👤Person: {person}\nLink: {url}", + "person_unknown": "Unknown", + "checking": "For url {url} ({person}):", + "no_stock": "Checked {url} - no stock found for sizes {sizes}.", + "store_not_supported": "Store not supported", + "error": "An error occurred with URL {url}: {error}" + }, + "tr": { + "stock_found": "🛍️{size} beden stokta!!!!\n👤Kişi: {person}\nLink: {url}", + "person_unknown": "Bilinmeyen", + "checking": "Url {url} için ({person}): ", + "no_stock": "Checked {url} - no stock found for sizes {sizes}.", + "store_not_supported": "Store not supported", + "error": "An error occurred with URL {url}: {error}" + } +} + +# Fallback to English if language not found +if LANGUAGE not in MESSAGES: + LANGUAGE = "en" + +t = MESSAGES[LANGUAGE] # Foolproof for not having .env and bot installed: TELEGRAM_ENABLED = False @@ -120,48 +148,48 @@ def send_discord_message(message): url = item.get("url") store = item.get("store") sizes_to_check = item.get("sizes", []) - person = item.get("person", "Bilinmeyen") + person = item.get("person", t["person_unknown"]) driver.get(url) print("--------------------------------") - print(f"Url {url} için ({person}): ") + print(t["checking"].format(url=url, person=person)) if store == "zara": # Check stock for the specified sizes size_in_stock = check_stock_zara(driver, sizes_to_check) if size_in_stock: - message = f"🛍️{size_in_stock} beden stokta!!!!\n👤Kişi: {person}\nLink: {url}" + message = t["stock_found"].format(size=size_in_stock, person=person, url=url) print(f"ALERT: {message}") play_sound('Crystal.mp3') send_telegram_message(message) send_discord_message(message) cart_status[url] = True else: - print(f"Checked {url} - no stock found for sizes {', '.join(sizes_to_check)}.") + print(t["no_stock"].format(url=url, sizes=', '.join(sizes_to_check))) elif store == "bershka": size_in_stock = check_stock_bershka(driver, sizes_to_check) if size_in_stock: - message = f"🛍️{size_in_stock} beden stokta!!!!\n👤Kişi: {person}\nLink: {url}" + message = t["stock_found"].format(size=size_in_stock, person=person, url=url) print(f"ALERT: {message}") play_sound('Crystal.mp3') send_telegram_message(message) send_discord_message(message) cart_status[url] = True else: - print(f"Checked {url} - no stock found for sizes {', '.join(sizes_to_check)}.") + print(t["no_stock"].format(url=url, sizes=', '.join(sizes_to_check))) elif store == "stradivarius": size_in_stock = check_stock_stradivarius(driver, sizes_to_check) if size_in_stock: - message = f"🛍️{size_in_stock} beden stokta!!!!\n👤Kişi: {person}\nLink: {url}" + message = t["stock_found"].format(size=size_in_stock, person=person, url=url) print(f"ALERT: {message}") play_sound('Crystal.mp3') send_telegram_message(message) send_discord_message(message) cart_status[url] = True else: - print(f"Checked {url} - no stock found for sizes {', '.join(sizes_to_check)}.") + print(t["no_stock"].format(url=url, sizes=', '.join(sizes_to_check))) else: - print("Store not supported") + print(t["store_not_supported"]) except Exception as e: - print(f"An error occurred with URL {url}: {e}") + print(t["error"].format(url=url, error=e)) finally: print("Closing the browser...") driver.quit() From 834fb87b749a1759501dd3a213a501806408d0af Mon Sep 17 00:00:00 2001 From: Rev <162006joel@gmail.com> Date: Sat, 27 Dec 2025 16:33:22 +0800 Subject: [PATCH 4/8] Add Docker support --- Dockerfile | 26 ++++++++++++++++++++++++++ docker-compose.yml | 15 +++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9e0f01c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# Use official Python image +FROM python:3.9-slim + +# Install system dependencies (Chrome + utilities) +RUN apt-get update && apt-get install -y \ + wget \ + gnupg \ + unzip \ + && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ + && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' \ + && apt-get update && apt-get install -y \ + google-chrome-stable \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Copy requirements and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Run the application +CMD ["python", "main.py"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..19f8b3f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.8' + +services: + inditex-tracker: + build: . + container_name: inditex_stock_tracker + restart: unless-stopped + volumes: + # Mount config so you can edit it without rebuilding + - ./config.json:/app/config.json + env_file: + - .env + environment: + # Optional overrides + - LANGUAGE=en From 5f1df9dda5cbf59ab33c022300fe75558f418a6b Mon Sep 17 00:00:00 2001 From: Rev <162006joel@gmail.com> Date: Sat, 27 Dec 2025 22:59:36 +0800 Subject: [PATCH 5/8] build: Update Dockerfile's Chrome repository key management. --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9e0f01c..9f80b93 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,8 +6,10 @@ RUN apt-get update && apt-get install -y \ wget \ gnupg \ unzip \ - && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ - && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' \ + ca-certificates \ + && mkdir -p /etc/apt/keyrings \ + && wget -q -O /etc/apt/keyrings/google-chrome.asc https://dl-ssl.google.com/linux/linux_signing_key.pub \ + && echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/google-chrome.asc] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \ && apt-get update && apt-get install -y \ google-chrome-stable \ && apt-get clean && rm -rf /var/lib/apt/lists/* From ea0769076479c7e082261228a02c6c2853a31fa2 Mon Sep 17 00:00:00 2001 From: Rev <162006joel@gmail.com> Date: Sat, 27 Dec 2025 23:11:31 +0800 Subject: [PATCH 6/8] Add Docker support for easy deployment --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 9f80b93..345498d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,9 @@ # Use official Python image FROM python:3.9-slim +# Disable Python output buffering (so logs appear in real-time) +ENV PYTHONUNBUFFERED=1 + # Install system dependencies (Chrome + utilities) RUN apt-get update && apt-get install -y \ wget \ From be74037fd7b67d62d7a35078a4f37e3abd79bc24 Mon Sep 17 00:00:00 2001 From: Rev <162006joel@gmail.com> Date: Sat, 3 Jan 2026 09:34:35 +0800 Subject: [PATCH 7/8] fix: Ensure proper network configuration in Docker Compose for my local server --- docker-compose.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 19f8b3f..ab10c0b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,3 +13,9 @@ services: environment: # Optional overrides - LANGUAGE=en + networks: + - proxy + +networks: + proxy: + external: true \ No newline at end of file From 77bf3f51139ddf9db5f0fed7ee77b47f0e155f11 Mon Sep 17 00:00:00 2001 From: Rev <162006joel@gmail.com> Date: Sat, 3 Jan 2026 09:35:59 +0800 Subject: [PATCH 8/8] Revert "fix: Ensure proper network configuration in Docker Compose for my local server" This reverts commit be74037fd7b67d62d7a35078a4f37e3abd79bc24. --- docker-compose.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ab10c0b..19f8b3f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,9 +13,3 @@ services: environment: # Optional overrides - LANGUAGE=en - networks: - - proxy - -networks: - proxy: - external: true \ No newline at end of file