AO3: Load All Comments

Automatically fetches and appends all pages of comments on AO3 — et carthago delenda est. Matches are AO3 and its TLDs that are not redirects directly to .org (no point in matching the TLDs that are simply redirects — cf. https://archiveofourown.org/faq/accessing-fanworks#archiveurl).

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         AO3: Load All Comments
// @namespace   https://greasyfork.org/en/users/1555174-charles-rockafellor
// @version      1.5 on 07 May 2026
// @description  Automatically fetches and appends all pages of comments on AO3 — et carthago delenda est. Matches are AO3 and its TLDs that are not redirects directly to .org (no point in matching the TLDs that are simply redirects — cf. https://archiveofourown.org/faq/accessing-fanworks#archiveurl).
// @author      Charles Rockafellor
// @homepageURL https://archiveofourown.org/users/Charles_Rockafellor/collections
// @match       https://archiveofourown.org/works/*
// @match       https://archiveofourown.gay/works/*
// @match       https://archive.transformativeworks.org/works/*
// @match       https://insecure.archiveofourown.org/works/*
// @icon         https://i.pinimg.com/1200x/98/af/04/98af04b09aedd599def9750ccf4c5ff9.jpg
// @grant        none
// @run-at        document-end
// @license     MIT; https://opensource.org/license/mit
// @connect     archiveofourown.org
// @history        1.5 — Included [successfully] the truncated threads' expansion without triggering too many requests at once.  If you encounter a "429 Too Many Requests" error, just let the connection cool down for a few seconds or minutes, and then try again.
// @history        1.4 — Trying to retain thread structure, ignoring the thread-truncation (the "[n-many] more comments in this thread" message) for now in order to hopefully avoid the "Too many requests" AO3-limit.
// @history        1.3 — Stripped back selectors to be as broad as possible, removed observer and added immediate console log to check console pane for script having been loaded or not.
// @history        1.2 — Swapped scheme from wildcard to https, added a MutationObserver.
// @history        1.1 — Initial build.
// ==/UserScript==

(function() {
    'use strict';

    const DELAY_MS = 2000; // Increased to 2s for safety with deep expansion

    const checkExist = setInterval(function() {
        const thread = document.querySelector('ol.thread');
        if (thread && !document.querySelector('#load-all-comments-btn')) {
            initScript(thread);
        }
    }, 1000);

    function initScript(thread) {
        const loadAllBtn = document.createElement('button');
        loadAllBtn.id = 'load-all-comments-btn';
        loadAllBtn.innerHTML = "LOAD ALL & EXPAND DEEP THREADS";
        loadAllBtn.style = "display: block; width: 100%; margin: 20px 0; padding: 15px; background: #900; color: #fff; border: 2px solid #333; cursor: pointer; font-weight: bold; font-family: sans-serif;";

        thread.parentNode.insertBefore(loadAllBtn, thread);

        loadAllBtn.addEventListener('click', (e) => {
            e.preventDefault();
            loadAllBtn.disabled = true;
            loadAllBtn.style.background = "#444";
            loadAllBtn.innerHTML = "PHASE 1: MERGING PAGES...";
            fetchNextPage(document);
        });
    }

    function fetchNextPage(doc) {
        const nextLink = doc.querySelector('li.next a');

        if (nextLink) {
            setTimeout(() => {
                fetch(nextLink.href).then(r => r.text()).then(html => {
                    const nextDoc = new DOMParser().parseFromString(html, 'text/html');
                    const remoteThread = nextDoc.querySelector('ol.thread');
                    if (remoteThread) document.querySelector('ol.thread').insertAdjacentHTML('beforeend', remoteThread.innerHTML);
                    fetchNextPage(nextDoc);
                });
            }, DELAY_MS);
        } else {
            console.log("AO3 Script: Starting Phase 2 (Expansion)...");
            expandTruncatedThreads();
        }
    }

    async function expandTruncatedThreads() {
        const btn = document.querySelector('#load-all-comments-btn');
        // Find all <li> that contain a "more comments in this thread" link
        let expansionLinks = Array.from(document.querySelectorAll('ol.thread li.comment a')).filter(a => a.textContent.includes('more comments in this thread'));

        if (expansionLinks.length === 0) {
            finish();
            return;
        }

        btn.innerHTML = `PHASE 2: EXPANDING ${expansionLinks.length} DEEP THREADS...`;

        for (let link of expansionLinks) {
            await new Promise(r => setTimeout(r, DELAY_MS)); // Safety delay
            try {
                const response = await fetch(link.href);
                const html = await response.text();
                const threadDoc = new DOMParser().parseFromString(html, 'text/html');

                // Find the new comments in the fetched thread
                const deepThread = threadDoc.querySelector('ol.thread');
                if (deepThread) {
                    // Replace the "X more comments" <li> with the actual content
                    link.closest('li').insertAdjacentHTML('afterend', deepThread.innerHTML);
                    link.closest('li').remove();
                }
            } catch (err) {
                console.error("Expansion failed for link:", link.href);
            }
        }

        // Check again in case the new content has EVEN DEEPER links
        const remaining = Array.from(document.querySelectorAll('ol.thread li.comment a')).filter(a => a.textContent.includes('more comments in this thread'));
        if (remaining.length > 0) {
            expandTruncatedThreads();
        } else {
            finish();
        }
    }

    function finish() {
        const btn = document.querySelector('#load-all-comments-btn');
        btn.innerHTML = "ALL PAGES LOADED & EXPANDED";
        document.querySelectorAll('ol.pagination').forEach(p => p.style.display = 'none');
    }
})();