Files
pokemon-stock-checker/chrome-extension/background.js
T
mmcghen 4fa50809dd Add bot protection warning, 30-sec intervals, longer page wait
- Send Discord ping when 0 products found (likely bot protection)
- Allow 0.5 minute (30 second) check intervals
- Increase page wait from 5s to 10s for slow loads

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-24 18:11:27 -04:00

362 lines
10 KiB
JavaScript

// 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);