novel2txt

downloading novel as txt from 69shuba.com

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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