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).

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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');
    }
})();