您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
自动跳过直播、智能屏蔽关键字(自动不感兴趣)、跳过广告、最高分辨率、分辨率筛选、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.8 // @description 自动跳过直播、智能屏蔽关键字(自动不感兴趣)、跳过广告、最高分辨率、分辨率筛选、AI智能筛选(自动点赞)、极速模式 // @author Frequenk // @license GPL-3.0 License // @run-at document-start // ==/UserScript== /* ==================== 网页抖音体验增强 ==================== 本脚本旨在提供更纯净、更高效的网页版抖音浏览体验。 [核心功能] 1. 自动跳过 - ⏭️ 直播间: 自动检测并跳过直播内容。 - ⏭️ 广告: 自动识别并跳过广告视频。 2. 智能屏蔽 ⭐ - 🚫 按账号屏蔽: 根据您设定的关键字列表,自动跳过包含这些关键字的账号。 - ⚙️ 智能处理: 可选“不感兴趣”(R键)或直接跳过。 - 📁 导入/导出: 支持通过.txt文件批量管理您的屏蔽关键字列表。 3. 画质优化 - 📺 自动最高分辨率: 智能选择当前视频可用的最高分辨率 (4K > 2K > 1080P ...)。 - 🔒 锁定4K: 找到4K视频后可自动关闭此功能,避免不必要的切换。 - ⚙️ 分辨率筛选: 只观看您指定分辨率的视频,不符合的将自动跳过。 4. AI智能筛选 (需本地Ollama) ⭐ - 🤖 内容识别: 自定义您想看的内容(如“风景”、“猫咪”),AI将自动为您筛选。 - ❤️ 智能点赞: 当AI判定为您喜欢的内容时,可选择自动点赞(Z键)。 - ⚡ 快速决策: 通过多时间点截图检测,实现快速精准判断。 5. 极速模式 - ⚡️ 定时切换: 每个视频播放指定时间后自动切换到下一个,适合快速浏览。 - 🕒 自定义时间: 可在1-60秒内自定义每个视频的播放时长。 [用户界面] - 🎛️ 统一控制面板: 所有功能集成在播放器右侧的设置面板中,方便开关和管理。 - ⚙️ 详细设置弹窗: 点击各项功能的标题文字,即可打开专属的详细设置弹窗。 - 📢 图标状态提示: 所有操作和状态变化都会有清晰的图标和文字提示。 - 💾 自动保存: 您的所有设置都将自动保存在浏览器本地。 [快捷键] - [=]: 开启/关闭“跳过直播”功能。 */ (function() { 'use strict'; function isElementInViewport(el,text="") { if (!el) return false; const rect = el.getBoundingClientRect(); return ( rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.right > 0 && rect.top < window.innerHeight && rect.left < window.innerWidth ); } function getBestVisibleElement(elements) { if (!elements || elements.length === 0) { return null; } const visibleElements = Array.from(elements).filter(isElementInViewport); if (visibleElements.length === 0) { return null; } if (visibleElements.length === 1) { return visibleElements[0]; } let bestCandidate = null; let minDistance = Infinity; for (const el of visibleElements) { const rect = el.getBoundingClientRect(); const distance = Math.abs(rect.top); if (distance < minDistance) { minDistance = distance; bestCandidate = el; } } return bestCandidate; } // ========== 通知管理器 ========== class NotificationManager { constructor() { this.container = null; } createContainer() { if (this.container && document.body.contains(this.container)) return; this.container = document.createElement('div'); Object.assign(this.container.style, { position: 'fixed', top: '100px', left: '50%', transform: 'translateX(-50%)', zIndex: '10001', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }); document.body.appendChild(this.container); } showMessage(message, duration = 2000) { this.createContainer(); const messageElement = document.createElement('div'); messageElement.textContent = message; Object.assign(messageElement.style, { background: 'rgba(0, 0, 0, 0.8)', color: 'white', padding: '10px 20px', borderRadius: '6px', fontSize: '14px', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', opacity: '0', transition: 'opacity 0.3s ease-in-out, transform 0.3s ease-in-out', transform: 'translateY(-20px)' }); this.container.appendChild(messageElement); // Animate in setTimeout(() => { messageElement.style.opacity = '1'; messageElement.style.transform = 'translateY(0)'; }, 10); // Animate out and remove setTimeout(() => { messageElement.style.opacity = '0'; messageElement.style.transform = 'translateY(-20px)'; setTimeout(() => { if (messageElement.parentElement) { messageElement.remove(); } if (this.container && this.container.childElementCount === 0) { this.container.remove(); this.container = null; } }, 300); }, duration); } } // ========== 配置管理模块 ========== 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']:has(video[src])", 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[src]' }; // ========== 视频控制器 ========== class VideoController { constructor(notificationManager) { this.skipCheckInterval = null; this.skipAttemptCount = 0; this.notificationManager = notificationManager; } skip(reason) { const tip=`跳过视频,原因:${reason}`; if (reason) { this.notificationManager.showMessage(tip); } console.log(tip); if (!document.body) return; const videoBefore = this.getCurrentVideoUrl(); this.sendKeyEvent('ArrowDown'); this.clearSkipCheck(); this.startSkipCheck(videoBefore); } like() { this.notificationManager.showMessage('AI喜好: ❤️ 自动点赞'); this.sendKeyEvent('z', 'KeyZ', 90); } pressR() { this.notificationManager.showMessage('屏蔽账号: 🚫 不感兴趣'); 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 activeContainers = document.querySelectorAll(SELECTORS.activeVideo); const lastActiveContainer = getBestVisibleElement(activeContainers); if (!lastActiveContainer) return ''; const videoEl = lastActiveContainer.querySelector(SELECTORS.videoElement); return videoEl?.src || ''; } clearSkipCheck() { if (this.skipCheckInterval) { clearInterval(this.skipCheckInterval); this.skipCheckInterval = null; } this.skipAttemptCount = 0; } startSkipCheck(urlBefore) { this.skipCheckInterval = setInterval(() => { if (this.skipAttemptCount >= 5) { this.notificationManager.showMessage('⚠️ 跳过失败,请手动操作'); this.clearSkipCheck(); return; } this.skipAttemptCount++; const urlAfter = this.getCurrentVideoUrl(); if (urlAfter && urlAfter !== urlBefore) { console.log('视频已成功切换'); this.clearSkipCheck(); return; } const attemptMessage = `跳过失败,正在重试 (${this.skipAttemptCount}/5)`; this.notificationManager.showMessage(attemptMessage, 1000); console.log(attemptMessage); this.sendKeyEvent('ArrowDown'); }, 500); } } // ========== 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, shortcut = null) { const btnContainer = document.createElement('xg-icon'); btnContainer.className = `xgplayer-autoplay-setting ${className}`; const shortcutHint = shortcut ? `<div class="xgTips"><span>${text.replace(/<[^>]*>/g, '')}</span><span class="shortcutKey">${shortcut}</span></div>` : ''; 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>${shortcutHint}`; 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, notificationManager) { this.config = config; this.videoController = videoController; this.notificationManager = notificationManager; this.initButtons(); } initButtons() { this.buttonConfigs = [ { text: '跳直播', className: 'skip-live-button', configKey: 'skipLive', shortcut: '=' }, { 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); if (config.configKey === 'skipLive') { this.notificationManager.showMessage(`功能开关: 跳过直播已 ${state ? '✅' : '❌'}`); } }, config.onClick, config.shortcut ); 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(); this.notificationManager.showMessage(`⚙️ 极速模式: 播放时间已设为 ${value} 秒`); 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); this.notificationManager.showMessage('🤖 AI喜好: 设置已保存'); 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); this.notificationManager.showMessage('🚫 屏蔽账号: 关键字列表已更新'); 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); this.notificationManager.showMessage('💾 屏蔽账号: 关键字已导出'); }; 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(); this.notificationManager.showMessage('📁 屏蔽账号: 关键字导入成功'); } 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(); this.notificationManager.showMessage(`⚙️ 分辨率筛选: 已设为 ${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) { this.hasSkipped = true; this.stopChecking = true; this.videoController.skip(`🤖 AI筛选: 非'${content}'`); } 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, notificationManager) { this.config = config; this.videoController = videoController; this.notificationManager = notificationManager; this.resolutionSkipped = false; } reset() { this.resolutionSkipped = false; } checkAd(container) { if (!this.config.isEnabled('skipAd')) return false; const adIndicator = container.querySelector(SELECTORS.adIndicator); if (adIndicator) { 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))) { // 如果开启了按R键功能,只按R键(视频会直接消失) if (pressREnabled) { this.videoController.pressR(); } else { // 如果没开启R键功能,则使用下键跳过 this.videoController.skip(`🚫 屏蔽账号: ${accountName}`); } 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) { if (!this.resolutionSkipped) { this.videoController.skip(`📺 分辨率筛选:非 ${targetResolution} 分辨率`); this.resolutionSkipped = true; } return true; } const targetOption = options.find(opt => opt.text.includes(targetResolution)); if (targetOption && !targetOption.element.classList.contains("selected")) { targetOption.element.click(); this.notificationManager.showMessage(`📺 分辨率: 已切换至 ${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(); const resolutionText = bestOption.element.textContent.trim(); this.notificationManager.showMessage(`📺 分辨率: 已切换至最高档 ${resolutionText}`); if (bestOption.text.includes("4K")) { this.config.setEnabled('autoHighRes', false); UIManager.updateToggleButtons('auto-high-resolution-button', false); this.notificationManager.showMessage("📺 分辨率: 已锁定4K,自动切换已关闭"); } return true; } } return false; } } // ========== 主应用程序 ========== class DouyinEnhancer { constructor() { this.notificationManager = new NotificationManager(); this.config = new ConfigManager(); this.videoController = new VideoController(this.notificationManager); this.uiManager = new UIManager(this.config, this.videoController, this.notificationManager); this.aiDetector = new AIDetector(this.videoController, this.config); this.strategies = new VideoDetectionStrategies(this.config, this.videoController, this.notificationManager); this.lastVideoUrl = ''; this.videoStartTime = 0; this.speedModeSkipped = false; this.lastSkippedLiveUrl = ''; this.isCurrentlySkipping = false; this.init(); } init() { this.injectStyles(); document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) { return; } if (e.key === '=') { const isEnabled = !this.config.isEnabled('skipLive'); this.config.setEnabled('skipLive', isEnabled); UIManager.updateToggleButtons('skip-live-button', isEnabled); this.notificationManager.showMessage(`功能开关: 跳过直播已 ${isEnabled ? '✅' : '❌'}`); } }); 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 elementsWithText = Array.from(document.querySelectorAll('div,span')) .filter(el => el.textContent.includes('进入直播间')); const innermostElements = elementsWithText.filter(el => { return !elementsWithText.some(otherEl => el !== otherEl && el.contains(otherEl)); }); const isLive = innermostElements.some(el => isElementInViewport(el)); if (isLive) { this.lastVideoUrl ="直播"; if (this.config.isEnabled('skipLive')) { if (!this.isCurrentlySkipping) { this.videoController.skip('⏭️ 自动跳过: 直播间'); this.isCurrentlySkipping = true; } } return; } this.isCurrentlySkipping = false; const activeContainers = document.querySelectorAll(SELECTORS.activeVideo); const activeContainer = getBestVisibleElement(activeContainers); if (!activeContainer) { 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(); this.strategies.reset(); console.log('===== 新视频开始 ====='); 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) { this.speedModeSkipped = true; this.videoController.skip(`⚡️ 极速模式: ${seconds}秒已到`); 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(); })();