您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
优化性能的YouTube视频评论自动展开
当前为
// ==UserScript== // @name YouTube Auto Expand Comments and Replies // @name:zh-CN YouTube 自动展开评论和回复 // @name:zh-TW YouTube 自動展開評論和回覆 // @name:ja YouTube コメントと返信を自動展開 // @name:ko YouTube 댓글 및 답글 자동 확장 // @name:es Expansión automática de comentarios y respuestas de YouTube // @name:fr Expansion automatique des commentaires et réponses YouTube // @name:de Automatische Erweiterung von YouTube-Kommentaren und Antworten // @namespace https://github.com/SuperNG6/YouTube-Comment-Script // @author SuperNG6 // @version 1.6 // @description Automatically expand comments and replies on YouTube with performance optimization // @license MIT // @description:zh-CN 优化性能的YouTube视频评论自动展开 // @description:zh-TW 優化性能的YouTube視頻評論自動展開 // @description:ja パフォーマンスを最適化したYouTubeコメント自動展開 // @description:ko 성능이 최적화된 YouTube 댓글 자동 확장 // @description:es Expansión automática de comentarios de YouTube con rendimiento optimizado // @description:fr Extension automatique des commentaires YouTube avec optimisation des performances // @description:de Automatische Erweiterung von YouTube-Kommentaren mit Leistungsoptimierung // @match https://www.youtube.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @grant none // @run-at document-end // ==/UserScript== (function() { 'use strict'; const CONFIG = Object.freeze({ // Performance settings SCROLL_THROTTLE: 250, // Throttle scroll events (ms) MUTATION_THROTTLE: 150, // Throttle mutation observer (ms) INITIAL_DELAY: 1500, // Initial delay before starting (ms) CLICK_INTERVAL: 500, // Interval between clicks (ms) // Operation limits MAX_RETRIES: 5, // Maximum retries for finding comments MAX_CLICKS_PER_BATCH: 3, // Maximum clicks per operation SCROLL_THRESHOLD: 0.8, // Scroll threshold for loading (0-1) // State tracking EXPANDED_CLASS: 'yt-auto-expanded', // Class to mark expanded items STATE_CHECK_INTERVAL: 2000, // Interval to check expanded state (ms) // Debug mode DEBUG: false }); // Selectors map for better maintainability const SELECTORS = Object.freeze({ COMMENTS: 'ytd-comments#comments', COMMENTS_SECTION: 'ytd-item-section-renderer#sections', REPLIES: 'ytd-comment-replies-renderer', MORE_COMMENTS: 'ytd-continuation-item-renderer #button:not([disabled])', SHOW_REPLIES: '#more-replies > yt-button-shape > button:not([disabled])', HIDDEN_REPLIES: 'ytd-comment-replies-renderer ytd-button-renderer#more-replies button:not([disabled])', EXPANDED_REPLIES: 'div#expander[expanded]', COMMENT_THREAD: 'ytd-comment-thread-renderer' }); class YouTubeCommentExpander { constructor() { this.observer = null; this.retryCount = 0; this.isProcessing = false; this.lastScrollTime = 0; this.lastMutationTime = 0; this.expandedComments = new Set(); this.scrollHandler = this.throttle(this.handleScroll.bind(this), CONFIG.SCROLL_THROTTLE); } log(...args) { if (CONFIG.DEBUG) { console.log('[YouTube Comment Expander]', ...args); } } // Utility: Throttle function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } // Utility: Generate unique ID for comment thread getCommentId(element) { const dataContext = element.getAttribute('data-context') || ''; const timestamp = element.querySelector('#header-author time')?.getAttribute('datetime') || ''; return `${dataContext}-${timestamp}`; } // Check if comment is already expanded isCommentExpanded(element) { const commentId = this.getCommentId(element); return this.expandedComments.has(commentId); } // Mark comment as expanded markAsExpanded(element) { const commentId = this.getCommentId(element); element.classList.add(CONFIG.EXPANDED_CLASS); this.expandedComments.add(commentId); } // Check if element is truly visible and clickable isElementClickable(element) { if (!element || !element.offsetParent || element.disabled) { return false; } const rect = element.getBoundingClientRect(); const isVisible = ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); // Additional checks for button state const isButton = element.tagName.toLowerCase() === 'button'; const isEnabled = !element.disabled && !element.hasAttribute('disabled'); const hasCorrectAriaExpanded = !element.hasAttribute('aria-expanded') || element.getAttribute('aria-expanded') === 'false'; return isVisible && isEnabled && (!isButton || hasCorrectAriaExpanded); } // Safely click elements with expanded state tracking async clickElements(selector, maxClicks = CONFIG.MAX_CLICKS_PER_BATCH) { let clickCount = 0; const elements = Array.from(document.querySelectorAll(selector)); for (const element of elements) { if (clickCount >= maxClicks) break; const commentThread = element.closest(SELECTORS.COMMENT_THREAD); if (commentThread && this.isCommentExpanded(commentThread)) { continue; } if (this.isElementClickable(element)) { try { element.scrollIntoView({ behavior: "auto", block: "center" }); await new Promise(resolve => setTimeout(resolve, 100)); const wasClicked = element.click(); if (wasClicked && commentThread) { this.markAsExpanded(commentThread); clickCount++; this.log(`Clicked and marked as expanded: ${selector}`); } await new Promise(resolve => setTimeout(resolve, CONFIG.CLICK_INTERVAL)); } catch (error) { this.log(`Click error: ${error.message}`); } } } return clickCount > 0; } // Monitor expanded state monitorExpandedState() { setInterval(() => { const expandedThreads = document.querySelectorAll(`${SELECTORS.COMMENT_THREAD}.${CONFIG.EXPANDED_CLASS}`); expandedThreads.forEach(thread => { const hasExpandedContent = thread.querySelector(SELECTORS.EXPANDED_REPLIES); if (!hasExpandedContent) { const commentId = this.getCommentId(thread); this.expandedComments.delete(commentId); thread.classList.remove(CONFIG.EXPANDED_CLASS); } }); }, CONFIG.STATE_CHECK_INTERVAL); } // Process visible elements async processVisibleElements() { if (this.isProcessing) return; this.isProcessing = true; try { const clickedMore = await this.clickElements(SELECTORS.MORE_COMMENTS); const clickedReplies = await this.clickElements(SELECTORS.SHOW_REPLIES); const clickedHidden = await this.clickElements(SELECTORS.HIDDEN_REPLIES); return clickedMore || clickedReplies || clickedHidden; } finally { this.isProcessing = false; } } // Handle scroll events async handleScroll() { const now = Date.now(); if (now - this.lastScrollTime < CONFIG.SCROLL_THROTTLE) return; this.lastScrollTime = now; const scrollPosition = window.scrollY + window.innerHeight; const documentHeight = document.documentElement.scrollHeight; if (scrollPosition / documentHeight > CONFIG.SCROLL_THRESHOLD) { await this.processVisibleElements(); } } // Setup mutation observer setupObserver() { const commentsSection = document.querySelector(SELECTORS.COMMENTS_SECTION); if (!commentsSection) return false; this.observer = new MutationObserver( this.throttle(async (mutations) => { const now = Date.now(); if (now - this.lastMutationTime < CONFIG.MUTATION_THROTTLE) return; this.lastMutationTime = now; const hasRelevantChanges = mutations.some(mutation => mutation.addedNodes.length > 0 || mutation.attributeName === 'hidden' || mutation.attributeName === 'disabled' ); if (hasRelevantChanges) { await this.processVisibleElements(); } }, CONFIG.MUTATION_THROTTLE) ); this.observer.observe(commentsSection, { childList: true, subtree: true, attributes: true, attributeFilter: ['hidden', 'disabled', 'aria-expanded'] }); return true; } // Initialize the expander async init() { if (this.retryCount >= CONFIG.MAX_RETRIES) { this.log('Max retries reached, aborting initialization'); return; } // Check if we're on a video page if (!window.location.pathname.startsWith('/watch')) { return; } // Wait for comments section if (!document.querySelector(SELECTORS.COMMENTS)) { this.retryCount++; this.log(`Retrying initialization (${this.retryCount}/${CONFIG.MAX_RETRIES})`); setTimeout(() => this.init(), CONFIG.INITIAL_DELAY); return; } // Setup observers and handlers if (this.setupObserver()) { window.addEventListener('scroll', this.scrollHandler, { passive: true }); this.monitorExpandedState(); await this.processVisibleElements(); this.log('Initialization complete'); } } // Cleanup resources cleanup() { if (this.observer) { this.observer.disconnect(); this.observer = null; } window.removeEventListener('scroll', this.scrollHandler); this.expandedComments.clear(); } } // Initialize the expander when the page is ready const expander = new YouTubeCommentExpander(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(() => expander.init(), CONFIG.INITIAL_DELAY)); } else { setTimeout(() => expander.init(), CONFIG.INITIAL_DELAY); } // Handle page navigation (for YouTube's SPA) let lastUrl = location.href; new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; expander.cleanup(); setTimeout(() => expander.init(), CONFIG.INITIAL_DELAY); } }).observe(document.querySelector('body'), { childList: true, subtree: true }); })();