diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..345498d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# 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 \ + gnupg \ + unzip \ + 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/* + +# 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/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/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 diff --git a/env_example b/env_example new file mode 100644 index 0000000..8694402 --- /dev/null +++ b/env_example @@ -0,0 +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 a1e103b..74cc9ed 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} @@ -26,18 +32,60 @@ 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: -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): - 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): @@ -57,6 +105,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() @@ -82,42 +148,48 @@ def send_telegram_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() 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 = [