Pokemon Stock Monitor - Initial commit
Chrome extension for PokemonCenter monitoring with Discord notifications. Includes Python scripts for Target monitoring. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
"""
|
||||
Pokemon Stock Monitor - Main entry point
|
||||
Monitors retail sites for Pokemon card restocks and new drops
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import signal
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
import schedule
|
||||
|
||||
from config import (
|
||||
CHECK_INTERVAL_SECONDS,
|
||||
SITES_ENABLED,
|
||||
POKEMON_CENTER_URLS,
|
||||
TARGET_URLS,
|
||||
KEYWORD_FILTER_ENABLED,
|
||||
KEYWORDS,
|
||||
LOG_LEVEL,
|
||||
)
|
||||
from browser import get_browser, shutdown_browser
|
||||
from product_tracker import ProductTracker
|
||||
from discord_notifier import send_stock_alert, send_startup_notification, send_error_notification
|
||||
from scrapers import PokemonCenterScraper, TargetScraper
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, LOG_LEVEL),
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
logging.FileHandler("monitor.log", encoding="utf-8"),
|
||||
],
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Global tracker
|
||||
tracker = ProductTracker()
|
||||
|
||||
# Scrapers
|
||||
scrapers = {
|
||||
"pokemoncenter": PokemonCenterScraper(),
|
||||
"target": TargetScraper(),
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
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"]
|
||||
|
||||
try:
|
||||
for url in TARGET_URLS:
|
||||
logger.info(f"Scraping: {url}")
|
||||
products = scraper.scrape_category_page(url)
|
||||
|
||||
# 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
|
||||
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="target",
|
||||
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="target",
|
||||
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 Target: {e}")
|
||||
send_error_notification(f"Error checking Target: {str(e)}", "Target")
|
||||
|
||||
|
||||
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()
|
||||
|
||||
if "target" in SITES_ENABLED:
|
||||
check_target()
|
||||
|
||||
logger.info("Check cycle complete")
|
||||
|
||||
|
||||
def graceful_shutdown(signum, frame):
|
||||
"""Handle shutdown gracefully"""
|
||||
logger.info("Shutting down...")
|
||||
shutdown_browser()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
print("=" * 50)
|
||||
print("Pokemon Stock Monitor")
|
||||
print("=" * 50)
|
||||
print(f"Monitoring: {', '.join(SITES_ENABLED)}")
|
||||
print(f"Check interval: {CHECK_INTERVAL_SECONDS} seconds")
|
||||
print(f"Keyword filter: {'ON' if KEYWORD_FILTER_ENABLED else 'OFF'}")
|
||||
print("=" * 50)
|
||||
print()
|
||||
|
||||
# Register signal handlers
|
||||
signal.signal(signal.SIGINT, graceful_shutdown)
|
||||
signal.signal(signal.SIGTERM, graceful_shutdown)
|
||||
|
||||
# Initialize browser
|
||||
logger.info("Initializing browser...")
|
||||
try:
|
||||
get_browser()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize browser: {e}")
|
||||
print(f"\nERROR: Failed to start browser. Make sure Playwright is installed:")
|
||||
print(" pip install playwright")
|
||||
print(" playwright install chromium")
|
||||
return
|
||||
|
||||
# Send startup notification
|
||||
send_startup_notification()
|
||||
|
||||
# Run initial check
|
||||
print("Running initial check...")
|
||||
run_checks()
|
||||
|
||||
# Schedule periodic checks
|
||||
schedule.every(CHECK_INTERVAL_SECONDS).seconds.do(run_checks)
|
||||
|
||||
print(f"\nMonitor running! Checking every {CHECK_INTERVAL_SECONDS} seconds.")
|
||||
print("Press Ctrl+C to stop.\n")
|
||||
|
||||
# Main loop
|
||||
try:
|
||||
while True:
|
||||
schedule.run_pending()
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
graceful_shutdown(None, None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user