// ==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();
}
})();