您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
提取YouTube视频数据(赞数、观看次数、发布日期),显示在可拖拽的半透明弹窗中,支持位置记录、侧边收起、全屏隐藏和数字格式化
// ==UserScript== // @name YouTube视频统计信息弹窗 // @namespace http://tampermonkey.net/ // @version 2.7.1 // @description 提取YouTube视频数据(赞数、观看次数、发布日期),显示在可拖拽的半透明弹窗中,支持位置记录、侧边收起、全屏隐藏和数字格式化 // @author 生财:一万 // @license MIT // @match *://*.youtube.com/** // @grant none // ==/UserScript== (function() { 'use strict'; // 调试开关 - 设置为false可关闭所有console输出 const DEBUG_MODE = false; // 调试输出封装函数 function debugLog(...args) { if (DEBUG_MODE) { console.log(...args); } } function debugError(...args) { if (DEBUG_MODE) { console.error(...args); } } let statsPopup = null; let miniTab = null; let isDragging = false; let dragOffsetX = 0; let dragOffsetY = 0; let isCollapsed = false; let isFullscreen = false; let wasHiddenForFullscreen = false; let fullscreenPreviousState = null; let isIconOnRight = true; // 图标位置状态 // 位置和状态记录相关变量 const POSITION_STORAGE_KEY = 'yt-stats-popup-position'; const COLLAPSE_STATE_KEY = 'yt-stats-popup-collapsed'; const ICON_POSITION_KEY = 'yt-stats-icon-position'; let savedPosition = null; // 保存弹窗位置到localStorage function savePopupPosition(x, y) { const position = { x: x, y: y }; try { localStorage.setItem(POSITION_STORAGE_KEY, JSON.stringify(position)); } catch (error) { debugError('YouTube Stats: 位置保存失败:', error); } } // 保存收起状态到localStorage function saveCollapseState(collapsed) { try { localStorage.setItem(COLLAPSE_STATE_KEY, collapsed.toString()); } catch (error) { debugError('YouTube Stats: 收起状态保存失败:', error); } } // 保存图标位置状态到localStorage function saveIconPosition(isRight, top) { try { const iconState = { isRight: isRight, top: top }; localStorage.setItem(ICON_POSITION_KEY, JSON.stringify(iconState)); } catch (error) { debugError('YouTube Stats: 图标位置保存失败:', error); } } // 从localStorage加载弹窗位置 function loadPopupPosition() { try { const positionStr = localStorage.getItem(POSITION_STORAGE_KEY); if (positionStr) { savedPosition = JSON.parse(positionStr); return savedPosition; } } catch (error) { debugError('YouTube Stats: 位置加载失败:', error); } // 返回默认位置 return { x: window.innerWidth - 300, y: 100 }; } // 从localStorage加载收起状态 function loadCollapseState() { try { const collapsed = localStorage.getItem(COLLAPSE_STATE_KEY); return collapsed === 'true'; } catch (error) { debugError('YouTube Stats: 收起状态加载失败:', error); } return false; } // 从localStorage加载图标位置状态 function loadIconPosition() { try { const iconStateStr = localStorage.getItem(ICON_POSITION_KEY); if (iconStateStr) { const iconState = JSON.parse(iconStateStr); return iconState; } } catch (error) { debugError('YouTube Stats: 图标位置加载失败:', error); } // 返回默认位置(右侧,顶部100px) return { isRight: true, top: 100 }; } // 检测全屏状态 function isInFullscreen() { return !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement); } // 处理全屏状态变化 function handleFullscreenChange() { const currentFullscreen = isInFullscreen(); if (currentFullscreen && !isFullscreen) { // 进入全屏 isFullscreen = true; if (statsPopup) { // 记录当前状态 fullscreenPreviousState = { collapsed: isCollapsed, visible: statsPopup.style.display !== 'none' }; // 隐藏弹窗和侧边图标 statsPopup.style.display = 'none'; if (miniTab) { miniTab.style.display = 'none'; } wasHiddenForFullscreen = true; debugLog('YouTube Stats: 全屏模式,已隐藏弹窗和侧边图标'); } } else if (!currentFullscreen && isFullscreen) { // 退出全屏 isFullscreen = false; if (wasHiddenForFullscreen && fullscreenPreviousState) { // 恢复之前的状态 if (statsPopup && fullscreenPreviousState.visible) { statsPopup.style.display = ''; } if (fullscreenPreviousState.collapsed && miniTab) { miniTab.style.display = 'flex'; } wasHiddenForFullscreen = false; fullscreenPreviousState = null; debugLog('YouTube Stats: 退出全屏,已恢复弹窗和侧边图标'); } } } // 数字格式化函数 - 转换为中文易读格式 function formatNumber(numStr) { if (!numStr || numStr === '未找到' || numStr === '无') return numStr; // 移除非数字字符,只保留数字 const cleanNum = numStr.replace(/[^\d]/g, ''); if (!cleanNum) return numStr; const num = parseInt(cleanNum); if (isNaN(num)) return numStr; // 转换为中文数字格式 if (num >= 100000000) { // 亿及以上 const yi = (num / 100000000).toFixed(1); return yi.endsWith('.0') ? yi.slice(0, -2) + '亿' : yi + '亿'; } else if (num >= 10000) { // 万及以上 const wan = (num / 10000).toFixed(1); return wan.endsWith('.0') ? wan.slice(0, -2) + '万' : wan + '万'; } else { // 小于万的直接显示 return num.toString(); } } // 日期格式化函数 - 转换为年月日顺序 function formatDate(value, label) { // 情况1: 标签包含"年" (例如: 值="6月14日" 标签="2025年") if (label.includes('年')) { if (value.includes('月') || value.includes('日')) { // 年份在标签中,月日在值中 -> "2025年6月14日" return label + value; } else { // 值可能是年份数字 -> "2025年" return value + label; } } // 情况2: 值包含年份,标签包含月日 (例如: 值="2025" 标签="年6月14日") else if (value.match(/^\d{4}$/) && (label.includes('月') || label.includes('日'))) { return value + '年' + label.replace('年', ''); } // 情况3: 默认直接组合 else { return value + label; } } // 创建弹窗样式 function createPopupStyles() { const style = document.createElement('style'); style.textContent = ` .yt-stats-popup { position: fixed; width: 280px; background: rgba(128, 128, 128, 0.3); color: white; border-radius: 8px; padding: 15px; font-family: 'Roboto', Arial, sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); z-index: 10000; border: 1px solid rgba(200, 200, 200, 0.3); user-select: none; cursor: move; backdrop-filter: blur(5px); transition: transform 0.3s ease-in-out; } .yt-stats-popup-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid rgba(255, 255, 255, 0.2); } .yt-stats-popup-title { font-weight: bold; font-size: 16px; color: #ff6b6b; } .yt-stats-collapse-btn { background: none; border: none; color: #fff; font-size: 18px; cursor: pointer; padding: 2px 6px; border-radius: 4px; opacity: 0.7; transition: opacity 0.2s; } .yt-stats-collapse-btn:hover { opacity: 1; background: rgba(255, 255, 255, 0.1); } .yt-stats-mini-tab { display: none; } .yt-stats-side-icon { position: fixed; width: 30px; height: 30px; background: rgba(128, 128, 128, 0.9); color: white; display: flex; justify-content: center; align-items: center; font-size: 18px; cursor: move; z-index: 10000; backdrop-filter: blur(5px); transition: all 0.3s ease-in-out; user-select: none; border: 1px solid rgba(200, 200, 200, 0.3); } .yt-stats-side-icon.right { right: 0; border-radius: 8px 0 0 8px; border-right: none; } .yt-stats-side-icon.left { left: 0; border-radius: 0 8px 8px 0; border-left: none; } .yt-stats-side-icon:hover { width: 35px; background: rgba(128, 128, 128, 0.95); } .yt-stats-mini-tab .mini-icon { font-size: 16px; margin-bottom: 2px; } .yt-stats-item { display: flex; justify-content: space-between; margin-bottom: 8px; padding: 6px 0; } .yt-stats-label { color: #ccc; font-weight: 500; } .yt-stats-value { color: #4fc3f7; font-weight: bold; } .yt-stats-popup.dragging { transition: none; } `; document.head.appendChild(style); } // 提取统计数据 - 支持普通视频和Shorts function extractVideoStats() { const stats = { likes: '未找到', views: '未找到', date: '未找到' }; // 优先检查factoids容器(无论什么页面类型) const factoidsContainer = document.getElementById('factoids'); if (factoidsContainer) { extractRegularVideoStats(stats); } else { // 如果没有factoids,可能是广告页面,显示"无" stats.likes = '无'; stats.views = '无'; stats.date = '无'; } return stats; } // 收起弹窗 function collapsePopup() { if (!statsPopup || isCollapsed) return; isCollapsed = true; // 隐藏弹窗 statsPopup.style.display = 'none'; // 创建侧边图标 createMiniTab(); // 保存收起状态 saveCollapseState(true); debugLog('YouTube Stats: 弹窗已收起'); } // 展开弹窗 function expandPopup() { if (!statsPopup || !isCollapsed) return; isCollapsed = false; // 显示弹窗 statsPopup.style.display = ''; // 移除侧边图标 if (miniTab) { miniTab.remove(); miniTab = null; } // 保存展开状态 saveCollapseState(false); debugLog('YouTube Stats: 弹窗已展开'); } // 切换收起状态 function toggleCollapseState() { if (isCollapsed) { expandPopup(); } else { collapsePopup(); } } // 创建侧边图标 function createMiniTab() { if (miniTab) { miniTab.remove(); } // 加载保存的图标位置状态 const iconState = loadIconPosition(); isIconOnRight = iconState.isRight; const icon = document.createElement('div'); icon.className = isIconOnRight ? 'yt-stats-side-icon right' : 'yt-stats-side-icon left'; // 使用保存的位置 icon.style.top = iconState.top + 'px'; icon.style.display = 'flex'; // 只显示图标 icon.textContent = '📊'; icon.title = '拖拽移动 | 点击展开'; // 添加拖拽和点击事件 let isDraggingIcon = false; let dragStartX = 0; let dragStartY = 0; icon.addEventListener('mousedown', (e) => { isDraggingIcon = false; dragStartX = e.clientX; dragStartY = e.clientY; const handleMouseMove = (e) => { if (!isDraggingIcon && (Math.abs(e.clientX - dragStartX) > 5 || Math.abs(e.clientY - dragStartY) > 5)) { isDraggingIcon = true; // 拖拽时移除定位类,使用绝对定位 icon.className = 'yt-stats-side-icon'; icon.style.transition = 'none'; } if (isDraggingIcon) { // 图标跟随鼠标移动 const x = e.clientX - 15; // 居中偏移 const y = e.clientY - 15; // 居中偏移 // 限制在屏幕范围内 const maxX = window.innerWidth - 30; const maxY = window.innerHeight - 30; icon.style.left = Math.max(0, Math.min(x, maxX)) + 'px'; icon.style.top = Math.max(0, Math.min(y, maxY)) + 'px'; icon.style.right = 'auto'; } }; const handleMouseUp = (e) => { if (!isDraggingIcon) { // 单击事件 expandPopup(); } else { // 拖拽结束,判断贴靠位置 const screenWidth = window.innerWidth; const iconCenterX = e.clientX; // 恢复过渡动画 icon.style.transition = 'all 0.3s ease-in-out'; if (iconCenterX < screenWidth / 2) { // 贴靠左侧 isIconOnRight = false; icon.className = 'yt-stats-side-icon left'; icon.style.left = '0'; icon.style.right = 'auto'; } else { // 贴靠右侧 isIconOnRight = true; icon.className = 'yt-stats-side-icon right'; icon.style.right = '0'; icon.style.left = 'auto'; } // 保存图标位置状态 const currentTop = parseInt(icon.style.top) || 0; saveIconPosition(isIconOnRight, currentTop); } document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); e.preventDefault(); }); document.body.appendChild(icon); miniTab = icon; } // 提取普通视频页面统计数据 function extractRegularVideoStats(stats) { const factoidsContainer = document.getElementById('factoids'); if (!factoidsContainer) { return; } debugLog('YouTube Stats: 开始实时提取factoids数据...'); // 重新查询确保数据最新 const freshFactoids = document.getElementById('factoids'); if (!freshFactoids) { debugLog('YouTube Stats: factoids容器消失'); return; } // 提取观看次数(view-count-factoid-renderer) const viewCountRenderer = freshFactoids.querySelector('view-count-factoid-renderer'); if (viewCountRenderer) { const viewValue = viewCountRenderer.querySelector('.ytwFactoidRendererValue'); if (viewValue && viewValue.textContent.trim()) { stats.views = viewValue.textContent.trim(); debugLog('YouTube Stats: 实时观看次数:', stats.views); } } // 提取赞数和日期(factoid-renderer元素) const factoidRenderers = freshFactoids.querySelectorAll('factoid-renderer'); debugLog(`YouTube Stats: 找到${factoidRenderers.length}个factoid元素`); factoidRenderers.forEach((renderer, index) => { const label = renderer.querySelector('.ytwFactoidRendererLabel'); const value = renderer.querySelector('.ytwFactoidRendererValue'); if (label && value) { const labelText = label.textContent.trim(); const valueText = value.textContent.trim(); debugLog(`YouTube Stats: 元素${index} - 标签:"${labelText}", 值:"${valueText}"`); if (labelText.includes('赞') || labelText.includes('点赞')) { stats.likes = valueText; debugLog('YouTube Stats: 实时赞数:', stats.likes); } else if (labelText.includes('年') || labelText.includes('月') || labelText.includes('日')) { // 格式化日期为年月日顺序 const fullDate = formatDate(valueText, labelText); stats.date = fullDate; debugLog('YouTube Stats: 实时日期:', stats.date); } else if (labelText.includes('前')) { // 相对时间,如"1天前" stats.date = valueText + labelText; debugLog('YouTube Stats: 实时相对时间:', stats.date); } } }); // 如果还没找到观看次数,尝试其他选择器 if (stats.views === '未找到') { debugLog('YouTube Stats: 尝试备用观看次数提取...'); const alternativeViewSelectors = [ '#factoids .ytwFactoidRendererValue', '#factoids span[class*="view"]', '#factoids span[aria-label*="观看"]' ]; for (const selector of alternativeViewSelectors) { const elements = freshFactoids.querySelectorAll(selector); for (const el of elements) { const text = el.textContent.trim(); // 检查是否包含数字且可能是观看次数 if (text && /^\d[\d,]*$/.test(text) && !text.includes('年') && !text.includes('月')) { stats.views = text; debugLog('YouTube Stats: 备用方法找到观看次数:', stats.views); break; } } if (stats.views !== '未找到') break; } } debugLog('YouTube Stats: 最终提取结果:', stats); } // 创建弹窗 - 使用安全的DOM操作 function createStatsPopup(stats) { if (statsPopup) { statsPopup.remove(); } // 创建主容器 const popup = document.createElement('div'); popup.className = 'yt-stats-popup'; // 创建头部 const header = document.createElement('div'); header.className = 'yt-stats-popup-header'; const title = document.createElement('div'); title.className = 'yt-stats-popup-title'; title.textContent = '📊 视频统计'; // 创建收起按钮 const collapseBtn = document.createElement('button'); collapseBtn.className = 'yt-stats-collapse-btn'; collapseBtn.textContent = '收起'; collapseBtn.title = '收起到侧边'; collapseBtn.addEventListener('click', (e) => { e.stopPropagation(); collapsePopup(); }); header.appendChild(title); header.appendChild(collapseBtn); // 创建数据项 function createStatsItem(icon, label, value) { const item = document.createElement('div'); item.className = 'yt-stats-item'; const labelSpan = document.createElement('span'); labelSpan.className = 'yt-stats-label'; labelSpan.textContent = `${icon} ${label}:`; const valueSpan = document.createElement('span'); valueSpan.className = 'yt-stats-value'; valueSpan.textContent = value; item.appendChild(labelSpan); item.appendChild(valueSpan); return item; } // 添加统计项(只对观看次数格式化) const likesItem = createStatsItem('👍', '赞数', stats.likes); const viewsItem = createStatsItem('👀', '观看', formatNumber(stats.views)); const dateItem = createStatsItem('📅', '发布', stats.date); // 组装弹窗 popup.appendChild(header); popup.appendChild(likesItem); popup.appendChild(viewsItem); popup.appendChild(dateItem); // 设置弹窗位置 const position = loadPopupPosition(); popup.style.left = position.x + 'px'; popup.style.top = position.y + 'px'; popup.style.right = 'auto'; // 取消right定位,使用left定位 // 添加拖拽功能 popup.addEventListener('mousedown', startDrag); document.body.appendChild(popup); statsPopup = popup; // 恢复收起状态 const savedCollapsed = loadCollapseState(); if (savedCollapsed) { // 延迟执行收起,确保弹窗完全创建 setTimeout(() => { collapsePopup(); }, 100); } return popup; } // 开始拖拽 function startDrag(e) { isDragging = true; statsPopup.classList.add('dragging'); const rect = statsPopup.getBoundingClientRect(); dragOffsetX = e.clientX - rect.left; dragOffsetY = e.clientY - rect.top; document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDrag); e.preventDefault(); } // 拖拽过程 function drag(e) { if (!isDragging || !statsPopup) return; const x = e.clientX - dragOffsetX; const y = e.clientY - dragOffsetY; // 限制拖拽范围 const maxX = window.innerWidth - statsPopup.offsetWidth; const maxY = window.innerHeight - statsPopup.offsetHeight; const finalX = Math.max(0, Math.min(x, maxX)); const finalY = Math.max(0, Math.min(y, maxY)); statsPopup.style.left = finalX + 'px'; statsPopup.style.top = finalY + 'px'; statsPopup.style.right = 'auto'; } // 停止拖拽 function stopDrag() { isDragging = false; if (statsPopup) { statsPopup.classList.remove('dragging'); // 保存当前位置 const rect = statsPopup.getBoundingClientRect(); savePopupPosition(rect.left, rect.top); } document.removeEventListener('mousemove', drag); document.removeEventListener('mouseup', stopDrag); } // 清空弹窗数据 function clearPopupData() { if (statsPopup) { const items = statsPopup.querySelectorAll('.yt-stats-value'); if (items.length >= 3) { items[0].textContent = '加载中...'; items[1].textContent = '加载中...'; items[2].textContent = '加载中...'; } } } // 清理所有弹窗元素 function cleanupPopup() { if (statsPopup) { statsPopup.remove(); statsPopup = null; } if (miniTab) { miniTab.remove(); miniTab = null; } isCollapsed = false; } // 更新统计信息 function updateStats() { try { const stats = extractVideoStats(); if (stats) { if (statsPopup) { // 更新现有弹窗内容(只对观看次数格式化) const items = statsPopup.querySelectorAll('.yt-stats-value'); if (items.length >= 3) { items[0].textContent = stats.likes || '未找到'; items[1].textContent = formatNumber(stats.views || '未找到'); items[2].textContent = stats.date || '未找到'; } } else { // 创建新弹窗 createStatsPopup(stats); debugLog('YouTube Stats: 统计弹窗已显示'); } } } catch (error) { debugError('YouTube Stats: 更新统计信息时出错:', error); } } // 初始化脚本 function init() { createPopupStyles(); // 延迟执行,等待页面完全加载 setTimeout(() => { updateStats(); }, 500); // 监听页面变化(YouTube是单页应用) let lastUrl = location.href; let factoidsObserver = null; new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; // 切换视频时清理弹窗 cleanupPopup(); // 断开之前的factoids监听器 if (factoidsObserver) { factoidsObserver.disconnect(); } // 延迟执行,等待新页面内容加载 setTimeout(() => { updateStats(); // 重新建立factoids监听 setupFactoidsObserver(); }, 800); } }).observe(document, {subtree: true, childList: true}); // 设置factoids变化监听器的函数 function setupFactoidsObserver() { factoidsObserver = new MutationObserver(() => { if (location.href.includes('/watch?') || location.href.includes('/shorts/')) { debugLog('YouTube Stats: 检测到factoids变化,立即更新...'); setTimeout(() => updateStats(), 100); } }); const checkFactoids = () => { const factoids = document.getElementById('factoids'); if (factoids) { factoidsObserver.observe(factoids, { childList: true, subtree: true, characterData: true }); debugLog('YouTube Stats: 开始监听factoids实时变化'); } else { setTimeout(checkFactoids, 500); } }; checkFactoids(); } // 高频实时更新统计信息 setInterval(() => { if (location.href.includes('/watch?') || location.href.includes('/shorts/')) { updateStats(); } }, 1000); // 初始化factoids监听 setupFactoidsObserver(); // 监听全屏状态变化 document.addEventListener('fullscreenchange', handleFullscreenChange); document.addEventListener('webkitfullscreenchange', handleFullscreenChange); document.addEventListener('mozfullscreenchange', handleFullscreenChange); document.addEventListener('msfullscreenchange', handleFullscreenChange); debugLog('YouTube Stats: 全屏监听已启动'); } // 页面加载完成后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();