您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自动跳过直播、屏蔽账号关键字、跳过广告、最高分辨率、AI喜好模式(可自定义)、极速模式、AI判定内容自动点赞
// ==UserScript== // @name 网页抖音体验增强 // @namespace Violentmonkey Scripts // @match https://www.douyin.com/?* // @match *://*.douyin.com/* // @match *://*.iesdouyin.com/* // @exclude *://lf-zt.douyin.com* // @grant none // @version 2.1 // @description 自动跳过直播、屏蔽账号关键字、跳过广告、最高分辨率、AI喜好模式(可自定义)、极速模式、AI判定内容自动点赞 // @author Frequenk // @license GPL-3.0 License // @run-at document-start // ==/UserScript== /* 功能说明: 1. 跳过直播功能 - 自动检测并跳过直播内容 - 可通过界面按钮开启/关闭 2. 屏蔽账号关键字 - 自动检测账号名称是否包含屏蔽关键字 - 点击按钮文字可自定义关键字列表(默认:"店"、"甄选") - 关键字保存在本地存储中 3. 跳过广告功能 - 自动检测并跳过广告视频 - 可通过界面按钮开启/关闭 4. 自动最高分辨率 - 自动选择最高可用分辨率(优先级:4K > 2K > 1080P > 720P > 540P > 智能) - 找到4K后自动关闭该功能 5. AI喜好模式(需本地AI) - 点击按钮文字可自定义想看的内容类型 - 支持选择或输入自定义AI模型 - 快速决策:0秒、1秒、2.5秒、4秒、6秒、8秒时检测 - 连续1次不符合立即跳过,连续2次符合停止检测 - 判定为喜好内容后自动点赞(Z键) - 需要安装Ollama并下载视觉模型 6. 极速模式 - 每个视频播放指定秒数后自动切换 - 点击按钮文字可设置时间(1-60秒,默认6秒) - 适合快速浏览大量内容 7. 界面控制 - 所有功能通过播放器设置面板的开关按钮控制 - 实时显示各功能状态 - 支持动态开关切换 */ (function() { 'use strict'; // ========== 配置管理模块 ========== class ConfigManager { constructor() { this.config = { skipLive: { enabled: true, key: 'skipLive' }, autoHighRes: { enabled: true, key: 'autoHighRes' }, blockKeywords: { enabled: true, key: 'blockKeywords', keywords: this.loadKeywords() }, skipAd: { enabled: true, key: 'skipAd' }, aiPreference: { enabled: false, key: 'aiPreference', content: this.loadAiContent(), model: this.loadAiModel() }, speedMode: { enabled: false, key: 'speedMode', seconds: this.loadSpeedSeconds() } }; } loadKeywords() { return JSON.parse(localStorage.getItem('douyin_blocked_keywords') || '["店", "甄选"]'); } loadSpeedSeconds() { return parseInt(localStorage.getItem('douyin_speed_mode_seconds') || '6'); } loadAiContent() { return localStorage.getItem('douyin_ai_content') || '露脸的美女'; } loadAiModel() { return localStorage.getItem('douyin_ai_model') || 'qwen2.5vl:7b'; } saveKeywords(keywords) { this.config.blockKeywords.keywords = keywords; localStorage.setItem('douyin_blocked_keywords', JSON.stringify(keywords)); } saveSpeedSeconds(seconds) { this.config.speedMode.seconds = seconds; localStorage.setItem('douyin_speed_mode_seconds', seconds.toString()); } saveAiContent(content) { this.config.aiPreference.content = content; localStorage.setItem('douyin_ai_content', content); } saveAiModel(model) { this.config.aiPreference.model = model; localStorage.setItem('douyin_ai_model', model); } get(key) { return this.config[key]; } setEnabled(key, value) { if (this.config[key]) { this.config[key].enabled = value; } } isEnabled(key) { return this.config[key]?.enabled || false; } } // ========== DOM选择器常量 ========== const SELECTORS = { activeVideo: "[data-e2e='feed-active-video']", resolutionOptions: ".xgplayer-playing div.virtual > div.item", accountName: '[data-e2e="feed-video-nickname"]', settingsPanel: 'xg-icon.xgplayer-autoplay-setting', adIndicator: 'svg[viewBox="0 0 30 16"]', videoElement: 'video' }; // ========== 视频控制器 ========== class VideoController { constructor() { this.skipCheckInterval = null; this.skipAttemptCount = 0; this.MAX_SKIP_ATTEMPTS = 20; } skip() { console.log('跳过视频'); if (!document.body) return; const videoBefore = this.getCurrentVideoUrl(); this.sendKeyEvent('ArrowDown'); this.clearSkipCheck(); this.startSkipCheck(videoBefore); } like() { console.log('【自动点赞】喜好内容'); this.sendKeyEvent('z', 'KeyZ', 90); } sendKeyEvent(key, code = null, keyCode = null) { try { const event = new KeyboardEvent('keydown', { key: key, code: code || (key === 'ArrowDown' ? 'ArrowDown' : code), keyCode: keyCode || (key === 'ArrowDown' ? 40 : keyCode), which: keyCode || (key === 'ArrowDown' ? 40 : keyCode), bubbles: true, cancelable: true }); document.body.dispatchEvent(event); } catch (error) { console.log('发送键盘事件失败:', error); } } getCurrentVideoUrl() { const videoEl = document.querySelector(`${SELECTORS.activeVideo} ${SELECTORS.videoElement}`); return videoEl?.src || ''; } clearSkipCheck() { if (this.skipCheckInterval) { clearInterval(this.skipCheckInterval); this.skipCheckInterval = null; } this.skipAttemptCount = 0; } startSkipCheck(urlBefore) { this.skipCheckInterval = setInterval(() => { this.skipAttemptCount++; const urlAfter = this.getCurrentVideoUrl(); if (urlAfter && urlAfter !== urlBefore) { console.log('视频已成功切换'); this.clearSkipCheck(); return; } if (this.skipAttemptCount >= this.MAX_SKIP_ATTEMPTS) { console.log('达到最大尝试次数,停止跳过'); this.clearSkipCheck(); return; } console.log(`视频未切换,第${this.skipAttemptCount + 1}次尝试跳过`); this.sendKeyEvent('ArrowDown'); }, 300); } } // ========== UI组件工厂 ========== class UIFactory { static createDialog(className, title, content, onSave, onCancel) { const existingDialog = document.querySelector(`.${className}`); if (existingDialog) { existingDialog.remove(); return; } const dialog = document.createElement('div'); dialog.className = className; Object.assign(dialog.style, { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', background: 'rgba(0, 0, 0, 0.9)', border: '1px solid rgba(255, 255, 255, 0.2)', borderRadius: '8px', padding: '20px', zIndex: '10000', minWidth: '250px' }); dialog.innerHTML = ` <div style="color: white; margin-bottom: 15px; font-size: 14px;">${title}</div> ${content} <div style="display: flex; gap: 10px; margin-top: 15px;"> <button class="dialog-confirm" style="flex: 1; padding: 5px; background: #fe2c55; color: white; border: none; border-radius: 4px; cursor: pointer;">确定</button> <button class="dialog-cancel" style="flex: 1; padding: 5px; background: rgba(255, 255, 255, 0.1); color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px; cursor: pointer;">取消</button> </div> `; document.body.appendChild(dialog); dialog.querySelector('.dialog-confirm').addEventListener('click', () => { if (onSave()) dialog.remove(); }); dialog.querySelector('.dialog-cancel').addEventListener('click', () => { dialog.remove(); if (onCancel) onCancel(); }); setTimeout(() => { document.addEventListener('click', function closeDialog(e) { if (!dialog.contains(e.target)) { dialog.remove(); document.removeEventListener('click', closeDialog); } }); }, 100); return dialog; } static createToggleButton(text, className, isEnabled, onToggle, onClick = null) { const btnContainer = document.createElement('xg-icon'); btnContainer.className = `xgplayer-autoplay-setting ${className}`; btnContainer.innerHTML = ` <div class="xgplayer-icon"> <div class="xgplayer-setting-label"> <button aria-checked="${isEnabled}" class="xg-switch ${isEnabled ? 'xg-switch-checked' : ''}"> <span class="xg-switch-inner"></span> </button> <span class="xgplayer-setting-title" style="${onClick ? 'cursor: pointer; text-decoration: underline;' : ''}">${text}</span> </div> </div>`; btnContainer.querySelector('button').addEventListener('click', (e) => { const newState = e.currentTarget.getAttribute('aria-checked') === 'false'; UIManager.updateToggleButtons(className, newState); onToggle(newState); }); if (onClick) { btnContainer.querySelector('.xgplayer-setting-title').addEventListener('click', (e) => { e.stopPropagation(); onClick(); }); } return btnContainer; } static showErrorDialog() { const dialog = document.createElement('div'); dialog.className = 'error-dialog-' + Date.now(); dialog.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.95); border: 2px solid rgba(254, 44, 85, 0.8); color: white; padding: 20px; border-radius: 8px; z-index: 10001; max-width: 400px; text-align: center; font-size: 14px; `; dialog.innerHTML = ` <div style="margin-bottom: 20px;"> <div style="color: #fe2c55; font-size: 40px; margin-bottom: 15px;">⚠️</div> <div style="text-align: left; line-height: 1.6;"> <div style="margin-bottom: 12px;"> <strong>请检查以下配置:</strong> </div> <div style="margin-bottom: 8px;"> 1. 安装 <a href="https://ollama.com/" target="_blank" style="color: #fe2c55; text-decoration: underline;">Ollama</a> 并下载视觉模型(默认:qwen2.5vl:7b) </div> <div> 2. 开启Ollama跨域模式,设置环境变量: <div style="margin-left: 20px; margin-top: 5px; font-family: monospace; background: rgba(255, 255, 255, 0.1); padding: 5px; border-radius: 4px;"> OLLAMA_HOST=0.0.0.0<br> OLLAMA_ORIGINS=* </div> <div style="margin-top: 8px;"> 参考配置教程:<a href="https://lobehub.com/zh/docs/self-hosting/examples/ollama" target="_blank" style="color: #fe2c55; text-decoration: underline;">Ollama跨域设置指南</a> </div> </div> </div> </div> <button class="error-dialog-confirm" style="padding: 8px 20px; background: #fe2c55; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;">确定</button> `; document.body.appendChild(dialog); dialog.querySelector('.error-dialog-confirm').addEventListener('click', () => { dialog.remove(); }); } } // ========== UI管理器 ========== class UIManager { constructor(config, videoController) { this.config = config; this.videoController = videoController; this.initButtons(); } initButtons() { this.buttonConfigs = [ { text: '跳过直播', className: 'skip-live-button', configKey: 'skipLive' }, { text: '寻找最高分辨率', className: 'auto-high-resolution-button', configKey: 'autoHighRes' }, { text: '屏蔽账号关键字', className: 'block-account-keyword-button', configKey: 'blockKeywords', onClick: () => this.showKeywordDialog() }, { text: '跳过广告', className: 'skip-ad-button', configKey: 'skipAd' }, { text: 'AI喜好模式', className: 'ai-preference-button', configKey: 'aiPreference', onClick: () => this.showAiPreferenceDialog() }, { text: `极速模式(${this.config.get('speedMode').seconds}秒)`, className: 'speed-mode-button', configKey: 'speedMode', onClick: () => this.showSpeedDialog() } ]; } insertButtons() { document.querySelectorAll(SELECTORS.settingsPanel).forEach(panel => { const parent = panel.parentNode; if (!parent) return; let lastButton = panel; this.buttonConfigs.forEach(config => { let button = parent.querySelector(`.${config.className}`); if (!button) { button = UIFactory.createToggleButton( config.text, config.className, this.config.isEnabled(config.configKey), (state) => this.config.setEnabled(config.configKey, state), config.onClick ); parent.insertBefore(button, lastButton.nextSibling); } lastButton = button; }); }); } static updateToggleButtons(className, isEnabled) { document.querySelectorAll(`.${className} .xg-switch`).forEach(sw => { sw.classList.toggle('xg-switch-checked', isEnabled); sw.setAttribute('aria-checked', String(isEnabled)); }); } updateSpeedModeText() { const seconds = this.config.get('speedMode').seconds; document.querySelectorAll('.speed-mode-button .xgplayer-setting-title').forEach(el => { el.textContent = `极速模式(${seconds}秒)`; }); } showSpeedDialog() { const seconds = this.config.get('speedMode').seconds; const content = ` <input type="number" class="speed-input" min="1" max="60" value="${seconds}" style="width: 100%; padding: 5px; margin-bottom: 15px; background: rgba(255, 255, 255, 0.1); color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px;"> `; UIFactory.createDialog('speed-mode-time-dialog', '设置极速模式时间(秒)', content, () => { const input = document.querySelector('.speed-input'); const value = parseInt(input.value); if (value >= 1 && value <= 60) { this.config.saveSpeedSeconds(value); this.updateSpeedModeText(); return true; } return false; }); } showAiPreferenceDialog() { const currentContent = this.config.get('aiPreference').content; const currentModel = this.config.get('aiPreference').model; const content = ` <div style="margin-bottom: 15px;"> <label style="color: rgba(255, 255, 255, 0.7); font-size: 12px; display: block; margin-bottom: 5px;"> 想看什么内容?(例如:露脸的美女、搞笑视频、猫咪) </label> <input type="text" class="ai-content-input" value="${currentContent}" placeholder="输入你想看的内容" style="width: 100%; padding: 8px; background: rgba(255, 255, 255, 0.1); color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px;"> </div> <div style="margin-bottom: 15px;"> <label style="color: rgba(255, 255, 255, 0.7); font-size: 12px; display: block; margin-bottom: 5px;"> AI模型选择 </label> <div style="position: relative;"> <select class="ai-model-select" style="width: 100%; padding: 8px; background: rgba(255, 255, 255, 0.1); color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px; appearance: none; cursor: pointer;"> <option value="qwen2.5vl:7b" style="background: rgba(0, 0, 0, 0.9); color: white;" ${currentModel === 'qwen2.5vl:7b' ? 'selected' : ''}>qwen2.5vl:7b (推荐)</option> <option value="custom" style="background: rgba(0, 0, 0, 0.9); color: white;" ${currentModel !== 'qwen2.5vl:7b' ? 'selected' : ''}>自定义模型</option> </select> <span style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); pointer-events: none; color: rgba(255, 255, 255, 0.5);">▼</span> </div> <input type="text" class="ai-model-input" value="${currentModel !== 'qwen2.5vl:7b' ? currentModel : ''}" placeholder="输入自定义模型名称" style="width: 100%; padding: 8px; margin-top: 10px; background: rgba(255, 255, 255, 0.1); color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px; display: ${currentModel !== 'qwen2.5vl:7b' ? 'block' : 'none'};"> </div> <div style="color: rgba(255, 255, 255, 0.5); font-size: 11px; margin-bottom: 10px;"> 提示:需要安装 <a href="https://ollama.com/" target="_blank" style="color: #fe2c55;">Ollama</a> 并下载视觉模型 </div> `; const dialog = UIFactory.createDialog('ai-preference-dialog', '设置AI喜好', content, () => { const contentInput = dialog.querySelector('.ai-content-input'); const modelSelect = dialog.querySelector('.ai-model-select'); const modelInput = dialog.querySelector('.ai-model-input'); const content = contentInput.value.trim(); let model = modelSelect.value === 'custom' ? modelInput.value.trim() : modelSelect.value; if (!content) { alert('请输入想看的内容'); return false; } if (!model) { alert('请选择或输入模型名称'); return false; } this.config.saveAiContent(content); this.config.saveAiModel(model); console.log('AI喜好设置已更新:', { content, model }); return true; }); // 处理模型选择切换 const modelSelect = dialog.querySelector('.ai-model-select'); const modelInput = dialog.querySelector('.ai-model-input'); modelSelect.addEventListener('change', (e) => { if (e.target.value === 'custom') { modelInput.style.display = 'block'; } else { modelInput.style.display = 'none'; modelInput.value = ''; } }); } showKeywordDialog() { const keywords = this.config.get('blockKeywords').keywords; let tempKeywords = [...keywords]; const updateList = () => { const container = document.querySelector('.keyword-list'); if (!container) return; container.innerHTML = tempKeywords.length === 0 ? '<div style="color: rgba(255, 255, 255, 0.5); text-align: center;">暂无关键字</div>' : tempKeywords.map((keyword, index) => ` <div style="display: flex; align-items: center; margin-bottom: 8px;"> <span style="flex: 1; color: white; padding: 5px 10px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; margin-right: 10px;">${keyword}</span> <button data-index="${index}" class="delete-keyword" style="padding: 5px 10px; background: #ff4757; color: white; border: none; border-radius: 4px; cursor: pointer;">删除</button> </div> `).join(''); container.querySelectorAll('.delete-keyword').forEach(btn => { btn.addEventListener('click', (e) => { tempKeywords.splice(parseInt(e.target.dataset.index), 1); updateList(); }); }); }; const content = ` <div style="color: rgba(255, 255, 255, 0.7); margin-bottom: 15px; font-size: 12px;"> 包含这些关键字的账号将被自动跳过 </div> <div style="display: flex; gap: 10px; margin-bottom: 10px;"> <input type="text" class="keyword-input" placeholder="输入新关键字" style="flex: 1; padding: 8px; background: rgba(255, 255, 255, 0.1); color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px;"> <button class="add-keyword" style="padding: 8px 15px; background: #00d639; color: white; border: none; border-radius: 4px; cursor: pointer;">添加</button> </div> <div class="keyword-list" style="margin-bottom: 15px; max-height: 200px; overflow-y: auto;"></div> `; const dialog = UIFactory.createDialog('keyword-setting-dialog', '管理屏蔽关键字', content, () => { this.config.saveKeywords(tempKeywords); console.log('屏蔽关键字已更新:', tempKeywords); return true; }); const addKeyword = () => { const input = dialog.querySelector('.keyword-input'); const keyword = input.value.trim(); if (keyword && !tempKeywords.includes(keyword)) { tempKeywords.push(keyword); updateList(); input.value = ''; } }; dialog.querySelector('.add-keyword').addEventListener('click', addKeyword); dialog.querySelector('.keyword-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') addKeyword(); }); updateList(); } } // ========== AI检测器 ========== class AIDetector { constructor(videoController, config) { this.videoController = videoController; this.config = config; this.API_URL = 'http://localhost:11434/api/generate'; this.checkSchedule = [0, 1000, 2500, 4000, 6000, 8000]; this.reset(); } reset() { this.currentCheckIndex = 0; this.checkResults = []; this.consecutiveYes = 0; this.consecutiveNo = 0; this.hasSkipped = false; this.stopChecking = false; this.hasLiked = false; this.isProcessing = false; } shouldCheck(videoPlayTime) { return !this.isProcessing && !this.stopChecking && !this.hasSkipped && this.currentCheckIndex < this.checkSchedule.length && videoPlayTime >= this.checkSchedule[this.currentCheckIndex]; } async processVideo(videoEl) { if (this.isProcessing || this.stopChecking || this.hasSkipped) return; this.isProcessing = true; try { const base64Image = await this.captureVideoFrame(videoEl); const aiResponse = await this.callAI(base64Image); this.handleResponse(aiResponse); this.currentCheckIndex++; } catch (error) { console.error('AI判断功能出错:', error); // 显示错误提示 UIFactory.showErrorDialog(); // 关闭AI喜好模式 this.config.setEnabled('aiPreference', false); UIManager.updateToggleButtons('ai-preference-button', false); this.stopChecking = true; } finally { this.isProcessing = false; } } async captureVideoFrame(videoEl) { const canvas = document.createElement('canvas'); const maxSize = 500; const aspectRatio = videoEl.videoWidth / videoEl.videoHeight; let targetWidth, targetHeight; if (videoEl.videoWidth > videoEl.videoHeight) { targetWidth = Math.min(videoEl.videoWidth, maxSize); targetHeight = Math.round(targetWidth / aspectRatio); } else { targetHeight = Math.min(videoEl.videoHeight, maxSize); targetWidth = Math.round(targetHeight * aspectRatio); } canvas.width = targetWidth; canvas.height = targetHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(videoEl, 0, 0, targetWidth, targetHeight); return canvas.toDataURL('image/jpeg', 0.8).split(',')[1]; } async callAI(base64Image) { const content = this.config.get('aiPreference').content; const model = this.config.get('aiPreference').model; const response = await fetch(this.API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: model, prompt: `这是${content}吗?回答『是』或者『不是』,不要说任何多余的字符`, images: [base64Image], stream: false }) }); if (!response.ok) { throw new Error(`AI请求失败: ${response.status}`); } const result = await response.json(); return result.response?.trim(); } handleResponse(aiResponse) { const content = this.config.get('aiPreference').content; this.checkResults.push(aiResponse); console.log(`AI检测结果[${this.checkResults.length}]:${aiResponse}`); if (aiResponse === '是') { this.consecutiveYes++; this.consecutiveNo = 0; } else { this.consecutiveYes = 0; this.consecutiveNo++; } if (this.consecutiveNo >= 1) { console.log(`【立即跳过】判定为非${content}`); this.hasSkipped = true; this.stopChecking = true; this.videoController.skip(); } else if (this.consecutiveYes >= 2) { console.log(`【停止检测】连续2次判定为${content},安心观看`); this.stopChecking = true; if (!this.hasLiked) { this.videoController.like(); this.hasLiked = true; } } } } // ========== 视频检测策略 ========== class VideoDetectionStrategies { constructor(config, videoController) { this.config = config; this.videoController = videoController; } checkAd(container) { if (!this.config.isEnabled('skipAd')) return false; const adIndicator = container.querySelector(SELECTORS.adIndicator); if (adIndicator) { console.log("检测到广告,已跳过"); this.videoController.skip(); return true; } return false; } checkBlockedAccount(container) { if (!this.config.isEnabled('blockKeywords')) return false; const accountEl = container.querySelector(SELECTORS.accountName); const accountName = accountEl?.textContent.trim(); const keywords = this.config.get('blockKeywords').keywords; if (accountName && keywords.some(kw => accountName.includes(kw))) { console.log(`检测到屏蔽关键字,已跳过账号: ${accountName}`); this.videoController.skip(); return true; } return false; } checkResolution(container) { if (!this.config.isEnabled('autoHighRes')) return false; const priorityOrder = ["4K", "2K", "1080P", "720P", "540P", "智能"]; const options = Array.from(container.querySelectorAll(SELECTORS.resolutionOptions)) .map(el => { const text = el.textContent.trim().toUpperCase(); return { element: el, text, priority: priorityOrder.findIndex(p => text.includes(p)) }; }) .filter(opt => opt.priority !== -1) .sort((a, b) => a.priority - b.priority); if (options.length > 0 && !options[0].element.classList.contains("selected")) { const bestOption = options[0]; bestOption.element.click(); console.log(`已切换至最高分辨率: ${bestOption.element.textContent}`); if (bestOption.text.includes("4K")) { this.config.setEnabled('autoHighRes', false); UIManager.updateToggleButtons('auto-high-resolution-button', false); console.log("已找到4K分辨率,自动关闭功能"); } return true; } return false; } } // ========== 主应用程序 ========== class DouyinEnhancer { constructor() { this.config = new ConfigManager(); this.videoController = new VideoController(); this.uiManager = new UIManager(this.config, this.videoController); this.aiDetector = new AIDetector(this.videoController, this.config); this.strategies = new VideoDetectionStrategies(this.config, this.videoController); this.lastVideoUrl = ''; this.videoStartTime = 0; this.speedModeSkipped = false; this.init(); } init() { setInterval(() => this.mainLoop(), 300); } mainLoop() { this.uiManager.insertButtons(); const activeContainer = document.querySelector(SELECTORS.activeVideo); if (!activeContainer) { if (this.config.isEnabled('skipLive')) { this.videoController.skip(); } return; } const videoEl = activeContainer.querySelector(SELECTORS.videoElement); if (!videoEl || !videoEl.src) return; const currentVideoUrl = videoEl.src; if (this.handleNewVideo(currentVideoUrl)) { return; } if (this.handleSpeedMode()) { return; } if (this.handleAIDetection(videoEl)) { return; } if (this.strategies.checkAd(activeContainer)) return; if (this.strategies.checkBlockedAccount(activeContainer)) return; this.strategies.checkResolution(activeContainer); } handleNewVideo(currentVideoUrl) { if (currentVideoUrl !== this.lastVideoUrl) { this.lastVideoUrl = currentVideoUrl; this.videoStartTime = Date.now(); this.speedModeSkipped = false; this.aiDetector.reset(); console.log('===== 新视频开始 ====='); if (this.config.isEnabled('speedMode')) { const seconds = this.config.get('speedMode').seconds; console.log(`【极速模式】已开启,${seconds}秒后自动切换`); } if (this.config.isEnabled('aiPreference')) { const content = this.config.get('aiPreference').content; console.log(`【AI喜好模式】已开启,筛选:${content}`); } return true; } return false; } handleSpeedMode() { if (!this.config.isEnabled('speedMode') || this.speedModeSkipped || this.aiDetector.hasSkipped) { return false; } const videoPlayTime = Date.now() - this.videoStartTime; const seconds = this.config.get('speedMode').seconds; if (videoPlayTime >= seconds * 1000) { console.log(`【极速模式】视频已播放${seconds}秒,自动切换`); this.speedModeSkipped = true; this.videoController.skip(); return true; } return false; } handleAIDetection(videoEl) { if (!this.config.isEnabled('aiPreference')) return false; const videoPlayTime = Date.now() - this.videoStartTime; if (this.aiDetector.shouldCheck(videoPlayTime)) { if (videoEl.readyState >= 2 && !videoEl.paused) { const timeInSeconds = (this.aiDetector.checkSchedule[this.aiDetector.currentCheckIndex] / 1000).toFixed(1); console.log(`【AI检测】第${this.aiDetector.currentCheckIndex + 1}次检测,时间点:${timeInSeconds}秒`); this.aiDetector.processVideo(videoEl); return true; } } if (videoPlayTime >= 10000 && !this.aiDetector.stopChecking) { console.log('【超时停止】视频播放已超过10秒,停止AI检测'); this.aiDetector.stopChecking = true; } return false; } } // 启动应用 const app = new DouyinEnhancer(); })();