YouTube Universal Progress Tracker

Left-aligned progress indicators with fixed thumbnail height

目前为 2025-01-30 提交的版本。查看 最新版本

// ==UserScript==
// @name         YouTube Universal Progress Tracker
// @namespace    http://tampermonkey.net/
// @version      3.2
// @description  Left-aligned progress indicators with fixed thumbnail height
// @author       ikigaiDH
// @match        https://www.youtube.com/*
// @grant        none
// @license      GPL-3.0-only
// ==/UserScript==

(function() {
    'use strict';

    const STORAGE_KEY = 'yt_watch_history';
    let watchHistory = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');

    // Universal video ID extractor
    const getVideoId = (element) => {
        try {
            const link = element.closest('a') || element.querySelector('a');
            if (!link) return null;

            const url = new URL(link.href);
            return url.searchParams.get('v') ||
                   url.pathname.split('/watch/')[1]?.split('?')[0] ||
                   url.pathname.split('/')[2];
        } catch {
            return null;
        }
    };

    // Progress indicator creation
    const createIndicator = () => {
        const indicator = document.createElement('div');
        Object.assign(indicator.style, {
            position: 'absolute',
            bottom: '4px',
            left: '4px',
            backgroundColor: '#cc0000',
            color: 'white',
            padding: '2px 6px',
            borderRadius: '2px',
            fontSize: '12px',
            fontWeight: 'bold',
            zIndex: '1000',
            fontFamily: 'Roboto, Arial, sans-serif',
            textTransform: 'uppercase'
        });
        indicator.className = 'yt-progress-indicator';
        return indicator;
    };

    // Main update function
    const updateAllThumbnails = () => {
        // First, clean up any existing indicators to prevent duplicates
        document.querySelectorAll('.yt-progress-indicator').forEach(ind => ind.remove());

        // Process all thumbnails
        document.querySelectorAll('ytd-thumbnail').forEach(thumbnail => {
            // Skip thumbnails that don't have proper structure
            if (!thumbnail.querySelector('#thumbnail')) return;

            const videoId = getVideoId(thumbnail);
            if (!videoId) return;

            const percentage = watchHistory[videoId] || 0;
            if (percentage > 0) {
                // Create new indicator
                const indicator = createIndicator();
                indicator.textContent = percentage >= 100 ? '>100%' : `${Math.round(percentage)}%`;
                
                // Find the correct container for the indicator
                const overlays = thumbnail.querySelector('#overlays');
                if (overlays) {
                    overlays.appendChild(indicator);
                } else {
                    const img = thumbnail.querySelector('#img');
                    if (img) {
                        if (!img.style.position) {
                            img.style.position = 'relative';
                        }
                        img.appendChild(indicator);
                    }
                }
            }
        });
    };

    // Video tracking with debouncing
    let currentVideoId = null;
    const trackVideo = () => {
        const video = document.querySelector('video');
        if (!video) return;

        const newVideoId = new URLSearchParams(window.location.search).get('v');
        if (newVideoId === currentVideoId) return;

        currentVideoId = newVideoId;
        const saveProgress = () => {
            if (video.duration > 0) {
                const percentage = (video.currentTime / video.duration) * 100;
                if (percentage > (watchHistory[currentVideoId] || 0)) {
                    watchHistory[currentVideoId] = percentage;
                    localStorage.setItem(STORAGE_KEY, JSON.stringify(watchHistory));
                    updateAllThumbnails();
                }
            }
        };

        video.addEventListener('timeupdate', saveProgress);
    };

    // Enhanced observation with debouncing
    let updateTimeout = null;
    const observer = new MutationObserver(() => {
        if (updateTimeout) {
            clearTimeout(updateTimeout);
        }
        updateTimeout = setTimeout(() => {
            updateAllThumbnails();
            trackVideo();
        }, 100);
    });

    // Initialization
    window.addEventListener('load', () => {
        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: false
        });
        updateAllThumbnails();
        trackVideo();
    });

    // Handle YouTube navigation
    document.addEventListener('yt-navigate-finish', updateAllThumbnails);
    document.addEventListener('yt-page-data-updated', updateAllThumbnails);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址