Greasy Fork

YouTube AI广告增强 v2.2.0 (修复播放列表跳片+兼容快捷键)

自动跳过广告、16x倍速、静音、AI选择器更新、防止播放列表误跳、兼容 Video Speed Controller 快捷键,完全本地运行、无UI。

// ==UserScript==
// @name         YouTube AI广告增强 v2.2.0 (修复播放列表跳片+兼容快捷键)
// @namespace    http://tampermonkey.net/
// @version      2.2.0
// @description  自动跳过广告、16x倍速、静音、AI选择器更新、防止播放列表误跳、兼容 Video Speed Controller 快捷键,完全本地运行、无UI。
// @author       little fool
// @match        *://www.youtube.com/*
// @match        *://m.youtube.com/*
// @match        *://music.youtube.com/*
// @match        *://www.youtube-nocookie.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      openkey.cloud
// @run-at       document-end
// ==/UserScript==

(function () {
  'use strict';

  const API_URL = 'https://openkey.cloud/v1/chat/completions';
  const API_KEY = ['sk-', '1ytLN', 'fSpk5R34n', 'jTF628665', '6331c426cAeCb95E266F8D377'].join('');
  const CACHE_KEY = 'yt_ad_selectors_cache';
  const HISTORY_KEY = 'yt_ad_selectors_history';
  const UPDATE_KEY = 'yt_ad_last_update';

  const BASE_SELECTORS = new Set([
    '.ytp-ad-module',
    '.ytp-ad-overlay-container',
    '.ytp-ad-player-overlay',
    '.ad-showing .video-ads',
    '#player-ads'
  ]);

  let dynamicSelectors = new Set();
  let lastUpdate = 0;
  let wasAdPlaying = false;
  let previousMuted = null;
  let refreshAttempts = 0;
  let adConfirmed = false;
  let adCheckTimeout = null;
  let userModifiedSpeed = false;

  const MAX_REFRESH_ATTEMPTS = 5;
  const REFRESH_INTERVAL = 5000;
  const UPDATE_INTERVAL = 3600000;

  const log = (...args) => console.log('[YT净化 v2.2.0]', ...args);
  const getVideo = () => document.querySelector('video');
  const isVideoPage = () => /\/watch|\/tv|\/embed/.test(location.pathname);
  const isHomePage = () => location.pathname === '/';

  const safeRun = (fn, desc = '') => {
    try { fn(); } catch (e) { log(`❌ [${desc}]`, e); }
  };

  async function updateSelectorsViaAI() {
    if (Date.now() - lastUpdate < UPDATE_INTERVAL) return;
    try {
      const res = await new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: 'POST',
          url: API_URL,
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${API_KEY}`
          },
          data: JSON.stringify({
            model: 'gpt-4o',
            messages: [{
              role: 'user',
              content: "请提供当前YouTube最新广告CSS选择器数组,仅返回JSON格式的数组。"
            }],
            temperature: 0.2
          }),
          onload: resolve,
          onerror: reject
        });
      });

      let result = JSON.parse(res.responseText);
      const content = result.choices?.[0]?.message?.content?.trim().replace(/```json|```/g, '');
      const selectors = JSON.parse(content);
      if (Array.isArray(selectors) && selectors.length > 0) {
        dynamicSelectors = new Set(selectors);
        GM_setValue(CACHE_KEY, [...dynamicSelectors]);
        GM_setValue(UPDATE_KEY, Date.now());
        lastUpdate = Date.now();
        const history = GM_getValue(HISTORY_KEY, []);
        GM_setValue(HISTORY_KEY, [selectors, ...history.slice(0, 9)]);
        log('✅ [AI更新成功]', selectors);
      } else throw new Error('选择器格式不正确');
    } catch (err) {
      const history = GM_getValue(HISTORY_KEY, []);
      dynamicSelectors = new Set(history[0] || []);
      log('⚠️ 使用缓存选择器', [...dynamicSelectors]);
    }
  }

  function hideAds() {
    const allSelectors = [...BASE_SELECTORS, ...dynamicSelectors];
    allSelectors.forEach(sel => {
      document.querySelectorAll(sel).forEach(el => {
        Object.assign(el.style, {
          display: 'none',
          opacity: '0.01',
          pointerEvents: 'none'
        });
      });
    });
  }

  function clickSkipButton() {
    const btn = document.querySelector('.ytp-ad-skip-button');
    if (btn && btn.offsetParent !== null) btn.click();
  }

  function accelerateAdPlayback() {
    const video = getVideo();
    if (!video || userModifiedSpeed) return;

    const adElement = document.querySelector('.ad-showing');

    if (adElement && video.currentTime > 1.5) {
      if (!adConfirmed) {
        clearTimeout(adCheckTimeout);
        adCheckTimeout = setTimeout(() => {
          adConfirmed = true;
          applyAdAcceleration(video);
        }, 500); // 延迟判断广告
      } else {
        applyAdAcceleration(video);
      }
    } else {
      adConfirmed = false;
      clearTimeout(adCheckTimeout);
      if (!userModifiedSpeed && video.playbackRate !== 1) video.playbackRate = 1;
      if (wasAdPlaying && previousMuted !== null) {
        video.muted = previousMuted;
        previousMuted = null;
      }
    }

    wasAdPlaying = !!adElement;
  }

  function applyAdAcceleration(video) {
    if (video.playbackRate !== 16) {
      video.playbackRate = 16;
      previousMuted = video.muted;
      video.muted = true;
      log('🎬 广告中:静音 + 加速');
    }
  }

  function removeAdSubtitles() {
    const keywords = ['广告', '贊助', '推广', 'Sponsored', 'Sponsor'];
    document.querySelectorAll('.ytp-caption-segment').forEach(el => {
      if (keywords.some(k => el.textContent.includes(k))) el.remove();
    });
  }

  function detectAdBlackScreen() {
    const video = getVideo();
    const stuck = document.querySelector('.ad-showing') && video && video.readyState >= 2 && video.videoWidth === 0;
    if (stuck) showTip('yt-black-tip', '⏳ 正在处理卡住的广告...');
    else removeTip('yt-black-tip');
  }

  function detectAdBlockWarning() {
    const signs = [
      '广告拦截器',
      'adblock',
      '关闭广告拦截',
      '广告支持我们的服务',
      'Ad blockers violate YouTube’s Terms'
    ];
    return signs.some(k => document.body.innerText.includes(k));
  }

  function showTip(id, msg) {
    if (document.getElementById(id)) return;
    const div = document.createElement('div');
    div.id = id;
    div.innerText = msg;
    Object.assign(div.style, {
      position: 'fixed',
      top: '20px',
      left: '50%',
      transform: 'translateX(-50%)',
      backgroundColor: '#c00',
      color: '#fff',
      padding: '10px 16px',
      fontSize: '14px',
      fontWeight: 'bold',
      borderRadius: '6px',
      zIndex: 9999
    });
    document.body.appendChild(div);
  }

  function removeTip(id) {
    const el = document.getElementById(id);
    if (el) el.remove();
  }

  function hideHomepageAds() {
    const keywords = ['广告', '推广', '赞助', 'Sponsored'];
    document.querySelectorAll('ytd-rich-item-renderer,ytd-video-renderer').forEach(el => {
      if (keywords.some(k => el.innerText.includes(k))) el.style.display = 'none';
    });
  }

  function observePage() {
    const observer = new MutationObserver(() => {
      safeRun(hideAds, '隐藏广告');
      safeRun(clickSkipButton, '跳过广告');
      safeRun(accelerateAdPlayback, '倍速广告');
      safeRun(removeAdSubtitles, '隐藏字幕');
    });
    observer.observe(document.body, { childList: true, subtree: true });
  }

  function init() {
    dynamicSelectors = new Set(GM_getValue(CACHE_KEY, []));
    lastUpdate = GM_getValue(UPDATE_KEY, 0);
    updateSelectorsViaAI();
    setInterval(updateSelectorsViaAI, UPDATE_INTERVAL);

    if (isVideoPage()) {
      observePage();
      setInterval(() => safeRun(detectAdBlackScreen, '黑屏检测'), 1000);
      setInterval(() => {
        if (detectAdBlockWarning()) {
          if (refreshAttempts++ < MAX_REFRESH_ATTEMPTS) {
            showTip('yt-refresh', `⚠️ 检测广告拦截提示,正在刷新...(${refreshAttempts})`);
            setTimeout(() => location.reload(), 500);
          } else removeTip('yt-refresh');
        } else {
          removeTip('yt-refresh');
        }
      }, REFRESH_INTERVAL);
    }

    if (isHomePage()) {
      setInterval(() => safeRun(hideHomepageAds, '隐藏首页广告'), 2000);
    }
  }

  // 🔧 检测用户是否用快捷键调整播放速度(如 D/S)
  document.addEventListener('keydown', (e) => {
    if (['d', 's', 'D', 'S'].includes(e.key)) {
      userModifiedSpeed = true;
      log('🎛️ 快捷键触发:用户已手动调节播放速度,脚本不再控制播放倍速');
    }
  });

  init();
})();