YouTube 自動展開評論和回覆

優化性能的YouTube視頻評論自動展開

  1. // ==UserScript==
  2. // @name YouTube Auto Expand Comments and Replies
  3. // @name:zh-CN YouTube 自动展开评论和回复
  4. // @name:zh-TW YouTube 自動展開評論和回覆
  5. // @name:ja YouTube コメントと返信を自動展開
  6. // @name:ko YouTube 댓글 및 답글 자동 확장
  7. // @name:es Expansión automática de comentarios y respuestas de YouTube
  8. // @name:fr Expansion automatique des commentaires et réponses YouTube
  9. // @name:de Automatische Erweiterung von YouTube-Kommentaren und Antworten
  10. // @namespace https://github.com/SuperNG6/YouTube-Comment-Script
  11. // @author SuperNG6
  12. // @version 1.6
  13. // @description Automatically expand comments and replies on YouTube with performance optimization
  14. // @license MIT
  15. // @description:zh-CN 优化性能的YouTube视频评论自动展开
  16. // @description:zh-TW 優化性能的YouTube視頻評論自動展開
  17. // @description:ja パフォーマンスを最適化したYouTubeコメント自動展開
  18. // @description:ko 성능이 최적화된 YouTube 댓글 자동 확장
  19. // @description:es Expansión automática de comentarios de YouTube con rendimiento optimizado
  20. // @description:fr Extension automatique des commentaires YouTube avec optimisation des performances
  21. // @description:de Automatische Erweiterung von YouTube-Kommentaren mit Leistungsoptimierung
  22. // @match https://www.youtube.com/*
  23. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  24. // @grant none
  25. // @run-at document-end
  26. // ==/UserScript==
  27.  
  28. (function() {
  29. 'use strict';
  30.  
  31. const CONFIG = Object.freeze({
  32. // Performance settings
  33. SCROLL_THROTTLE: 250, // Throttle scroll events (ms)
  34. MUTATION_THROTTLE: 150, // Throttle mutation observer (ms)
  35. INITIAL_DELAY: 1500, // Initial delay before starting (ms)
  36. CLICK_INTERVAL: 500, // Interval between clicks (ms)
  37. // Operation limits
  38. MAX_RETRIES: 5, // Maximum retries for finding comments
  39. MAX_CLICKS_PER_BATCH: 3, // Maximum clicks per operation
  40. SCROLL_THRESHOLD: 0.8, // Scroll threshold for loading (0-1)
  41. // State tracking
  42. EXPANDED_CLASS: 'yt-auto-expanded', // Class to mark expanded items
  43. STATE_CHECK_INTERVAL: 2000, // Interval to check expanded state (ms)
  44. // Debug mode
  45. DEBUG: false
  46. });
  47.  
  48. // Selectors map for better maintainability
  49. const SELECTORS = Object.freeze({
  50. COMMENTS: 'ytd-comments#comments',
  51. COMMENTS_SECTION: 'ytd-item-section-renderer#sections',
  52. REPLIES: 'ytd-comment-replies-renderer',
  53. MORE_COMMENTS: 'ytd-continuation-item-renderer #button:not([disabled])',
  54. SHOW_REPLIES: '#more-replies > yt-button-shape > button:not([disabled])',
  55. HIDDEN_REPLIES: 'ytd-comment-replies-renderer ytd-button-renderer#more-replies button:not([disabled])',
  56. EXPANDED_REPLIES: 'div#expander[expanded]',
  57. COMMENT_THREAD: 'ytd-comment-thread-renderer'
  58. });
  59.  
  60. class YouTubeCommentExpander {
  61. constructor() {
  62. this.observer = null;
  63. this.retryCount = 0;
  64. this.isProcessing = false;
  65. this.lastScrollTime = 0;
  66. this.lastMutationTime = 0;
  67. this.expandedComments = new Set();
  68. this.scrollHandler = this.throttle(this.handleScroll.bind(this), CONFIG.SCROLL_THROTTLE);
  69. }
  70.  
  71. log(...args) {
  72. if (CONFIG.DEBUG) {
  73. console.log('[YouTube Comment Expander]', ...args);
  74. }
  75. }
  76.  
  77. // Utility: Throttle function
  78. throttle(func, limit) {
  79. let inThrottle;
  80. return function(...args) {
  81. if (!inThrottle) {
  82. func.apply(this, args);
  83. inThrottle = true;
  84. setTimeout(() => inThrottle = false, limit);
  85. }
  86. };
  87. }
  88.  
  89. // Utility: Generate unique ID for comment thread
  90. getCommentId(element) {
  91. const dataContext = element.getAttribute('data-context') || '';
  92. const timestamp = element.querySelector('#header-author time')?.getAttribute('datetime') || '';
  93. return `${dataContext}-${timestamp}`;
  94. }
  95.  
  96. // Check if comment is already expanded
  97. isCommentExpanded(element) {
  98. const commentId = this.getCommentId(element);
  99. return this.expandedComments.has(commentId);
  100. }
  101.  
  102. // Mark comment as expanded
  103. markAsExpanded(element) {
  104. const commentId = this.getCommentId(element);
  105. element.classList.add(CONFIG.EXPANDED_CLASS);
  106. this.expandedComments.add(commentId);
  107. }
  108.  
  109. // Check if element is truly visible and clickable
  110. isElementClickable(element) {
  111. if (!element || !element.offsetParent || element.disabled) {
  112. return false;
  113. }
  114. const rect = element.getBoundingClientRect();
  115. const isVisible = (
  116. rect.top >= 0 &&
  117. rect.left >= 0 &&
  118. rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
  119. rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  120. );
  121.  
  122. // Additional checks for button state
  123. const isButton = element.tagName.toLowerCase() === 'button';
  124. const isEnabled = !element.disabled && !element.hasAttribute('disabled');
  125. const hasCorrectAriaExpanded = !element.hasAttribute('aria-expanded') ||
  126. element.getAttribute('aria-expanded') === 'false';
  127.  
  128. return isVisible && isEnabled && (!isButton || hasCorrectAriaExpanded);
  129. }
  130.  
  131. // Safely click elements with expanded state tracking
  132. async clickElements(selector, maxClicks = CONFIG.MAX_CLICKS_PER_BATCH) {
  133. let clickCount = 0;
  134. const elements = Array.from(document.querySelectorAll(selector));
  135. for (const element of elements) {
  136. if (clickCount >= maxClicks) break;
  137. const commentThread = element.closest(SELECTORS.COMMENT_THREAD);
  138. if (commentThread && this.isCommentExpanded(commentThread)) {
  139. continue;
  140. }
  141.  
  142. if (this.isElementClickable(element)) {
  143. try {
  144. element.scrollIntoView({ behavior: "auto", block: "center" });
  145. await new Promise(resolve => setTimeout(resolve, 100));
  146. const wasClicked = element.click();
  147. if (wasClicked && commentThread) {
  148. this.markAsExpanded(commentThread);
  149. clickCount++;
  150. this.log(`Clicked and marked as expanded: ${selector}`);
  151. }
  152. await new Promise(resolve => setTimeout(resolve, CONFIG.CLICK_INTERVAL));
  153. } catch (error) {
  154. this.log(`Click error: ${error.message}`);
  155. }
  156. }
  157. }
  158. return clickCount > 0;
  159. }
  160.  
  161. // Monitor expanded state
  162. monitorExpandedState() {
  163. setInterval(() => {
  164. const expandedThreads = document.querySelectorAll(`${SELECTORS.COMMENT_THREAD}.${CONFIG.EXPANDED_CLASS}`);
  165. expandedThreads.forEach(thread => {
  166. const hasExpandedContent = thread.querySelector(SELECTORS.EXPANDED_REPLIES);
  167. if (!hasExpandedContent) {
  168. const commentId = this.getCommentId(thread);
  169. this.expandedComments.delete(commentId);
  170. thread.classList.remove(CONFIG.EXPANDED_CLASS);
  171. }
  172. });
  173. }, CONFIG.STATE_CHECK_INTERVAL);
  174. }
  175.  
  176. // Process visible elements
  177. async processVisibleElements() {
  178. if (this.isProcessing) return;
  179. this.isProcessing = true;
  180.  
  181. try {
  182. const clickedMore = await this.clickElements(SELECTORS.MORE_COMMENTS);
  183. const clickedReplies = await this.clickElements(SELECTORS.SHOW_REPLIES);
  184. const clickedHidden = await this.clickElements(SELECTORS.HIDDEN_REPLIES);
  185.  
  186. return clickedMore || clickedReplies || clickedHidden;
  187. } finally {
  188. this.isProcessing = false;
  189. }
  190. }
  191.  
  192. // Handle scroll events
  193. async handleScroll() {
  194. const now = Date.now();
  195. if (now - this.lastScrollTime < CONFIG.SCROLL_THROTTLE) return;
  196. this.lastScrollTime = now;
  197.  
  198. const scrollPosition = window.scrollY + window.innerHeight;
  199. const documentHeight = document.documentElement.scrollHeight;
  200. if (scrollPosition / documentHeight > CONFIG.SCROLL_THRESHOLD) {
  201. await this.processVisibleElements();
  202. }
  203. }
  204.  
  205. // Setup mutation observer
  206. setupObserver() {
  207. const commentsSection = document.querySelector(SELECTORS.COMMENTS_SECTION);
  208. if (!commentsSection) return false;
  209.  
  210. this.observer = new MutationObserver(
  211. this.throttle(async (mutations) => {
  212. const now = Date.now();
  213. if (now - this.lastMutationTime < CONFIG.MUTATION_THROTTLE) return;
  214. this.lastMutationTime = now;
  215.  
  216. const hasRelevantChanges = mutations.some(mutation =>
  217. mutation.addedNodes.length > 0 ||
  218. mutation.attributeName === 'hidden' ||
  219. mutation.attributeName === 'disabled'
  220. );
  221.  
  222. if (hasRelevantChanges) {
  223. await this.processVisibleElements();
  224. }
  225. }, CONFIG.MUTATION_THROTTLE)
  226. );
  227.  
  228. this.observer.observe(commentsSection, {
  229. childList: true,
  230. subtree: true,
  231. attributes: true,
  232. attributeFilter: ['hidden', 'disabled', 'aria-expanded']
  233. });
  234.  
  235. return true;
  236. }
  237.  
  238. // Initialize the expander
  239. async init() {
  240. if (this.retryCount >= CONFIG.MAX_RETRIES) {
  241. this.log('Max retries reached, aborting initialization');
  242. return;
  243. }
  244.  
  245. // Check if we're on a video page
  246. if (!window.location.pathname.startsWith('/watch')) {
  247. return;
  248. }
  249.  
  250. // Wait for comments section
  251. if (!document.querySelector(SELECTORS.COMMENTS)) {
  252. this.retryCount++;
  253. this.log(`Retrying initialization (${this.retryCount}/${CONFIG.MAX_RETRIES})`);
  254. setTimeout(() => this.init(), CONFIG.INITIAL_DELAY);
  255. return;
  256. }
  257.  
  258. // Setup observers and handlers
  259. if (this.setupObserver()) {
  260. window.addEventListener('scroll', this.scrollHandler, { passive: true });
  261. this.monitorExpandedState();
  262. await this.processVisibleElements();
  263. this.log('Initialization complete');
  264. }
  265. }
  266.  
  267. // Cleanup resources
  268. cleanup() {
  269. if (this.observer) {
  270. this.observer.disconnect();
  271. this.observer = null;
  272. }
  273. window.removeEventListener('scroll', this.scrollHandler);
  274. this.expandedComments.clear();
  275. }
  276. }
  277.  
  278. // Initialize the expander when the page is ready
  279. const expander = new YouTubeCommentExpander();
  280. if (document.readyState === 'loading') {
  281. document.addEventListener('DOMContentLoaded', () => setTimeout(() => expander.init(), CONFIG.INITIAL_DELAY));
  282. } else {
  283. setTimeout(() => expander.init(), CONFIG.INITIAL_DELAY);
  284. }
  285.  
  286. // Handle page navigation (for YouTube's SPA)
  287. let lastUrl = location.href;
  288. new MutationObserver(() => {
  289. const url = location.href;
  290. if (url !== lastUrl) {
  291. lastUrl = url;
  292. expander.cleanup();
  293. setTimeout(() => expander.init(), CONFIG.INITIAL_DELAY);
  294. }
  295. }).observe(document.querySelector('body'), { childList: true, subtree: true });
  296. })();

QingJ © 2025

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