// ==UserScript==
// @name YouTube コメントと返信を自動展開 ✅
// @name:en YouTube Auto Expand Comments and Replies ✅
// @name:ja YouTube コメントと返信を自動展開 ✅
// @name:zh-CN 自动展开 YouTube 评论与回复 ✅
// @name:zh-TW 自動展開 YouTube 評論與回覆 ✅
// @name:ko YouTube 댓글 및 답글 자동 확장 ✅
// @name:fr Déploiement automatique des commentaires YouTube ✅
// @name:es Expansión automática de comentarios de YouTube ✅
// @name:de YouTube-Kommentare automatisch erweitern ✅
// @name:pt-BR Expandir automaticamente os comentários do YouTube ✅
// @name:ru Авторазворачивание комментариев на YouTube ✅
// @version 2.8.0
// @description 安定動作でYouTubeのコメントと返信、「他の返信を表示」も自動展開!現行UIに完全対応。
// @description:en Reliably auto-expands YouTube comments, replies, and "Show more replies". Fully updated for current UI.
// @description:zh-CN 稳定展开YouTube评论和回复,包括“显示更多回复”。兼容新界面。
// @description:zh-TW 穩定展開YouTube評論和回覆,包括「顯示更多回覆」。支援最新介面。
// @description:ko YouTube의 댓글과 답글을 안정적으로 자동 확장. 최신 UI에 대응.
// @description:fr Déploie automatiquement les commentaires et réponses YouTube. Compatible avec la nouvelle interface.
// @description:es Expande automáticamente los comentarios y respuestas en YouTube. Totalmente actualizado para la nueva interfaz.
// @description:de Erweiterung von YouTube-Kommentaren und Antworten – automatisch und zuverlässig. Für aktuelle Oberfläche optimiert.
// @description:pt-BR Expande automaticamente comentários e respostas no YouTube. Compatível com a nova UI.
// @description:ru Автоматически разворачивает комментарии и ответы на YouTube. Полностью адаптирован к новому интерфейсу.
// @namespace https://github.com/koyasi777/youtube-auto-expand-comments
// @author koyasi777
// @match https://www.youtube.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant none
// @run-at document-end
// @license MIT
// @homepageURL https://github.com/koyasi777/youtube-auto-expand-comments
// @supportURL https://github.com/koyasi777/youtube-auto-expand-comments/issues
// ==/UserScript==
(function() {
'use strict';
const CONFIG = Object.freeze({
SCROLL_THROTTLE: 250,
MUTATION_THROTTLE: 150,
INITIAL_DELAY: 1500,
CLICK_INTERVAL: 500,
EXPANDED_CLASS: 'yt-auto-expanded',
DEBUG: false
});
const SELECTORS = Object.freeze({
COMMENTS: 'ytd-comments#comments',
COMMENT_THREAD: 'ytd-comment-thread-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])',
CONTINUATION_REPLIES: 'ytd-comment-replies-renderer ytd-continuation-item-renderer ytd-button-renderer button:not([disabled])'
});
class YouTubeCommentExpander {
constructor() {
this.observer = null;
this.isProcessing = false;
this.expandedComments = new Set();
this.scrollHandler = this.throttle(() => this.processVisibleElements(), CONFIG.SCROLL_THROTTLE);
}
log(...args) {
if (CONFIG.DEBUG) console.log('[YTCExpander]', ...args);
}
throttle(fn, delay) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= delay) {
last = now;
fn.apply(this, args);
}
};
}
getCommentId(thread) {
const timestamp = thread.querySelector('#header-author time')?.getAttribute('datetime') || '';
const id = thread.getAttribute('data-thread-id') || '';
return `${id}-${timestamp}`;
}
isCommentExpanded(thread) {
return this.expandedComments.has(this.getCommentId(thread));
}
markAsExpanded(thread) {
thread.classList.add(CONFIG.EXPANDED_CLASS);
this.expandedComments.add(this.getCommentId(thread));
}
async clickElements(selector) {
const elements = Array.from(document.querySelectorAll(selector));
let clickedAny = false;
for (const el of elements) {
const thread = el.closest(SELECTORS.COMMENT_THREAD);
if (thread && this.isCommentExpanded(thread)) continue;
el.scrollIntoView({ behavior: 'auto', block: 'center' });
await new Promise(r => setTimeout(r, 100));
if (el.disabled || el.getAttribute('aria-expanded') === 'true') continue;
try {
el.click();
clickedAny = true;
if (thread) this.markAsExpanded(thread);
this.log('Clicked:', selector);
await new Promise(r => setTimeout(r, CONFIG.CLICK_INTERVAL));
} catch (e) {
this.log('Click error:', e);
}
}
return clickedAny;
}
async processVisibleElements() {
if (this.isProcessing) return;
this.isProcessing = true;
try {
let loop;
do {
loop = false;
loop = await this.clickElements(SELECTORS.MORE_COMMENTS) || loop;
loop = await this.clickElements(SELECTORS.SHOW_REPLIES) || loop;
loop = await this.clickElements(SELECTORS.HIDDEN_REPLIES) || loop;
loop = await this.clickElements(SELECTORS.CONTINUATION_REPLIES) || loop;
} while (loop);
} finally {
this.isProcessing = false;
}
}
setupMutationObserver() {
const container = document.querySelector(SELECTORS.COMMENTS);
if (!container) return false;
this.observer = new MutationObserver(this.throttle(() => {
this.processVisibleElements();
}, CONFIG.MUTATION_THROTTLE));
this.observer.observe(container, { childList: true, subtree: true });
return true;
}
setupIntersectionObserver() {
const io = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
try { entry.target.click(); }
catch(e) { this.log('IO click error', e); }
}
});
}, { rootMargin: '0px', threshold: 0 });
const observeButtons = () => {
document.querySelectorAll(
`${SELECTORS.MORE_COMMENTS},${SELECTORS.SHOW_REPLIES},${SELECTORS.HIDDEN_REPLIES},${SELECTORS.CONTINUATION_REPLIES}`
).forEach(btn => io.observe(btn));
};
observeButtons();
const container = document.querySelector(SELECTORS.COMMENTS);
if (container) {
new MutationObserver(observeButtons).observe(container, { childList: true, subtree: true });
}
}
async init() {
if (!location.pathname.startsWith('/watch')) return;
for (let i = 0; i < 10 && !document.querySelector(SELECTORS.COMMENTS); i++) {
await new Promise(r => setTimeout(r, CONFIG.INITIAL_DELAY));
}
if (!document.querySelector(SELECTORS.COMMENTS)) return;
window.addEventListener('scroll', this.scrollHandler, { passive: true });
this.setupMutationObserver();
this.setupIntersectionObserver();
await this.processVisibleElements();
this.log('Expander initialized');
}
cleanup() {
if (this.observer) this.observer.disconnect();
window.removeEventListener('scroll', this.scrollHandler);
this.expandedComments.clear();
}
}
const expander = new YouTubeCommentExpander();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(() => expander.init(), CONFIG.INITIAL_DELAY));
} else {
setTimeout(() => expander.init(), CONFIG.INITIAL_DELAY);
}
let lastUrl = location.href;
new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
expander.cleanup();
setTimeout(() => expander.init(), CONFIG.INITIAL_DELAY);
}
}).observe(document.body, { childList: true, subtree: true });
})();