WayBackTube

Travel back in time on YouTube; simulate what it was like in any date.

当前为 2025-07-17 提交的版本,查看 最新版本

// ==UserScript==
// @name         WayBackTube
// @namespace    http://tampermonkey.net/
// @license MIT
// @version      15.0
// @description  Travel back in time on YouTube; simulate what it was like in any date.
// @author       You
// @match        https://www.youtube.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @connect      youtube.com
// @connect      googleapis.com
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // === CONFIGURATION ===
    const CONFIG = {
        updateInterval: 300,
        debugMode: true,
        videosPerChannel: 20,
        maxHomepageVideos: 60,
        maxVideoPageVideos: 20,
        cacheExpiry: {
            videos: 2 * 60 * 60 * 1000, // 2 hours
            channelVideos: 1 * 60 * 60 * 1000, // 1 hour
            searchResults: 30 * 60 * 1000 // 30 minutes
        },
        maxConcurrentRequests: 2,
        batchSize: 3,
        apiCooldown: 200,
        autoLoadOnHomepage: true,
        autoLoadDelay: 1500,
        autoAdvanceDays: true // New feature
    };

    // === API MANAGER WITH UNLIMITED ROTATION ===
    class APIManager {
        constructor() {
            this.keys = GM_getValue('ytApiKeys', []);
            this.currentKeyIndex = GM_getValue('ytCurrentKeyIndex', 0);
            this.keyStats = GM_getValue('ytKeyStats', {});
            this.baseUrl = 'https://www.googleapis.com/youtube/v3';
            this.initializeKeys();
        }

        initializeKeys() {
            // Reset daily statistics
            const now = Date.now();
            const oneDayAgo = now - (24 * 60 * 60 * 1000);

            Object.keys(this.keyStats).forEach(key => {
                if (this.keyStats[key].lastFailed && this.keyStats[key].lastFailed < oneDayAgo) {
                    this.keyStats[key].failed = false;
                    this.keyStats[key].quotaExceeded = false;
                }
            });

            // Validate current key index
            if (this.currentKeyIndex >= this.keys.length) {
                this.currentKeyIndex = 0;
            }

            this.log(`🔑 API Manager initialized with ${this.keys.length} keys`);
        }

        get currentKey() {
            if (this.keys.length === 0) return null;
            return this.keys[this.currentKeyIndex];
        }

        addKey(apiKey) {
            if (!apiKey || apiKey.length < 35) return false;

            if (!this.keys.includes(apiKey)) {
                this.keys.push(apiKey);
                this.keyStats[apiKey] = {
                    failed: false,
                    quotaExceeded: false,
                    lastUsed: 0,
                    requestCount: 0,
                    successCount: 0
                };
                this.saveKeys();
                this.log(`✅ Added API key: ${apiKey.substring(0, 8)}...`);
                return true;
            }
            return false;
        }

        removeKey(apiKey) {
            const index = this.keys.indexOf(apiKey);
            if (index > -1) {
                this.keys.splice(index, 1);
                delete this.keyStats[apiKey];

                // Adjust current index
                if (this.currentKeyIndex >= this.keys.length) {
                    this.currentKeyIndex = Math.max(0, this.keys.length - 1);
                } else if (index <= this.currentKeyIndex && this.currentKeyIndex > 0) {
                    this.currentKeyIndex--;
                }

                this.saveKeys();
                this.log(`🗑️ Removed API key: ${apiKey.substring(0, 8)}...`);
                return true;
            }
            return false;
        }

        // UNLIMITED KEY ROTATION
        rotateToNextKey() {
            if (this.keys.length <= 1) return false;

            const startIndex = this.currentKeyIndex;
            let attempts = 0;

            // Try ALL keys, not just a limited number
            while (attempts < this.keys.length) {
                this.currentKeyIndex = (this.currentKeyIndex + 1) % this.keys.length;
                attempts++;

                const currentKey = this.currentKey;
                const stats = this.keyStats[currentKey];

                if (!stats || (!stats.quotaExceeded && !stats.failed)) {
                    this.saveKeys();
                    this.log(`🔄 Rotated to key ${this.currentKeyIndex + 1}/${this.keys.length}`);
                    return true;
                }
            }

            // If all keys are problematic, reset to first key and try anyway
            this.currentKeyIndex = 0;
            this.saveKeys();
            this.log('⚠️ All keys have issues, reset to first key');
            return false;
        }

        markKeySuccess(apiKey) {
            if (!this.keyStats[apiKey]) {
                this.keyStats[apiKey] = {};
            }

            this.keyStats[apiKey].lastUsed = Date.now();
            this.keyStats[apiKey].requestCount = (this.keyStats[apiKey].requestCount || 0) + 1;
            this.keyStats[apiKey].successCount = (this.keyStats[apiKey].successCount || 0) + 1;
            this.keyStats[apiKey].failed = false;

            this.saveKeys();
        }

        markKeyFailed(apiKey, errorMessage) {
            if (!this.keyStats[apiKey]) {
                this.keyStats[apiKey] = {};
            }

            this.keyStats[apiKey].failed = true;
            this.keyStats[apiKey].lastFailed = Date.now();

            // Check for quota errors
            const quotaErrors = ['quota', 'exceeded', 'dailyLimitExceeded', 'rateLimitExceeded'];
            if (quotaErrors.some(error => errorMessage.toLowerCase().includes(error))) {
                this.keyStats[apiKey].quotaExceeded = true;
            }

            this.saveKeys();
            this.log(`❌ Key failed: ${apiKey.substring(0, 8)}... - ${errorMessage}`);
        }

        // IMPROVED REQUEST SYSTEM
        async makeRequest(endpoint, params) {
            return new Promise((resolve, reject) => {
                if (this.keys.length === 0) {
                    reject(new Error('No API keys available'));
                    return;
                }

                const attemptRequest = (attemptCount = 0) => {
                    const maxAttempts = Math.min(this.keys.length, 10); // Reasonable limit for retries

                    if (attemptCount >= maxAttempts) {
                        reject(new Error('All API keys exhausted'));
                        return;
                    }

                    const currentKey = this.currentKey;
                    if (!currentKey) {
                        reject(new Error('No valid API key'));
                        return;
                    }

                    // Build request URL
                    const urlParams = new URLSearchParams({ ...params, key: currentKey });
                    const url = `${endpoint}?${urlParams.toString()}`;

                    this.log(`🌐 Request attempt ${attemptCount + 1} with key ${this.currentKeyIndex + 1}`);

                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: url,
                        timeout: 10000,
                        headers: {
                            'Accept': 'application/json'
                        },
                        onload: (response) => {
                            if (response.status === 200) {
                                try {
                                    const data = JSON.parse(response.responseText);
                                    this.markKeySuccess(currentKey);
                                    resolve(data);
                                } catch (e) {
                                    reject(new Error('Invalid JSON response'));
                                }
                            } else {
                                let errorMessage = `HTTP ${response.status}`;
                                try {
                                    const errorData = JSON.parse(response.responseText);
                                    if (errorData.error && errorData.error.message) {
                                        errorMessage = errorData.error.message;
                                    }
                                } catch (e) {
                                    // Use default error message
                                }

                                this.markKeyFailed(currentKey, errorMessage);

                                if (this.rotateToNextKey()) {
                                    setTimeout(() => attemptRequest(attemptCount + 1), 500);
                                } else {
                                    reject(new Error(`API Error: ${errorMessage}`));
                                }
                            }
                        },
                        onerror: () => {
                            this.markKeyFailed(currentKey, 'Network error');
                            if (this.rotateToNextKey()) {
                                setTimeout(() => attemptRequest(attemptCount + 1), 1000);
                            } else {
                                reject(new Error('Network error'));
                            }
                        },
                        ontimeout: () => {
                            this.markKeyFailed(currentKey, 'Request timeout');
                            if (this.rotateToNextKey()) {
                                setTimeout(() => attemptRequest(attemptCount + 1), 500);
                            } else {
                                reject(new Error('Request timeout'));
                            }
                        }
                    });
                };

                attemptRequest(0);
            });
        }

        // TEST ALL KEYS (not just 3)
        async testAllKeys() {
            const results = [];

            if (this.keys.length === 0) {
                return ['❌ No API keys configured'];
            }

            this.log(`🧪 Testing all ${this.keys.length} keys...`);

            // Test each key individually
            for (let i = 0; i < this.keys.length; i++) {
                const testKey = this.keys[i];

                try {
                    const testParams = {
                        part: 'snippet',
                        q: 'test',
                        maxResults: 1,
                        type: 'video',
                        key: testKey
                    };

                    const url = `${this.baseUrl}/search?${new URLSearchParams(testParams)}`;

                    const response = await new Promise((resolve, reject) => {
                        GM_xmlhttpRequest({
                            method: 'GET',
                            url: url,
                            timeout: 8000,
                            onload: resolve,
                            onerror: reject,
                            ontimeout: reject
                        });
                    });

                    if (response.status === 200) {
                        const data = JSON.parse(response.responseText);
                        if (data.items && data.items.length > 0) {
                            results.push(`✅ Key ${i + 1}: Working perfectly`);
                        } else {
                            results.push(`⚠️ Key ${i + 1}: Valid but no results`);
                        }
                    } else {
                        let errorMsg = `HTTP ${response.status}`;
                        try {
                            const errorData = JSON.parse(response.responseText);
                            if (errorData.error) {
                                errorMsg = errorData.error.message || errorMsg;
                            }
                        } catch (e) {
                            // Use default error
                        }
                        results.push(`❌ Key ${i + 1}: ${errorMsg}`);
                    }
                } catch (error) {
                    results.push(`❌ Key ${i + 1}: ${error.message || 'Network error'}`);
                }

                // Small delay between tests
                await new Promise(resolve => setTimeout(resolve, 100));
            }

            return results;
        }

        saveKeys() {
            GM_setValue('ytApiKeys', this.keys);
            GM_setValue('ytCurrentKeyIndex', this.currentKeyIndex);
            GM_setValue('ytKeyStats', this.keyStats);
        }

        getCache(key) {
            const cached = GM_getValue(`cache_${key}`, null);
            if (cached) {
                try {
                    const data = JSON.parse(cached);
                    if (Date.now() - data.timestamp < CONFIG.cacheExpiry.videos) {
                        return data.value;
                    }
                } catch (e) {
                    // Invalid cache entry
                }
            }
            return null;
        }

        setCache(key, value) {
            const cacheData = {
                timestamp: Date.now(),
                value: value
            };
            GM_setValue(`cache_${key}`, JSON.stringify(cacheData));
        }

        clearCache() {
            const keys = GM_listValues();
            keys.forEach(key => {
                if (key.startsWith('cache_')) {
                    GM_deleteValue(key);
                }
            });
        }

        log(...args) {
            if (CONFIG.debugMode) {
                console.log('[API Manager]', ...args);
            }
        }
    }

    // === SUBSCRIPTION MANAGER ===
    class SubscriptionManager {
        constructor() {
            this.subscriptions = GM_getValue('ytSubscriptions', []);
        }

        addSubscription(channelName, channelId = null) {
            if (!channelName || channelName.trim() === '') return false;

            const subscription = {
                name: channelName.trim(),
                id: channelId,
                addedAt: Date.now()
            };

            // Check for duplicates
            const exists = this.subscriptions.some(sub =>
                sub.name.toLowerCase() === subscription.name.toLowerCase() ||
                (channelId && sub.id === channelId)
            );

            if (!exists) {
                this.subscriptions.push(subscription);
                this.save();
                return true;
            }
            return false;
        }

        removeSubscription(index) {
            if (index >= 0 && index < this.subscriptions.length) {
                this.subscriptions.splice(index, 1);
                this.save();
                return true;
            }
            return false;
        }

        getSubscriptions() {
            return this.subscriptions;
        }

        save() {
            GM_setValue('ytSubscriptions', this.subscriptions);
        }
    }

    // === MAIN TIME MACHINE CLASS ===
    class YouTubeTimeMachine {
        constructor() {
            this.apiManager = new APIManager();
            this.subscriptionManager = new SubscriptionManager();

            this.settings = {
                date: GM_getValue('ytTimeMachineDate', '2014-06-14'),
                active: GM_getValue('ytTimeMachineActive', true),
                uiVisible: GM_getValue('ytTimeMachineUIVisible', true),
                lastAdvancedDate: GM_getValue('ytLastAdvancedDate', new Date().toDateString())
            };

            // Auto-advance date feature
            this.checkAndAdvanceDate();

            this.maxDate = new Date(this.settings.date);
            this.isProcessing = false;
            this.videoCache = new Map();

            this.stats = {
                filtered: 0,
                processed: 0,
                apiCalls: 0,
                cacheHits: 0
            };

            this.init();
        }

        // NEW: Auto-advance date feature
        checkAndAdvanceDate() {
            if (!CONFIG.autoAdvanceDays) return;

            const today = new Date().toDateString();
            const lastAdvanced = this.settings.lastAdvancedDate;

            if (today !== lastAdvanced) {
                // Advance the date by one day
                const currentDate = new Date(this.settings.date);
                currentDate.setDate(currentDate.getDate() + 1);

                // Don't advance beyond today
                if (currentDate <= new Date()) {
                    this.settings.date = currentDate.toISOString().split('T')[0];
                    this.settings.lastAdvancedDate = today;

                    GM_setValue('ytTimeMachineDate', this.settings.date);
                    GM_setValue('ytLastAdvancedDate', today);

                    // Clear cache to force reload of new videos
                    this.apiManager.clearCache();

                    this.log('📅 Date auto-advanced to:', this.settings.date);
                }
            }
        }

        // NEW: Generate realistic view count based on video age
        generateViewCount(publishedAt, referenceDate) {
            const videoDate = new Date(publishedAt);
            const refDate = new Date(referenceDate);
            const daysSinceUpload = Math.floor((refDate - videoDate) / (1000 * 60 * 60 * 24));

            // Base view count ranges based on days since upload
            let minViews, maxViews;

            if (daysSinceUpload <= 1) {
                minViews = 100;
                maxViews = 50000;
            } else if (daysSinceUpload <= 7) {
                minViews = 1000;
                maxViews = 500000;
            } else if (daysSinceUpload <= 30) {
                minViews = 5000;
                maxViews = 2000000;
            } else if (daysSinceUpload <= 365) {
                minViews = 10000;
                maxViews = 10000000;
            } else {
                minViews = 50000;
                maxViews = 50000000;
            }

            // Random multiplier for variety
            const multiplier = Math.random() * 0.8 + 0.2; // 0.2 to 1.0
            const viewCount = Math.floor(minViews + (maxViews - minViews) * multiplier);

            return this.formatViewCount(viewCount);
        }

        // NEW: Format view count (1.2M, 543K, etc.)
        formatViewCount(count) {
            if (count >= 1000000) {
                return (count / 1000000).toFixed(1) + 'M';
            } else if (count >= 1000) {
                return (count / 1000).toFixed(1) + 'K';
            }
            return count.toString();
        }

        // NEW: Format relative date (like YouTube does)
        formatRelativeDate(publishedAt, referenceDate) {
            const videoDate = new Date(publishedAt);
            const refDate = new Date(referenceDate);
            const diffMs = refDate - videoDate;
            const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));

            if (diffDays === 0) {
                const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
                if (diffHours === 0) {
                    const diffMinutes = Math.floor(diffMs / (1000 * 60));
                    return diffMinutes <= 1 ? '1 minute ago' : `${diffMinutes} minutes ago`;
                }
                return diffHours === 1 ? '1 hour ago' : `${diffHours} hours ago`;
            } else if (diffDays === 1) {
                return '1 day ago';
            } else if (diffDays < 7) {
                return `${diffDays} days ago`;
            } else if (diffDays < 30) {
                const weeks = Math.floor(diffDays / 7);
                return weeks === 1 ? '1 week ago' : `${weeks} weeks ago`;
            } else if (diffDays < 365) {
                const months = Math.floor(diffDays / 30);
                return months === 1 ? '1 month ago' : `${months} months ago`;
            } else {
                const years = Math.floor(diffDays / 365);
                return years === 1 ? '1 year ago' : `${years} years ago`;
            }
        }

        init() {
            this.log('🕰️ YouTube Time Machine initializing...');

            // Add styles
            this.addStyles();

            // Setup UI
            this.setupUI();

            // Start filtering if active
            if (this.settings.active) {
                this.startFiltering();
                this.setupSearchInterception();
                this.startHomepageReplacement();
                this.startVideoPageEnhancement(); // NEW
            }

            this.log('✅ YouTube Time Machine ready!');
        }

        addStyles() {
            GM_addStyle(`
                /* Hide Shorts completely */
                ytd-rich-shelf-renderer[is-shorts],
                ytd-reel-shelf-renderer,
                ytd-shorts,
                [overlay-style="SHORTS"],
                ytd-thumbnail-overlay-time-status-renderer[overlay-style="SHORTS"],
                ytd-video-renderer:has([overlay-style="SHORTS"]),
                ytd-grid-video-renderer:has([overlay-style="SHORTS"]),
                ytd-rich-item-renderer:has([overlay-style="SHORTS"]),
                ytd-compact-video-renderer:has([overlay-style="SHORTS"]),
                #shorts-container,
                ytd-guide-entry-renderer a[title="Shorts"],
                ytd-mini-guide-entry-renderer[aria-label="Shorts"],
                a[href="/shorts"],
                [href*="/shorts/"],
                ytd-thumbnail[href*="/shorts/"] {
                    display: none !important;
                }

                .yt-time-machine-hidden {
                    display: none !important;
                }

                /* UI Styles */
                #timeMachineUI {
                    position: fixed;
                    top: 80px;
                    right: 20px;
                    width: 400px;
                    max-height: 80vh;
                    overflow-y: auto;
                    background: linear-gradient(135deg, #0f0f0f, #1a1a1a);
                    border: 1px solid #333;
                    border-radius: 12px;
                    padding: 20px;
                    color: white;
                    font-family: Roboto, Arial, sans-serif;
                    z-index: 999999;
                    box-shadow: 0 8px 32px rgba(0,0,0,0.8);
                    backdrop-filter: blur(10px);
                }

                #timeMachineUI.hidden {
                    display: none;
                }

                #timeMachineToggle {
                    position: fixed;
                    top: 80px;
                    right: 20px;
                    width: 50px;
                    height: 50px;
                    background: #ff0000;
                    border: none;
                    border-radius: 50%;
                    color: white;
                    font-size: 24px;
                    cursor: pointer;
                    z-index: 999998;
                    box-shadow: 0 4px 16px rgba(255,0,0,0.4);
                    display: none;
                }

                #timeMachineToggle.visible {
                    display: block;
                }

                .tm-section {
                    margin-bottom: 20px;
                    padding: 15px;
                    background: rgba(255,255,255,0.05);
                    border-radius: 8px;
                    border-left: 3px solid #ff0000;
                }

                .tm-section h3 {
                    margin: 0 0 10px 0;
                    font-size: 14px;
                    color: #ff6b6b;
                }

                .tm-input-group {
                    display: flex;
                    gap: 8px;
                    margin-bottom: 10px;
                }

                .tm-input {
                    flex: 1;
                    padding: 8px;
                    background: rgba(255,255,255,0.1);
                    border: 1px solid #333;
                    border-radius: 4px;
                    color: white;
                    font-size: 12px;
                }

                .tm-button {
                    padding: 8px 12px;
                    background: #ff0000;
                    border: none;
                    border-radius: 4px;
                    color: white;
                    cursor: pointer;
                    font-size: 12px;
                    transition: background 0.2s;
                }

                .tm-button:hover {
                    background: #cc0000;
                }

                .tm-button:disabled {
                    background: #666;
                    cursor: not-allowed;
                }

                .tm-list {
                    max-height: 150px;
                    overflow-y: auto;
                    font-size: 12px;
                }

                .tm-list-item {
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    padding: 6px 0;
                    border-bottom: 1px solid rgba(255,255,255,0.1);
                }

                .tm-remove-btn {
                    background: #444;
                    border: none;
                    color: white;
                    padding: 2px 6px;
                    border-radius: 3px;
                    cursor: pointer;
                    font-size: 10px;
                }

                .tm-stats {
                    display: grid;
                    grid-template-columns: 1fr 1fr;
                    gap: 10px;
                    font-size: 12px;
                }

                .tm-stat {
                    background: rgba(255,255,255,0.05);
                    padding: 8px;
                    border-radius: 4px;
                    text-align: center;
                }

                .tm-stat-value {
                    font-size: 16px;
                    font-weight: bold;
                    color: #ff6b6b;
                }

                /* Homepage replacement - 3 columns */
                .tm-homepage {
                    padding: 20px;
                    max-width: 1200px;
                    margin: 0 auto;
                }

                .tm-video-grid {
                    display: grid;
                    grid-template-columns: repeat(3, 1fr);
                    gap: 20px;
                    margin-top: 20px;
                }

                .tm-video-card {
                    background: var(--yt-spec-raised-background);
                    border-radius: 8px;
                    overflow: hidden;
                    cursor: pointer;
                    transition: transform 0.2s;
                }

                .tm-video-card:hover {
                    transform: translateY(-4px);
                }

                .tm-video-thumbnail {
                    width: 100%;
                    aspect-ratio: 16/9;
                    object-fit: cover;
                    position: relative;
                }

                .tm-video-info {
                    padding: 12px;
                }

                .tm-video-title {
                    font-size: 14px;
                    font-weight: 500;
                    line-height: 1.3;
                    margin-bottom: 8px;
                    color: var(--yt-spec-text-primary);
                    display: -webkit-box;
                    -webkit-line-clamp: 2;
                    -webkit-box-orient: vertical;
                    overflow: hidden;
                }

                .tm-video-channel {
                    font-size: 12px;
                    color: var(--yt-spec-text-secondary);
                    margin-bottom: 4px;
                }

                .tm-video-meta {
                    display: flex;
                    justify-content: space-between;
                    font-size: 11px;
                    color: var(--yt-spec-text-secondary);
                }

                .tm-video-views {
                    font-weight: 500;
                }

                .tm-video-date {
                    font-size: 11px;
                    color: var(--yt-spec-text-secondary);
                }

                /* Video page enhancement */
                .tm-video-page-section {
                    margin-top: 20px;
                    padding: 16px;
                    background: var(--yt-spec-raised-background);
                    border-radius: 8px;
                }

                .tm-video-page-title {
                    font-size: 16px;
                    font-weight: 500;
                    margin-bottom: 16px;
                    color: var(--yt-spec-text-primary);
                }

                .tm-video-page-grid {
                    display: grid;
                    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
                    gap: 16px;
                }

                .tm-video-page-card {
                    display: flex;
                    gap: 12px;
                    cursor: pointer;
                    transition: background 0.2s;
                    padding: 8px;
                    border-radius: 8px;
                }

                .tm-video-page-card:hover {
                    background: rgba(255,255,255,0.05);
                }

                .tm-video-page-thumbnail {
                    width: 168px;
                    height: 94px;
                    object-fit: cover;
                    border-radius: 4px;
                    flex-shrink: 0;
                }

                .tm-video-page-info {
                    flex: 1;
                    display: flex;
                    flex-direction: column;
                    justify-content: space-between;
                }

                .tm-video-page-video-title {
                    font-size: 14px;
                    font-weight: 500;
                    line-height: 1.3;
                    color: var(--yt-spec-text-primary);
                    margin-bottom: 4px;
                    display: -webkit-box;
                    -webkit-line-clamp: 2;
                    -webkit-box-orient: vertical;
                    overflow: hidden;
                }

                .tm-video-page-channel {
                    font-size: 12px;
                    color: var(--yt-spec-text-secondary);
                    margin-bottom: 4px;
                }

                .tm-video-page-meta {
                    display: flex;
                    gap: 8px;
                    font-size: 11px;
                    color: var(--yt-spec-text-secondary);
                }

                .tm-loading {
                    text-align: center;
                    padding: 40px;
                    color: var(--yt-spec-text-secondary);
                }

                .tm-spinner {
                    border: 2px solid #333;
                    border-top: 2px solid #ff0000;
                    border-radius: 50%;
                    width: 30px;
                    height: 30px;
                    animation: spin 1s linear infinite;
                    margin: 0 auto 16px;
                }

                @keyframes spin {
                    0% { transform: rotate(0deg); }
                    100% { transform: rotate(360deg); }
                }

                /* Responsive design */
                @media (max-width: 1200px) {
                    .tm-video-grid {
                        grid-template-columns: repeat(2, 1fr);
                    }
                }

                @media (max-width: 768px) {
                    .tm-video-grid {
                        grid-template-columns: 1fr;
                    }

                    .tm-video-page-grid {
                        grid-template-columns: 1fr;
                    }
                }
            `);
        }

        setupUI() {
            // Create main UI
            const ui = document.createElement('div');
            ui.id = 'timeMachineUI';
            if (!this.settings.uiVisible) {
                ui.classList.add('hidden');
            }

            ui.innerHTML = this.getUIHTML();

            // Create toggle button
            const toggle = document.createElement('button');
            toggle.id = 'timeMachineToggle';
            toggle.innerHTML = '🕰️';
            toggle.title = 'Show Time Machine';
            if (this.settings.uiVisible) {
                toggle.classList.remove('visible');
            } else {
                toggle.classList.add('visible');
            }

            // Add to page
            const addToPage = () => {
                if (document.body) {
                    document.body.appendChild(ui);
                    document.body.appendChild(toggle);
                    this.attachEventListeners();
                } else {
                    setTimeout(addToPage, 100);
                }
            };

            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', addToPage);
            } else {
                addToPage();
            }
        }

        getUIHTML() {
            const subscriptions = this.subscriptionManager.getSubscriptions();

            return `
                <div style="display: flex; justify-content: between; align-items: center; margin-bottom: 15px;">
                    <h2 style="margin: 0; font-size: 16px; color: #ff6b6b;">🕰️ Time Machine</h2>
                    <button id="tmHideBtn" style="background: none; border: none; color: white; cursor: pointer; font-size: 18px;">×</button>
                </div>

                <div class="tm-section">
                    <h3>📅 Target Date</h3>
                    <div class="tm-input-group">
                        <input type="date" id="tmDateInput" class="tm-input" value="${this.settings.date}" max="${new Date().toISOString().split('T')[0]}">
                        <button id="tmSetDate" class="tm-button">Set Date</button>
                    </div>
                    <div style="font-size: 11px; color: #aaa; margin-top: 5px;">
                        Currently traveling to: ${this.maxDate.toLocaleDateString()}
                        ${CONFIG.autoAdvanceDays ? '(Auto-advancing daily)' : ''}
                    </div>
                </div>

                <div class="tm-section">
                    <h3>🔑 API Keys (${this.apiManager.keys.length})</h3>
                    <div class="tm-input-group">
                        <input type="password" id="tmApiInput" class="tm-input" placeholder="Enter YouTube API key">
                        <button id="tmAddApi" class="tm-button">Add</button>
                    </div>
                    <div class="tm-list" id="tmApiList">${this.getApiKeyListHTML()}</div>
                    <div style="margin-top: 8px;">
                        <button id="tmTestAll" class="tm-button" style="width: 100%;">Test All Keys</button>
                    </div>
                </div>

                <div class="tm-section">
                    <h3>📺 Subscriptions (${subscriptions.length})</h3>
                    <div class="tm-input-group">
                        <input type="text" id="tmSubInput" class="tm-input" placeholder="Enter channel name">
                        <button id="tmAddSub" class="tm-button">Add</button>
                    </div>
                    <div class="tm-list" id="tmSubList">${this.getSubscriptionListHTML()}</div>
                    <div style="margin-top: 8px;">
                        <button id="tmLoadVideos" class="tm-button" style="width: 100%;">Load Videos</button>
                    </div>
                </div>

                <div class="tm-section">
                    <h3>📊 Statistics</h3>
                    <div class="tm-stats">
                        <div class="tm-stat">
                            <div class="tm-stat-value">${this.stats.processed}</div>
                            <div>Processed</div>
                        </div>
                        <div class="tm-stat">
                            <div class="tm-stat-value">${this.stats.filtered}</div>
                            <div>Filtered</div>
                        </div>
                        <div class="tm-stat">
                            <div class="tm-stat-value">${this.stats.apiCalls}</div>
                            <div>API Calls</div>
                        </div>
                        <div class="tm-stat">
                            <div class="tm-stat-value">${this.stats.cacheHits}</div>
                            <div>Cache Hits</div>
                        </div>
                    </div>
                </div>

                <div class="tm-section">
                    <h3>⚙️ Controls</h3>
                    <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
                        <button id="tmToggle" class="tm-button">${this.settings.active ? 'Disable' : 'Enable'}</button>
                        <button id="tmClearCache" class="tm-button">Clear Cache</button>
                    </div>
                </div>

                <div style="font-size: 10px; color: #666; text-align: center; margin-top: 15px;">
                    Press Ctrl+Shift+T to toggle UI
                </div>
            `;
        }

        getApiKeyListHTML() {
            if (this.apiManager.keys.length === 0) {
                return '<div style="text-align: center; color: #666; font-style: italic;">No API keys added</div>';
            }

            return this.apiManager.keys.map((key, index) => {
                const stats = this.apiManager.keyStats[key] || {};
                const isCurrent = index === this.apiManager.currentKeyIndex;
                let status = 'Unused';
                let statusColor = '#666';

                if (isCurrent) {
                    status = 'Active';
                    statusColor = '#4caf50';
                } else if (stats.quotaExceeded) {
                    status = 'Quota Exceeded';
                    statusColor = '#ff9800';
                } else if (stats.failed) {
                    status = 'Failed';
                    statusColor = '#f44336';
                } else if (stats.successCount > 0) {
                    status = 'Standby';
                    statusColor = '#2196f3';
                }

                return `
                    <div class="tm-list-item">
                        <div>
                            <div style="font-weight: bold;">Key ${index + 1}: ${key.substring(0, 8)}...</div>
                            <div style="font-size: 10px; color: ${statusColor};">${status} (${stats.successCount || 0} requests)</div>
                        </div>
                        <button class="tm-remove-btn" onclick="timeMachine.removeApiKey(${index})">Remove</button>
                    </div>
                `;
            }).join('');
        }

        getSubscriptionListHTML() {
            const subscriptions = this.subscriptionManager.getSubscriptions();

            if (subscriptions.length === 0) {
                return '<div style="text-align: center; color: #666; font-style: italic;">No subscriptions added</div>';
            }

            return subscriptions.map((sub, index) => `
                <div class="tm-list-item">
                    <div>
                        <div style="font-weight: bold;">${sub.name}</div>
                        <div style="font-size: 10px; color: #666;">Added ${new Date(sub.addedAt).toLocaleDateString()}</div>
                    </div>
                    <button class="tm-remove-btn" onclick="timeMachine.removeSubscription(${index})">Remove</button>
                </div>
            `).join('');
        }

        attachEventListeners() {
            // Make globally accessible
            window.timeMachine = this;

            // Hide button
            document.getElementById('tmHideBtn').addEventListener('click', () => {
                this.toggleUI();
            });

            // Show button
            document.getElementById('timeMachineToggle').addEventListener('click', () => {
                this.toggleUI();
            });

            // Date setting
            document.getElementById('tmSetDate').addEventListener('click', () => {
                const newDate = document.getElementById('tmDateInput').value;
                if (newDate) {
                    this.settings.date = newDate;
                    this.maxDate = new Date(newDate);
                    GM_setValue('ytTimeMachineDate', newDate);
                    this.apiManager.clearCache();
                    this.updateUI();
                    this.log('📅 Date updated to:', newDate);
                }
            });

            // API key management
            document.getElementById('tmAddApi').addEventListener('click', () => {
                const key = document.getElementById('tmApiInput').value.trim();
                if (this.apiManager.addKey(key)) {
                    document.getElementById('tmApiInput').value = '';
                    this.updateUI();
                }
            });

            document.getElementById('tmTestAll').addEventListener('click', async () => {
                const btn = document.getElementById('tmTestAll');
                btn.disabled = true;
                btn.textContent = 'Testing...';

                try {
                    const results = await this.apiManager.testAllKeys();
                    alert(results.join('\n'));
                } finally {
                    btn.disabled = false;
                    btn.textContent = 'Test All Keys';
                }
            });

            // Subscription management
            document.getElementById('tmAddSub').addEventListener('click', () => {
                const name = document.getElementById('tmSubInput').value.trim();
                if (this.subscriptionManager.addSubscription(name)) {
                    document.getElementById('tmSubInput').value = '';
                    this.updateUI();
                }
            });

            document.getElementById('tmLoadVideos').addEventListener('click', async () => {
                const btn = document.getElementById('tmLoadVideos');
                btn.disabled = true;
                btn.textContent = 'Loading...';

                try {
                    await this.loadVideosFromSubscriptions();
                    btn.textContent = 'Videos Loaded!';
                } catch (error) {
                    btn.textContent = 'Load Failed';
                    this.log('❌ Failed to load videos:', error);
                } finally {
                    setTimeout(() => {
                        btn.disabled = false;
                        btn.textContent = 'Load Videos';
                    }, 2000);
                }
            });

            // Controls
            document.getElementById('tmToggle').addEventListener('click', () => {
                this.settings.active = !this.settings.active;
                GM_setValue('ytTimeMachineActive', this.settings.active);
                location.reload();
            });

            document.getElementById('tmClearCache').addEventListener('click', () => {
                this.apiManager.clearCache();
                this.videoCache.clear();
                this.updateUI();
            });

            // Keyboard shortcuts
            document.addEventListener('keydown', (e) => {
                if (e.ctrlKey && e.shiftKey && e.key === 'T') {
                    e.preventDefault();
                    this.toggleUI();
                }
            });
        }

        toggleUI() {
            this.settings.uiVisible = !this.settings.uiVisible;
            GM_setValue('ytTimeMachineUIVisible', this.settings.uiVisible);

            const ui = document.getElementById('timeMachineUI');
            const toggle = document.getElementById('timeMachineToggle');

            if (this.settings.uiVisible) {
                ui.classList.remove('hidden');
                toggle.classList.remove('visible');
            } else {
                ui.classList.add('hidden');
                toggle.classList.add('visible');
            }
        }

        updateUI() {
            const ui = document.getElementById('timeMachineUI');
            if (ui) {
                ui.innerHTML = this.getUIHTML();
                this.attachEventListeners();
            }
        }

        removeApiKey(index) {
            if (this.apiManager.removeKey(this.apiManager.keys[index])) {
                this.updateUI();
            }
        }

        removeSubscription(index) {
            if (this.subscriptionManager.removeSubscription(index)) {
                this.updateUI();
            }
        }

        // VIDEO LOADING - ONLY PUBLIC ENDPOINTS
        async loadVideosFromSubscriptions() {
            const subscriptions = this.subscriptionManager.getSubscriptions();

            if (subscriptions.length === 0) {
                throw new Error('No subscriptions to load from');
            }

            this.log('📺 Loading videos from subscriptions...');

            const allVideos = [];
            const endDate = new Date(this.maxDate);
            endDate.setHours(23, 59, 59, 999);

            // Process subscriptions in batches
            for (let i = 0; i < subscriptions.length; i += CONFIG.batchSize) {
                const batch = subscriptions.slice(i, i + CONFIG.batchSize);

                const batchPromises = batch.map(async (sub) => {
                    try {
                        // First, try to get channel ID if we don't have it
                        let channelId = sub.id;
                        if (!channelId) {
                            channelId = await this.getChannelIdByName(sub.name);
                        }

                        if (channelId) {
                            const videos = await this.getChannelVideos(channelId, sub.name, endDate);
                            return videos;
                        }
                        return [];
                    } catch (error) {
                        this.log(`❌ Failed to load videos for ${sub.name}:`, error);
                        return [];
                    }
                });

                const batchResults = await Promise.all(batchPromises);
                batchResults.forEach(videos => allVideos.push(...videos));

                // Small delay between batches
                if (i + CONFIG.batchSize < subscriptions.length) {
                    await new Promise(resolve => setTimeout(resolve, CONFIG.apiCooldown));
                }
            }

            // Cache the results
            this.videoCache.set('subscription_videos', allVideos);
            this.log(`✅ Loaded ${allVideos.length} videos from subscriptions`);

            return allVideos;
        }

        async getChannelIdByName(channelName) {
            const cacheKey = `channel_id_${channelName}`;
            let channelId = this.apiManager.getCache(cacheKey);

            if (!channelId) {
                try {
                    const response = await this.apiManager.makeRequest(`${this.apiManager.baseUrl}/search`, {
                        part: 'snippet',
                        q: channelName,
                        type: 'channel',
                        maxResults: 1
                    });

                    if (response.items && response.items.length > 0) {
                        channelId = response.items[0].snippet.channelId;
                        this.apiManager.setCache(cacheKey, channelId);
                        this.stats.apiCalls++;
                    }
                } catch (error) {
                    this.log(`❌ Failed to find channel ID for ${channelName}:`, error);
                }
            } else {
                this.stats.cacheHits++;
            }

            return channelId;
        }

        async getChannelVideos(channelId, channelName, endDate) {
            const cacheKey = `channel_videos_${channelId}_${this.settings.date}`;
            let videos = this.apiManager.getCache(cacheKey);

            if (!videos) {
                try {
                    const response = await this.apiManager.makeRequest(`${this.apiManager.baseUrl}/search`, {
                        part: 'snippet',
                        channelId: channelId,
                        type: 'video',
                        order: 'date',
                        publishedBefore: endDate.toISOString(),
                        maxResults: CONFIG.videosPerChannel
                    });

                    videos = response.items ? response.items.map(item => ({
                        id: item.id.videoId,
                        title: item.snippet.title,
                        channel: item.snippet.channelTitle || channelName,
                        channelId: item.snippet.channelId,
                        thumbnail: item.snippet.thumbnails?.medium?.url || item.snippet.thumbnails?.default?.url,
                        publishedAt: item.snippet.publishedAt,
                        description: item.snippet.description || '',
                        viewCount: this.generateViewCount(item.snippet.publishedAt, endDate),
                        relativeDate: this.formatRelativeDate(item.snippet.publishedAt, endDate)
                    })) : [];

                    this.apiManager.setCache(cacheKey, videos);
                    this.stats.apiCalls++;
                } catch (error) {
                    this.log(`❌ Failed to get videos for channel ${channelName}:`, error);
                    videos = [];
                }
            } else {
                this.stats.cacheHits++;
            }

            return videos;
        }

        // FILTERING SYSTEM
        startFiltering() {
            this.log('🔥 Starting video filtering...');

            const filterVideos = () => {
                if (this.isProcessing) return;
                this.isProcessing = true;

                const videoSelectors = [
                    'ytd-video-renderer',
                    'ytd-grid-video-renderer',
                    'ytd-rich-item-renderer',
                    'ytd-compact-video-renderer'
                ];

                let processed = 0;
                let filtered = 0;

                videoSelectors.forEach(selector => {
                    const videos = document.querySelectorAll(selector);
                    videos.forEach(video => {
                        if (video.dataset.tmProcessed) return;

                        video.dataset.tmProcessed = 'true';
                        processed++;

                        if (this.shouldHideVideo(video)) {
                            video.classList.add('yt-time-machine-hidden');
                            filtered++;
                        }
                    });
                });

                if (processed > 0) {
                    this.stats.processed += processed;
                    this.stats.filtered += filtered;
                    this.log(`🔥 Processed ${processed}, filtered ${filtered}`);
                }

                this.isProcessing = false;
            };

            // Initial filter
            filterVideos();

            // Periodic filtering
            setInterval(filterVideos, CONFIG.updateInterval);

            // Observer for dynamic content
            const observer = new MutationObserver(() => {
                setTimeout(filterVideos, 100);
            });

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

        shouldHideVideo(videoElement) {
            // Check for Shorts
            if (this.isShorts(videoElement)) {
                return true;
            }

            // Check video date
            const videoDate = this.extractVideoDate(videoElement);
            if (videoDate && videoDate > this.maxDate) {
                return true;
            }

            return false;
        }

        isShorts(videoElement) {
            const shortsIndicators = [
                '[overlay-style="SHORTS"]',
                '[href*="/shorts/"]',
                '.shorts-thumbnail-overlay'
            ];

            return shortsIndicators.some(selector =>
                videoElement.querySelector(selector) ||
                videoElement.matches(selector)
            );
        }

        extractVideoDate(videoElement) {
            const dateSelectors = [
                '#metadata-line span:last-child',
                '.ytd-video-meta-block span:last-child',
                '#published-time-text',
                '[aria-label*="ago"]'
            ];

            for (const selector of dateSelectors) {
                const element = videoElement.querySelector(selector);
                if (element) {
                    const dateText = element.textContent?.trim();
                    if (dateText) {
                        return this.parseRelativeDate(dateText);
                    }
                }
            }

            return null;
        }

        parseRelativeDate(dateText) {
            if (!dateText || !dateText.includes('ago')) return null;

            const now = new Date();
            const text = dateText.toLowerCase();

            const patterns = [
                { regex: /(\d+)\s*second/i, multiplier: 1000 },
                { regex: /(\d+)\s*minute/i, multiplier: 60 * 1000 },
                { regex: /(\d+)\s*hour/i, multiplier: 60 * 60 * 1000 },
                { regex: /(\d+)\s*day/i, multiplier: 24 * 60 * 60 * 1000 },
                { regex: /(\d+)\s*week/i, multiplier: 7 * 24 * 60 * 60 * 1000 },
                { regex: /(\d+)\s*month/i, multiplier: 30 * 24 * 60 * 60 * 1000 },
                { regex: /(\d+)\s*year/i, multiplier: 365 * 24 * 60 * 60 * 1000 }
            ];

            for (const pattern of patterns) {
                const match = text.match(pattern.regex);
                if (match) {
                    const amount = parseInt(match[1]);
                    if (amount > 0) {
                        return new Date(now.getTime() - (amount * pattern.multiplier));
                    }
                }
            }

            return null;
        }

        // SEARCH INTERCEPTION
        setupSearchInterception() {
            const interceptSearch = () => {
                const searchInputs = document.querySelectorAll('input#search');
                searchInputs.forEach(input => {
                    if (!input.dataset.tmIntercepted) {
                        input.dataset.tmIntercepted = 'true';

                        input.addEventListener('keydown', (e) => {
                            if (e.key === 'Enter' && input.value.trim()) {
                                if (!input.value.includes('before:')) {
                                    e.preventDefault();
                                    input.value += ` before:${this.settings.date}`;

                                    setTimeout(() => {
                                        const searchBtn = document.querySelector('#search-icon-legacy button');
                                        if (searchBtn) searchBtn.click();
                                    }, 50);
                                }
                            }
                        });
                    }
                });
            };

            setInterval(interceptSearch, 1000);
        }

        // HOMEPAGE REPLACEMENT
        startHomepageReplacement() {
            const replaceHomepage = () => {
                if (this.isHomePage()) {
                    const container = document.querySelector('ytd-browse[page-subtype="home"] ytd-rich-grid-renderer');
                    if (container && !container.dataset.tmReplaced) {
                        container.dataset.tmReplaced = 'true';
                        this.replaceHomepage(container);
                    }
                }
            };

            setInterval(replaceHomepage, 1000);
        }

        isHomePage() {
            return location.pathname === '/' || location.pathname === '';
        }

        async replaceHomepage(container) {
            this.log('🏠 Replacing homepage...');

            container.innerHTML = `
                <div class="tm-homepage">
                    <div class="tm-loading">
                        <div class="tm-spinner"></div>
                        <div>Loading your time capsule from ${this.maxDate.toLocaleDateString()}...</div>
                    </div>
                </div>
            `;

            try {
                let videos = this.videoCache.get('subscription_videos');
                if (!videos || videos.length === 0) {
                    videos = await this.loadVideosFromSubscriptions();
                }

                if (videos.length > 0) {
                    const homepageHTML = this.createHomepageHTML(videos);
                    container.innerHTML = homepageHTML;
                    this.attachVideoClickHandlers();
                } else {
                    container.innerHTML = `
                        <div class="tm-homepage">
                            <div class="tm-loading">
                                <div>No videos found from ${this.maxDate.toLocaleDateString()}</div>
                                <div style="margin-top: 10px; font-size: 12px; opacity: 0.7;">
                                    Add subscriptions and API keys to see content
                                </div>
                            </div>
                        </div>
                    `;
                }
            } catch (error) {
                this.log('❌ Homepage replacement failed:', error);
                container.innerHTML = `
                    <div class="tm-homepage">
                        <div class="tm-loading">
                            <div>Failed to load time capsule</div>
                            <div style="margin-top: 10px; font-size: 12px; opacity: 0.7;">
                                ${error.message}
                            </div>
                        </div>
                    </div>
                `;
            }
        }

        createHomepageHTML(videos) {
            const sortedVideos = videos
                .filter(video => new Date(video.publishedAt) <= this.maxDate)
                .sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt))
                .slice(0, CONFIG.maxHomepageVideos);

            const videoCards = sortedVideos.map(video => `
                <div class="tm-video-card" data-video-id="${video.id}">
                    <img class="tm-video-thumbnail" src="${video.thumbnail}" alt="${video.title}" loading="lazy">
                    <div class="tm-video-info">
                        <div class="tm-video-title">${video.title}</div>
                        <div class="tm-video-channel">${video.channel}</div>
                        <div class="tm-video-meta">
                            <span class="tm-video-views">${video.viewCount} views</span>
                            <span class="tm-video-date">${video.relativeDate}</span>
                        </div>
                    </div>
                </div>
            `).join('');

            return `
                <div class="tm-homepage">
                    <h2 style="margin-bottom: 10px; color: var(--yt-spec-text-primary);">
                        🕰️ Your Time Capsule from ${this.maxDate.toLocaleDateString()}
                    </h2>
                    <div style="font-size: 14px; color: var(--yt-spec-text-secondary); margin-bottom: 20px;">
                        ${sortedVideos.length} videos from your subscriptions
                    </div>
                    <div class="tm-video-grid">
                        ${videoCards}
                    </div>
                </div>
            `;
        }

        attachVideoClickHandlers() {
            document.querySelectorAll('.tm-video-card').forEach(card => {
                card.addEventListener('click', () => {
                    const videoId = card.dataset.videoId;
                    if (videoId) {
                        window.location.href = `/watch?v=${videoId}`;
                    }
                });
            });
        }

        // NEW: Video page enhancement
        startVideoPageEnhancement() {
            const enhanceVideoPage = () => {
                if (this.isVideoPage()) {
                    const sidebar = document.querySelector('#secondary');
                    if (sidebar && !sidebar.dataset.tmEnhanced) {
                        sidebar.dataset.tmEnhanced = 'true';
                        this.enhanceVideoPage(sidebar);
                    }
                }
            };

            setInterval(enhanceVideoPage, 2000);
        }

        isVideoPage() {
            return location.pathname === '/watch';
        }

        async enhanceVideoPage(sidebar) {
            this.log('📺 Enhancing video page...');

            const currentChannelId = this.getCurrentChannelId();

            try {
                let videos = this.videoCache.get('subscription_videos');
                if (!videos || videos.length === 0) {
                    videos = await this.loadVideosFromSubscriptions();
                }

                if (videos.length > 0) {
                    // Separate videos by channel
                    const currentChannelVideos = videos.filter(v => v.channelId === currentChannelId);
                    const otherVideos = videos.filter(v => v.channelId !== currentChannelId);

                    // Prioritize current channel, then mix in others
                    const prioritizedVideos = [
                        ...currentChannelVideos.slice(0, Math.floor(CONFIG.maxVideoPageVideos * 0.6)),
                        ...otherVideos.slice(0, Math.floor(CONFIG.maxVideoPageVideos * 0.4))
                    ].sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt));

                    const videoPageHTML = this.createVideoPageHTML(prioritizedVideos);

                    // Insert after existing related videos
                    const relatedContainer = sidebar.querySelector('#related');
                    if (relatedContainer) {
                        const tmSection = document.createElement('div');
                        tmSection.innerHTML = videoPageHTML;
                        relatedContainer.parentNode.insertBefore(tmSection, relatedContainer.nextSibling);
                        this.attachVideoPageClickHandlers();
                    }
                }
            } catch (error) {
                this.log('❌ Video page enhancement failed:', error);
            }
        }

        getCurrentChannelId() {
            const channelLink = document.querySelector('ytd-video-owner-renderer a');
            if (channelLink) {
                const href = channelLink.getAttribute('href');
                if (href) {
                    const match = href.match(/\/(channel|c|user)\/([^\/]+)/);
                    if (match) {
                        return match[2];
                    }
                }
            }
            return null;
        }

        createVideoPageHTML(videos) {
            const videoCards = videos.slice(0, CONFIG.maxVideoPageVideos).map(video => `
                <div class="tm-video-page-card" data-video-id="${video.id}">
                    <img class="tm-video-page-thumbnail" src="${video.thumbnail}" alt="${video.title}" loading="lazy">
                    <div class="tm-video-page-info">
                        <div class="tm-video-page-video-title">${video.title}</div>
                        <div class="tm-video-page-channel">${video.channel}</div>
                        <div class="tm-video-page-meta">
                            <span class="tm-video-views">${video.viewCount} views</span>
                            <span>•</span>
                            <span class="tm-video-date">${video.relativeDate}</span>
                        </div>
                    </div>
                </div>
            `).join('');

            return `
                <div class="tm-video-page-section">
                    <h3 class="tm-video-page-title">🕰️ From your Time Capsule (${this.maxDate.toLocaleDateString()})</h3>
                    <div class="tm-video-page-grid">
                        ${videoCards}
                    </div>
                </div>
            `;
        }

        attachVideoPageClickHandlers() {
            document.querySelectorAll('.tm-video-page-card').forEach(card => {
                card.addEventListener('click', () => {
                    const videoId = card.dataset.videoId;
                    if (videoId) {
                        window.location.href = `/watch?v=${videoId}`;
                    }
                });
            });
        }

        log(...args) {
            if (CONFIG.debugMode) {
                console.log('[Time Machine]', ...args);
            }
        }
    }

    // Initialize when page loads
    function init() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                new YouTubeTimeMachine();
            });
        } else {
            new YouTubeTimeMachine();
        }
    }

    init();

})();

QingJ © 2025

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