Untitled

 avatar
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