Untitled
unknown
plain_text
24 days ago
21 kB
98
No Index
// ==UserScript== // @name NVIDIA RTX 5090 FE Stock Monitor // @namespace http://tampermonkey.net/ // @version 0.1 // @description Monitor NVIDIA store for RTX 5090 FE stock and SKU changes // @author You // @match https://marketplace.nvidia.com/*/consumer/graphics-cards/nvidia-geforce-rtx-* // @grant GM_xmlhttpRequest // ==/UserScript== /* NVIDIA GPU Stock Checker ----------------------- This script monitors NVIDIA's store for FE GPU stock and SKU changes. It will notify you with sound and visual alerts when your desired GPU becomes available. How to Use: 1. Install the script in Tampermonkey/Greasemonkey/Violentmonkey/etc. 2. Visit the NVIDIA store page for your desired GPU: - https://marketplace.nvidia.com/xx-xx/consumer/graphics-cards/nvidia-geforce-rtx-5090 - https://marketplace.nvidia.com/xx-xx/consumer/graphics-cards/nvidia-geforce-rtx-5080 (Replace xx-xx with your locale, e.g., de-de, en-us) 3. The script will automatically start monitoring Features: - Automatic stock checking (default: every 1 second) - SKU change detection (default: every 10 seconds) - Voice notifications when stock is found or SKU changes - Visual status display in top-right corner - Error rate monitoring and alerts - Opens product URL in new tab when stock is found - 5-minute cooldown between redirects to prevent IP bans URL Parameters: - stockInterval: Customize stock check interval (in milliseconds) - skuInterval: Customize SKU check interval (in milliseconds) Example: https://marketplace.nvidia.com/de-de/consumer/graphics-cards/nvidia-geforce-rtx-5090/?stockInterval=2000&skuCheckInterval=15000 Troubleshooting: - If you get constant errors, try increasing the stockInterval to avoid rate limiting - If you get constant errors & have smth like notify-fe open as well, try to close it - If voice notifications aren't working, check your browser's audio permissions - For persistent issues, check the browser console (F12) for detailed error messages - If you're getting blocked from Proshop, wait at least 5 minutes between visits to the product URL Important Notes: - The script requires a URL containing locale (xx-xx) and RTX model number - Script only works on the dedicated RTX 5080/5090 product pages - Status display can be minimized using the - button - When stock is found, the script will open the product URL in a new tab - A 5-minute cooldown is enforced between redirects to prevent IP bans from Proshop - DO NOT repeatedly visit the product URL or you risk getting your IP banned by Proshop Test Functions (Console): - checkStockNow(): Force immediate stock check - checkSkuUpdatesNow(): Force immediate SKU check - simulateSKUChange(): Simulate SKU change event - triggerStockFound(): Simulate stock found event - toggleTestMode(true/false): Enable/disable API response failure testing mode - setFailureRate(0-1): Set API failure rate for testing - simulateEmptyAPIResponse(true/false): Enable/disable empty response simulation */ (async function () { // Configuration const CONFIG = { gpuModel: null, locale: null, stockCheckInterval: 500, // default 1 second skuCheckInterval: 5000, // default 10 seconds ttsVolume: 1, cacheBusting: { enabled: true, // Add cache busting configuration feInventory: true, search: true, }, errorTracking: { enabled: true, windowDurationMinutes: 1, // 1 minute window errorThreshold: 0.5, // 50% error rate threshold }, testMode: { enabled: false, failureRate: 0, // 50% failure rate simulateEmptyResponse: false, // Add this new flag errorCodes: [400, 401, 403, 429, 500, 502, 503], errorMessages: [ "Rate limited", "Unauthorized", "Service unavailable", "Internal server error", "Bad gateway", ], }, }; // Update error tracking configuration const errorTracking = { recentErrors: [], consecutiveEmptyResponses: 0, }; // Add these at the top of the IIFE, after CONFIG const MONITORING_STATE = { isActive: true, lastStockFound: null, }; // Create UI elements first // Create a div for status messages const statusDiv = document.createElement("div"); statusDiv.style.cssText = 'position: fixed; top: 10px; right: 10px; padding: 15px; background: rgba(0, 0, 0, 0.8); color: #fff; font-family: "Consolas", monospace; border-radius: 5px; z-index: 9999; max-width: 500px; display: flex; flex-direction: column; max-height: 27vh;'; // Create a container for messages const messagesDiv = document.createElement("div"); messagesDiv.style.cssText = "overflow-y: auto; flex-grow: 1; display: flex; flex-direction: column; scrollbar-width: none; -ms-overflow-style: none;"; messagesDiv.id = "messages-container"; // Add webkit scrollbar style const style = document.createElement("style"); style.textContent = ` #messages-container::-webkit-scrollbar { display: none; } `; document.head.appendChild(style); statusDiv.appendChild(messagesDiv); // Add minimize button const minimizeBtn = document.createElement("button"); minimizeBtn.innerHTML = "−"; minimizeBtn.style.cssText = "position: absolute; top: 5px; right: 5px; background: none; border: none; color: #fff; cursor: pointer; font-size: 16px; padding: 0 5px; z-index: 10000;"; statusDiv.appendChild(minimizeBtn); // Add click handler for minimize let isMinimized = false; minimizeBtn.addEventListener("click", () => { if (isMinimized) { // Restore full view messagesDiv.style.display = "block"; statusDiv.style.height = "auto"; statusDiv.style.maxHeight = "27vh"; statusDiv.style.padding = "15px"; minimizeBtn.innerHTML = "−"; } else { // Just minimize the container statusDiv.style.height = "auto"; statusDiv.style.maxHeight = "95px"; statusDiv.style.padding = "5px 15px"; minimizeBtn.innerHTML = "+"; } isMinimized = !isMinimized; }); document.body.appendChild(statusDiv); // Now parse URL for configuration const urlPath = window.location.pathname; const urlParams = new URLSearchParams(window.location.search); const urlMatches = urlPath.match(/\/([a-z]{2}-[a-z]{2})\/.+?rtx-(\d{4})/i); // Get intervals from URL parameters CONFIG.stockCheckInterval = parseInt(urlParams.get("stockInterval")) || CONFIG.stockCheckInterval; CONFIG.skuCheckInterval = parseInt(urlParams.get("skuInterval")) || CONFIG.skuCheckInterval; if (urlMatches) { CONFIG.locale = urlMatches[1].toLowerCase(); CONFIG.gpuModel = urlMatches[2]; updateStatus( `Detected - Model: ${CONFIG.gpuModel}, Locale: ${CONFIG.locale}, Intervals: Stock ${CONFIG.stockCheckInterval}ms / SKU ${CONFIG.skuCheckInterval}ms` ); } else { updateStatus( "Failed to detect GPU model and locale from URL. Script cannot continue.", true ); throw new Error( "Required URL parameters missing. URL must contain locale (xx-xx) and RTX model number." ); } // Create a speech queue system const speechQueue = { queue: [], speaking: false, add(message) { this.queue.push(message); if (!this.speaking) { this.processQueue(); } }, processQueue() { if (this.queue.length === 0) { this.speaking = false; return; } this.speaking = true; const message = this.queue[0]; const utterance = new SpeechSynthesisUtterance(message); utterance.rate = 1.0; utterance.pitch = 1.0; utterance.volume = CONFIG.ttsVolume; utterance.onend = () => { this.queue.shift(); // Remove the spoken message this.processQueue(); // Process next message if any }; utterance.onerror = (event) => { console.error("Speech synthesis error:", event); this.queue.shift(); // Remove the failed message this.processQueue(); // Continue with next message }; speechSynthesis.speak(utterance); }, }; // Replace the old speak function with the new non-blocking version function speak(message) { speechQueue.add(message); } // Function to update status function updateStatus(message, isError = false) { const time = new Date().toLocaleTimeString(); const color = isError ? "#ff6b6b" : "#4cd137"; const messageElement = document.createElement("div"); messageElement.style.cssText = "word-wrap: break-word; white-space: pre-wrap; margin-top: auto;"; messageElement.style.color = color; messageElement.textContent = `[${time}] ${message}`; // Append new message at the bottom messagesDiv.appendChild(messageElement); // Keep only last ~20 messages const messages = messagesDiv.getElementsByTagName("div"); while (messages.length > 20) { messages[0].remove(); } // Always scroll to bottom messagesDiv.scrollTop = messagesDiv.scrollHeight; console.log("[" + time + "] " + message); } // Create global SKU variable window.SKU = ""; // Update makeApiRequest to track errors async function makeApiRequest(url, options = {}) { try { if (CONFIG.testMode.enabled) { // Add empty response simulation if (CONFIG.testMode.simulateEmptyResponse) { return {}; // Simulate empty API response } // Simulate random failures based on failureRate if (Math.random() < CONFIG.testMode.failureRate) { const errorCode = CONFIG.testMode.errorCodes[ Math.floor(Math.random() * CONFIG.testMode.errorCodes.length) ]; const errorMessage = CONFIG.testMode.errorMessages[ Math.floor(Math.random() * CONFIG.testMode.errorMessages.length) ]; throw new Error( `HTTP error! status: ${errorCode}, message: ${errorMessage}` ); } } const response = await fetch(url, options); if (!response.ok) { throw new Error("HTTP error! status: " + response.status); } return response.json(); } catch (error) { // Add new error if (CONFIG.errorTracking.enabled) { const now = Date.now(); errorTracking.recentErrors.push({ timestamp: now, error: error.message, }); // Remove errors older than windowDurationMinutes const windowDurationMs = CONFIG.errorTracking.windowDurationMinutes * 60 * 1000; // Convert minutes to milliseconds errorTracking.recentErrors = errorTracking.recentErrors.filter( (error) => now - error.timestamp <= windowDurationMs ); // Calculate expected API calls in the time window const expectedCalls = Math.ceil( windowDurationMs / Math.min(CONFIG.stockCheckInterval, CONFIG.skuCheckInterval) ); // Calculate error rate based on actual errors vs expected calls const errorRate = errorTracking.recentErrors.length / expectedCalls; if (errorRate >= CONFIG.errorTracking.errorThreshold) { speak( `Warning: High error rate detected. ${Math.round( errorRate * 100 )}% of API calls failed in the last ${ CONFIG.errorTracking.windowDurationMinutes } minutes.` ); // Reset error tracking after notification to prevent spam errorTracking.recentErrors = []; } } throw error; } } // Function to check SKU updates async function checkSkuUpdates() { try { updateStatus("Checking for SKU updates..."); const cacheBuster = CONFIG.cacheBusting.enabled && CONFIG.cacheBusting.search ? `&_=${Date.now()}` : ""; const data = await makeApiRequest( `https://api.nvidia.partners/edge/product/search?page=1&limit=12&locale=${CONFIG.locale}&gpu=RTX%205090,RTX%205080&gpu_filter=RTX%205090~2,RTX%205080~2,RTX%204090~2,RTX%204080%20SUPER~4,RTX%204080~1,RTX%204070%20Ti%20SUPER~24,RTX%204070%20Ti~8,RTX%204060%20Ti~12,RTX%204070%20SUPER~22,RTX%204070~10,RTX%204060~7,RTX%203070~1,RTX%203060%20Ti~1,RTX%203060~6,RTX%203050~4&category=GPU${cacheBuster}` ); const targetGpu = data.searchedProducts?.productDetails?.find( (p) => p.displayName.includes(CONFIG.gpuModel) && p.isFounderEdition === true ); if (targetGpu && targetGpu.productSKU) { const newSku = targetGpu.productSKU; updateStatus( `${CONFIG.gpuModel} FE found in product list (SKU: ${newSku})` ); if (window.SKU === "") { window.SKU = newSku; updateStatus("✅ Initial SKU set: " + newSku); } else if (window.SKU !== newSku) { updateStatus( `🔄 ${CONFIG.gpuModel} FE SKU change detected! Updating to new SKU...` ); speak(`${CONFIG.gpuModel} SKU change detected!`); window.SKU = newSku; } } else { updateStatus( `${CONFIG.gpuModel} Founder's Edition not found in product list yet.`, true ); } } catch (error) { console.error("Error checking SKU updates:", error); updateStatus("SKU Check Error: " + error.message, true); speak("SKU check error."); } } // Function to handle when stock is found function handleStockFound(productUrl) { // Check if we're still actively monitoring if (!MONITORING_STATE.isActive) { console.log("Stock found but monitoring already stopped"); return; } // Immediately stop all monitoring MONITORING_STATE.isActive = false; MONITORING_STATE.lastStockFound = Date.now(); updateStatus(`🎉 RTX ${CONFIG.gpuModel} IN STOCK! 🎉`); speak(`RTX ${CONFIG.gpuModel} Found in stock!`); // For test simulations, use proshop.de if (!productUrl) { window.open("https://www.proshop.de", "_blank"); return; } // Handle real product URL redirect with 5-minute cooldown if (productUrl) { const now = Date.now(); const lastRedirect = localStorage.getItem("nvidiafe_last_redirect"); const fiveMinutes = 5 * 1000; if (!lastRedirect || now - parseInt(lastRedirect) > fiveMinutes) { localStorage.setItem("nvidiafe_last_redirect", now.toString()); window.open(productUrl, "_blank"); } else { updateStatus( "Skipping auto-redirect - cooldown active. YOU WILL GET BANNED IF YOU VISIT PROSHOP LINK TOO MUCH.", true ); } } } // Expose functions to unsafeWindow for Tampermonkey unsafeWindow.triggerStockFound = handleStockFound; unsafeWindow.simulateSKUChange = function () { updateStatus("🔄 Simulating SKU change..."); speak(`${CONFIG.gpuModel} SKU change detected!`); window.SKU = "SIMULATED_NEW_SKU_" + Date.now(); updateStatus(`${CONFIG.gpuModel} SKU updated to: ${window.SKU}`); }; unsafeWindow.checkSkuUpdatesNow = checkSkuUpdates; unsafeWindow.checkStockNow = checkStock; // Function to check stock async function checkStock() { if (!MONITORING_STATE.isActive) return; try { const cacheBuster = CONFIG.cacheBusting.enabled && CONFIG.cacheBusting.feInventory ? `&_=${Date.now()}` : ""; const data = await makeApiRequest( `https://api.store.nvidia.com/partner/v1/feinventory?skus=${window.SKU}&locale=${CONFIG.locale}${cacheBuster}`, { method: "GET", headers: { Accept: "application/json", }, } ); if (!MONITORING_STATE.isActive) return; // Check again after API call console.log("API Response:", data); if (data.listMap && data.listMap.length > 0) { // Reset counter when we get valid data errorTracking.consecutiveEmptyResponses = 0; const item = data.listMap[0]; if (item.is_active !== "false") { handleStockFound(item.product_url); return; } updateStatus("Stock check: Not in stock (Product inactive)"); } else { errorTracking.consecutiveEmptyResponses++; // Increment counter updateStatus( "No product data found in response. API endpoint might have changed.", true ); if (errorTracking.consecutiveEmptyResponses > 1) { const msg = `Warning: ${errorTracking.consecutiveEmptyResponses} consecutive empty API responses detected. API endpoint might have changed.`; updateStatus(msg, true); if (errorTracking.consecutiveEmptyResponses === 4) { speak(msg); } } } } catch (error) { if (!MONITORING_STATE.isActive) return; console.error("Error checking stock:", error); updateStatus("Error: " + error.message, true); } } // Start monitoring updateStatus("Monitor started"); // Replace the initializeMonitoring function and related monitoring logic async function initializeMonitoring() { // Helper function to sleep const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // Smart monitoring function that adjusts timing async function smartMonitor(checkFn, interval, name) { while (MONITORING_STATE.isActive) { // Add check for active state const startTime = Date.now(); try { if (!MONITORING_STATE.isActive) break; // Double-check before API call await checkFn(); } catch (error) { if (!MONITORING_STATE.isActive) break; updateStatus(`Error in ${name}: ${error.message}`, true); } if (!MONITORING_STATE.isActive) break; const executionTime = Date.now() - startTime; if (executionTime > interval) { updateStatus( `⚠️ Warning: ${name} took ${executionTime}ms (longer than ${interval}ms interval)`, true ); continue; } const sleepTime = interval - executionTime; await sleep(sleepTime); } } // Start both monitors await checkSkuUpdates(); // Initial SKU check if (window.SKU) { // Start stock monitoring smartMonitor(checkStock, CONFIG.stockCheckInterval, "Stock Check"); // Start SKU monitoring smartMonitor(checkSkuUpdates, CONFIG.skuCheckInterval, "SKU Check"); } else { updateStatus("⚠️ No SKU found, only monitoring for SKU updates", true); // Only start SKU monitoring smartMonitor(checkSkuUpdates, CONFIG.skuCheckInterval, "SKU Check"); } } // Start the monitoring process initializeMonitoring(); // Add these test helper functions unsafeWindow.toggleTestMode = function (enabled = true) { CONFIG.testMode.enabled = enabled; updateStatus(`Test mode ${enabled ? "enabled" : "disabled"}`); }; unsafeWindow.setFailureRate = function (rate) { CONFIG.testMode.failureRate = rate; updateStatus(`Failure rate set to ${rate * 100}%`); }; // Add new test function to unsafeWindow unsafeWindow.simulateEmptyAPIResponse = function (enabled = true) { CONFIG.testMode.simulateEmptyResponse = enabled; updateStatus( `Empty response simulation ${enabled ? "enabled" : "disabled"}` ); }; // Add cache busting control functions unsafeWindow.toggleCacheBusting = function (enabled = true) { CONFIG.cacheBusting.enabled = enabled; updateStatus(`Cache busting ${enabled ? "enabled" : "disabled"}`); }; unsafeWindow.toggleEndpointCacheBusting = function ( endpoint, enabled = true ) { if (endpoint === "feinventory") { CONFIG.cacheBusting.feInventory = enabled; updateStatus( `FE inventory cache busting ${enabled ? "enabled" : "disabled"}` ); } else if (endpoint === "search") { CONFIG.cacheBusting.search = enabled; updateStatus(`Search cache busting ${enabled ? "enabled" : "disabled"}`); } }; })();
Editor is loading...
Leave a Comment