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:
2026-04-01 22:18:35 -04:00
parent 74c2a45d50
commit e5527628a5
11 changed files with 530 additions and 468 deletions
+125 -68
View File
@@ -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()