Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
38 changes: 3 additions & 35 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions env_example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
BOT_API=your_token
CHAT_ID=your_id
DISCORD_WEBHOOK_URL=your_discord_webhook_url_here
LANGUAGE=en
106 changes: 89 additions & 17 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,26 +23,69 @@
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}

# Bot message fetch variables:
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):
Expand All @@ -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()
Expand All @@ -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()
Expand Down
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ selenium==4.11.2
webdriver-manager
requests
certifi
urllib3
urllib3<2.0

python-dotenv
25 changes: 19 additions & 6 deletions scraperHelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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...")
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = [
Expand Down