diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7216c9c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+# Prevent users from accidentally committing secrets.
+.venv/
+
+.idea/
diff --git a/MCSA.py b/MCSA.py
index 0a1cf5d..a86b088 100644
--- a/MCSA.py
+++ b/MCSA.py
@@ -2,37 +2,55 @@
import sys
import time
import smtplib
+from datetime import datetime as dt
from dotenv import load_dotenv
from selenium import webdriver
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
+from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
-def set_store_cookie(driver):
- # Get the main store page URL so you don't overload the product webpage. Selenium limitation
- driver.get("https://www.microcenter.com")
+STORES = {
+ 'Tustin': '101',
+ 'Denver': '181',
+ 'Miami': '185',
+ 'Duluth': '065',
+ 'Marietta': '041',
+ 'Chicago': '151',
+ 'Westmont': '025',
+ 'Indianapolis': '165',
+ 'Overland Park': '191',
+ 'Cambridge': '121',
+ 'Rockville': '085',
+ 'Parkville': '125',
+ 'Madison Heights': '055',
+ 'St. Louis Park': '045',
+ 'Brentwood': '095',
+ 'Charlotte': '175',
+ 'North Jersey': '075',
+ 'Westbury': '171',
+ 'Brooklyn': '115',
+ 'Flushing': '145',
+ 'Yonkers': '105',
+ 'Columbus': '141',
+ 'Mayfield Heights': '051',
+ 'Sharonville': '071',
+ 'St. Davids': '061',
+ 'Houston': '155',
+ 'Dallas': '131',
+ 'Fairfax': '081',
+}
- # Wait for the page to load, adjust as needed (seconds)
- time.sleep(0)
+# Update this with whatever products you want to monitor.
+PRODUCTS = {
+ 'fan': 'https://www.microcenter.com/product/667290/lian-li-uni-fan-reverse-sl-infinity-fluid-dynamic-bearing-120mm-case-fan-white',
+ 'cpu': 'https://www.microcenter.com/product/687907/amd-ryzen-7-9800x3d-granite-ridge-am5-470ghz-8-core-boxed-processor-heatsink-not-included',
+}
- # Set the storeSelected cookie. Default: 131 (Dallas store)
- driver.add_cookie({
- 'name': 'storeSelected',
- 'value': '131',
- 'domain': '.microcenter.com',
- 'path': '/',
- 'secure': True,
- 'httpOnly': False
- })
- # Get the product page URL
- driver.get("https://www.microcenter.com/product/687907/amd-ryzen-7-9800x3d-granite-ridge-am5-470ghz-8-core-boxed-processor-heatsink-not-included")
-
- # Wait for the page to load, adjust as needed (seconds)
- time.sleep(0)
-def prompt():
+def prompt_for_email_details():
# Prompts the user for email and password securely
print("Security Disclaimer:\n"
"Your personal information can only be accessed by a system administrator while the program is running.\n"
@@ -48,24 +66,22 @@ def prompt():
os.environ["email"] = input("Enter your email: ")
os.environ["password"] = input("Enter your password: ")
-def test_email():
- # Send the test email
+
+def send_test_email():
try:
# Get the environment variables
email_user = os.getenv("email")
email_password = os.getenv("password")
- # Confirm whether email credentials were entered
- if not email_user or not email_password:
- raise Exception("Test email credentials not found")
-
# Set up the email
msg = MIMEMultipart()
msg['From'] = email_user
msg['To'] = email_user
msg['Subject'] = 'Test Email Successful'
- msg.attach(MIMEText('This is a test email to confirm that your email login is correctly working alongside SMTP within the Microcenter Stock Python Application.\n\n'
- 'You may now let the application run in the background while it actively checks stock of your selected Microcenter product.','plain'))
+ msg.attach(MIMEText(
+ 'This is a test email to confirm that your email login is correctly working alongside SMTP within the Microcenter Stock Python Application.\n\n'
+ 'You may now let the application run in the background while it actively checks stock of your selected Microcenter product.',
+ 'plain'))
# Connect to the email server and send the email
with smtplib.SMTP("smtp.gmail.com", 587) as server:
@@ -73,29 +89,94 @@ def test_email():
server.login(email_user, email_password)
server.sendmail(email_user, email_user, msg.as_string())
- print("Test email sent successfully!")
-
- # Test email exception
+ print(f"[{dt.now()}] Test email sent successfully!")
except Exception as e:
- print(f"Failed to send test email: {e}")
- print("Application will continue without user email")
+ print(f"[{dt.now()}] Failed to send test email: {e}")
+ print(f"[{dt.now()}] Application will continue without user email")
+
+
+def prompt_for_product():
+ print(f"The following products are enabled: {', '.join(PRODUCTS.keys())}")
+
+ os.environ["selected_product"] = input('Select your desired product: ').strip().lower()
+
+ while not os.getenv("selected_product") or os.getenv("selected_product") not in PRODUCTS:
+ print('Invalid product selected')
+ os.environ["selected_product"] = input('Select your desired product: ').strip().lower()
+
+
+# Helper method to generate the list of stores to their IDs if needed.
+# Since Micro Center store locations are pretty static,
+# we can just run this once and update the mapping.
+def get_stores():
+ # Set up Chrome options to enable headless mode
+ chrome_options = Options()
+ chrome_options.add_argument("--headless") # Enable headless mode
+ chrome_options.add_argument("--disable-gpu") # Disable GPU acceleration
+ chrome_options.add_argument("--no-sandbox") # Fix potential environment issues
+
+ driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options)
+ driver.get('https://www.microcenter.com')
+
+ # Wait for the page to load, adjust as needed (seconds)
+ time.sleep(1)
+
+ store_selector_elements = driver.find_elements(By.CSS_SELECTOR,
+ '.changeMyStore ul.dropdown-menu li.dropdown-itemLI')
+ stores = {}
+ for store_element in store_selector_elements:
+ store_name = store_element.find_element(By.CLASS_NAME, 'storeName').get_attribute('innerText')
+ store_id = store_element.get_attribute('class').split()[-1].split('_')[
+ -1] # e.g., 'store_041' -> ['store', '041']
+ stores[store_name] = store_id
+
+ driver.quit()
+
+ return stores
+
+
+def set_current_store(driver, store_id):
+ # Get the main store page URL so you don't overload the product webpage. Selenium limitation
+ driver.get("https://www.microcenter.com")
-def send_email():
+ # Wait for the page to load, adjust as needed (seconds)
+ time.sleep(0)
+
+ # Micro Center uses the 'storeSelected' cookie to determine the currently selected store. Default: 131 (Dallas store)
+ driver.add_cookie({
+ 'name': 'storeSelected',
+ 'value': store_id,
+ 'domain': '.microcenter.com',
+ 'path': '/',
+ 'secure': True,
+ 'httpOnly': False
+ })
+
+
+def navigate_to_product(driver, product_link):
+ driver.get(product_link)
+
+ # Wait for the page to load, adjust as needed (seconds)
+ time.sleep(0)
+
+
+def send_in_stock_email(store, product_name, quantity_in_stock):
try:
# Get the environment variables
email_user = os.getenv("email")
email_password = os.getenv("password")
- # Confirm whether email credentials were entered
- if not email_user or not email_password:
- raise Exception("Email credentials not found")
-
# Set up the email
msg = MIMEMultipart()
msg['From'] = email_user
msg['To'] = email_user
msg['Subject'] = 'Your Microcenter Product is Now In Stock'
- msg.attach(MIMEText('Your product is in stock. Reserve it online or head to the store to get it while supplies last.','plain'))
+ msg.attach(
+ MIMEText(
+ f'{product_name} is in stock at {store}. There are {quantity_in_stock} in stock. Reserve it online or head to the store to get it while supplies last.',
+ 'plain'
+ )
+ )
# Connect to the email server and send the email
with smtplib.SMTP("smtp.gmail.com", 587) as server:
@@ -103,53 +184,73 @@ def send_email():
server.login(email_user, email_password)
server.sendmail(email_user, email_user, msg.as_string())
- print("Email sent successfully!")
-
- # Test email exception
+ print(f"[{dt.now()}] Email sent successfully!")
except Exception as e:
- print(f"Failed to send email: {e}")
+ print(f"[{dt.now()}] Failed to send email: {e}")
+
def check_stock(driver):
- # Set the store cookie to Dallas (storeSelected: 131)
- set_store_cookie(driver)
-
- # Get the page source (HTML)
- page_source = driver.page_source
-
- # Search for 'inStock': 'False' in the page source
- if "'inStock':'True'" in page_source:
- print("In Stock!")
- send_email()
- driver.quit() # Close the browser
- sys.exit()
- else:
- print("Out of Stock!")
+ # Micro Center's site uses a DOM element with the class 'inventoryCnt', which also lists the quantity in stock if present.
+ # If no such DOM element exists, the product is sold out.
+ try:
+ inventory_count_element = driver.find_element(By.CLASS_NAME, 'inventoryCnt')
+ return inventory_count_element.get_attribute('innerText').split()[0].strip() # It'll be either 1-24 or 25+.
+ except:
+ # Selenium throws an exception if no such element can be found. This is expected.
+ pass
+
+ # Just to double check, we'll also check their Google Tag Manager event, which uses 'inStock': 'True|False' to indicate its availability.
+ # Inventory count is not available that way though, so we'll just consider it 1 or 0 to be simple.
+ return str(int("'inStock':'True'" in driver.page_source))
+
def main():
- # Enter email and password
- prompt()
+ prompt_for_product()
- # Specify the config.env path
+ # Load email credentials from the config.env file if possible
config_path = ".venv/config.env"
-
- # Load email credentials from the config.env file
load_dotenv(config_path)
- # Test email
- test_email()
+ if not os.getenv("email") or not os.getenv("password"):
+ prompt_for_email_details()
- while True:
- # Set up Chrome options to enable headless mode
- chrome_options = Options()
- chrome_options.add_argument("--headless") # Enable headless mode
- chrome_options.add_argument("--disable-gpu") # Disable GPU acceleration
- chrome_options.add_argument("--no-sandbox") # Fix potential environment issues
+ # Confirm whether email credentials were entered
+ if not os.getenv("email") or not os.getenv("password"):
+ raise Exception("Email credentials not found")
+
+ if not os.getenv("disable_test_email") or (os.getenv("disable_test_email") and os.getenv("disable_test_email").strip().lower() != 'true'):
+ send_test_email()
+ else:
+ print(f'[{dt.now()}] Skipping test email')
- # Set up the WebDriver with the specified options
+ # Set up Chrome options to enable headless mode
+ chrome_options = Options()
+ chrome_options.add_argument("--headless") # Enable headless mode
+ chrome_options.add_argument("--disable-gpu") # Disable GPU acceleration
+ chrome_options.add_argument("--no-sandbox") # Fix potential environment issues
+
+ stores_to_check = ['Dallas', 'Houston'] # Update this with the stores you want to check. Reference the map at the start.
+ while True:
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options)
+ for store in stores_to_check:
+ set_current_store(driver, STORES[store])
+ navigate_to_product(driver, PRODUCTS[os.getenv('selected_product')])
+ product_name = driver.find_element(By.CLASS_NAME, 'product-header').get_attribute('innerText')
+ quantity_in_stock = check_stock(driver)
+
+ if quantity_in_stock != '0':
+ print(f'[{dt.now()}] {product_name} is in stock at {store}! Available quantity: {quantity_in_stock}')
+ send_in_stock_email(store, product_name, quantity_in_stock)
+ driver.quit() # Close the browser
+ sys.exit()
+ else:
+ print(f'[{dt.now()}] {product_name} is out of stock at {store}!')
+
+ time.sleep(10) # An arbitrary delay between checking stores to hopefully avoid being flagged as a bot.
+ driver.quit() # Use a new driver and close it afterwards for every loop to mitigate memory leaks.
+ time.sleep(
+ 300) # Change the number to check stock sooner/faster (seconds). Default: checks stock every ~5 minutes
- # Call the function to check stock
- check_stock(driver) # Check stock status once
- time.sleep(300) # Change the number to check stock sooner/faster (seconds). Default: checks stock every ~5 minutes
-main()
\ No newline at end of file
+if __name__ == '__main__':
+ main()
diff --git a/README.md b/README.md
index fb45816..bd5ce13 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ Check the stock of a specific Micro Center product. Optionally, securely link yo
## Installation
-**Note: The default code looks for an AMD Ryzen 7 9800X3D at a Micro Center Store in Dallas, TX
+**Note: The default code checks the Micro Center Stores in Dallas, TX and Houston, TX
Step-by-step instructions for setting up the project:
@@ -24,33 +24,22 @@ Check the stock of a specific Micro Center product. Optionally, securely link yo
pip install -r requirements.txt
```
*Alternatively: To use your system's command console/terminal window to install the items listed within the requirements folder:
- 1\. Change directory (cd) to the project folder's location that contains requirements.txt
+ 1\. Change directory (`cd`) to the project folder's location that contains requirements.txt
2\. Run the above command
-3. On Line 23 within the code, replace the Store Identification Number with your local Micro Center Store Identification Number:
+3. On Line 232 within the code, replace the store names with the name of your local Micro Center store(s):
```python
- # Line 23
- 'value': 'store number here',
- ```
-
- To find your local Micro Center Store Number:
- 1. Make sure you have selected a store on www.microcenter.com
- 2. Right-Click the page, choose 'Inspect'
- 3. Click the double arrow (>>) at the top of the Inspect Element console and select 'Application'
- 4. Under 'Storage', click 'Cookies'
- 5. Under the 'Name' column, look for 'storeSelected' and find the appropriate value associated with that cookie.
- 6. Replace the number on Line 23 in the code with the appropriate Store Identification Number as shown in the example below.
-
- ```python
- # Example
- # Line 23
- 'value': '131',
+ # Line 232
+ stores_to_check = ['Dallas']
```
+
-4. On Line 30 within the code, replace the Product Page URL with your desired product:
+4. On Line 47 within the code, add or update the Product Page URL with your desired product:
```python
- # Line 30 - Get the product page URL
- driver.get("https://www.microcenter.com/your-product-url-here")
+ # Line 47 - Get the product page URL
+ PRODUCTS = {
+ 'simple-part-name-here': 'https://www.microcenter.com/your-product-url-here',
+ }
```
5. (Optional) Adjust the speed that the program functions:
@@ -58,7 +47,7 @@ Check the stock of a specific Micro Center product. Optionally, securely link yo
Slower Internet = Higher Number (~30)
```python
- # Line 18 & Line 33 - Wait for the page to load,
+ # Line 122, 143, 160, 249, 252 - Wait for the page to load,
# adjust as needed (seconds)
time.sleep(30)
```
@@ -67,7 +56,7 @@ Check the stock of a specific Micro Center product. Optionally, securely link yo
Faster Internet = Lower Number (~5)
```python
- # Line 18 & Line 33 - Wait for the page to load,
+ # Line 122, 143, 160, 249, 252 - Wait for the page to load,
# adjust as needed (seconds)
time.sleep(5)
```
@@ -88,15 +77,15 @@ Check the stock of a specific Micro Center product. Optionally, securely link yo
5\. Enter your email
6\. Enter your password
7\. Expect to receive a test email from yourself confirming that your email is linked with the application
+ 8\. To avoid having to re-enter your email and app password every time, it is recommended to place them in `./venv/config.env`. See `config.env.example` for reference.
That's all! Leave the program running in the background. If you shut down the computer, be sure to run through the steps again to get it working again.
### Future Update Plans
-1. Show and email the amount in stock
-2. GUI window
-3. Multiple Micro Center stores listed - no code-changing required
-4. Works for other websites
+1. GUI window
+2. Multiple Micro Center stores listed - no code-changing required
+3. Works for other websites
### If this program helped you, buy me a coffee :)
diff --git a/config.env b/config.env
deleted file mode 100644
index e69de29..0000000
diff --git a/config.env.example b/config.env.example
new file mode 100644
index 0000000..9d57e34
--- /dev/null
+++ b/config.env.example
@@ -0,0 +1,3 @@
+email=
+password=""
+disable_test_email=false