Files
pokemon-stock-checker/product_tracker.py
T
mmcghen 9d49a99916 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>
2026-03-24 12:39:09 -04:00

137 lines
4.7 KiB
Python

"""
Product tracker - keeps track of known products to detect new drops and restocks
"""
import json
import logging
from pathlib import Path
from typing import Dict, List, Set, Optional
from dataclasses import asdict
from datetime import datetime
from scrapers.base import Product
logger = logging.getLogger(__name__)
# File to store known products
PRODUCTS_FILE = Path(__file__).parent / "products.json"
class ProductTracker:
"""Tracks known products to detect new listings and stock changes"""
def __init__(self, products_file: Path = PRODUCTS_FILE):
self.products_file = products_file
self.products: Dict[str, dict] = {} # URL -> product data
self.load()
def load(self):
"""Load products from file"""
if self.products_file.exists():
try:
with open(self.products_file, "r", encoding="utf-8") as f:
self.products = json.load(f)
logger.info(f"Loaded {len(self.products)} tracked products")
except (json.JSONDecodeError, IOError) as e:
logger.error(f"Error loading products file: {e}")
self.products = {}
else:
self.products = {}
logger.info("No existing products file, starting fresh")
def save(self):
"""Save products to file"""
try:
with open(self.products_file, "w", encoding="utf-8") as f:
json.dump(self.products, f, indent=2, ensure_ascii=False)
logger.debug(f"Saved {len(self.products)} products")
except IOError as e:
logger.error(f"Error saving products file: {e}")
def process_products(self, products: List[Product]) -> tuple[List[Product], List[Product]]:
"""
Process a list of scraped products and detect changes
Args:
products: List of products from scraper
Returns:
Tuple of (new_products, restocked_products)
"""
new_products = []
restocked_products = []
for product in products:
url = product.url
if url not in self.products:
# New product!
new_products.append(product)
self.products[url] = {
"name": product.name,
"url": url,
"price": product.price,
"in_stock": product.in_stock,
"image_url": product.image_url,
"site": product.site,
"product_id": product.product_id,
"first_seen": datetime.now().isoformat(),
"last_seen": datetime.now().isoformat(),
"last_in_stock": datetime.now().isoformat() if product.in_stock else None,
}
logger.info(f"NEW PRODUCT: {product.name}")
else:
# Existing product - check for restock
existing = self.products[url]
was_in_stock = existing.get("in_stock", False)
# Update last seen
existing["last_seen"] = datetime.now().isoformat()
existing["price"] = product.price or existing.get("price")
existing["image_url"] = product.image_url or existing.get("image_url")
if product.in_stock and not was_in_stock:
# RESTOCK!
restocked_products.append(product)
existing["last_in_stock"] = datetime.now().isoformat()
logger.info(f"RESTOCK: {product.name}")
existing["in_stock"] = product.in_stock
self.products[url] = existing
self.save()
return new_products, restocked_products
def get_known_urls(self) -> Set[str]:
"""Get all known product URLs"""
return set(self.products.keys())
def get_product(self, url: str) -> Optional[dict]:
"""Get a specific product by URL"""
return self.products.get(url)
def mark_out_of_stock(self, url: str):
"""Mark a product as out of stock"""
if url in self.products:
self.products[url]["in_stock"] = False
self.save()
def clear(self):
"""Clear all tracked products"""
self.products = {}
self.save()
logger.info("Cleared all tracked products")
def get_stats(self) -> dict:
"""Get tracking statistics"""
total = len(self.products)
in_stock = sum(1 for p in self.products.values() if p.get("in_stock", False))
out_of_stock = total - in_stock
return {
"total_products": total,
"in_stock": in_stock,
"out_of_stock": out_of_stock,
}