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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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