YouTube Enhancer (Thumbnail Preview)

View Original Avatar, Banner, Video and Shorts Thumbnails.

// ==UserScript==
// @name         YouTube Enhancer (Thumbnail Preview)
// @description  View Original Avatar, Banner, Video and Shorts Thumbnails.
// @icon         https://raw.githubusercontent.com/exyezed/youtube-enhancer/refs/heads/main/extras/youtube-enhancer.png
// @version      1.6
// @author       exyezed
// @namespace    https://github.com/exyezed/youtube-enhancer/
// @supportURL   https://github.com/exyezed/youtube-enhancer/issues
// @license      MIT
// @match        https://www.youtube.com/*
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  const css = `
        .thumbnail-overlay-container {
            position: absolute;
            bottom: 8px;
            left: 8px;
            z-index: 9999;
            opacity: 0;
            transition: opacity 0.2s ease;
        }

        .thumbnail-overlay-button {
            width: 28px;
            height: 28px;
            background: rgba(0, 0, 0, 0.7);
            border: none;
            border-radius: 4px;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            position: relative;
        }

        .thumbnail-overlay-button:hover {
            background: rgba(0, 0, 0, 0.9);
        }

        .thumbnail-overlay-button svg {
            width: 18px;
            height: 18px;
            fill: currentColor;
        }

        .thumbnail-dropdown {
            position: absolute;
            bottom: 100%;
            left: 0;
            background: rgba(0, 0, 0, 0.9);
            border-radius: 4px;
            padding: 4px;
            margin-bottom: 4px;
            display: none;
            flex-direction: column;
            min-width: 140px;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
            z-index: 10000;
        }

        .thumbnail-dropdown.show {
            display: flex !important;
        }

        .thumbnail-dropdown-item {
            background: none;
            border: none;
            color: white;
            padding: 8px 12px;
            cursor: pointer;
            border-radius: 2px;
            font-size: 12px;
            text-align: left;
            white-space: nowrap;
            transition: background-color 0.2s ease;
        }

        .thumbnail-dropdown-item:hover {
            background: rgba(255, 255, 255, 0.1);
        }

        /* Sidebar thumbnails */
        .yt-lockup-view-model-wiz__content-image:hover .thumbnail-overlay-container {
            opacity: 1;
        }

        .yt-lockup-view-model-wiz__content-image {
            position: relative;
        }

        .shortsLockupViewModelHostEndpoint:hover .thumbnail-overlay-container {
            opacity: 1;
        }

        .shortsLockupViewModelHostEndpoint {
            position: relative;
        }

        /* Channel page thumbnails */
        ytd-thumbnail:hover .thumbnail-overlay-container {
            opacity: 1;
        }

        ytd-thumbnail {
            position: relative;
        }

        ytm-shorts-lockup-view-model:hover .thumbnail-overlay-container {
            opacity: 1;
        }

        ytm-shorts-lockup-view-model {
            position: relative;
        }

        /* For shorts in channel page */
        .shortsLockupViewModelHostThumbnailContainer:hover .thumbnail-overlay-container {
            opacity: 1;
        }

        .shortsLockupViewModelHostThumbnailContainer {
            position: relative;
        }

        /* For shorts containers in general */
        ytm-shorts-lockup-view-model:hover .thumbnail-overlay-container {
            opacity: 1;
        }

        ytm-shorts-lockup-view-model {
            position: relative;
        }

        /* Watch page custom thumbnail */
        #thumbnailPreview-custom-image {
            width: 100%;
            height: auto;
            margin-bottom: 10px;
            box-sizing: border-box;
            border-radius: 10px;
            cursor: pointer;
        }

        /* Avatar and banner preview buttons */
        .thumbnailPreview-button {
            position: absolute;
            bottom: 10px;
            left: 5px;
            background-color: rgba(0, 0, 0, 0.75);
            color: white;
            border: none;
            border-radius: 3px;
            padding: 3px;
            font-size: 18px;
            cursor: pointer;
            z-index: 2000;
            opacity: 0;
            transition: opacity 0.3s;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .thumbnailPreview-container {
            position: relative;
        }

        .thumbnailPreview-container:hover .thumbnailPreview-button {
            opacity: 1;
        }
    `;

  const style = document.createElement("style");
  style.textContent = css;
  document.head.appendChild(style);

  function extractVideoId(url) {
    const regularMatch = url.match(/[?&]v=([^&]+)/);
    if (regularMatch) {
      return regularMatch[1];
    }

    const shortsMatch = url.match(/\/shorts\/([^?&]+)/);
    if (shortsMatch) {
      return shortsMatch[1];
    }

    return null;
  }

  function openImageInNewTab(url) {
    window.open(url, "_blank");
  }

  function createSVGElement(pathD) {
    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    const path = document.createElementNS("http://www.w3.org/2000/svg", "path");

    svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
    svg.setAttribute("width", "1em");
    svg.setAttribute("height", "1em");
    svg.setAttribute("viewBox", "0 0 24 24");

    path.setAttribute("fill", "currentColor");
    path.setAttribute("d", pathD);

    svg.appendChild(path);
    return svg;
  }

  const defaultIconPath =
    "M18 20H4V6h9V4H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-9h-2zm-7.79-3.17l-1.96-2.36L5.5 18h11l-3.54-4.71zM20 4V1h-2v3h-3c.01.01 0 2 0 2h3v2.99c.01.01 2 0 2 0V6h3V4";
  const hoverIconPath =
    "M19 7v2.99s-1.99.01-2 0V7h-3s.01-1.99 0-2h3V2h2v3h3v2zm-3 4V8h-3V5H5c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-8zM5 19l3-4l2 3l3-4l4 5z";

  let thumbnailPreviewCurrentVideoId = "";
  let thumbnailInsertionAttempts = 0;
  const MAX_ATTEMPTS = 10;
  const RETRY_DELAY = 500;

  function createOverlayButton(videoId, isShorts = false) {
    const container = document.createElement("div");
    container.className = "thumbnail-overlay-container";

    const button = document.createElement("button");
    button.className = "thumbnail-overlay-button";

    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute("viewBox", "0 0 24 24");
    svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");

    const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
    path.setAttribute(
      "d",
      "M18 20H4V6h9V4H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-9h-2zm-7.79-3.17l-1.96-2.36L5.5 18h11l-3.54-4.71zM20 4V1h-2v3h-3c.01.01 0 2 0 2h3v2.99c.01.01 2 0 2 0V6h3V4"
    );

    svg.appendChild(path);
    button.appendChild(svg);

    const dropdown = document.createElement("div");
    dropdown.className = "thumbnail-dropdown";

    let thumbnailOptions;

    if (isShorts) {
      thumbnailOptions = [
        { name: "Default", filename: "oardefault.jpg" },
        { name: "Alternative 1", filename: "oar1.jpg" },
        { name: "Alternative 2 (Default)", filename: "oar2.jpg" },
        { name: "Alternative 3", filename: "oar3.jpg" },
      ];
    } else {
      thumbnailOptions = [
        { name: "Default (120x90)", filename: "default.jpg" },
        { name: "Medium (320x180)", filename: "mqdefault.jpg" },
        { name: "High (480x360)", filename: "hqdefault.jpg" },
        { name: "Standard (640x480)", filename: "sddefault.jpg" },
        { name: "Max Res (1280x720)", filename: "maxresdefault.jpg" },
      ];
    }

    thumbnailOptions.forEach((option) => {
      const item = document.createElement("button");
      item.className = "thumbnail-dropdown-item";
      item.textContent = option.name;

      item.addEventListener("click", function (e) {
        e.preventDefault();
        e.stopPropagation();
        const thumbnailUrl = `https://i.ytimg.com/vi/${videoId}/${option.filename}`;
        openImageInNewTab(thumbnailUrl);
        dropdown.classList.remove("show");
      });

      dropdown.appendChild(item);
    });

    button.addEventListener("click", function (e) {
      e.preventDefault();
      e.stopPropagation();

      document.querySelectorAll(".thumbnail-dropdown.show").forEach((d) => {
        if (d !== dropdown) d.classList.remove("show");
      });

      dropdown.classList.toggle("show");
    });

    document.addEventListener("click", function (e) {
      if (!container.contains(e.target)) {
        dropdown.classList.remove("show");
      }
    });

    document.addEventListener("keydown", function (e) {
      if (e.key === "Escape") {
        dropdown.classList.remove("show");
      }
    });

    container.appendChild(dropdown);
    container.appendChild(button);

    return container;
  }

  function addButtonToElement(element, getFullSizeUrl) {
    if (!element.closest(".thumbnailPreview-container")) {
      const container = document.createElement("div");
      container.className = "thumbnailPreview-container";
      element.parentNode.insertBefore(container, element);
      container.appendChild(element);

      const button = document.createElement("button");
      button.className = "thumbnailPreview-button";

      const defaultIcon = createSVGElement(defaultIconPath);
      button.appendChild(defaultIcon);

      button.addEventListener("mouseenter", () => {
        button.innerHTML = "";
        button.appendChild(createSVGElement(hoverIconPath));
      });

      button.addEventListener("mouseleave", () => {
        button.innerHTML = "";
        button.appendChild(createSVGElement(defaultIconPath));
      });

      button.addEventListener("click", function (e) {
        e.preventDefault();
        e.stopPropagation();

        const url = getFullSizeUrl(element.src);
        if (url) {
          openImageInNewTab(url);
        }
      });

      container.appendChild(button);
    }
  }

  function isWatchPage() {
    const url = new URL(window.location.href);
    return url.pathname === "/watch" && url.searchParams.has("v");
  }

  function addOrUpdateThumbnailImage() {
    if (!isWatchPage()) return;

    const newVideoId = new URLSearchParams(window.location.search).get("v");

    if (!newVideoId || newVideoId === thumbnailPreviewCurrentVideoId) {
      return;
    }

    thumbnailPreviewCurrentVideoId = newVideoId;

    function attemptInsertion() {
      const targetElement = document.querySelector("#secondary-inner #panels");
      const existingImg = document.getElementById(
        "thumbnailPreview-custom-image"
      );

      if (existingImg) {
        existingImg.src = `https://i.ytimg.com/vi/${thumbnailPreviewCurrentVideoId}/mqdefault.jpg`;
        thumbnailInsertionAttempts = 0;
        return;
      }

      if (!targetElement) {
        thumbnailInsertionAttempts++;
        if (thumbnailInsertionAttempts < MAX_ATTEMPTS) {
          setTimeout(attemptInsertion, RETRY_DELAY);
        } else {
          thumbnailInsertionAttempts = 0;
        }
        return;
      }

      const img = document.createElement("img");
      img.id = "thumbnailPreview-custom-image";
      img.src = `https://i.ytimg.com/vi/${thumbnailPreviewCurrentVideoId}/mqdefault.jpg`;

      img.addEventListener("click", function (e) {
        e.preventDefault();
        e.stopPropagation();

        const maxResUrl = `https://i.ytimg.com/vi/${thumbnailPreviewCurrentVideoId}/maxresdefault.jpg`;
        openImageInNewTab(maxResUrl);
      });

      targetElement.parentNode.insertBefore(img, targetElement);
      thumbnailInsertionAttempts = 0;
    }

    attemptInsertion();
  }

  function processAvatars() {
    const avatars = document.querySelectorAll(
      "yt-avatar-shape img, yt-img-shadow#avatar img"
    );
    avatars.forEach((img) => {
      if (!img.closest(".thumbnailPreview-container")) {
        addButtonToElement(img, (src) =>
          src.replace(/=s\d+-c-k-c0x00ffffff-no-rj.*/, "=s0")
        );

        if (isWatchPage()) {
          const button = img
            .closest(".thumbnailPreview-container")
            .querySelector(".thumbnailPreview-button");
          if (button) {
            button.style.display = "none";
          }
        }
      }
    });
  }

  function processChannelBanners() {
    const banners = document.querySelectorAll("yt-image-banner-view-model img");
    banners.forEach((img) => {
      if (!img.closest(".thumbnailPreview-container")) {
        addButtonToElement(img, (src) => src.replace(/=w\d+-.*/, "=s0"));
      }
    });
  }

  function cleanupDuplicateButtons() {
    const shortsWithMultipleButtons = document.querySelectorAll(
      "ytm-shorts-lockup-view-model"
    );
    shortsWithMultipleButtons.forEach((shortsContainer) => {
      const buttons = shortsContainer.querySelectorAll(
        ".thumbnail-overlay-container"
      );
      if (buttons.length > 1) {
        const thumbnailContainer = shortsContainer.querySelector(
          ".shortsLockupViewModelHostThumbnailContainer"
        );
        const preferredButton = thumbnailContainer
          ? thumbnailContainer.querySelector(".thumbnail-overlay-container")
          : buttons[0];

        buttons.forEach((button) => {
          if (button !== preferredButton) {
            button.remove();
          }
        });
      }
    });
  }

  function addOverlayButtons() {
    const sidebarThumbnails = document.querySelectorAll(
      ".yt-lockup-view-model-wiz__content-image:not([data-overlay-added])"
    );
    sidebarThumbnails.forEach((container) => {
      const href = container.getAttribute("href");
      if (href) {
        const videoId = extractVideoId(href);
        if (videoId) {
          const overlayButton = createOverlayButton(videoId, false);
          container.appendChild(overlayButton);
          container.setAttribute("data-overlay-added", "true");
        }
      }
    });

    const channelThumbnails = document.querySelectorAll(
      "ytd-thumbnail:not([data-overlay-added])"
    );
    channelThumbnails.forEach((thumbnail) => {
      const link = thumbnail.querySelector('a[href*="/watch?v="]');
      if (link) {
        const href = link.getAttribute("href");
        const videoId = extractVideoId(href);
        if (videoId) {
          const overlayButton = createOverlayButton(videoId, false);
          thumbnail.appendChild(overlayButton);
          thumbnail.setAttribute("data-overlay-added", "true");
        }
      }
    });

    const allShortsContainers = document.querySelectorAll(
      "ytm-shorts-lockup-view-model:not([data-overlay-added])"
    );
    console.log("Found shorts containers:", allShortsContainers.length);

    allShortsContainers.forEach((shortsContainer) => {
      const link = shortsContainer.querySelector('a[href*="/shorts/"]');
      if (link) {
        const href = link.getAttribute("href");
        const videoId = extractVideoId(href);
        console.log("Processing shorts:", videoId, href);

        if (videoId) {
          const overlayButton = createOverlayButton(videoId, true);

          const thumbnailContainer = shortsContainer.querySelector(
            ".shortsLockupViewModelHostThumbnailContainer"
          );
          if (thumbnailContainer) {
            console.log("Adding button to thumbnail container");
            thumbnailContainer.appendChild(overlayButton);
          } else {
            console.log("Adding button to main container");
            shortsContainer.appendChild(overlayButton);
          }

          shortsContainer.setAttribute("data-overlay-added", "true");
        }
      }
    });

    const sidebarShorts = document.querySelectorAll(
      ".shortsLockupViewModelHostEndpoint:not([data-overlay-added])"
    );
    sidebarShorts.forEach((container) => {
      if (container.closest("ytm-shorts-lockup-view-model")) {
        return;
      }

      const href = container.getAttribute("href");
      if (href) {
        const videoId = extractVideoId(href);
        if (videoId) {
          const overlayButton = createOverlayButton(videoId, true);
          container.appendChild(overlayButton);
          container.setAttribute("data-overlay-added", "true");
        }
      }
    });

    addOrUpdateThumbnailImage();

    processAvatars();
    processChannelBanners();

    cleanupDuplicateButtons();
  }

  function observeChanges() {
    const observer = new MutationObserver(function (mutations) {
      let shouldCheck = false;
      mutations.forEach(function (mutation) {
        if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
          mutation.addedNodes.forEach(function (node) {
            if (node.nodeType === Node.ELEMENT_NODE) {
              if (
                node.querySelector &&
                (node.querySelector(
                  ".yt-lockup-view-model-wiz__content-image"
                ) ||
                  node.classList.contains(
                    "yt-lockup-view-model-wiz__content-image"
                  ) ||
                  node.querySelector(".shortsLockupViewModelHostEndpoint") ||
                  node.classList.contains(
                    "shortsLockupViewModelHostEndpoint"
                  ) ||
                  node.querySelector("ytd-thumbnail") ||
                  node.classList.contains("ytd-thumbnail") ||
                  node.querySelector("ytm-shorts-lockup-view-model") ||
                  node.classList.contains("ytm-shorts-lockup-view-model") ||
                  node.querySelector("yt-avatar-shape") ||
                  node.classList.contains("yt-avatar-shape") ||
                  node.querySelector("yt-image-banner-view-model") ||
                  node.classList.contains("yt-image-banner-view-model"))
              ) {
                shouldCheck = true;
              }
            }
          });
        }
      });

      if (shouldCheck) {
        setTimeout(addOverlayButtons, 100);
      }
    });

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

  function setupUrlChangeDetection() {
    let currentUrl = location.href;

    const originalPushState = history.pushState;
    const originalReplaceState = history.replaceState;

    history.pushState = function () {
      originalPushState.apply(history, arguments);
      setTimeout(() => {
        if (location.href !== currentUrl) {
          currentUrl = location.href;
          setTimeout(addOverlayButtons, 500);
        }
      }, 100);
    };

    history.replaceState = function () {
      originalReplaceState.apply(history, arguments);
      setTimeout(() => {
        if (location.href !== currentUrl) {
          currentUrl = location.href;
          setTimeout(addOverlayButtons, 500);
        }
      }, 100);
    };

    window.addEventListener("popstate", function () {
      setTimeout(() => {
        if (location.href !== currentUrl) {
          currentUrl = location.href;
          setTimeout(addOverlayButtons, 500);
        }
      }, 100);
    });

    setInterval(function () {
      if (location.href !== currentUrl) {
        currentUrl = location.href;
        setTimeout(addOverlayButtons, 300);
      }
    }, 500);

    document.addEventListener("yt-navigate-start", function () {
      setTimeout(addOverlayButtons, 1000);
    });

    document.addEventListener("yt-navigate-finish", function () {
      setTimeout(addOverlayButtons, 500);
    });
  }

  function observePageChanges() {
    const contentObserver = new MutationObserver((mutations) => {
      let shouldProcessRegular = false;

      mutations.forEach((mutation) => {
        if (mutation.addedNodes.length > 0) {
          shouldProcessRegular = true;
        }
      });

      if (shouldProcessRegular) {
        processAvatars();
        processChannelBanners();
      }
    });

    const panelObserver = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        if (
          mutation.type === "childList" &&
          (mutation.target.id === "secondary" ||
            mutation.target.id === "secondary-inner")
        ) {
          addOrUpdateThumbnailImage();
        }
      }
    });

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

    const observeSecondary = () => {
      const secondary = document.getElementById("secondary");
      if (secondary) {
        panelObserver.observe(secondary, {
          childList: true,
          subtree: true,
        });
      } else {
        setTimeout(observeSecondary, 1000);
      }
    };

    observeSecondary();
  }

  function init() {
    addOverlayButtons();
    observeChanges();
    observePageChanges();
    setupUrlChangeDetection();

    window.addEventListener("yt-navigate-finish", () => {
      addOrUpdateThumbnailImage();
      setTimeout(addOverlayButtons, 1000);
    });

    setTimeout(addOverlayButtons, 2000);
    setTimeout(addOverlayButtons, 5000);
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init);
  } else {
    init();
  }
})();