// ==UserScript== // @name 抖音自动跳过广告视频 (精准版 v3.7) // @namespace http://tampermonkey.net/ // @version 3.7.0 // @description 精准检测并跳过抖音网页版的广告视频、购物视频和直播带货视频,首次加载自动开启声音和最高清晰度 // @author Assistant // @match https://www.douyin.com/* // @icon https://www.douyin.com/favicon.ico // @license CC BY-NC-ND 4.0 // @grant none // @run-at document-end // @downloadURL https://update.greasyfork.icu/scripts/558072/%E6%8A%96%E9%9F%B3%E8%87%AA%E5%8A%A8%E8%B7%B3%E8%BF%87%E5%B9%BF%E5%91%8A%E8%A7%86%E9%A2%91%20%28%E7%B2%BE%E5%87%86%E7%89%88%20v37%29.user.js // @updateURL https://update.greasyfork.icu/scripts/558072/%E6%8A%96%E9%9F%B3%E8%87%AA%E5%8A%A8%E8%B7%B3%E8%BF%87%E5%B9%BF%E5%91%8A%E8%A7%86%E9%A2%91%20%28%E7%B2%BE%E5%87%86%E7%89%88%20v37%29.meta.js // ==/UserScript== (function() { 'use strict'; // ==================== 配置项 ==================== const CONFIG = { checkInterval: 100, skipDelay: 100, stabilityDelay: 400, maxRetries: 3, retryDelay: 200, debug: true, showNotification: true, notificationDuration: 500, skipAds: true, skipShopping: true, skipLive: true, // 初始化设置 autoUnmute: false, autoHighQuality: true, initSettingsDelay: 1000 }; // 清晰度优先级列表(从高到低) const QUALITY_PRIORITY = ['8K', '4K', '2K', '1080P', '720P', '540P', '480P', '360P']; // ==================== 状态管理 ==================== const state = { currentVideoId: null, lastSkipTime: 0, processedVideos: new Set(), isChecking: false, checkTimeout: null, skipInProgress: false, initSettingsDone: false, lastLiveSkipTime: 0 // 新增:防止直播重复跳过 }; // ==================== 工具函数 ==================== function log(...args) { if (CONFIG.debug) { console.log('%c[抖音跳广告 v3.7]', 'color: #ff4757; font-weight: bold;', ...args); } } function showNotification(message, type = 'ad') { if (!CONFIG.showNotification) return; const existing = document.getElementById('dy-ad-skip-notify'); if (existing) existing.remove(); const colors = { ad: 'linear-gradient(135deg, #ff4757, #ff6b81)', shopping: 'linear-gradient(135deg, #ffa502, #ff7f50)', live: 'linear-gradient(135deg, #e74c3c, #c0392b)', info: 'linear-gradient(135deg, #2ed573, #7bed9f)', setting: 'linear-gradient(135deg, #3742fa, #5352ed)' }; const icons = { ad: '🚫', shopping: '🛒', live: '📺', info: '✓', setting: '⚙️' }; const div = document.createElement('div'); div.id = 'dy-ad-skip-notify'; div.innerHTML = `
${icons[type] || icons.ad}${message}
`; document.body.appendChild(div); setTimeout(() => { div.style.transition = 'all 0.3s ease'; div.style.opacity = '0'; setTimeout(() => div.remove(), 300); }, CONFIG.notificationDuration); } /** * 模拟真实点击 */ function simulateClick(element) { if (!element) return false; try { element.click(); return true; } catch (e) { log('click() 失败,尝试其他方式'); } try { const rect = element.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const mousedownEvent = new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window, clientX: centerX, clientY: centerY }); const mouseupEvent = new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window, clientX: centerX, clientY: centerY }); const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window, clientX: centerX, clientY: centerY }); element.dispatchEvent(mousedownEvent); element.dispatchEvent(mouseupEvent); element.dispatchEvent(clickEvent); return true; } catch (e) { log('MouseEvent 派发失败:', e); return false; } } // ==================== 初始化设置功能 ==================== /** * 取消静音 - 打开声音 */ function unmute() { const volumeBtn = document.querySelector('.xgplayer-volume'); if (!volumeBtn) { log('未找到音量控制按钮,稍后重试...'); return false; } const currentState = volumeBtn.getAttribute('data-state'); log('当前音量状态:', currentState); if (currentState === 'mute') { const iconDiv = volumeBtn.querySelector('.xgplayer-icon'); if (iconDiv) { log('尝试点击音量图标...'); simulateClick(iconDiv); setTimeout(() => { const newState = volumeBtn.getAttribute('data-state'); if (newState !== 'mute') { log('✓ 已取消静音,当前状态:', newState); } else { log('点击后状态仍为静音,尝试再次点击'); simulateClick(iconDiv); } }, 100); return true; } } else { log('✓ 当前已经是非静音状态:', currentState); return true; } return false; } /** * 获取清晰度优先级分数 */ function getQualityScore(text) { for (let i = 0; i < QUALITY_PRIORITY.length; i++) { if (text.includes(QUALITY_PRIORITY[i])) { return QUALITY_PRIORITY.length - i; } } return 0; } /** * 设置最高清晰度 */ function setHighestQuality() { const clarityContainer = document.querySelector('.xgplayer-playclarity-setting'); if (!clarityContainer) { log('未找到清晰度设置容器'); return false; } const items = clarityContainer.querySelectorAll('.gear .virtual .item'); if (!items || items.length === 0) { log('未找到清晰度选项'); return false; } let highestQualityItem = null; let highestScore = -1; let highestQualityName = ''; for (const item of items) { const text = item.textContent.trim(); const score = getQualityScore(text); log(`清晰度选项: "${text}", 分数: ${score}`); if (score > highestScore) { highestScore = score; highestQualityItem = item; highestQualityName = text; } } if (highestQualityItem && highestScore > 0) { if (highestQualityItem.classList.contains('selected')) { log('✓ 已经是最高清晰度:', highestQualityName); return { success: true, name: highestQualityName }; } const gear = clarityContainer.querySelector('.gear'); if (gear) { gear.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); setTimeout(() => { simulateClick(highestQualityItem); log('✓ 已设置清晰度为:', highestQualityName); setTimeout(() => { gear.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true })); }, 100); }, 300); return { success: true, name: highestQualityName }; } } return { success: false, name: '' }; } /** * 执行初始化设置 */ function performInitSettings() { if (state.initSettingsDone) { log('初始化设置已执行过,跳过'); return; } log('执行初始化设置...'); let settingsApplied = []; if (CONFIG.autoUnmute) { const volumeBtn = document.querySelector('.xgplayer-volume'); if (volumeBtn) { if (unmute()) { settingsApplied.push('🔊 声音已开启'); } } else { log('音量按钮未找到,500ms后重试'); setTimeout(() => { if (unmute()) { showNotification('🔊 声音已开启', 'setting'); } }, 500); } } if (CONFIG.autoHighQuality) { setTimeout(() => { const result = setHighestQuality(); if (result && result.success && result.name) { settingsApplied.push(`📺 ${result.name}`); if (settingsApplied.length > 0) { showNotification(settingsApplied.join(' | '), 'setting'); } } }, 600); } state.initSettingsDone = true; log('初始化设置标记完成'); } // ==================== 核心检测逻辑 ==================== /** * 获取当前活跃的视频或直播 * 修复:同时支持普通视频和直播 */ function getActiveVideo() { // 1. 首先检查普通视频 const videoContainer = document.querySelector('[data-e2e="feed-active-video"]'); if (videoContainer) { const videoId = videoContainer.getAttribute('data-e2e-vid'); return { container: videoContainer, videoId, type: 'video' }; } // 2. 检查直播视频 (feed-live) const liveContainer = document.querySelector('[data-e2e="feed-live"]'); if (liveContainer) { // 直播没有固定的 vid,使用元素ID或生成唯一标识 const liveId = 'live_' + (liveContainer.id || 'current'); return { container: liveContainer, videoId: liveId, type: 'live' }; } // 3. 备用:检查 slider-card(某些直播使用这个) const sliderCard = document.querySelector('#slider-card[data-e2e="feed-live"]'); if (sliderCard) { return { container: sliderCard, videoId: 'live_slider', type: 'live' }; } return null; } /** * 检测直播带货特征(新增) */ function checkLiveSalesFeatures(container) { if (!CONFIG.skipLive) return { isLiveSales: false, reason: '' }; // 检查 data-e2e="feed-live" 属性(最直接的判断) if (container.getAttribute('data-e2e') === 'feed-live') { // 检查是否有购物车/商品列表 //const yellowCart = container.querySelector('[data-e2e="yellowCart-container"]'); //if (yellowCart) { // return { isLiveSales: true, reason: '直播带货(购物车)' }; //} // 检查"全部商品"按钮 const allGoodsBtn = container.querySelector('.oUumeR8j'); if (allGoodsBtn && allGoodsBtn.textContent.includes('全部商品')) { return { isLiveSales: true, reason: '直播带货(全部商品)' }; } // 检查直播中标签 //const liveTag = container.querySelector('.semi-tag-content'); //if (liveTag && (liveTag.textContent.includes('直播中') || liveTag.textContent.includes('直播'))) { // return { isLiveSales: true, reason: '直播中' }; //} // 检查直播加载中 //const liveLoading = container.querySelector('.douyin-player-loading-text'); //if (liveLoading && liveLoading.textContent.includes('直播')) { // return { isLiveSales: true, reason: '直播加载中' }; //} // 检查进入直播间提示 //const enterLiveText = container.textContent; //if (enterLiveText.includes('进入直播间') || enterLiveText.includes('点击进入直播')) { // return { isLiveSales: true, reason: '直播入口' }; //} // 如果是 feed-live 但没有明显特征,也跳过(保守策略) //return { isLiveSales: true, reason: '直播视频' }; } return { isLiveSales: false, reason: '' }; } function checkAdFeatures(container) { if (!CONFIG.skipAds) return { isAd: false, reason: '' }; const playbackRatio = container.querySelector('.xgplayer-setting-playbackRatio'); if (playbackRatio && playbackRatio.classList.contains('disabled')) { return { isAd: true, reason: '广告视频(倍速禁用)' }; } const tips = container.querySelector('.xgplayer-playback-setting .xgTips'); if (tips && tips.textContent.includes('广告视频不支持倍速功能')) { return { isAd: true, reason: '广告视频(提示文字)' }; } return { isAd: false, reason: '' }; } function checkShoppingFeatures(container) { if (!CONFIG.skipShopping) return { isShopping: false, reason: '' }; const shopAnchor = container.querySelector('.xgplayer-shop-anchor'); if (shopAnchor && shopAnchor.offsetWidth > 0) { return { isShopping: true, reason: '购物链接' }; } const sideBar = container.querySelector('#videoSideBar'); if (sideBar) { const productCard = sideBar.querySelector('[class*="goods"], [class*="product"], [class*="commodity"]'); if (productCard && productCard.offsetWidth > 0) { if (!sideBar.textContent.includes('全部商品') && !sideBar.textContent.includes('直播')) { return { isShopping: true, reason: '商品详情' }; } } } const embeddedCard = container.querySelector('.xgplayer-shop-anchor, [class*="shopAnchor"]'); if (embeddedCard && embeddedCard.offsetWidth > 0) { return { isShopping: true, reason: '嵌入式购物卡片' }; } return { isShopping: false, reason: '' }; } function checkLiveFeatures(container) { if (!CONFIG.skipLive) return { isLive: false, reason: '' }; const player = container.querySelector('.xgplayer') || container; // 检查直播标签 const liveTag = player.querySelector('[class*="live-tag"], [class*="liveTag"], [class*="LiveTag"]'); if (liveTag && liveTag.offsetWidth > 0 && liveTag.offsetWidth < 150) { const text = liveTag.textContent.trim(); if (text.includes('直播中') || text.includes('直播')) { return { isLive: true, reason: '直播中标签' }; } } // 检查 semi-tag(抖音新版直播标签) const semiTag = container.querySelector('.semi-tag-content'); if (semiTag && semiTag.textContent.includes('直播')) { return { isLive: true, reason: '直播标签(semi-tag)' }; } const livePlayer = container.querySelector('[class*="live-player"], [class*="livePlayer"]'); if (livePlayer) { return { isLive: true, reason: '直播播放器' }; } const sideBar = container.querySelector('#videoSideBar'); if (sideBar) { const sideBarText = sideBar.textContent; if (/全部商品\s*\d+/.test(sideBarText)) { return { isLive: true, reason: '直播商品列表' }; } if (/\d{1,2}月\d{1,2}日.*\d{1,2}:\d{2}/.test(sideBarText) && sideBarText.includes('开播')) { return { isLive: true, reason: '直播预告' }; } } const overlays = container.querySelectorAll('[class*="overlay"], [class*="cover"], [class*="mask"]'); for (const overlay of overlays) { if (overlay.offsetWidth > 0) { const text = overlay.textContent.trim(); if (text === '直播中' || text === '直播加载中' || text.match(/^直播中.*进入直播间$/)) { return { isLive: true, reason: '直播覆盖层' }; } } } const enterLiveBtn = container.querySelector('[class*="enter-live"], [class*="enterLive"], [class*="goLive"]'); if (enterLiveBtn && enterLiveBtn.offsetWidth > 0) { return { isLive: true, reason: '进入直播间按钮' }; } const descArea = container.querySelector('[class*="desc"], [class*="info"], [class*="meta"]'); if (descArea) { const descText = descArea.textContent; if (descText.includes('正在直播') || descText.includes('进入直播间')) { return { isLive: true, reason: '直播描述' }; } } return { isLive: false, reason: '' }; } function detectVideoType(container, type) { if (!container) return { shouldSkip: false, reason: '', type: 'normal' }; // 如果已经标识为直播类型,直接检测直播特征 if (type === 'live') { const liveSalesResult = checkLiveSalesFeatures(container); if (liveSalesResult.isLiveSales) { return { shouldSkip: true, reason: liveSalesResult.reason, type: 'live' }; } } // 检测广告 const adResult = checkAdFeatures(container); if (adResult.isAd) { return { shouldSkip: true, reason: adResult.reason, type: 'ad' }; } // 检测直播特征(针对普通视频容器中的直播元素) const liveResult = checkLiveFeatures(container); if (liveResult.isLive) { return { shouldSkip: true, reason: liveResult.reason, type: 'live' }; } // 检测购物 const shoppingResult = checkShoppingFeatures(container); if (shoppingResult.isShopping) { return { shouldSkip: true, reason: shoppingResult.reason, type: 'shopping' }; } return { shouldSkip: false, reason: '', type: 'normal' }; } function skipVideo() { if (state.skipInProgress) { log('跳过操作正在进行中,忽略重复请求'); return; } state.skipInProgress = true; log('执行跳过...'); const slideList = document.querySelector('#slidelist'); if (slideList) { const activeItem = document.querySelector('[data-e2e="feed-active-video"]')?.closest('[data-e2e="recommend-item"]') || document.querySelector('[data-e2e="feed-live"]')?.closest('[data-e2e="recommend-item"]'); if (activeItem) { const itemHeight = activeItem.offsetHeight || window.innerHeight; slideList.scrollTo({ top: slideList.scrollTop + itemHeight, behavior: 'smooth' }); setTimeout(() => { state.skipInProgress = false; }, 600); return; } } const event = new KeyboardEvent('keydown', { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40, which: 40, bubbles: true, cancelable: true, view: window }); document.body.dispatchEvent(event); setTimeout(() => { state.skipInProgress = false; }, 600); } async function detectWithRetry(container, videoId, type, retryCount = 0) { const result = detectVideoType(container, type); if (result.shouldSkip) return result; if (retryCount < CONFIG.maxRetries) { await new Promise(resolve => setTimeout(resolve, CONFIG.retryDelay)); const currentVideo = getActiveVideo(); if (currentVideo && currentVideo.videoId === videoId) { return detectWithRetry(currentVideo.container, videoId, currentVideo.type, retryCount + 1); } } return result; } async function checkAndSkip() { if (state.isChecking || state.skipInProgress) return; state.isChecking = true; try { const video = getActiveVideo(); if (!video) { state.isChecking = false; return; } const { container, videoId, type } = video; // 直播类型的特殊处理 if (type === 'live') { // 防止短时间内重复跳过同一个直播 const now = Date.now(); if (now - state.lastLiveSkipTime < 1000) { log('直播跳过冷却中...'); state.isChecking = false; return; } if (CONFIG.skipLive) { const liveSalesResult = checkLiveSalesFeatures(container); if (liveSalesResult.isLiveSales) { log('🚫 检测到直播带货视频,准备跳过:', liveSalesResult.reason); showNotification(`已跳过: ${liveSalesResult.reason}`, 'live'); state.lastLiveSkipTime = now; await new Promise(resolve => setTimeout(resolve, CONFIG.skipDelay)); skipVideo(); } } state.isChecking = false; return; } // 普通视频的处理逻辑 if (videoId === state.currentVideoId) { state.isChecking = false; return; } log('视频切换:', state.currentVideoId, '->', videoId); state.currentVideoId = videoId; if (state.processedVideos.has(videoId)) { log('该视频已处理过,跳过检测'); state.isChecking = false; return; } await new Promise(resolve => setTimeout(resolve, CONFIG.stabilityDelay)); const currentVideo = getActiveVideo(); if (!currentVideo || currentVideo.videoId !== videoId) { log('视频已切换,取消检测'); state.isChecking = false; return; } const result = await detectWithRetry(currentVideo.container, videoId, currentVideo.type); if (result.shouldSkip) { const typeNames = { ad: '广告', shopping: '购物', live: '直播带货' }; log(`🚫 检测到${typeNames[result.type]}视频,准备跳过:`, result.reason); state.processedVideos.add(videoId); if (state.processedVideos.size > 100) { state.processedVideos = new Set(Array.from(state.processedVideos).slice(-50)); } showNotification(`已跳过: ${result.reason}`, result.type); await new Promise(resolve => setTimeout(resolve, CONFIG.skipDelay)); skipVideo(); state.lastSkipTime = Date.now(); } else { log('✓ 正常视频:', videoId); } } catch (e) { log('检测出错:', e); } finally { state.isChecking = false; } } function forceCheck() { state.currentVideoId = null; state.isChecking = false; state.skipInProgress = false; state.lastLiveSkipTime = 0; checkAndSkip(); } function toggleFeature(feature, enable) { if (feature === 'shopping') CONFIG.skipShopping = enable; else if (feature === 'ad') CONFIG.skipAds = enable; else if (feature === 'live') CONFIG.skipLive = enable; const names = { shopping: '购物视频', ad: '广告视频', live: '直播带货' }; log(`${names[feature]}跳过: ${enable ? '开启' : '关闭'}`); showNotification(`${names[feature]}跳过: ${enable ? '已开启' : '已关闭'}`, 'info'); } function init() { log('插件启动 v3.7.0 - 修复直播带货检测'); log(`广告跳过: ${CONFIG.skipAds ? '开启' : '关闭'}`); log(`购物跳过: ${CONFIG.skipShopping ? '开启' : '关闭'}`); log(`直播跳过: ${CONFIG.skipLive ? '开启' : '关闭'}`); log(`自动开启声音: ${CONFIG.autoUnmute ? '开启' : '关闭'}`); log(`自动最高清晰度: ${CONFIG.autoHighQuality ? '开启' : '关闭'}`); setInterval(checkAndSkip, CONFIG.checkInterval); const observer = new MutationObserver(() => { clearTimeout(state.checkTimeout); state.checkTimeout = setTimeout(checkAndSkip, 150); }); document.addEventListener('keydown', (e) => { if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { setTimeout(checkAndSkip, 600); } }); let wheelTimeout; document.addEventListener('wheel', () => { clearTimeout(wheelTimeout); wheelTimeout = setTimeout(checkAndSkip, 600); }, { passive: true }); setTimeout(() => { const slideList = document.querySelector('#slidelist'); if (slideList) { observer.observe(slideList, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-e2e-vid', 'class', 'data-e2e'] }); } performInitSettings(); checkAndSkip(); showNotification('广告跳过 v3.7 已启动 ✓', 'info'); }, CONFIG.initSettingsDelay); window._dyAdSkip = { forceCheck, toggleShopping: (enable) => toggleFeature('shopping', enable), toggleAd: (enable) => toggleFeature('ad', enable), toggleLive: (enable) => toggleFeature('live', enable), unmute, setHighestQuality, resetInitSettings: () => { state.initSettingsDone = false; performInitSettings(); }, state, config: CONFIG }; log('控制台命令: _dyAdSkip.forceCheck() / _dyAdSkip.toggleLive(true/false)'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();