novel2txt

downloading novel as txt from 69shuba.com

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

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