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