Starburst YouTube

加速跳過 YouTube 的廣告,快... 要更快! 還要更快!!!

// ==UserScript==
// @name             Starburst YouTube
// @namespace        C2FFB7B4-83BC-11EE-85E1-21839234574D
// @version          1.1.8
// @description      加速跳過 YouTube 的廣告,快... 要更快! 還要更快!!!
// @description:en   Speed up to skip YouTube ads, Faster.. Faster!... Even Faster!!!
// @author           Rick0
// @match            https://www.youtube.com/*
// @icon             https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @compatible       firefox Tampermonkey
// @compatible       chrome Tampermonkey
// @run-at           document-end
// @license          MIT
// @noframes
// ==/UserScript==

(function() {
  "use strict";
  const addStyle = (cssCode) => {
    const style = document.createElement("style");
    style.innerHTML = cssCode, document.head.append(style);
  }, canRun = () => location.href.search(/[?&]v=/) > -1, setSkipAdObserver = () => {
    console.debug("[setSkipAdObserver] start.");
    const abortController = new AbortController(), videoContainer = document.querySelector("#movie_player"), videoPlayer = videoContainer.querySelector(".video-stream"), isAd = () => ["ad-showing", "ad-interrupting"].some(
      (className) => videoContainer.classList.contains(className)
    ), clickSkipAdsButton = (buttonContainer) => {
      [
        "button.ytp-ad-skip-button",
        "button.ytp-ad-skip-button-modern"
      ].map((selector) => buttonContainer.querySelector(selector)).filter((button) => button !== null).forEach((button) => {
        button.click(), console.debug("[clickSkipAdsButton]", button);
      });
    }, fadVideo = () => {
      const floorDuration = Math.floor(videoPlayer.duration);
      videoPlayer.currentTime !== floorDuration && (videoPlayer.muted = !0, videoPlayer.currentTime = floorDuration, videoPlayer.paused && videoPlayer.play(), console.debug("[fadVideo]", videoPlayer));
    };
    {
      isAd() && videoPlayer.readyState > 0 && fadVideo();
      const handleLoadedmetadata = () => {
        console.debug("[loadedmetadata] start."), isAd() && fadVideo(), console.debug("[loadedmetadata] finish.");
      };
      videoPlayer.addEventListener("loadedmetadata", handleLoadedmetadata), abortController.signal.addEventListener("abort", () => {
        videoPlayer.removeEventListener("loadedmetadata", handleLoadedmetadata), console.debug("[abort] loadedmetadata.");
      });
    }
    return new Promise((resolve) => {
      const adsContainer = videoContainer.querySelector(".video-ads");
      if (adsContainer !== null) {
        resolve(adsContainer);
        return;
      }
      const adsContainerObserver = new MutationObserver(() => {
        const adsContainer2 = videoContainer.querySelector(".video-ads");
        adsContainer2 !== null && (adsContainerObserver.disconnect(), resolve(adsContainer2));
      });
      adsContainerObserver.observe(videoContainer, { childList: !0 }), abortController.signal.addEventListener("abort", () => {
        adsContainerObserver.disconnect(), resolve(null), console.debug("[adsContainerObserver] disconnect.");
      });
    }).then((adsContainer) => {
      if (adsContainer === null)
        return;
      clickSkipAdsButton(adsContainer);
      const skipAdsButtonObserver = new MutationObserver(() => {
        clickSkipAdsButton(adsContainer);
      });
      skipAdsButtonObserver.observe(adsContainer, {
        childList: !0,
        subtree: !0
      }), abortController.signal.addEventListener("abort", () => {
        skipAdsButtonObserver.disconnect(), console.debug("[skipAdsButtonObserver] disconnect");
      });
    }).catch((err) => {
      console.debug(err);
    }), console.debug("[setSkipAdObserver] finish."), abortController;
  }, setCssStyle = () => {
    const hiddenCssCode = [
      // 右側聊天室上方的廣告
      'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]',
      // 右側聊天室下方的廣告
      "#player-ads",
      // 右側推薦影片最上第一個會有廣告
      "ytd-ad-slot-renderer",
      // 搜尋結果的第一個項目是廣告(包含有 ytd-ad-slot-renderer 的基本都是廣告)
      "ytd-rich-item-renderer:has(ytd-ad-slot-renderer)",
      // 搜尋結果的上方 Banner 那塊
      "#masthead-ad",
      // == 影片相關 ==
      // 影片廣告的控制按鈕之類的內容
      ".html5-video-player > .video-ads",
      // 影片右上角資訊欄會彈出的內容
      ".html5-video-player .ytp-cards-teaser",
      // 影片結束後的推薦影片
      ".html5-video-player > .ytp-ce-element",
      // 影片右下角的浮水印按鈕
      ".html5-video-player .annotation.annotation-type-custom.iv-branding",
      // 影片左下角推薦商品
      ".html5-video-player .ytp-featured-product"
    ].join(",") + "{ display: none!important; }", invisibleCssCode = [
      // 播放中的廣告影片
      ".ad-showing video.video-stream, .ad-interrupting video.video-stream",
      // 廣告影片標題
      ".ad-showing > .ytp-chrome-top, .ad-interrupting > .ytp-chrome-top",
      // 廣告的進度條
      ".ad-showing .ytp-progress-list > .ytp-play-progress, .ad-interrupting .ytp-progress-list > .ytp-play-progress",
      ".ad-showing .ytp-progress-list > .ytp-load-progress, .ad-interrupting .ytp-progress-list > .ytp-load-progress",
      // 廣告的字幕
      ".ad-showing > .ytp-caption-window-container, .ad-interrupting > .ytp-caption-window-container",
      // 廣告的背景圖
      ".ad-showing > .ytp-cued-thumbnail-overlay, .ad-interrupting > .ytp-cued-thumbnail-overlay",
      // 廣告的 loading spinner
      ".ad-showing > .ytp-spinner, .ad-interrupting > .ytp-spinner"
    ].join(",") + "{ visibility: hidden!important; }", setStyleCssCode = [
      // 設定播放器背景色,當處於一般影片模式+淺色主題時,隱藏廣告影片會透出一片白色,很突兀
      "ytd-player#ytd-player { background-color: black!important; }"
    ].join("");
    addStyle([hiddenCssCode, invisibleCssCode, setStyleCssCode].join("")), console.debug("[setCssStyle]");
  }, setCloseDialogObserver = () => {
    const clickCloseDialogButton = (dialog) => {
      if ([
        // 關閉推薦試用 YouTube Premium
        ".yt-mealbar-promo-renderer > yt-button-renderer#dismiss-button > yt-button-shape > button"
      ].map((selector) => dialog.querySelector(selector)).filter((button) => button !== null).forEach((button) => {
        button.click(), console.debug("[clickCloseDialogButton]", button);
      }), dialog.querySelectorAll(".buttons yt-button-shape > button").length === 1) {
        const confirmButton = dialog.querySelector(
          "yt-button-renderer#confirm-button > yt-button-shape > button"
        );
        confirmButton !== null && (confirmButton.click(), console.debug("[clickCloseDialogButton]", confirmButton));
      }
    };
    new Promise((resolve) => {
      const ytdApp = document.querySelector("ytd-app"), dialog = ytdApp.querySelector("ytd-popup-container");
      if (dialog !== null) {
        resolve(dialog);
        return;
      }
      new MutationObserver((_m, observer) => {
        const dialog2 = ytdApp.querySelector("ytd-popup-container");
        dialog2 !== null && (observer.disconnect(), resolve(dialog2));
      }).observe(ytdApp, { childList: !0 });
    }).then((dialog) => {
      clickCloseDialogButton(dialog), new MutationObserver(() => {
        clickCloseDialogButton(dialog);
      }).observe(dialog, {
        childList: !0,
        subtree: !0
      });
    }).catch((err) => {
      console.debug(err);
    }), console.debug("[setCloseDialogObserver]");
  }, runSkipAd = (() => {
    let abortController = null;
    return () => {
      abortController == null || abortController.abort(), canRun() && (abortController = setSkipAdObserver());
    };
  })();
  document.addEventListener("yt-navigate-start", () => {
    console.debug(`[yt-navigate-start] ${location.href}`), runSkipAd();
  }), window.addEventListener("popstate", () => {
    console.debug(`[popstate] ${location.href}`), runSkipAd();
  }), setCssStyle(), runSkipAd(), setCloseDialogObserver(), console.debug("[main] first run");
})();