novel2txt

downloading novel as txt from 69shuba.com

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         novel2txt
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  downloading novel as txt from 69shuba.com
// @author       @gzmonkey
// @match        *://www.69shuba.com/*
// @grant        none
// @license MIT
// ==/UserScript==

function initButton() {
    'use strict';

    const button = document.createElement('button');
    button.innerText = 'Copy TXT';
    button.style.position = 'fixed';
    button.style.top = '16px';
    button.style.right = '16px';
    button.style.padding = '14px 20px';
    button.style.fontSize = '18px';
    button.style.fontWeight = '700';
    button.style.background = '#ffcc00';
    button.style.color = '#000';
    button.style.border = '2px solid #000';
    button.style.borderRadius = '8px';
    button.style.zIndex = '2147483647';
    button.style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.25)';

    button.onclick = function() {
        console.log('[TM] Button clicked.');
        const chapterLinks = Array.from(document.querySelectorAll('#catalog ul li a'));
        if (chapterLinks.length === 0) {
            console.warn('[TM] No chapter links found with selector:', '#catalog ul li a');
            return;
        }
        const listText = chapterLinks.map((link, idx) => {
            const title = (link.textContent || '').trim();
            const url = (link.href || '').trim();
            return `${idx + 1}. ${title}\n${url}`;
        }).join('\n\n');
        showEditablePopup(listText, chapterLinks);
    };

    if (!document.body) {
        console.warn('[TM] document.body is not available.');
        return;
    }
    document.body.appendChild(button);
}

function showEditablePopup(listText, chapterLinks) {
    const overlay = document.createElement('div');
    overlay.style.position = 'fixed';
    overlay.style.inset = '0';
    overlay.style.background = 'rgba(0, 0, 0, 0.6)';
    overlay.style.zIndex = '2147483647';

    const panel = document.createElement('div');
    panel.style.position = 'absolute';
    panel.style.top = '50%';
    panel.style.left = '50%';
    panel.style.transform = 'translate(-50%, -50%)';
    panel.style.width = '80vw';
    panel.style.maxWidth = '900px';
    panel.style.height = '70vh';
    panel.style.background = '#fff';
    panel.style.borderRadius = '10px';
    panel.style.padding = '16px';
    panel.style.boxShadow = '0 12px 30px rgba(0, 0, 0, 0.35)';
    panel.style.display = 'flex';
    panel.style.flexDirection = 'column';
    panel.style.gap = '10px';

    const title = document.createElement('div');
    title.textContent = 'Chapter List (edit to select)';
    title.style.fontSize = '18px';
    title.style.fontWeight = '700';

    const textarea = document.createElement('textarea');
    textarea.value = listText;
    textarea.style.flex = '1';
    textarea.style.width = '100%';
    textarea.style.resize = 'none';
    textarea.style.fontSize = '14px';
    textarea.style.fontFamily = 'monospace';

    const status = document.createElement('input');
    status.type = 'text';
    status.readOnly = true;
    status.value = '';
    status.style.fontSize = '14px';
    status.style.fontFamily = 'monospace';
    status.style.border = '1px solid #ccc';
    status.style.padding = '4px';
    status.style.marginTop = '4px';

    const actions = document.createElement('div');
    actions.style.display = 'flex';
    actions.style.gap = '8px';
    actions.style.justifyContent = 'flex-end';

    const startBtn = document.createElement('button');
    startBtn.textContent = 'Start Fetch';
    startBtn.style.padding = '8px 12px';
    const stopBtn = document.createElement('button');
    stopBtn.textContent = 'Stop';
    stopBtn.style.padding = '8px 12px';
    stopBtn.disabled = true;
    let stopRequested = false;
    startBtn.onclick = () => {
        const lines = textarea.value.split('\n').filter(Boolean);
        const urls = lines.filter(line => line.startsWith('https://'));
        startBtn.disabled = true;
        startBtn.textContent = 'Fetching...';
        stopBtn.disabled = false;
        stopRequested = false;
        fetchAndAppend(urls, textarea, status, () => {
            startBtn.textContent = 'Done';
            stopBtn.disabled = true;
        }, () => stopRequested);
    };
    stopBtn.onclick = () => {
        stopRequested = true;
        stopBtn.disabled = true;
        stopBtn.textContent = 'Stopping...';
        textarea.value += '\n[Stopped by user]';
    };

    const closeBtn = document.createElement('button');
    closeBtn.textContent = 'Close';
    closeBtn.style.padding = '8px 12px';
    closeBtn.onclick = () => overlay.remove();

    actions.appendChild(startBtn);
    actions.appendChild(stopBtn);
    actions.appendChild(closeBtn);
    panel.appendChild(title);
    panel.appendChild(textarea);
    panel.appendChild(status);
    panel.appendChild(actions);
    overlay.appendChild(panel);
    overlay.addEventListener('click', (event) => {
        if (event.target === overlay) {
            overlay.remove();
        }
    });
    document.body.appendChild(overlay);
}

async function fetchAndAppend(urls, textarea, status, doneCallback, shouldStop) {
    textarea.value = '';
    const maxSize = 3.5 * 1024 * 1024; // 3.5 MB
    for (let i = 0; i < urls.length; i++) {
        if (shouldStop && shouldStop()) {
            textarea.value += '\n[Stopped by user]';
            status.value = 'Stopped by user';
            console.warn('[TM] Stop requested by user.');
            break;
        }
        const url = urls[i];
        try {
            status.value = `Fetching ${i + 1}/${urls.length}...`;
            const content = await fetchChapterContent(url);
            const title = `Chapter ${i + 1}`;
            const block = `[${title}]\n${content}\n\n`;
            const newSize = new Blob([textarea.value + block]).size;
            if (newSize > maxSize) {
                textarea.value += `\n[Stopped: size limit reached]`;
                status.value = 'Stopped: size limit reached';
                console.warn('[TM] Size limit reached, stopping.');
                break;
            }
            textarea.value += block;
            textarea.scrollTop = textarea.scrollHeight;
            status.value = `Fetched: ${title}`;
            console.log('[TM] Fetched:', title);
            if (i < urls.length - 1) {
                const delayMs = 500 + Math.floor(Math.random() * 2500);
                await delay(delayMs);
            }
        } catch (error) {
            console.error('[TM] Fetch failed:', url, error);
            textarea.value += `[Fetch failed]\n${url}\n\n`;
            status.value = `Fetch failed: Chapter ${i + 1}`;
            break;
        }
    }
    if (doneCallback) doneCallback();
}

function delay(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}

async function fetchChapterContent(url) {
    const response = await fetch(url, { credentials: 'include' });
    if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
    }
    const buffer = await response.arrayBuffer();
    const html = new TextDecoder('gbk').decode(buffer);
    const doc = new DOMParser().parseFromString(html, 'text/html');
    const contentRoot = doc.querySelector('.txtnav');
    if (!contentRoot) {
        return '';
    }
    const cleanupSelectors = ['.txtinfo', '#txtright', '.contentadv', '.bottom-ad', '.bottom-ad2', '.page1', 'script'];
    cleanupSelectors.forEach((selector) => {
        contentRoot.querySelectorAll(selector).forEach((node) => node.remove());
    });
    const htmlWithBreaks = contentRoot.innerHTML.replace(/<br\s*\/?>/gi, '\n');
    const temp = doc.createElement('div');
    temp.innerHTML = htmlWithBreaks;
    const rawText = temp.textContent || '';
    return rawText.replace(/\n{3,}/g, '\n\n').trim();
}

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initButton);
} else {
    initButton();
}