YouTube视频统计信息弹窗

提取YouTube视频数据(赞数、观看次数、发布日期),显示在可拖拽的半透明弹窗中,支持位置记录和数字格式化

当前为 2025-06-22 提交的版本,查看 最新版本

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

})();