// ==UserScript== // @name 监控任意网站播放器时长并显示+自动下集 // @namespace http://tampermonkey.net/ // @version 1.7 // @description 监控当前网页播放器的当前时长和总时长,并在页面右下角显示,播放结束自动跳转下集(单击隐藏/显示面板) // @author You // @match *://*/* // @grant none // @require https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js // @require https://cdn.jsdelivr.net/npm/dayjs@1.11.10/dayjs.min.js // @require https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/duration.js // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/537837/%E7%9B%91%E6%8E%A7%E4%BB%BB%E6%84%8F%E7%BD%91%E7%AB%99%E6%92%AD%E6%94%BE%E5%99%A8%E6%97%B6%E9%95%BF%E5%B9%B6%E6%98%BE%E7%A4%BA%2B%E8%87%AA%E5%8A%A8%E4%B8%8B%E9%9B%86.user.js // @updateURL https://update.greasyfork.icu/scripts/537837/%E7%9B%91%E6%8E%A7%E4%BB%BB%E6%84%8F%E7%BD%91%E7%AB%99%E6%92%AD%E6%94%BE%E5%99%A8%E6%97%B6%E9%95%BF%E5%B9%B6%E6%98%BE%E7%A4%BA%2B%E8%87%AA%E5%8A%A8%E4%B8%8B%E9%9B%86.meta.js // ==/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 = $('
👁️
').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 = $('
\n
\n
\n
').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 = $('
').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 = $(`
`).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(); }); $('