// ==UserScript==
// @name YouTube视频统计信息弹窗
// @namespace http://tampermonkey.net/
// @version 2.0.1
// @description 提取YouTube视频数据(赞数、观看次数、发布日期),显示在可拖拽的半透明弹窗中,支持位置记录和数字格式化
// @author 生财:一万
// @license MIT
// @match *://*.youtube.com/**
// @grant none
// ==/UserScript==
(function() {
'use strict';
let statsPopup = null;
let isDragging = false;
let dragOffsetX = 0;
let dragOffsetY = 0;
// 位置记录相关变量
const POSITION_STORAGE_KEY = 'yt-stats-popup-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) {
console.error('YouTube Stats: 位置保存失败:', error);
}
}
// 从localStorage加载弹窗位置
function loadPopupPosition() {
try {
const positionStr = localStorage.getItem(POSITION_STORAGE_KEY);
if (positionStr) {
savedPosition = JSON.parse(positionStr);
return savedPosition;
}
} catch (error) {
console.error('YouTube Stats: 位置加载失败:', error);
}
// 返回默认位置
return { x: window.innerWidth - 300, y: 100 };
}
// 数字格式化函数 - 转换为中文易读格式
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.85);
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);
}
.yt-stats-popup-header {
display: flex;
justify-content: center;
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-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 extractRegularVideoStats(stats) {
const factoidsContainer = document.getElementById('factoids');
if (!factoidsContainer) {
return;
}
console.log('YouTube Stats: 开始实时提取factoids数据...');
// 重新查询确保数据最新
const freshFactoids = document.getElementById('factoids');
if (!freshFactoids) {
console.log('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();
console.log('YouTube Stats: 实时观看次数:', stats.views);
}
}
// 提取赞数和日期(factoid-renderer元素)
const factoidRenderers = freshFactoids.querySelectorAll('factoid-renderer');
console.log(`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();
console.log(`YouTube Stats: 元素${index} - 标签:"${labelText}", 值:"${valueText}"`);
if (labelText.includes('赞') || labelText.includes('点赞')) {
stats.likes = valueText;
console.log('YouTube Stats: 实时赞数:', stats.likes);
} else if (labelText.includes('年') || labelText.includes('月') || labelText.includes('日')) {
// 格式化日期为年月日顺序
const fullDate = formatDate(valueText, labelText);
stats.date = fullDate;
console.log('YouTube Stats: 实时日期:', stats.date);
} else if (labelText.includes('前')) {
// 相对时间,如"1天前"
stats.date = valueText + labelText;
console.log('YouTube Stats: 实时相对时间:', stats.date);
}
}
});
// 如果还没找到观看次数,尝试其他选择器
if (stats.views === '未找到') {
console.log('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;
console.log('YouTube Stats: 备用方法找到观看次数:', stats.views);
break;
}
}
if (stats.views !== '未找到') break;
}
}
console.log('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 = '📊 视频统计';
header.appendChild(title);
// 创建数据项
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;
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 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);
console.log('YouTube Stats: 统计弹窗已显示');
}
}
} catch (error) {
console.error('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;
// 切换视频时立即清空弹窗数据
if (statsPopup) {
clearPopupData();
}
// 断开之前的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/')) {
console.log('YouTube Stats: 检测到factoids变化,立即更新...');
setTimeout(() => updateStats(), 100);
}
});
const checkFactoids = () => {
const factoids = document.getElementById('factoids');
if (factoids) {
factoidsObserver.observe(factoids, {
childList: true,
subtree: true,
characterData: true
});
console.log('YouTube Stats: 开始监听factoids实时变化');
} else {
setTimeout(checkFactoids, 500);
}
};
checkFactoids();
}
// 高频实时更新统计信息
setInterval(() => {
if (location.href.includes('/watch?') || location.href.includes('/shorts/')) {
updateStats();
}
}, 1000);
// 初始化factoids监听
setupFactoidsObserver();
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();