// Pokemon Stock Monitor - Background Service Worker const DEFAULT_CONFIG = { discordWebhook: "", checkIntervalMinutes: 1, enabled: true, urls: [ "https://www.pokemoncenter.com/category/tcg-cards?sort=relevance" ], keywords: [], // Empty = all products, or ["chaos rising", "booster"] etc notifyNewProducts: true, notifyRestocks: true }; // Store known products let knownProducts = {}; let config = DEFAULT_CONFIG; // Initialize chrome.runtime.onInstalled.addListener(() => { console.log("Pokemon Stock Monitor installed"); loadConfig(); loadProducts(); setupAlarm(); }); // Load on startup chrome.runtime.onStartup.addListener(() => { loadConfig(); loadProducts(); setupAlarm(); }); // Handle alarm chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === "stockCheck") { runStockCheck(); } }); // Setup periodic alarm function setupAlarm() { // Chrome minimum is 0.5 minutes (30 seconds) for packed extensions const interval = Math.max(0.5, config.checkIntervalMinutes); chrome.alarms.create("stockCheck", { periodInMinutes: interval }); console.log(`Alarm set for every ${interval} minute(s) (${interval * 60} seconds)`); } // Load config from storage async function loadConfig() { const stored = await chrome.storage.local.get("config"); if (stored.config) { config = { ...DEFAULT_CONFIG, ...stored.config }; } console.log("Config loaded:", config); } // Save config to storage async function saveConfig() { await chrome.storage.local.set({ config }); setupAlarm(); // Reset alarm with new interval } // Load known products from storage async function loadProducts() { const stored = await chrome.storage.local.get("knownProducts"); if (stored.knownProducts) { knownProducts = stored.knownProducts; } console.log(`Loaded ${Object.keys(knownProducts).length} known products`); } // Save known products to storage async function saveProducts() { await chrome.storage.local.set({ knownProducts }); } // Main stock check function async function runStockCheck() { if (!config.enabled) { console.log("Stock check disabled"); return; } console.log("Running stock check..."); for (const url of config.urls) { try { const products = await fetchAndParseProducts(url); // Alert if 0 products found - likely bot protection if (products.length === 0) { console.warn("WARNING: 0 products found - possible bot protection!"); await sendWarningNotification("Bot Protection Detected", "Found 0 products - you may need to manually verify you're human on PokemonCenter.com"); continue; } const { newProducts, restockedProducts } = processProducts(products); // Send notifications if (config.notifyNewProducts) { for (const product of newProducts) { if (product.inStock) { await sendDiscordNotification(product, "new_drop"); } } } if (config.notifyRestocks) { for (const product of restockedProducts) { await sendDiscordNotification(product, "restock"); } } console.log(`Check complete: ${products.length} products, ${newProducts.length} new, ${restockedProducts.length} restocks`); } catch (error) { console.error(`Error checking ${url}:`, error); } } await saveProducts(); } // Fetch and parse products from a URL using content script async function fetchAndParseProducts(url) { return new Promise(async (resolve, reject) => { try { // Create a tab to load the page const tab = await chrome.tabs.create({ url, active: false }); console.log(`Created tab ${tab.id} for ${url}`); // Wait for tab to finish loading const waitForLoad = () => { return new Promise((res) => { const listener = (tabId, changeInfo) => { if (tabId === tab.id && changeInfo.status === "complete") { chrome.tabs.onUpdated.removeListener(listener); res(); } }; chrome.tabs.onUpdated.addListener(listener); // Timeout after 30 seconds setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); res(); }, 30000); }); }; await waitForLoad(); console.log(`Tab ${tab.id} loaded`); // Give extra time for dynamic content to load console.log("Waiting for dynamic content..."); await new Promise(r => setTimeout(r, 10000)); // 10 seconds for slow loads // Send message to content script to extract products let products = []; try { const response = await chrome.tabs.sendMessage(tab.id, { type: "extractProducts" }); products = response || []; console.log(`Content script returned ${products.length} products`); } catch (e) { console.log("Content script not ready, injecting manually..."); // Inject content script if not already there await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ["content.js"] }); await new Promise(r => setTimeout(r, 500)); const response = await chrome.tabs.sendMessage(tab.id, { type: "extractProducts" }); products = response || []; } // Apply keyword filter if (config.keywords && config.keywords.length > 0) { products = products.filter(p => { const nameLower = p.name.toLowerCase(); return config.keywords.some(kw => nameLower.includes(kw.toLowerCase())); }); console.log(`After keyword filter: ${products.length} products`); } // Close the tab await chrome.tabs.remove(tab.id); console.log(`Closed tab ${tab.id}`); resolve(products); } catch (error) { reject(error); } }); } // Note: Product parsing is now done by the content script (content.js) // Process products - detect new and restocked function processProducts(products) { const newProducts = []; const restockedProducts = []; const now = new Date().toISOString(); for (const product of products) { const existing = knownProducts[product.url]; if (!existing) { // New product newProducts.push(product); knownProducts[product.url] = { ...product, firstSeen: now, lastSeen: now, lastInStock: product.inStock ? now : null }; } else { // Existing product - check for restock if (product.inStock && !existing.inStock) { restockedProducts.push(product); existing.lastInStock = now; } // Update existing.inStock = product.inStock; existing.price = product.price || existing.price; existing.lastSeen = now; } } return { newProducts, restockedProducts }; } // Send warning notification (for bot protection, errors, etc.) async function sendWarningNotification(title, message) { if (!config.discordWebhook) { console.log("No Discord webhook configured"); return; } const embed = { title: `⚠️ ${title}`, description: message, color: 0xFF6600, // Orange for warnings footer: { text: "Pokemon Stock Monitor (Chrome Extension)" }, timestamp: new Date().toISOString() }; try { const response = await fetch(config.discordWebhook, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: "@everyone", embeds: [embed] }) }); if (response.ok) { console.log(`Warning notification sent: ${title}`); } } catch (error) { console.error("Error sending warning notification:", error); } } // Send Discord notification async function sendDiscordNotification(product, alertType) { if (!config.discordWebhook) { console.log("No Discord webhook configured"); return; } const color = alertType === "restock" ? 0x00FF00 : 0x0099FF; const title = alertType === "restock" ? "RESTOCK ALERT" : "NEW DROP"; const embed = { title: title, description: `**${product.name}**`, url: product.url, color: color, fields: [ { name: "Price", value: product.price || "See link", inline: true }, { name: "Store", value: "Pokemon Center", inline: true }, { name: "Link", value: `[BUY NOW](${product.url})`, inline: false } ], footer: { text: "Pokemon Stock Monitor (Chrome Extension)" }, timestamp: new Date().toISOString() }; if (product.imageUrl) { embed.thumbnail = { url: product.imageUrl }; } try { const response = await fetch(config.discordWebhook, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: "@everyone", embeds: [embed] }) }); if (response.ok) { console.log(`Discord notification sent for: ${product.name}`); // Also show browser notification chrome.notifications.create({ type: "basic", iconUrl: "icon128.png", title: title, message: product.name }); } else { console.error("Discord notification failed:", response.status); } } catch (error) { console.error("Error sending Discord notification:", error); } } // Listen for messages from popup chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === "getConfig") { sendResponse(config); } else if (message.type === "saveConfig") { config = { ...config, ...message.config }; saveConfig(); sendResponse({ success: true }); } else if (message.type === "runCheck") { runStockCheck(); sendResponse({ success: true }); } else if (message.type === "getStats") { sendResponse({ totalProducts: Object.keys(knownProducts).length, enabled: config.enabled }); } else if (message.type === "clearProducts") { knownProducts = {}; saveProducts(); sendResponse({ success: true }); } return true; }); // Run initial check after a short delay setTimeout(() => { loadConfig().then(() => { loadProducts().then(() => { if (config.enabled && config.discordWebhook) { runStockCheck(); } }); }); }, 5000);