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