油管播放量统计【播放量/时间】(仅限中文)

Calculate and display views per hour for YouTube videos with improved parsing

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         油管播放量统计【播放量/时间】(仅限中文)
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Calculate and display views per hour for YouTube videos with improved parsing
// @author       Grok
// @match        https://www.youtube.com/*
// @grant        Zola
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // 将时间字符串转换为小时数
    function timeToHours(timeStr) {
        try {
            const cleanTimeStr = timeStr.replace(/[•·]/g, '').trim();
            const timeMatch = cleanTimeStr.match(/(\d+(\.\d+)?)\s*(小时|天|周|个月|年)(前)?/);
            if (!timeMatch) return null;

            const value = parseFloat(timeMatch[1]);
            const unit = timeMatch[3];

            switch (unit) {
                case '小时': return value;
                case '天': return value * 24;
                case '周': return value * 24 * 7;
                case '个月': return value * 24 * 30;
                case '年': return value * 24 * 365;
                default: return null;
            }
        } catch (e) {
            console.error('Error in timeToHours:', e);
            return null;
        }
    }

    // 将观看次数字符串转换为数字
    function viewsToNumber(viewStr) {
        try {
            const cleanViewStr = viewStr.trim();
            const viewMatch = cleanViewStr.match(/(\d[\d,.]*)\s*(万)?\s*次观看/);
            if (!viewMatch) return null;

            let value = parseFloat(viewMatch[1].replace(/,/g, ''));
            if (viewMatch[2] === '万') {
                value *= 10000;
            }
            return value;
        } catch (e) {
            console.error('Error in viewsToNumber:', e);
            return null;
        }
    }

    // 计算每小时观看次数
    function calculateViewsPerHour(viewsStr, timeStr) {
        try {
            const views = viewsToNumber(viewsStr);
            const hours = timeToHours(timeStr);

            if (views === null || hours === null || hours === 0) {
                return 'N/A';
            }

            return (views / hours).toFixed(2) + ' 次/小时';
        } catch (e) {
            console.error('Error in calculateViewsPerHour:', e);
            return 'N/A';
        }
    }

    // 处理单个视频块的元数据
    function processVideoBlock(block) {
        try {
            const spans = block.querySelectorAll('span.inline-metadata-item');
            let viewsText = null;
            let timeText = null;

            spans.forEach(span => {
                const text = span.textContent.trim();
                if (text.includes('次观看')) {
                    viewsText = text;
                } else if (text.includes('前')) {
                    timeText = text;
                }
            });

            if (viewsText && timeText) {
                const viewsPerHour = calculateViewsPerHour(viewsText, timeText);
                let perHourSpan = block.querySelector('span.views-per-hour');
                if (!perHourSpan) {
                    perHourSpan = document.createElement('span');
                    perHourSpan.className = 'inline-metadata-item style-scope ytd-video-meta-block views-per-hour';
                    block.appendChild(perHourSpan);
                }
                perHourSpan.textContent = ` • ${viewsPerHour}`;
            }
        } catch (e) {
            console.error('Error in processVideoBlock:', e);
        }
    }

    // 处理新加载的视频
    function processVideos() {
        try {
            const metaBlocks = document.querySelectorAll('ytd-video-meta-block #metadata-line:not(.processed)');
            metaBlocks.forEach(block => {
                block.classList.add('processed');
                processVideoBlock(block);
            });
        } catch (e) {
            console.error('Error in processVideos:', e);
        }
    }

    // 刷新所有视频的显示数据
    function refreshAllVideos() {
        try {
            const metaBlocks = document.querySelectorAll('ytd-video-meta-block #metadata-line');
            metaBlocks.forEach(block => {
                processVideoBlock(block);
            });
        } catch (e) {
            console.error('Error in refreshAllVideos:', e);
        }
    }

    // 初始加载
    try {
        processVideos();
    } catch (e) {
        console.error('Error in initial processVideos:', e);
    }

    // 监听 DOM 变化以处理动态加载的视频
    let lastRefresh = 0;
    const refreshInterval = 1000; // 限制刷新频率为每秒一次
    const observer = new MutationObserver(() => {
        const now = Date.now();
        if (now - lastRefresh >= refreshInterval) {
            lastRefresh = now;
            processVideos(); // 处理新加载的视频
            refreshAllVideos(); // 刷新所有视频的显示
        }
    });

    try {
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    } catch (e) {
        console.error('Error setting up MutationObserver:', e);
    }
})();