novel2txt

downloading novel as txt from 69shuba.com

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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