Prime Video Enhancer

Enhanced Prime Video experience with X-ray hiding, ad skipping, cursor management, and more

// ==UserScript==
// @name         Prime Video Enhancer
// @namespace    https://github.com/bernardopg
// @version      2.0
// @description  Enhanced Prime Video experience with X-ray hiding, ad skipping, cursor management, and more
// @author       bernardopg
// @icon         https://www.primevideo.com/favicon.ico
// @match        https://www.primevideo.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  // Configuration
  const CONFIG = {
    logging: false, // Set to true for debugging
    adSkip: {
      tries: 3,
      delay: 1500,
      selectors: [
        ".adSkipButton.skippable",
        '[data-testid="skip-ad-button"]',
        ".atvwebplayersdk-skipelements-button",
      ],
    },
    xray: {
      selectors: [
        ".xrayQuickView",
        '[data-testid="x-ray-panel"]',
        ".dv-player-fullscreen .xrayQuickView",
      ],
    },
    cursor: {
      hideDelay: 3000,
      playerSelectors: [
        ".webPlayerUIContainer",
        '[data-testid="video-player"]',
        ".dv-player-fullscreen",
      ],
    },
  };

  // Utility functions
  const Utils = {
    log: function (message, type = "info") {
      if (!CONFIG.logging) return;
      const prefix = "[Prime Video Enhancer]";
      console[type](`${prefix} ${message}`);
    },

    isElementVisible: function (element) {
      if (!element) return false;
      const rect = element.getBoundingClientRect();
      return rect.width > 0 && rect.height > 0;
    },

    waitForElement: function (selector, timeout = 10000) {
      return new Promise((resolve) => {
        const element = document.querySelector(selector);
        if (element) {
          resolve(element);
          return;
        }

        const observer = new MutationObserver(() => {
          const element = document.querySelector(selector);
          if (element) {
            observer.disconnect();
            resolve(element);
          }
        });

        observer.observe(document.body, {
          childList: true,
          subtree: true,
        });

        setTimeout(() => {
          observer.disconnect();
          resolve(null);
        }, timeout);
      });
    },

    debounce: function (func, wait) {
      let timeout;
      return function executedFunction(...args) {
        const later = () => {
          clearTimeout(timeout);
          func(...args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
      };
    },
  };

  // X-ray Panel Manager
  const XrayManager = {
    styleInjected: false,

    init: function () {
      this.injectStyles();
      this.observeXrayElements();
      Utils.log("X-ray manager initialized");
    },

    injectStyles: function () {
      if (this.styleInjected) return;

      const style = document.createElement("style");
      style.type = "text/css";
      style.id = "prime-video-enhancer-xray";

      const css = `
                ${CONFIG.xray.selectors.join(", ")} {
                    visibility: hidden !important;
                    opacity: 0 !important;
                    pointer-events: none !important;
                }
            `;

      style.textContent = css;
      document.head.appendChild(style);
      this.styleInjected = true;
      Utils.log("X-ray hiding styles injected");
    },

    observeXrayElements: function () {
      const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType === Node.ELEMENT_NODE) {
              CONFIG.xray.selectors.forEach((selector) => {
                if (node.matches && node.matches(selector)) {
                  this.hideElement(node);
                }
                const elements =
                  node.querySelectorAll && node.querySelectorAll(selector);
                if (elements) {
                  elements.forEach((el) => this.hideElement(el));
                }
              });
            }
          });
        });
      });

      observer.observe(document.body, {
        childList: true,
        subtree: true,
      });
    },

    hideElement: function (element) {
      if (element) {
        element.style.setProperty("visibility", "hidden", "important");
        element.style.setProperty("opacity", "0", "important");
        element.style.setProperty("pointer-events", "none", "important");
        Utils.log("X-ray element hidden");
      }
    },
  };

  // Ad Skipper
  const AdSkipper = {
    init: function () {
      this.hookFetch();
      this.observeAds();
      Utils.log("Ad skipper initialized");
    },

    hookFetch: function () {
      const originalFetch = window.fetch;
      if (typeof originalFetch !== "function") return;

      window.fetch = function (...args) {
        const result = originalFetch.apply(this, args);
        result
          .then(() => {
            AdSkipper.checkForAds();
          })
          .catch(() => {
            // Silently handle fetch errors
          });
        return result;
      };
    },

    observeAds: function () {
      const observer = new MutationObserver(
        Utils.debounce(() => {
          this.checkForAds();
        }, 500)
      );

      observer.observe(document.body, {
        childList: true,
        subtree: true,
      });
    },

    checkForAds: function () {
      CONFIG.adSkip.selectors.forEach((selector) => {
        const button = document.querySelector(selector);
        if (button && Utils.isElementVisible(button)) {
          this.skipAd(button);
        }
      });
    },

    skipAd: function (button) {
      try {
        button.click();
        Utils.log("Ad skipped successfully");
      } catch (error) {
        Utils.log(`Failed to skip ad: ${error.message}`, "error");
      }
    },
  };

  // Cursor Manager
  const CursorManager = {
    hideTimeout: null,
    isPlayerActive: false,

    init: function () {
      this.observePlayer();
      Utils.log("Cursor manager initialized");
    },

    observePlayer: function () {
      const observer = new MutationObserver(() => {
        this.setupCursorHiding();
      });

      observer.observe(document.body, {
        childList: true,
        subtree: true,
      });

      this.setupCursorHiding();
    },

    setupCursorHiding: function () {
      CONFIG.cursor.playerSelectors.forEach((selector) => {
        const player = document.querySelector(selector);
        if (player && !player.dataset.cursorSetup) {
          this.attachCursorEvents(player);
          player.dataset.cursorSetup = "true";
        }
      });
    },

    attachCursorEvents: function (player) {
      const videoElement = player.querySelector("video");

      player.addEventListener("mouseenter", () => {
        this.isPlayerActive = true;
        this.scheduleCursorHide(player);
      });

      player.addEventListener("mouseleave", () => {
        this.isPlayerActive = false;
        this.showCursor(player);
      });

      player.addEventListener("mousemove", () => {
        if (this.isPlayerActive) {
          this.showCursor(player);
          this.scheduleCursorHide(player);
        }
      });

      // Show cursor when video is paused
      if (videoElement) {
        videoElement.addEventListener("pause", () => {
          this.showCursor(player);
        });
      }
    },

    scheduleCursorHide: function (player) {
      clearTimeout(this.hideTimeout);
      this.hideTimeout = setTimeout(() => {
        if (this.isPlayerActive) {
          this.hideCursor(player);
        }
      }, CONFIG.cursor.hideDelay);
    },

    hideCursor: function (player) {
      player.style.cursor = "none";
      Utils.log("Cursor hidden");
    },

    showCursor: function (player) {
      clearTimeout(this.hideTimeout);
      player.style.cursor = "default";
    },
  };

  // Quality Manager (New Feature)
  const QualityManager = {
    init: function () {
      this.addKeyboardShortcuts();
      Utils.log("Quality manager initialized");
    },

    addKeyboardShortcuts: function () {
      document.addEventListener("keydown", (event) => {
        // Only trigger on video player pages
        if (!document.querySelector("video")) return;

        switch (event.key.toLowerCase()) {
          case "h":
            if (event.ctrlKey) {
              event.preventDefault();
              this.openQualitySettings();
            }
            break;
          case "f":
            if (event.ctrlKey) {
              event.preventDefault();
              this.toggleFullscreen();
            }
            break;
        }
      });
    },

    openQualitySettings: function () {
      const settingsButton =
        document.querySelector('[data-testid="settings-button"]') ||
        document.querySelector(".atvwebplayersdk-settings-button");
      if (settingsButton) {
        settingsButton.click();
        Utils.log("Quality settings opened");
      }
    },

    toggleFullscreen: function () {
      const fullscreenButton =
        document.querySelector('[data-testid="fullscreen-button"]') ||
        document.querySelector(".atvwebplayersdk-fullscreen-button");
      if (fullscreenButton) {
        fullscreenButton.click();
        Utils.log("Fullscreen toggled");
      }
    },
  };

  // Auto-play Manager (New Feature)
  const AutoPlayManager = {
    init: function () {
      this.handleNextEpisode();
      Utils.log("Auto-play manager initialized");
    },

    handleNextEpisode: function () {
      const observer = new MutationObserver(() => {
        const nextButton =
          document.querySelector('[data-testid="next-episode-button"]') ||
          document.querySelector(".nextupcard-button");

        if (nextButton && Utils.isElementVisible(nextButton)) {
          setTimeout(() => {
            if (nextButton.parentNode) {
              nextButton.click();
              Utils.log("Auto-played next episode");
            }
          }, 2000); // Wait 2 seconds before auto-clicking
        }
      });

      observer.observe(document.body, {
        childList: true,
        subtree: true,
      });
    },
  };

  // Main initialization
  function initialize() {
    Utils.log("Prime Video Enhancer starting...");

    // Wait for the page to be ready
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", startEnhancements);
    } else {
      startEnhancements();
    }
  }

  function startEnhancements() {
    try {
      XrayManager.init();
      AdSkipper.init();
      CursorManager.init();
      QualityManager.init();
      AutoPlayManager.init();

      Utils.log("All enhancements initialized successfully");
    } catch (error) {
      Utils.log(`Initialization error: ${error.message}`, "error");
    }
  }

  // Start the enhancer
  initialize();
})();