YouTube 4K 视频过滤器 - 高级版

通过检查视频设置菜单识别并筛选YouTube上的4K视频

目前为 2025-03-28 提交的版本。查看 最新版本

// ==UserScript==
// @license MIT
// @name         YouTube 4K 视频过滤器 - 高级版
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  通过检查视频设置菜单识别并筛选YouTube上的4K视频
// @author       You
// @match        https://www.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_openInTab
// ==/UserScript==

(function() {
    'use strict';

    // 存储空间键名
    const STORAGE_KEY = 'youtube_4k_videos';
    const SCANNING_KEY = 'youtube_4k_scanning';
    const buttonId = 'yt-4k-filter-button';
    const scanButtonId = 'yt-4k-scan-button';
    const badgeClass = 'yt-4k-badge';

    // 存储和获取4K视频记录
    function saveVideoQuality(videoId, is4K) {
        const storage = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
        storage[videoId] = {
            is4K: is4K,
            timestamp: Date.now()
        };
        localStorage.setItem(STORAGE_KEY, JSON.stringify(storage));
        console.log(`保存视频 ${videoId} 4K状态: ${is4K}`);
    }

    function getVideoQuality(videoId) {
        const storage = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
        return storage[videoId] || null;
    }

    function getAllVideoQualities() {
        return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
    }

    // 检测是否在视频详情页
    function isVideoPage() {
        return window.location.pathname === '/watch';
    }

    // 检测是否在频道或浏览页面
    function isListingPage() {
        const path = window.location.pathname;
        return path.includes('/channel/') ||
               path.includes('/c/') ||
               path.includes('/user/') ||
               path.includes('/@') ||
               path === '/' ||
               path.includes('/results') ||
               path.includes('/feed/');
    }

    // 从URL中提取视频ID
    function extractVideoId(url) {
        const regExp = /(?:\/|v=)([a-zA-Z0-9_-]{11})(?:\?|&|\/|$)/;
        const match = url.match(regExp);
        return match ? match[1] : null;
    }

    // 获取当前视频ID
    function getCurrentVideoId() {
        if (isVideoPage()) {
            const urlParams = new URLSearchParams(window.location.search);
            return urlParams.get('v');
        }
        return null;
    }

    // === 视频页面功能 ===

    // 检查当前视频是否有4K选项
    async function checkCurrentVideoFor4K() {
        const videoId = getCurrentVideoId();
        if (!videoId) return;

        // 如果已经检查过此视频,则跳过
        const existingData = getVideoQuality(videoId);
        if (existingData) {
            console.log(`视频 ${videoId} 已检查过,跳过`);
            return;
        }

        console.log(`检查视频 ${videoId} 是否有4K选项`);

        try {
            // 等待视频播放器加载
            await waitForElement('button.ytp-settings-button');

            // 点击设置按钮
            const settingsButton = document.querySelector('button.ytp-settings-button');
            settingsButton.click();
            await sleep(300);

            // 点击质量选项
            const qualityButton = Array.from(document.querySelectorAll('.ytp-panel-menu .ytp-menuitem'))
                .find(el => el.textContent.includes('画质') || el.textContent.includes('Quality'));

            if (qualityButton) {
                qualityButton.click();
                await sleep(300);

                // 检查是否有4K选项
                const qualityOptions = document.querySelectorAll('.ytp-quality-menu .ytp-menuitem');
                let has4K = false;

                qualityOptions.forEach(option => {
                    const text = option.textContent.trim();
                    if (text.includes('2160p') || text.includes('4K')) {
                        has4K = true;
                        console.log(`发现4K选项: ${text}`);
                    }
                });

                // 保存结果
                saveVideoQuality(videoId, has4K);

                // 关闭菜单
                const closeButton = document.querySelector('.ytp-popup .ytp-panel-back-button');
                if (closeButton) closeButton.click();
                await sleep(100);
                settingsButton.click();

                // 如果是由扫描模式打开的,向列表页面发送消息并关闭此页面
                if (isScanningMode()) {
                    // 将此视频标记为已扫描
                    markVideoAsScanned(videoId);

                    // 1秒后关闭页面
                    setTimeout(() => {
                        window.close();
                    }, 1000);
                }
            }
        } catch (error) {
            console.error('检查视频质量时出错:', error);
        }
    }

    // 等待元素出现
    function waitForElement(selector, timeout = 5000) {
        return new Promise((resolve, reject) => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }

            const observer = new MutationObserver(mutations => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });

            // 设置超时
            setTimeout(() => {
                observer.disconnect();
                reject(new Error(`等待元素 ${selector} 超时`));
            }, timeout);
        });
    }

    // 睡眠函数
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    // === 列表页面功能 ===

    // 创建4K过滤按钮
    function createFilterButton() {
        if (document.getElementById(buttonId)) return;

        console.log('创建4K过滤按钮');

        const container = document.createElement('div');
        container.id = 'yt-4k-filter-container';
        container.style.position = 'fixed';
        container.style.top = '120px';
        container.style.right = '20px';
        container.style.zIndex = '9999';
        container.style.display = 'flex';
        container.style.flexDirection = 'column';
        container.style.gap = '10px';

        // 过滤按钮
        const filterButton = document.createElement('button');
        filterButton.id = buttonId;
        filterButton.textContent = '显示4K视频';
        filterButton.style.padding = '10px 16px';
        filterButton.style.backgroundColor = 'red';
        filterButton.style.color = 'white';
        filterButton.style.border = '2px solid white';
        filterButton.style.borderRadius = '4px';
        filterButton.style.cursor = 'pointer';
        filterButton.style.fontWeight = 'bold';
        filterButton.style.boxShadow = '0 2px 5px rgba(0,0,0,0.5)';
        filterButton.addEventListener('click', toggleFilter);

        // 扫描按钮
        const scanButton = document.createElement('button');
        scanButton.id = scanButtonId;
        scanButton.textContent = '扫描4K视频';
        scanButton.style.padding = '10px 16px';
        scanButton.style.backgroundColor = 'blue';
        scanButton.style.color = 'white';
        scanButton.style.border = '2px solid white';
        scanButton.style.borderRadius = '4px';
        scanButton.style.cursor = 'pointer';
        scanButton.style.fontWeight = 'bold';
        scanButton.style.boxShadow = '0 2px 5px rgba(0,0,0,0.5)';
        scanButton.addEventListener('click', startScanningVideos);

        // 状态显示
        const statusDisplay = document.createElement('div');
        statusDisplay.id = 'yt-4k-status';
        statusDisplay.style.backgroundColor = 'rgba(0,0,0,0.7)';
        statusDisplay.style.color = 'white';
        statusDisplay.style.padding = '8px';
        statusDisplay.style.borderRadius = '4px';
        statusDisplay.style.fontSize = '12px';
        statusDisplay.style.display = 'none';

        container.appendChild(filterButton);
        container.appendChild(scanButton);
        container.appendChild(statusDisplay);
        document.body.appendChild(container);
    }

    // 更新状态显示
    function updateStatus(message, show = true) {
        const statusEl = document.getElementById('yt-4k-status');
        if (statusEl) {
            statusEl.textContent = message;
            statusEl.style.display = show ? 'block' : 'none';
        }
    }

    // 切换过滤状态
    let filterActive = false;
    function toggleFilter() {
        filterActive = !filterActive;
        const button = document.getElementById(buttonId);

        if (filterActive) {
            button.textContent = '显示所有视频';
            button.style.backgroundColor = 'green';

            const storage = getAllVideoQualities();
            let count4K = 0;

            // 筛选视频
            const videoElements = document.querySelectorAll('ytd-grid-video-renderer, ytd-rich-item-renderer, ytd-video-renderer');
            videoElements.forEach(video => {
                const linkElement = video.querySelector('a#thumbnail') ||
                                   video.querySelector('a.yt-simple-endpoint');

                if (!linkElement || !linkElement.href) return;

                const videoId = extractVideoId(linkElement.href);
                if (!videoId) return;

                const qualityData = storage[videoId];
                const is4K = qualityData && qualityData.is4K;

                if (is4K) {
                    count4K++;
                    // 添加4K标记
                    if (!video.querySelector(`.${badgeClass}`)) {
                        addBadgeToVideo(video, linkElement);
                    }
                    video.style.display = '';
                } else {
                    video.style.display = 'none';
                }
            });

            updateStatus(`找到 ${count4K} 个4K视频`);
        } else {
            button.textContent = '显示4K视频';
            button.style.backgroundColor = 'red';

            // 恢复所有视频显示
            document.querySelectorAll('ytd-grid-video-renderer, ytd-rich-item-renderer, ytd-video-renderer').forEach(video => {
                video.style.display = '';
            });

            updateStatus('', false);
        }
    }

    // 为视频添加4K标记
    function addBadgeToVideo(videoElement, linkElement) {
        const badge = document.createElement('div');
        badge.className = badgeClass;
        badge.textContent = '4K';
        badge.style.position = 'absolute';
        badge.style.top = '5px';
        badge.style.right = '5px';
        badge.style.backgroundColor = 'red';
        badge.style.color = 'white';
        badge.style.padding = '3px 6px';
        badge.style.borderRadius = '3px';
        badge.style.fontWeight = 'bold';
        badge.style.fontSize = '12px';
        badge.style.zIndex = '2';

        if (linkElement) {
            linkElement.style.position = 'relative';
            linkElement.appendChild(badge);
        }
    }

    // 扫描功能
    function isScanningMode() {
        return localStorage.getItem(SCANNING_KEY) === 'true';
    }

    function setScanningMode(isScanning) {
        localStorage.setItem(SCANNING_KEY, isScanning ? 'true' : 'false');
    }

    function markVideoAsScanned(videoId) {
        const scannedVideos = JSON.parse(localStorage.getItem('scanned_videos') || '[]');
        if (!scannedVideos.includes(videoId)) {
            scannedVideos.push(videoId);
            localStorage.setItem('scanned_videos', JSON.stringify(scannedVideos));
        }
    }

    function isVideoScanned(videoId) {
        const scannedVideos = JSON.parse(localStorage.getItem('scanned_videos') || '[]');
        return scannedVideos.includes(videoId);
    }

    async function startScanningVideos() {
        const scanButton = document.getElementById(scanButtonId);
        if (isScanningMode()) {
            // 停止扫描
            setScanningMode(false);
            scanButton.textContent = '扫描4K视频';
            scanButton.style.backgroundColor = 'blue';
            updateStatus('扫描已停止', true);
            setTimeout(() => updateStatus('', false), 3000);
            return;
        }

        // 开始扫描
        setScanningMode(true);
        scanButton.textContent = '停止扫描';
        scanButton.style.backgroundColor = 'orange';

        // 获取页面上的所有视频
        const videoElements = document.querySelectorAll('ytd-grid-video-renderer, ytd-rich-item-renderer, ytd-video-renderer');
        const totalVideos = videoElements.length;
        let scannedCount = 0;
        let pendingCount = 0;

        // 清除已扫描记录
        localStorage.setItem('scanned_videos', '[]');

        // 遍历视频元素
        for (let i = 0; i < videoElements.length; i++) {
            if (!isScanningMode()) break; // 如果扫描被中止则退出

            const video = videoElements[i];
            const linkElement = video.querySelector('a#thumbnail') ||
                               video.querySelector('a.yt-simple-endpoint');

            if (!linkElement || !linkElement.href) continue;

            const videoId = extractVideoId(linkElement.href);
            if (!videoId) continue;

            // 检查是否已经知道此视频的4K状态
            const qualityData = getVideoQuality(videoId);
            if (qualityData) {
                scannedCount++;
                updateStatus(`扫描进度: ${scannedCount}/${totalVideos},已知视频跳过`);
                continue;
            }

            // 在新标签页中打开视频
            updateStatus(`扫描进度: ${scannedCount}/${totalVideos},打开视频 ${videoId}`);

            // 限制并发标签页数量
            pendingCount++;
            if (pendingCount >= 3) {
                // 等待至少一个视频被扫描
                await waitForAnyVideoScanned();
                pendingCount--;
            }

            // 打开新标签页
            window.open(`https://www.youtube.com/watch?v=${videoId}`, '_blank');

            // 等待一段时间再继续
            await sleep(1500);
        }

        // 扫描完成
        setScanningMode(false);
        scanButton.textContent = '扫描4K视频';
        scanButton.style.backgroundColor = 'blue';
        updateStatus('扫描完成!', true);
        setTimeout(() => updateStatus('', false), 5000);
    }

    // 等待任意视频被扫描
    function waitForAnyVideoScanned() {
        return new Promise(resolve => {
            const initialCount = JSON.parse(localStorage.getItem('scanned_videos') || '[]').length;

            const checkInterval = setInterval(() => {
                const currentCount = JSON.parse(localStorage.getItem('scanned_videos') || '[]').length;
                if (currentCount > initialCount || !isScanningMode()) {
                    clearInterval(checkInterval);
                    resolve();
                }
            }, 500);

            // 设置超时,避免永久等待
            setTimeout(() => {
                clearInterval(checkInterval);
                resolve();
            }, 10000);
        });
    }

    // 标记已知的4K视频
    function markKnown4KVideos() {
        if (filterActive) return; // 如果正在过滤则跳过

        const storage = getAllVideoQualities();
        const videoElements = document.querySelectorAll('ytd-grid-video-renderer, ytd-rich-item-renderer, ytd-video-renderer');

        videoElements.forEach(video => {
            const linkElement = video.querySelector('a#thumbnail') ||
                               video.querySelector('a.yt-simple-endpoint');

            if (!linkElement || !linkElement.href) return;

            const videoId = extractVideoId(linkElement.href);
            if (!videoId) return;

            const qualityData = storage[videoId];
            if (qualityData && qualityData.is4K && !video.querySelector(`.${badgeClass}`)) {
                addBadgeToVideo(video, linkElement);
            }
        });
    }

    // === 初始化 ===

    // 根据页面类型初始化功能
    function init() {
        if (isVideoPage()) {
            // 视频详情页
            checkCurrentVideoFor4K();
        } else if (isListingPage()) {
            // 视频列表页
            createFilterButton();
            markKnown4KVideos();

            // 监听DOM变化
            const observer = new MutationObserver(mutations => {
                if (!document.getElementById(buttonId)) {
                    createFilterButton();
                }

                // 延迟执行,避免频繁调用
                clearTimeout(window.markTimeout);
                window.markTimeout = setTimeout(() => {
                    markKnown4KVideos();
                    if (filterActive) toggleFilter();
                }, 1000);
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }
    }

    // 启动程序
    function start() {
        console.log('YouTube 4K 过滤器启动');

        // 尝试多次初始化,确保在页面加载后运行
        setTimeout(init, 5000);

        // 监听URL变化
        let lastUrl = location.href;
        const urlObserver = new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                // URL变化时重新初始化
                setTimeout(init, 1500);
            }
        });

        urlObserver.observe(document.body, { subtree: true, childList: true });
    }

    start();
})();