// ==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
// @downloadURL https://update.greasyfork.icu/scripts/539942/%E7%BD%91%E9%A1%B5%E6%8A%96%E9%9F%B3%E4%BD%93%E9%AA%8C%E5%A2%9E%E5%BC%BA.user.js
// @updateURL https://update.greasyfork.icu/scripts/539942/%E7%BD%91%E9%A1%B5%E6%8A%96%E9%9F%B3%E4%BD%93%E9%AA%8C%E5%A2%9E%E5%BC%BA.meta.js
// ==/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 = `
${title}
${content}
`;
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
? `${text.replace(/<[^>]*>/g, '')}${shortcut}
`
: '';
btnContainer.innerHTML = `
${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 = `
⚠️
请检查以下配置:
1. 安装
Ollama
并下载视觉模型(默认:qwen2.5vl:7b)
2. 开启Ollama跨域模式,设置环境变量:
OLLAMA_HOST=0.0.0.0
OLLAMA_ORIGINS=*
`;
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 = `
`;
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 = `
`;
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
? '暂无关键字
'
: tempKeywords.map((keyword, index) => `
${keyword}
`).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 = `
包含这些关键字的账号将被自动跳过
`;
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 = `
▼
提示:只播放包含所选分辨率关键字的视频,没有找到则自动跳过
`;
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();
})();