您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自动跳过直播、智能屏蔽关键字(自动不感兴趣)、跳过广告、最高分辨率、分辨率筛选、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.6 // @description 自动跳过直播、智能屏蔽关键字(自动不感兴趣)、跳过广告、最高分辨率、分辨率筛选、AI智能筛选(自动点赞)、极速模式 // @author Frequenk // @license GPL-3.0 License // @run-at document-start // ==/UserScript== /* ==== 网页抖音体验增强 ==== 核心功能: 1. 跳过直播功能 - 自动检测并跳过直播内容 - 通过设置面板开关控制 2. 智能屏蔽关键字 ⭐ - 自动检测账号名称中的屏蔽关键字(默认:"店"、"甄选") - 智能处理:可选择"不感兴趣"(R键)或直接跳过,默认使用R键 - 按R键后视频直接消失,更符合抖音行为逻辑 - 📁 导入导出功能:支持txt文件批量管理关键字列表 - 点击按钮文字打开管理弹窗,支持增删改查 3. 跳过广告功能 - 自动识别并跳过广告视频 - 通过设置面板开关控制 4. 自动最高分辨率 - 智能选择最高可用分辨率(4K > 2K > 1080P > 720P > 540P > 智能) - 找到4K分辨率后自动关闭功能 5. 分辨率筛选 ⭐ - 只看指定分辨率的视频(4K、2K、1080P、720P、540P) - 没有指定分辨率时自动跳过 - 点击按钮文字选择想要的分辨率 6. AI智能筛选(需本地AI)⭐ - 🤖 内容识别:自定义想看的内容类型(如"露脸的美女"、"搞笑视频"等) - 👍 智能点赞:AI判定喜欢内容时可选择自动点赞,默认开启 - 🔧 模型支持:支持多种AI模型,默认qwen2.5vl:7b - ⚡ 快速决策:多时间点检测(0s、1s、2.5s、4s、6s、8s) - 🎯 精准判断:连续1次不符合立即跳过,连续2次符合停止检测 - 📋 环境要求:需安装Ollama并下载视觉模型 7. 极速模式 - 每个视频播放指定时间后自动切换到下一个 - 可自定义播放时间(1-60秒,默认6秒) - 适合快速浏览大量内容 8. 用户界面 - 🎛️ 统一控制:所有功能集成在播放器设置面板中 - ⚙️ 详细设置:点击按钮文字打开功能专属设置弹窗 - 📊 状态显示:实时显示各功能开关状态和参数 - 💾 自动保存:所有设置自动保存到本地存储 */ (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(), pressR: this.loadPressRSetting() }, skipAd: { enabled: true, key: 'skipAd' }, onlyResolution: { enabled: false, key: 'onlyResolution', resolution: this.loadTargetResolution() }, aiPreference: { enabled: false, key: 'aiPreference', content: this.loadAiContent(), model: this.loadAiModel(), autoLike: this.loadAutoLikeSetting() }, 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'; } loadTargetResolution() { return localStorage.getItem('douyin_target_resolution') || '4K'; } loadPressRSetting() { return localStorage.getItem('douyin_press_r_enabled') !== 'false'; // 默认开启 } loadAutoLikeSetting() { return localStorage.getItem('douyin_auto_like_enabled') !== 'false'; // 默认开启 } 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); } saveTargetResolution(resolution) { this.config.onlyResolution.resolution = resolution; localStorage.setItem('douyin_target_resolution', resolution); } savePressRSetting(enabled) { this.config.blockKeywords.pressR = enabled; localStorage.setItem('douyin_press_r_enabled', enabled.toString()); } saveAutoLikeSetting(enabled) { this.config.aiPreference.autoLike = enabled; localStorage.setItem('douyin_auto_like_enabled', enabled.toString()); } 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; } 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); } pressR() { console.log('【不感兴趣】屏蔽关键字内容'); this.sendKeyEvent('r', 'KeyR', 82); } 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; } 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: 'skip-ad-button', configKey: 'skipAd' }, { text: '账号屏蔽', className: 'block-account-keyword-button', configKey: 'blockKeywords', onClick: () => this.showKeywordDialog() }, { text: '最高清', className: 'auto-high-resolution-button', configKey: 'autoHighRes' }, { text: `${this.config.get('onlyResolution').resolution}筛选`, className: 'resolution-filter-button', configKey: 'onlyResolution', onClick: () => this.showResolutionDialog() }, { 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}秒切`; }); } updateResolutionText() { const resolution = this.config.get('onlyResolution').resolution; document.querySelectorAll('.resolution-filter-button .xgplayer-setting-title').forEach(el => { el.textContent = `${resolution}筛选`; }); } 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 autoLikeEnabled = this.config.get('aiPreference').autoLike; 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="margin-bottom: 15px; padding: 10px; background: rgba(255, 255, 255, 0.05); border-radius: 6px;"> <label style="display: flex; align-items: center; cursor: pointer; color: white; font-size: 13px;"> <input type="checkbox" class="auto-like-checkbox" ${autoLikeEnabled ? 'checked' : ''} style="margin-right: 8px; transform: scale(1.2);"> AI判定为喜欢的内容将自动点赞(Z键) </label> <div style="color: rgba(255, 255, 255, 0.5); font-size: 11px; margin-top: 5px; margin-left: 24px;"> 帮助抖音算法了解你喜欢此类内容 </div> </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 autoLikeCheckbox = dialog.querySelector('.auto-like-checkbox'); 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); this.config.saveAutoLikeSetting(autoLikeCheckbox.checked); console.log('AI喜好设置已更新:', { content, model, autoLike: autoLikeCheckbox.checked }); 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 = ''; } }); // 防止复选框点击时关闭弹窗 dialog.querySelector('.auto-like-checkbox').addEventListener('click', (e) => { e.stopPropagation(); }); } 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.onclick = (e) => { if (e.target.classList.contains('delete-keyword')) { e.stopPropagation(); // 阻止事件冒泡,防止触发弹窗关闭 const index = parseInt(e.target.dataset.index); tempKeywords.splice(index, 1); updateList(); } }; }; const pressREnabled = this.config.get('blockKeywords').pressR; const content = ` <div style="color: rgba(255, 255, 255, 0.7); margin-bottom: 15px; font-size: 12px;"> 包含这些关键字的账号将被自动跳过 </div> <div style="margin-bottom: 15px; padding: 10px; background: rgba(255, 255, 255, 0.05); border-radius: 6px;"> <label style="display: flex; align-items: center; cursor: pointer; color: white; font-size: 13px;"> <input type="checkbox" class="press-r-checkbox" ${pressREnabled ? 'checked' : ''} style="margin-right: 8px; transform: scale(1.2);"> 跳过时自动按R键(不感兴趣) </label> <div style="color: rgba(255, 255, 255, 0.5); font-size: 11px; margin-top: 5px; margin-left: 24px;"> 帮助抖音算法了解你不喜欢此类内容 </div> </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 style="display: flex; gap: 10px; margin-bottom: 10px;"> <button class="import-keywords" style="flex: 1; padding: 8px 12px; background: rgba(52, 152, 219, 0.8); color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;"> 📁 导入关键字 </button> <button class="export-keywords" style="flex: 1; padding: 8px 12px; background: rgba(155, 89, 182, 0.8); color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;"> 💾 导出关键字 </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, () => { const pressRCheckbox = dialog.querySelector('.press-r-checkbox'); this.config.saveKeywords(tempKeywords); this.config.savePressRSetting(pressRCheckbox.checked); console.log('屏蔽关键字已更新:', tempKeywords); console.log('自动按R键设置已更新:', pressRCheckbox.checked); 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', (e) => { e.stopPropagation(); // 阻止事件冒泡,防止触发弹窗关闭 addKeyword(); }); dialog.querySelector('.keyword-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.stopPropagation(); // 阻止事件冒泡 addKeyword(); } }); // 防止在输入框内点击时关闭弹窗 dialog.querySelector('.keyword-input').addEventListener('click', (e) => { e.stopPropagation(); }); // 防止复选框点击时关闭弹窗 dialog.querySelector('.press-r-checkbox').addEventListener('click', (e) => { e.stopPropagation(); }); // 导出功能 const exportKeywords = () => { const content = tempKeywords.join('\n'); const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `抖音屏蔽关键字_${new Date().toISOString().split('T')[0]}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); console.log('关键字已导出:', tempKeywords); }; dialog.querySelector('.export-keywords').addEventListener('click', (e) => { e.stopPropagation(); exportKeywords(); }); // 导入功能 const importKeywords = () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.txt'; input.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { const content = e.target.result; const importedKeywords = content.split('\n') .map(line => line.trim()) .filter(line => line.length > 0); if (importedKeywords.length > 0) { // 合并关键字,去重 const allKeywords = [...new Set([...tempKeywords, ...importedKeywords])]; tempKeywords.splice(0, tempKeywords.length, ...allKeywords); updateList(); console.log('关键字导入完成:', importedKeywords); console.log('当前关键字列表:', tempKeywords); } else { alert('文件内容为空或格式不正确!'); } }; reader.onerror = () => { alert('文件读取失败!'); }; reader.readAsText(file, 'utf-8'); } }); input.click(); }; dialog.querySelector('.import-keywords').addEventListener('click', (e) => { e.stopPropagation(); importKeywords(); }); updateList(); } showResolutionDialog() { const currentResolution = this.config.get('onlyResolution').resolution; const resolutions = ['4K', '2K', '1080P', '720P', '540P']; 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> <div style="position: relative;"> <select class="resolution-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;"> ${resolutions.map(res => `<option value="${res}" style="background: rgba(0, 0, 0, 0.9); color: white;" ${currentResolution === res ? 'selected' : ''}>${res}</option>` ).join('')} </select> <span style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); pointer-events: none; color: rgba(255, 255, 255, 0.5);">▼</span> </div> </div> <div style="color: rgba(255, 255, 255, 0.5); font-size: 11px; margin-bottom: 10px;"> 提示:只播放包含所选分辨率关键字的视频,没有找到则自动跳过 </div> `; const dialog = UIFactory.createDialog('resolution-dialog', '分辨率筛选设置', content, () => { const resolutionSelect = dialog.querySelector('.resolution-select'); const resolution = resolutionSelect.value; this.config.saveTargetResolution(resolution); this.updateResolutionText(); console.log('分辨率筛选已更新:', resolution); return true; }); } } // ========== 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; // 检查是否开启了自动点赞功能 const autoLikeEnabled = this.config.get('aiPreference').autoLike; if (!this.hasLiked && autoLikeEnabled) { this.videoController.like(); this.hasLiked = true; } else if (!autoLikeEnabled) { console.log('【自动点赞】功能已关闭,跳过点赞'); } } } } // ========== 视频检测策略 ========== 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; const pressREnabled = this.config.get('blockKeywords').pressR; if (accountName && keywords.some(kw => accountName.includes(kw))) { console.log(`检测到屏蔽关键字,已跳过账号: ${accountName}`); // 如果开启了按R键功能,只按R键(视频会直接消失) if (pressREnabled) { this.videoController.pressR(); } else { // 如果没开启R键功能,则使用下键跳过 this.videoController.skip(); } return true; } return false; } checkResolution(container) { if (!this.config.isEnabled('autoHighRes') && !this.config.isEnabled('onlyResolution')) 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 (this.config.isEnabled('onlyResolution')) { const targetResolution = this.config.get('onlyResolution').resolution.toUpperCase(); const hasTarget = options.some(opt => opt.text.includes(targetResolution)); if (!hasTarget) { console.log(`【分辨率筛选】未找到${targetResolution}分辨率,跳过视频`); this.videoController.skip(); return true; } const targetOption = options.find(opt => opt.text.includes(targetResolution)); if (targetOption && !targetOption.element.classList.contains("selected")) { targetOption.element.click(); console.log(`【分辨率筛选】已切换至${targetResolution}分辨率`); return true; } return false; } // 原有的最高分辨率逻辑 if (this.config.isEnabled('autoHighRes')) { 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() { this.injectStyles(); setInterval(() => this.mainLoop(), 300); } injectStyles() { const style = document.createElement('style'); style.innerHTML = ` /* 让右侧按钮容器高度自适应,防止按钮换行时被隐藏 */ .xg-right-grid { height: auto !important; max-height: none !important; overflow: visible !important; } /* 确保按钮容器可以正确换行显示 */ .xg-right-grid xg-icon { display: inline-block !important; margin: -12px 0 !important; } /* 防止父容器限制高度导致内容被裁剪 */ .xgplayer-controls { overflow: visible !important; } /* 让控制栏底部区域高度自适应 */ .xgplayer-controls-bottom { height: auto !important; min-height: 50px !important; } `; document.head.appendChild(style); } 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(); })();