Starburst YouTube

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

当前为 2023-11-19 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name             Starburst YouTube
// @namespace        C2FFB7B4-83BC-11EE-85E1-21839234574D
// @version          1.1.2
// @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 = () => videoContainer.classList.contains("ad-showing") || videoContainer.classList.contains("ad-interrupting"), clickSkipAdsButton = (buttonContainer) => {
      const skipAdsButtonList = [
        "button.ytp-ad-skip-button",
        "button.ytp-ad-skip-button-modern"
      ];
      buttonContainer.querySelectorAll(skipAdsButtonList.join(",")).forEach((button) => {
        button.click(), console.debug("[clickSkipAdsButton]", button);
      });
    }, fadVideo = () => {
      videoPlayer.pause(), videoPlayer.muted = !0, videoPlayer.currentTime = videoPlayer.duration, 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",
      // 搜尋結果的第一個項目是廣告,火狐不支援 has,使用其他的寫法
      // 'ytd-rich-item-renderer:has(ytd-ad-slot-renderer)'
      "ytd-rich-grid-row:first-child ytd-rich-item-renderer:first-child",
      // 搜尋結果的上方 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"
    ].join(",") + "{ visibility: hidden!important; }", setStyleCssCode = [
      // 設定播放器背景色,當處於一般影片模式+淺色主題時,隱藏廣告影片會透出一片白色,很突兀
      "ytd-player#ytd-player { background-color: black!important; }",
      // 影片上方加入 10px 的間距,避免被上方 header bar 蓋到
      "#player-full-bleed-container { margin-top: 10px!important; }",
      // 播放器控制列按鈕,只有在 hover 或暫停時才會顯示
      ".ytp-chrome-bottom { opacity: 0!important; transition: none!important; }",
      ".ytp-chrome-bottom:hover { opacity: 1!important; }",
      ".html5-video-player.paused-mode > .ytp-chrome-bottom { opacity: 1!important; }",
      // shorts 下方資訊欄 hover 時才顯示
      "ytd-reel-player-header-renderer.ytd-reel-player-overlay-renderer { opacity: 0; pointer-events: initial; }",
      "ytd-reel-player-header-renderer.ytd-reel-player-overlay-renderer:hover { opacity: 1 }",
      // shorts 下方的進度條
      "#progress-bar-line[hidden] { display: block!important; }",
      // shorts 上方控制列,只有 hover 時才會顯示
      "ytd-shorts-player-controls.ytd-reel-video-renderer { opacity: 0!important; pointer-events: initial; }",
      ".player-controls.ytd-reel-video-renderer:hover > ytd-shorts-player-controls.ytd-reel-video-renderer { opacity: 1!important; }"
    ].join("");
    addStyle([hiddenCssCode, invisibleCssCode, setStyleCssCode].join("")), console.debug("[setCssStyle]");
  }, setCloseDialogObserver = () => {
    const clickCloseDialogButton = (dialog) => {
      const closeButtonList = [
        'yt-button-renderer#dismiss-button button[aria-label="不用了,謝謝"]',
        // 關閉推薦試用 YouTube Premium
        'yt-button-renderer#confirm-button button[aria-label="是"]'
        // 背景播放音樂,會出現是否繼續播放,
      ];
      dialog.querySelectorAll(closeButtonList.join(",")).forEach((button) => {
        button.click(), console.debug("[clickCloseDialogButton]", button);
      });
    };
    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());
    };
  })();
  setCssStyle(), runSkipAd(), setCloseDialogObserver(), document.addEventListener("yt-navigate-start", () => {
    console.debug(`[yt-navigate-start] ${location.href}`), runSkipAd();
  }), console.debug("[main] first run");
})();