// ==UserScript==
// @name 监控任意网站播放器时长并显示+自动下集
// @namespace http://tampermonkey.net/
// @version 1.7
// @description 监控当前网页播放器的当前时长和总时长,并在页面右下角显示,播放结束自动跳转下集(单击隐藏/显示面板)
// @author You
// @match *://*/*
// @grant none
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/dayjs.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/plugin/duration.js
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// dayjs duration 插件初始化
dayjs.extend(dayjs_plugin_duration);
// 常量定义
const STORAGE_KEY = 'video_helper_settings';
const DEFAULT_SETTINGS = {
autoNext: true,
panelVisible: true // 添加面板可见性设置
};
const PLAYER_CHECK_INTERVAL = 2000; // ms
// 视频助手主模块
const VideoHelper = {
settings: JSON.parse(localStorage.getItem(STORAGE_KEY)) || DEFAULT_SETTINGS,
panel: null,
player: null,
hasJumped: false,
toggleButton: null, // 新增切换按钮引用
// 初始化
init() {
this.panel = this.createPanel();
if (!this.panel) {
console.error("视频助手面板创建失败!");
return;
}
// 创建悬浮切换按钮
this.toggleButton = this.createToggleButton();
// 根据设置初始化面板可见性
if (!this.settings.panelVisible) {
this.panel.addClass('vtp-hidden');
}
this.setupPlayerListener();
console.debug('视频助手已初始化');
},
// 创建悬浮切换按钮
createToggleButton() {
let $toggleBtn = $('<div id="video-panel-toggle-btn">👁️</div>').css({
position: 'fixed',
right: '24px',
bottom: '80px',
width: '28px',
height: '28px',
borderRadius: '50%',
background: 'rgba(0,0,0,0.5)',
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
zIndex: 99999,
fontSize: '14px',
opacity: 0.6,
transition: 'all 0.3s ease',
boxShadow: '0 2px 6px rgba(0,0,0,0.2)'
}).hover(
function(){ $(this).css('opacity', 1).css('transform', 'scale(1.1)'); },
function(){ $(this).css('opacity', 0.6).css('transform', 'scale(1)'); }
).click(() => {
this.togglePanelVisibility();
this.updateToggleButtonIcon();
}).appendTo('body');
this.updateToggleButtonIcon();
return $toggleBtn;
},
// 更新切换按钮的图标
updateToggleButtonIcon() {
if (!this.toggleButton) return;
if (this.settings.panelVisible) {
this.toggleButton.html('👁️').attr('title', '点击隐藏面板');
} else {
this.toggleButton.html('👁️🗨️').attr('title', '点击显示面板');
}
},
// 创建显示面板
createPanel() {
return createPanel();
},
// 设置播放器监听
setupPlayerListener() {
this.findPlayer();
},
// 查找播放器
findPlayer() {
let timer = setInterval(() => {
let $player = $('#playerCnt_html5_api');
if (!$player.length) {
$('iframe').each(function() {
try {
if (!isSameOrigin(this.src)) return;
const iframeDoc = this.contentDocument;
const iframePlayer = $(iframeDoc).find('#playerCnt_html5_api');
if (iframePlayer.length) {
$player = iframePlayer;
return false;
}
} catch (e) {
console.debug('安全限制iframe:', e.message);
}
});
}
if ($player.length) {
clearInterval(timer);
this.player = $player;
this.setupPlayerEvents();
}
}, PLAYER_CHECK_INTERVAL);
},
// 设置播放器事件
setupPlayerEvents() {
const self = this;
this.updatePanel(this.player[0].currentTime, this.player[0].duration);
this.player.on('timeupdate', function() {
self.updatePanel(this.currentTime, this.duration);
if (!isNaN(this.duration) && this.duration > 0) {
if (!self.hasJumped && this.currentTime >= this.duration - 1) {
self.hasJumped = true;
self.handleVideoEnd();
}
}
});
this.player.on('loadedmetadata', function() {
self.updatePanel(this.currentTime, this.duration);
});
},
// 更新面板显示
updatePanel(current, duration) {
if (!this.panel) return;
let percent = (!isNaN(current) && !isNaN(duration) && duration > 0) ? Math.min(100, Math.max(0, current / duration * 100)) : 0;
this.panel.find('.vtp-bar').css('width', percent + '%');
this.panel.find('.vtp-time').text(` ${formatTime(current)} / ${formatTime(duration)}`);
if (isNaN(current) || isNaN(duration)) {
this.panel.find('.vtp-time').text('未检测到播放器');
this.panel.find('.vtp-bar').css('width', '0%');
}
},
// 处理视频结束
handleVideoEnd() {
if (!this.settings.autoNext) return;
console.debug('视频即将结束,尝试寻找下一集按钮...');
let nextBtn = findNextEpisodeBtn();
if (nextBtn && nextBtn.length) {
console.log('找到下集按钮,准备点击');
// 稍微延迟点击,以防止多次触发
setTimeout(() => nextBtn[0].click(), 500);
} else {
console.log('未找到适合的下一集按钮');
}
},
// 切换面板可见性
togglePanelVisibility() {
if (!this.panel) return;
this.settings.panelVisible = !this.settings.panelVisible;
this.panel.toggleClass('vtp-hidden');
this.saveSettings();
// 更新切换按钮图标
this.updateToggleButtonIcon();
},
// 保存设置
saveSettings() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.settings));
}
};
// 新增按钮选择器库
const nextEpisodeSelectors = {
// 文本匹配 - 按优先级排序
textSelectors: [
'a:contains("下一集")',
'a:contains("下集")',
'a:contains("下一话")',
'a:contains("下一章")',
'a:contains("下一页")',
'a:contains("Next")',
'a:contains("next episode")',
'.player-btns-next',
'.next-btn',
],
// 图标匹配
iconSelectors: [
'a:has(.fa-forward)',
'a:has(.fa-caret-down)',
'a:has(.fa-caret-right)',
'a:has(.fa-step-forward)',
'a:has(.fa-arrow-right)',
'a:has(.fa-chevron-right)',
'.icon-next',
'.icon-forward'
],
// 常见网站特定选择器
siteSpecificSelectors: {
'v.qq.com': '.txp_btn_next',
'bilibili.com': '.bilibili-player-video-btn-next',
'youku.com': '.control-next-video',
'iqiyi.com': '.iqp-btn-next'
},
// 常见下一集按钮区域
regionSelectors: [
'.player-controls',
'.video-controls',
'.player-container',
'.myui-player__operate',
'.video-operate'
]
};
// 创建显示面板
function createPanel() {
if ($('#video-time-panel').length > 0) {
return $('#video-time-panel');
}
let $panel = $('<div id="video-time-panel">\n <div class="vtp-bar-bg"><div class="vtp-bar"></div></div>\n <div class="vtp-time"></div>\n</div>').css({
position: 'fixed',
right: '24px',
bottom: '24px',
cursor: 'pointer', // 改为pointer以指示可点击
touchAction: 'none',
background: 'rgba(30,30,30,0.35)',
color: '#fff',
padding: '6px 14px 6px 14px',
borderRadius: '16px',
fontSize: '14px',
zIndex: 99999,
fontFamily: 'monospace',
boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
minWidth: '120px',
maxWidth: '220px',
opacity: 0.7,
transition: 'all 0.3s ease',
userSelect: 'none',
pointerEvents: 'auto',
}).appendTo('body');
let isDragging = false;
let startX, startY, initialLeft, initialTop;
let clickStartTime = 0;
// 添加单击事件处理
$panel.on('mousedown', function(e) {
isDragging = false; // 初始状态为非拖拽
startX = e.clientX;
startY = e.clientY;
initialLeft = parseInt($panel.css('left')) || ($panel.offset() && $panel.offset().left) || 0;
initialTop = parseInt($panel.css('top')) || ($panel.offset() && $panel.offset().top) || 0;
$panel.css('transition', 'none');
clickStartTime = Date.now(); // 记录点击开始时间
});
$(document).on('mousemove', function(e) {
if (clickStartTime === 0) return;
// 如果移动超过5px,视为拖拽而非点击
if (Math.abs(e.clientX - startX) > 5 || Math.abs(e.clientY - startY) > 5) {
isDragging = true;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
const newLeft = initialLeft + deltaX;
const newTop = initialTop + deltaY;
$panel.css({
left: newLeft + 'px',
top: newTop + 'px',
right: 'auto',
bottom: 'auto'
});
}
});
$(document).on('mouseup', function(e) {
if (clickStartTime === 0) return;
const clickDuration = Date.now() - clickStartTime;
// 如果不是拖拽且点击时间短,则视为点击
if (!isDragging && clickDuration < 200) {
// 检查点击是否在设置按钮上
const $target = $(e.target);
if (!$target.hasClass('vtp-settings-btn') && !$target.closest('.vtp-settings-panel').length) {
// 不再在面板点击时切换可见性,而是仅用悬浮按钮控制
// VideoHelper.togglePanelVisibility();
}
}
if (isDragging) {
$panel.css('transition', 'opacity 0.2s');
}
clickStartTime = 0;
isDragging = false;
});
$panel.hover(
function(){ $(this).css('opacity', 1); },
function(){
if (!$(this).find('.vtp-settings-panel').is(':visible')) {
$(this).css('opacity', 0.7);
}
}
);
// 触摸设备支持
$panel.on('touchstart', function(e) {
clickStartTime = Date.now();
isDragging = false;
const touch = e.touches[0];
startX = touch.clientX;
startY = touch.clientY;
initialLeft = parseInt($panel.css('left')) || ($panel.offset() && $panel.offset().left) || 0;
initialTop = parseInt($panel.css('top')) || ($panel.offset() && $panel.offset().top) || 0;
$panel.css('transition', 'none');
});
$panel.on('touchmove', function(e) {
if (clickStartTime === 0) return;
// 如果移动超过10px,视为拖拽而非点击
const touch = e.touches[0];
if (Math.abs(touch.clientX - startX) > 10 || Math.abs(touch.clientY - startY) > 10) {
isDragging = true;
const deltaX = touch.clientX - startX;
const deltaY = touch.clientY - startY;
const newLeft = initialLeft + deltaX;
const newTop = initialTop + deltaY;
$panel.css({
left: newLeft + 'px',
top: newTop + 'px',
right: 'auto',
bottom: 'auto'
});
}
});
$panel.on('touchend', function(e) {
if (clickStartTime === 0) return;
const clickDuration = Date.now() - clickStartTime;
// 如果不是拖拽且点击时间短,则视为点击
if (!isDragging && clickDuration < 300) {
// 检查点击是否在设置按钮上
const $target = $(e.target);
if (!$target.hasClass('vtp-settings-btn') && !$target.closest('.vtp-settings-panel').length) {
// 不再在面板点击时切换可见性,而是仅用悬浮按钮控制
// VideoHelper.togglePanelVisibility();
}
}
clickStartTime = 0;
isDragging = false;
$panel.css('transition', 'opacity 0.2s');
});
// 添加设置按钮
const $settingsBtn = $('<div class="vtp-settings-btn">⚙</div>').css({
position: 'absolute',
top: '2px',
right: '6px',
fontSize: '12px',
cursor: 'pointer',
opacity: '0.6',
transition: 'opacity 0.2s'
}).hover(
function(){ $(this).css('opacity', 1); },
function(){ $(this).css('opacity', 0.6); }
).click(function(e) {
e.stopPropagation(); // 阻止事件冒泡,避免触发面板点击事件
toggleSettings(e);
});
// 设置面板
const $settingsPanel = $(`
<div class="vtp-settings-panel">
<label><input type="checkbox" id="autoNext"> 自动下一集</label>
</div>
`).css({
display: 'none',
position: 'absolute',
top: '30px',
right: '0',
background: 'rgba(0,0,0,0.8)',
padding: '10px',
borderRadius: '8px',
minWidth: '140px',
zIndex: 100000
}).click(function(e) {
e.stopPropagation(); // 阻止事件冒泡,避免触发面板点击事件
});
$panel.append($settingsBtn, $settingsPanel);
// 初始化开关状态
$settingsPanel.find('#autoNext').prop('checked', VideoHelper.settings.autoNext);
// 绑定事件
$settingsPanel.find('input').on('change', function() {
VideoHelper.settings[this.id] = this.checked;
VideoHelper.saveSettings();
});
$('<style>').text(`
#video-time-panel .vtp-bar-bg {
width: 100%;
height: 6px;
background: rgba(255,255,255,0.15);
border-radius: 3px;
margin-bottom: 4px;
overflow: hidden;
}
#video-time-panel .vtp-bar {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #ff2d55 0%, #ffd700 100%);
border-radius: 3px;
transition: width 0.3s;
}
#video-time-panel .vtp-time {
text-align: right;
font-size: 13px;
letter-spacing: 1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.vtp-settings-panel label {
display: block;
color: #fff;
margin: 6px 0;
font-size: 13px;
}
.vtp-settings-panel input {
margin-right: 8px;
}
.vtp-hidden {
transform: translateY(150%);
opacity: 0 !important;
}
#video-time-panel.vtp-hidden:hover {
opacity: 0 !important;
}
#video-panel-toggle-btn {
box-shadow: 0 1px 4px rgba(0,0,0,0.3);
transform-origin: center center;
}
#video-panel-toggle-btn:active {
transform: scale(0.95);
}
@media (max-width: 768px) {
#video-time-panel {
right: 8px!important;
bottom: 8px!important;
font-size: 12px!important;
}
#video-panel-toggle-btn {
right: 8px!important;
bottom: 50px!important;
}
}
`).appendTo('head');
return $panel;
}
function formatTime(sec) {
if (isNaN(sec)) return '--:--';
return dayjs.duration(sec, 'seconds').format('mm:ss');
}
function findNextEpisodeBtn() {
let nextBtn = null;
const host = window.location.hostname;
let debugInfo = ''; // 用于记录按钮查找过程的信息
// 检查是否有网站特定选择器
const siteSelector = Object.keys(nextEpisodeSelectors.siteSpecificSelectors).find(
site => host.includes(site)
);
if (siteSelector) {
nextBtn = findElement(nextEpisodeSelectors.siteSpecificSelectors[siteSelector]);
if (nextBtn && nextBtn.length) {
debugInfo = `特定网站选择器: ${siteSelector}`;
return nextBtn;
}
}
// 检查文本选择器
for (const selector of nextEpisodeSelectors.textSelectors) {
nextBtn = findElement(selector);
if (nextBtn && nextBtn.length) {
debugInfo = `文本选择器: ${selector}`;
return nextBtn;
}
}
// 检查图标选择器
for (const selector of nextEpisodeSelectors.iconSelectors) {
nextBtn = findElement(selector);
if (nextBtn && nextBtn.length) {
debugInfo = `图标选择器: ${selector}`;
return nextBtn;
}
}
// 在常见区域内查找可能的下一集按钮
for (const regionSelector of nextEpisodeSelectors.regionSelectors) {
const region = $(regionSelector);
if (region.length) {
// 在区域内查找链接
const links = region.find('a');
const nextLink = links.filter(function() {
const text = $(this).text().toLowerCase();
const href = $(this).attr('href') || '';
// 检查文本是否包含"下一集"相关词汇
if (/下一|下集|next|forward/i.test(text)) return true;
// 检查链接是否包含视频序号相关的模式
if (/\/vod(play|detail)\/\d+-\d+-\d+/.test(href)) return true;
// 检查是否包含常见图标类
if ($(this).find('.fa-forward,.fa-caret-right,.fa-arrow-right').length) return true;
return false;
});
if (nextLink.length) {
debugInfo = `区域分析: ${regionSelector}`;
return nextLink.first();
}
}
}
// 启发式查找:寻找URL中有序列号且文本相似的链接
try {
const currentUrl = window.location.href;
const match = currentUrl.match(/(\d+)(-|_)(\d+)(-|_)(\d+)/);
if (match) {
const currentNum = parseInt(match[5]);
const nextNum = currentNum + 1;
const pattern = match[0].replace(match[5], nextNum);
// 寻找URL模式匹配的链接
const possibleNextLinks = $('a').filter(function() {
const href = $(this).attr('href') || '';
return href.includes(pattern);
});
if (possibleNextLinks.length) {
debugInfo = `URL模式匹配: ${pattern}`;
return possibleNextLinks.first();
}
}
} catch (e) {
console.debug('URL模式匹配失败:', e);
}
// 使用传统定位逻辑作为后备
nextBtn = findElement('.myui-player__operate a:has(.fa-caret-down), .myui-player__operate a:contains("下集")');
if (nextBtn && nextBtn.length) {
debugInfo = '传统定位逻辑';
return nextBtn;
}
return null;
}
function findElement(selectors, frames = true) {
// 主文档查找
let result = $(selectors);
// 如果找到多个,返回第一个
if (result.length > 1) {
// 默认使用第一个,不需要记录
result = result.first();
}
// 如果主文档没找到且允许在iframe中查找
if (result.length === 0 && frames) {
result = checkFrames(selectors);
}
// 如果找到按钮,高亮显示它
if (result && result.length) {
highlightButton(result);
}
return result;
}
function highlightButton(btn) {
btn.css({
outline: '3px solid #00ff00',
background: 'rgba(0,255,0,0.15)',
transition: 'all 0.3s',
transform: 'scale(1.05)',
'box-shadow': '0 2px 8px rgba(0,255,0,0.3)'
});
// 2秒后取消高亮
setTimeout(() => {
btn.css({
outline: '',
background: '',
transition: '',
transform: '',
'box-shadow': ''
});
}, 2000);
}
function checkFrames(selectors) {
let result = null;
$('iframe').each(function() {
try {
// 提前检测跨域iframe
if (!isSameOrigin(this.src)) {
return; // 跳过跨域iframe
}
const iframeDoc = this.contentDocument;
const $iframeBody = $(iframeDoc.body);
const $btn = $iframeBody.find(selectors);
if ($btn.length) {
result = $btn[0];
return false;
}
} catch (e) {
if (e.name === 'SecurityError') {
console.debug('安全策略限制的iframe:', this.src);
}
}
});
if (result) return $(result);
return null;
}
function isSameOrigin(url) {
try {
if (!url || url.startsWith('about:') || url.startsWith('javascript:')) {
return false;
}
const iframeOrigin = new URL(url, location.href).origin;
return iframeOrigin === location.origin;
} catch (e) {
console.debug("Error in isSameOrigin with URL:", url, e);
return false;
}
}
function toggleSettings(e) {
e.stopPropagation();
if (VideoHelper.panel) {
VideoHelper.panel.find('.vtp-settings-panel').toggle();
}
}
// 初始化
$(document).ready(() => VideoHelper.init());
})();