YouTube Universal Progress Tracker

Universal YouTube progress tracker with playlist support

目前為 2025-01-30 提交的版本,檢視 最新版本

// ==UserScript==
// @name         YouTube Universal Progress Tracker
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  Universal YouTube progress tracker with playlist support
// @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);
            // Handle different URL formats
            return url.searchParams.get('v') ||
                   url.pathname.split('/watch/')[1]?.split('?')[0] ||
                   url.pathname.split('/')[2];
        } catch {
            return null;
        }
    };

    // Comprehensive thumbnail selector
    const getThumbnails = () => {
        const selectors = [
            'ytd-thumbnail', // Default thumbnails
            'ytd-playlist-video-renderer #thumbnail', // Playlist items
            'ytd-compact-video-renderer #thumbnail', // Sidebar items
            'ytd-rich-item-renderer #thumbnail' // Homepage rich items
        ];
        return document.querySelectorAll(selectors.join(','));
    };

    // Progress indicator creation
    const createIndicator = () => {
        const indicator = document.createElement('div');
        Object.assign(indicator.style, {
            position: 'absolute',
            bottom: '4px',
            leftt: '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 = () => {
        getThumbnails().forEach(thumbnail => {
            const videoId = getVideoId(thumbnail);
            if (!videoId) return;

            const percentage = watchHistory[videoId] || 0;
            let indicator = thumbnail.querySelector('.yt-progress-indicator');

            if (percentage > 0) {
                if (!indicator) {
                    indicator = createIndicator();
                    thumbnail.style.position = 'relative';
                    thumbnail.appendChild(indicator);
                }
                indicator.textContent = percentage >= 100 ? '>100%' : `${Math.round(percentage)}%`;
                indicator.style.display = 'block';
            } else if (indicator) {
                indicator.style.display = 'none';
            }
        });
    };

    // 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
    const observer = new MutationObserver(mutations => {
        if (mutations.some(m => m.addedNodes.length)) {
            updateAllThumbnails();
            trackVideo();
        }
    });

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