Untitled
unknown
plain_text
a year ago
21 kB
126
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