Browser session recovery, price tracking, and dashboard improvements
- Add browser session validation and auto-restart for BestBuy/GameStop scrapers - Add price change detection and notifications - Remove PokemonCenter scraper (now uses Chrome Extension) - Dashboard UI improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,6 @@ import schedule
|
||||
from config import (
|
||||
CHECK_INTERVAL_SECONDS,
|
||||
SITES_ENABLED,
|
||||
POKEMON_CENTER_URLS,
|
||||
TARGET_URLS,
|
||||
GAMESTOP_URLS,
|
||||
BESTBUY_URLS,
|
||||
@@ -23,13 +22,14 @@ from config import (
|
||||
KEYWORDS,
|
||||
LOG_LEVEL,
|
||||
NEW_DROP_NOTIFICATIONS,
|
||||
PRICE_CHANGE_NOTIFICATIONS,
|
||||
MAX_PAGES,
|
||||
)
|
||||
from src.browser import get_browser, shutdown_browser
|
||||
from src.product_tracker import ProductTracker
|
||||
from src.discord_notifier import send_stock_alert, send_startup_notification, send_error_notification
|
||||
from src.discord_notifier import send_stock_alert, send_startup_notification, send_error_notification, send_price_change_alert
|
||||
# Note: Pokemon Center uses Chrome Extension (chrome-extension/), not a Python scraper
|
||||
from scrapers import (
|
||||
PokemonCenterScraper,
|
||||
TargetScraper,
|
||||
GameStopScraper,
|
||||
BestBuyScraper,
|
||||
@@ -50,9 +50,8 @@ logger = logging.getLogger(__name__)
|
||||
# Global tracker
|
||||
tracker = ProductTracker()
|
||||
|
||||
# Scrapers
|
||||
# Scrapers (Pokemon Center uses Chrome Extension instead)
|
||||
scrapers = {
|
||||
"pokemoncenter": PokemonCenterScraper(),
|
||||
"target": TargetScraper(),
|
||||
"gamestop": GameStopScraper(),
|
||||
"bestbuy": BestBuyScraper(),
|
||||
@@ -60,67 +59,14 @@ scrapers = {
|
||||
}
|
||||
|
||||
|
||||
def check_pokemoncenter():
|
||||
"""Check PokemonCenter for restocks and new drops"""
|
||||
logger.info("Checking PokemonCenter...")
|
||||
scraper = scrapers["pokemoncenter"]
|
||||
|
||||
try:
|
||||
for url in POKEMON_CENTER_URLS:
|
||||
logger.info(f"Scraping: {url}")
|
||||
products = scraper.scrape_category_page(url, max_pages=MAX_PAGES.get("pokemoncenter", 1))
|
||||
|
||||
# Apply keyword filter if enabled
|
||||
if KEYWORD_FILTER_ENABLED and KEYWORDS:
|
||||
products = scraper.filter_by_keywords(products, KEYWORDS)
|
||||
logger.info(f"After keyword filter: {len(products)} products")
|
||||
|
||||
# Process and detect changes
|
||||
new_products, restocked_products = tracker.process_products(products)
|
||||
|
||||
# Send notifications for new products (if enabled)
|
||||
if NEW_DROP_NOTIFICATIONS:
|
||||
for product in new_products:
|
||||
if product.in_stock:
|
||||
send_stock_alert(
|
||||
product_name=product.name,
|
||||
product_url=product.url,
|
||||
price=product.price or "See link",
|
||||
site="pokemoncenter",
|
||||
alert_type="new_drop",
|
||||
image_url=product.image_url,
|
||||
)
|
||||
logger.info(f"Sent notification for new product: {product.name}")
|
||||
|
||||
# Send notifications for restocks
|
||||
for product in restocked_products:
|
||||
send_stock_alert(
|
||||
product_name=product.name,
|
||||
product_url=product.url,
|
||||
price=product.price or "See link",
|
||||
site="pokemoncenter",
|
||||
alert_type="restock",
|
||||
image_url=product.image_url,
|
||||
)
|
||||
logger.info(f"Sent notification for restock: {product.name}")
|
||||
|
||||
# Log stats
|
||||
stats = tracker.get_stats()
|
||||
logger.info(
|
||||
f"Stats: {stats['total_products']} total, "
|
||||
f"{stats['in_stock']} in stock, "
|
||||
f"{stats['out_of_stock']} out of stock"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking PokemonCenter: {e}")
|
||||
send_error_notification(f"Error checking PokemonCenter: {str(e)}", "PokemonCenter")
|
||||
|
||||
|
||||
def check_target():
|
||||
"""Check Target for restocks and new drops"""
|
||||
logger.info("Checking Target...")
|
||||
scraper = scrapers["target"]
|
||||
start_time = time.time()
|
||||
total_products = 0
|
||||
total_new = 0
|
||||
total_restocks = 0
|
||||
|
||||
try:
|
||||
for url in TARGET_URLS:
|
||||
@@ -135,8 +81,12 @@ def check_target():
|
||||
products = scraper.filter_by_keywords(products, KEYWORDS)
|
||||
logger.info(f"After keyword filter: {len(products)} products")
|
||||
|
||||
total_products += len(products)
|
||||
|
||||
# Process and detect changes
|
||||
new_products, restocked_products = tracker.process_products(products)
|
||||
new_products, restocked_products, price_changed_products = tracker.process_products(products)
|
||||
total_new += len(new_products)
|
||||
total_restocks += len(restocked_products)
|
||||
|
||||
# Send notifications for new products (if enabled)
|
||||
if NEW_DROP_NOTIFICATIONS:
|
||||
@@ -164,6 +114,20 @@ def check_target():
|
||||
)
|
||||
logger.info(f"Sent notification for restock: {product.name}")
|
||||
|
||||
# Send notifications for price changes (if enabled)
|
||||
if PRICE_CHANGE_NOTIFICATIONS:
|
||||
for product, old_price, new_price in price_changed_products:
|
||||
send_price_change_alert(
|
||||
product_name=product.name,
|
||||
product_url=product.url,
|
||||
old_price=old_price,
|
||||
new_price=new_price,
|
||||
site="target",
|
||||
in_stock=product.in_stock,
|
||||
image_url=product.image_url,
|
||||
)
|
||||
logger.info(f"Sent notification for price change: {product.name} ({old_price} -> {new_price})")
|
||||
|
||||
# Log stats
|
||||
stats = tracker.get_stats()
|
||||
logger.info(
|
||||
@@ -172,15 +136,26 @@ def check_target():
|
||||
f"{stats['out_of_stock']} out of stock"
|
||||
)
|
||||
|
||||
# Log successful check to database
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
tracker.log_check("target", total_products, total_new, total_restocks, duration_ms, success=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking Target: {e}")
|
||||
send_error_notification(f"Error checking Target: {str(e)}", "Target")
|
||||
# Log failed check to database
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
tracker.log_check("target", total_products, total_new, total_restocks, duration_ms, success=False, error_message=str(e))
|
||||
|
||||
|
||||
def check_gamestop():
|
||||
"""Check GameStop for restocks and new drops"""
|
||||
logger.info("Checking GameStop...")
|
||||
scraper = scrapers["gamestop"]
|
||||
start_time = time.time()
|
||||
total_products = 0
|
||||
total_new = 0
|
||||
total_restocks = 0
|
||||
|
||||
try:
|
||||
for url in GAMESTOP_URLS:
|
||||
@@ -195,8 +170,12 @@ def check_gamestop():
|
||||
products = scraper.filter_by_keywords(products, KEYWORDS)
|
||||
logger.info(f"After keyword filter: {len(products)} products")
|
||||
|
||||
total_products += len(products)
|
||||
|
||||
# Process and detect changes
|
||||
new_products, restocked_products = tracker.process_products(products)
|
||||
new_products, restocked_products, price_changed_products = tracker.process_products(products)
|
||||
total_new += len(new_products)
|
||||
total_restocks += len(restocked_products)
|
||||
|
||||
# Send notifications for new products (if enabled)
|
||||
if NEW_DROP_NOTIFICATIONS:
|
||||
@@ -224,6 +203,20 @@ def check_gamestop():
|
||||
)
|
||||
logger.info(f"Sent notification for restock: {product.name}")
|
||||
|
||||
# Send notifications for price changes (if enabled)
|
||||
if PRICE_CHANGE_NOTIFICATIONS:
|
||||
for product, old_price, new_price in price_changed_products:
|
||||
send_price_change_alert(
|
||||
product_name=product.name,
|
||||
product_url=product.url,
|
||||
old_price=old_price,
|
||||
new_price=new_price,
|
||||
site="gamestop",
|
||||
in_stock=product.in_stock,
|
||||
image_url=product.image_url,
|
||||
)
|
||||
logger.info(f"Sent notification for price change: {product.name} ({old_price} -> {new_price})")
|
||||
|
||||
# Log stats
|
||||
stats = tracker.get_stats()
|
||||
logger.info(
|
||||
@@ -232,15 +225,26 @@ def check_gamestop():
|
||||
f"{stats['out_of_stock']} out of stock"
|
||||
)
|
||||
|
||||
# Log successful check to database
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
tracker.log_check("gamestop", total_products, total_new, total_restocks, duration_ms, success=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking GameStop: {e}")
|
||||
send_error_notification(f"Error checking GameStop: {str(e)}", "GameStop")
|
||||
# Log failed check to database
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
tracker.log_check("gamestop", total_products, total_new, total_restocks, duration_ms, success=False, error_message=str(e))
|
||||
|
||||
|
||||
def check_bestbuy():
|
||||
"""Check Best Buy for restocks and new drops"""
|
||||
logger.info("Checking Best Buy...")
|
||||
scraper = scrapers["bestbuy"]
|
||||
start_time = time.time()
|
||||
total_products = 0
|
||||
total_new = 0
|
||||
total_restocks = 0
|
||||
|
||||
try:
|
||||
for url in BESTBUY_URLS:
|
||||
@@ -255,8 +259,12 @@ def check_bestbuy():
|
||||
products = scraper.filter_by_keywords(products, KEYWORDS)
|
||||
logger.info(f"After keyword filter: {len(products)} products")
|
||||
|
||||
total_products += len(products)
|
||||
|
||||
# Process and detect changes
|
||||
new_products, restocked_products = tracker.process_products(products)
|
||||
new_products, restocked_products, price_changed_products = tracker.process_products(products)
|
||||
total_new += len(new_products)
|
||||
total_restocks += len(restocked_products)
|
||||
|
||||
# Send notifications for new products (if enabled)
|
||||
if NEW_DROP_NOTIFICATIONS:
|
||||
@@ -284,6 +292,20 @@ def check_bestbuy():
|
||||
)
|
||||
logger.info(f"Sent notification for restock: {product.name}")
|
||||
|
||||
# Send notifications for price changes (if enabled)
|
||||
if PRICE_CHANGE_NOTIFICATIONS:
|
||||
for product, old_price, new_price in price_changed_products:
|
||||
send_price_change_alert(
|
||||
product_name=product.name,
|
||||
product_url=product.url,
|
||||
old_price=old_price,
|
||||
new_price=new_price,
|
||||
site="bestbuy",
|
||||
in_stock=product.in_stock,
|
||||
image_url=product.image_url,
|
||||
)
|
||||
logger.info(f"Sent notification for price change: {product.name} ({old_price} -> {new_price})")
|
||||
|
||||
# Log stats
|
||||
stats = tracker.get_stats()
|
||||
logger.info(
|
||||
@@ -292,15 +314,26 @@ def check_bestbuy():
|
||||
f"{stats['out_of_stock']} out of stock"
|
||||
)
|
||||
|
||||
# Log successful check to database
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
tracker.log_check("bestbuy", total_products, total_new, total_restocks, duration_ms, success=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking Best Buy: {e}")
|
||||
send_error_notification(f"Error checking Best Buy: {str(e)}", "Best Buy")
|
||||
# Log failed check to database
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
tracker.log_check("bestbuy", total_products, total_new, total_restocks, duration_ms, success=False, error_message=str(e))
|
||||
|
||||
|
||||
def check_walmart():
|
||||
"""Check Walmart for restocks and new drops"""
|
||||
logger.info("Checking Walmart...")
|
||||
scraper = scrapers["walmart"]
|
||||
start_time = time.time()
|
||||
total_products = 0
|
||||
total_new = 0
|
||||
total_restocks = 0
|
||||
|
||||
try:
|
||||
for url in WALMART_URLS:
|
||||
@@ -315,8 +348,12 @@ def check_walmart():
|
||||
products = scraper.filter_by_keywords(products, KEYWORDS)
|
||||
logger.info(f"After keyword filter: {len(products)} products")
|
||||
|
||||
total_products += len(products)
|
||||
|
||||
# Process and detect changes
|
||||
new_products, restocked_products = tracker.process_products(products)
|
||||
new_products, restocked_products, price_changed_products = tracker.process_products(products)
|
||||
total_new += len(new_products)
|
||||
total_restocks += len(restocked_products)
|
||||
|
||||
# Send notifications for new products (if enabled)
|
||||
if NEW_DROP_NOTIFICATIONS:
|
||||
@@ -344,6 +381,20 @@ def check_walmart():
|
||||
)
|
||||
logger.info(f"Sent notification for restock: {product.name}")
|
||||
|
||||
# Send notifications for price changes (if enabled)
|
||||
if PRICE_CHANGE_NOTIFICATIONS:
|
||||
for product, old_price, new_price in price_changed_products:
|
||||
send_price_change_alert(
|
||||
product_name=product.name,
|
||||
product_url=product.url,
|
||||
old_price=old_price,
|
||||
new_price=new_price,
|
||||
site="walmart",
|
||||
in_stock=product.in_stock,
|
||||
image_url=product.image_url,
|
||||
)
|
||||
logger.info(f"Sent notification for price change: {product.name} ({old_price} -> {new_price})")
|
||||
|
||||
# Log stats
|
||||
stats = tracker.get_stats()
|
||||
logger.info(
|
||||
@@ -352,17 +403,23 @@ def check_walmart():
|
||||
f"{stats['out_of_stock']} out of stock"
|
||||
)
|
||||
|
||||
# Log successful check to database
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
tracker.log_check("walmart", total_products, total_new, total_restocks, duration_ms, success=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking Walmart: {e}")
|
||||
send_error_notification(f"Error checking Walmart: {str(e)}", "Walmart")
|
||||
# Log failed check to database
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
tracker.log_check("walmart", total_products, total_new, total_restocks, duration_ms, success=False, error_message=str(e))
|
||||
|
||||
|
||||
def run_checks():
|
||||
"""Run all enabled site checks"""
|
||||
logger.info(f"Running checks at {datetime.now().strftime('%H:%M:%S')}")
|
||||
|
||||
if "pokemoncenter" in SITES_ENABLED:
|
||||
check_pokemoncenter()
|
||||
# Note: Pokemon Center is monitored via Chrome Extension, not here
|
||||
|
||||
if "target" in SITES_ENABLED:
|
||||
check_target()
|
||||
|
||||
Reference in New Issue
Block a user