Greasy Fork

来自缓存

Greasy Fork is available in English.

启迪小说TXT下载器

支持选择章节范围的小说下载器,优化性能警告

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         启迪小说TXT下载器
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  支持选择章节范围的小说下载器,优化性能警告
// @author       GitHub:Dolphin-QvQ
// @match        *://www.qidiy.com/book/*/
// @grant        GM_download
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 配置参数
    const CONFIG = {
        collectInterval: 100,
        downloadInterval: 100
    };

    let chapterList = [];
    let isCollecting = false;
    let baseUrl = '';
    let bookId = '';
    let protocol = 'https:';
    // 缓存DOM元素,避免频繁查询
    let dom = {
        statusDiv: null,
        countDiv: null,
        progressFill: null,
        collectBtn: null,
        downloadBtn: null
    };

    // 初始化基础信息和DOM缓存
    function init() {
        protocol = window.location.protocol;
        const pathMatch = window.location.pathname.match(/\/book\/(\d+)(?:\/|_|\d+\/)/);
        if (pathMatch && pathMatch[1]) {
            bookId = pathMatch[1];
            baseUrl = `${protocol}//www.qidiy.com/book/${bookId}/`;
        }
        createControlPanel();
        // 缓存DOM元素
        dom.statusDiv = document.getElementById('downloadStatus');
        dom.countDiv = document.getElementById('chapterCount');
        dom.progressFill = document.getElementById('progressFill');
        dom.collectBtn = document.querySelector('button:first-of-type');
        dom.downloadBtn = document.querySelector('button:last-of-type');
    }

    // 创建控制界面(减少DOM操作次数)
    function createControlPanel() {
        const panel = document.createElement('div');
        panel.style.cssText = `
            position: fixed; top: 20px; right: 20px; z-index: 9999;
            background: white; padding: 15px; border: 1px solid #ccc;
            border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            max-width: 500px;
        `;

        // 一次性拼接HTML,减少DOM插入次数
        panel.innerHTML = `
            <h3 style="margin-top:0; margin-bottom:15px;">小说章节下载器(范围选择)</h3>
            <div style="margin-bottom:15px; font-size:13px;">
                <label>收集间隔(毫秒): </label>
                <input type="number" value="${CONFIG.collectInterval}" min="50" max="5000" style="width:80px;" id="collectInterval">
                <label> 下载间隔(毫秒): </label>
                <input type="number" value="${CONFIG.downloadInterval}" min="50" max="5000" style="width:80px;" id="downloadInterval">
            </div>
            <div style="margin-bottom:15px; padding:10px; border:1px solid #eee; border-radius:3px;">
                <div style="margin-bottom:10px; font-weight:bold;">下载范围选择:</div>
                <label>开始章节: </label>
                <select id="startChapter" style="width:150px;" disabled></select>
                <label> 结束章节: </label>
                <select id="endChapter" style="width:150px;" disabled></select>
            </div>
            <div id="downloadStatus" style="margin-bottom:10px; min-height:40px;">准备就绪,点击"收集所有章节"开始</div>
            <div id="chapterCount" style="margin-bottom:10px; font-size:13px;">已收集章节: 0</div>
            <div style="height:10px; background:#eee; border-radius:5px; overflow:hidden; margin-bottom:10px; display:none;">
                <div id="progressFill" style="height:100%; background:#4CAF50; width:0%;"></div>
            </div>
            <div style="display:flex; gap:10px;">
                <button style="padding:6px 10px; background:#2196F3; color:white; border:none; border-radius:3px; cursor:pointer; flex:1;">收集所有章节</button>
                <button style="padding:6px 10px; background:#4CAF50; color:white; border:none; border-radius:3px; cursor:pointer; flex:1;" disabled>下载选中范围</button>
            </div>
        `;

        document.body.appendChild(panel);

        // 绑定间隔输入框事件
        document.getElementById('collectInterval').addEventListener('change', e => {
            CONFIG.collectInterval = parseInt(e.target.value) || 500;
        });
        document.getElementById('downloadInterval').addEventListener('change', e => {
            CONFIG.downloadInterval = parseInt(e.target.value) || 1000;
        });

        // 绑定按钮事件
        panel.querySelector('button:first-of-type').addEventListener('click', startCollectingChapters);
        panel.querySelector('button:last-of-type').addEventListener('click', downloadSelectedRange);
    }

    // 更新章节选择下拉框(优化DOM操作)
    function updateChapterSelectors() {
        const startSelect = document.getElementById('startChapter');
        const endSelect = document.getElementById('endChapter');
        // 先清空再批量添加,减少重排
        startSelect.innerHTML = '';
        endSelect.innerHTML = '';

        const fragment = document.createDocumentFragment(); // 文档片段,减少DOM插入次数
        const startOptions = [];
        const endOptions = [];

        chapterList.forEach((chapter, index) => {
            const optionText = `[${index + 1}] ${chapter.title} (${chapter.id})`;

            const startOpt = document.createElement('option');
            startOpt.value = chapter.id;
            startOpt.textContent = optionText;
            startOptions.push(startOpt);

            const endOpt = document.createElement('option');
            endOpt.value = chapter.id;
            endOpt.textContent = optionText;
            endOptions.push(endOpt);
        });

        // 批量添加选项
        startOptions.forEach(opt => startSelect.appendChild(opt));
        endOptions.forEach(opt => endSelect.appendChild(opt));

        if (chapterList.length > 0) {
            endSelect.selectedIndex = chapterList.length - 1;
            startSelect.disabled = false;
            endSelect.disabled = false;
            dom.downloadBtn.disabled = false;
        }
    }

    // 提取当前页正文章节
    function extractCurrentPageChapters(doc) {
        const contentTit = Array.from(doc.querySelectorAll('.layout-tit')).find(
            el => el.textContent.trim().includes('正文')
        );
        if (!contentTit) return [];

        const sectionBox = contentTit.nextElementSibling;
        if (!sectionBox || !sectionBox.classList.contains('section-box')) return [];

        const links = sectionBox.querySelectorAll('.section-list a');
        const chapters = [];
        links.forEach(link => {
            const href = link.getAttribute('href');
            const idMatch = href.match(/\/book\/\d+\/(\d+)\.html/);
            if (idMatch && idMatch[1]) {
                chapters.push({ id: idMatch[1], title: link.textContent.trim() });
            }
        });
        return chapters;
    }

    // 开始收集所有章节
    function startCollectingChapters() {
        if (isCollecting || !bookId) return;

        isCollecting = true;
        chapterList = [];
        dom.statusDiv.textContent = '开始收集章节...';
        dom.collectBtn.disabled = true;
        dom.downloadBtn.disabled = true;

        fetch(baseUrl)
            .then(response => response.ok ? response.text() : Promise.reject(`HTTP错误: ${response.status}`))
            .then(html => {
                const doc = new DOMParser().parseFromString(html, 'text/html');
                const currentChapters = extractCurrentPageChapters(doc);
                chapterList.push(...currentChapters);
                updateChapterCount();

                const select = doc.querySelector('select[name="pageselect"]');
                if (!select) {
                    dom.statusDiv.textContent = '未找到分页控件,仅收集到当前页章节';
                    finishCollecting();
                    return;
                }

                // 修正分页URL为HTTPS
                Array.from(select.options).forEach(option => {
                    if (option.value) {
                        option.value = option.value.startsWith('http')
                            ? option.value.replace('http://', 'https://')
                            : `${protocol}//www.qidiy.com${option.value}`;
                    }
                });

                traverseAllPages(select, 0);
            })
            .catch(error => {
                console.error('获取书籍首页失败:', error);
                dom.statusDiv.textContent = `获取失败: ${error}`;
                isCollecting = false;
                dom.collectBtn.disabled = false;
            });
    }

    // 遍历所有分页
    function traverseAllPages(select, currentIndex) {
        if (!isCollecting) return;

        const totalPages = select.options.length;
        dom.statusDiv.textContent = `正在收集第 ${currentIndex + 1}/${totalPages} 页章节...`;

        if (currentIndex === 0) {
            processNextPage(select, currentIndex, totalPages);
            return;
        }

        let pageUrl = select.options[currentIndex].value.replace('http://', 'https://');
        fetch(pageUrl)
            .then(response => response.ok ? response.text() : Promise.reject(`HTTP错误: ${response.status}`))
            .then(html => {
                const doc = new DOMParser().parseFromString(html, 'text/html');
                const pageChapters = extractCurrentPageChapters(doc);

                // 去重添加
                pageChapters.forEach(chapter => {
                    if (!chapterList.some(c => c.id === chapter.id)) {
                        chapterList.push(chapter);
                    }
                });

                updateChapterCount();
                processNextPage(select, currentIndex, totalPages);
                doc.body.innerHTML = ''; // 释放内存
            })
            .catch(error => {
                console.error(`加载分页失败:`, error);
                if (confirm(`第 ${currentIndex + 1} 页加载失败,是否继续?`)) {
                    processNextPage(select, currentIndex, totalPages);
                } else {
                    finishCollecting();
                }
            });
    }

    // 处理下一页
    function processNextPage(select, currentIndex, totalPages) {
        if (currentIndex + 1 < totalPages) {
            setTimeout(() => traverseAllPages(select, currentIndex + 1), CONFIG.collectInterval);
        } else {
            finishCollecting();
        }
    }

    // 完成收集
    function finishCollecting() {
        isCollecting = false;
        chapterList.sort((a, b) => parseInt(a.id) - parseInt(b.id));
        updateChapterSelectors();
        dom.statusDiv.textContent = `章节收集完成!共找到 ${chapterList.length} 个正文章节`;
        dom.collectBtn.disabled = false;
    }

    // 更新章节计数(减少DOM操作)
    function updateChapterCount() {
        dom.countDiv.textContent = `已收集章节: ${chapterList.length}`;
    }

    // 提取单页内容
    function extractPageContent(doc) {
        const titleElem = doc.querySelector('h1.title');
        let chapterTitle = titleElem ? titleElem.textContent.trim() : '未知章节';

        const contentElem = doc.getElementById('content');
        if (!contentElem) return { chapterTitle, content: '', totalPages: 1 };

        let totalPages = 1;
        const contentHtml = contentElem.innerHTML;
        const pageMatch = contentHtml.match(/\(第(\d+)\/(\d+)页\)/i);
        if (pageMatch && pageMatch.length === 3) {
            totalPages = parseInt(pageMatch[2], 10);
        }

        let content = contentHtml
            .replace(/<script[\s\S]*?<\/script>/gi, '')
            .replace(/<br\s*\/?>/gi, '\n')
            .replace(/<[^>]+>/g, '')
            .replace(/ /g, ' ')
            .replace(/(本章未完,请点击下一页继续阅读)/g, '')
            .trim();

        const lines = content.split('\n');
        if (lines.length > 0 && lines[0].match(/\(第\d+\/\d+页\)/i)) {
            content = lines.slice(1).join('\n').trim();
        }

        doc.body.innerHTML = '';
        return { chapterTitle, content: content.replace(/\n+/g, '\n').trim(), totalPages };
    }

    // 获取章节分页URL
    async function getChapterPageUrls(chapterId) {
        const firstPageUrl = `${baseUrl}${chapterId}.html`.replace('http://', 'https://');
        const urls = [firstPageUrl];

        try {
            const response = await fetch(firstPageUrl);
            if (!response.ok) throw new Error(`请求失败: ${response.status}`);

            const html = await response.text();
            const doc = new DOMParser().parseFromString(html, 'text/html');
            const { totalPages } = extractPageContent(doc);

            if (totalPages > 1) {
                for (let i = 2; i <= totalPages; i++) {
                    urls.push(`${baseUrl}${chapterId}/${i}.html`.replace('http://', 'https://'));
                }
            }
            return urls;
        } catch (e) {
            console.error(`获取分页失败:`, e);
            return urls;
        }
    }

    // 下载单个章节
    async function downloadChapter(chapter) {
        const pageUrls = await getChapterPageUrls(chapter.id);
        let fullContent = '';
        let chapterTitle = chapter.title;

        for (let i = 0; i < pageUrls.length; i++) {
            try {
                const response = await fetch(pageUrls[i].replace('http://', 'https://'));
                if (!response.ok) throw new Error(`HTTP错误: ${response.status}`);

                const html = await response.text();
                const doc = new DOMParser().parseFromString(html, 'text/html');
                const { content } = extractPageContent(doc);
                fullContent += content + '\n';
            } catch (e) {
                fullContent += `【第${i+1}页下载失败: ${e.message}】\n`;
            }
        }

        return { chapterTitle, content: fullContent.replace(/\n+/g, '\n').trim() };
    }

    // 下载选中范围
    async function downloadSelectedRange() {
        const startId = document.getElementById('startChapter').value;
        const endId = document.getElementById('endChapter').value;

        if (!startId || !endId || parseInt(startId) > parseInt(endId)) {
            alert('请选择有效的章节范围(开始章节 ≤ 结束章节)');
            return;
        }

        const selectedChapters = chapterList
            .filter(ch => parseInt(ch.id) >= parseInt(startId) && parseInt(ch.id) <= parseInt(endId))
            .sort((a, b) => parseInt(a.id) - parseInt(b.id));

        if (selectedChapters.length === 0) {
            alert('未找到选中范围内的章节');
            return;
        }

        const total = selectedChapters.length;
        let completed = 0;
        let allContent = '';
        const progressBar = dom.progressFill.parentElement;
        progressBar.style.display = 'block';
        dom.statusDiv.textContent = `开始下载 ${total} 个章节...`;
        dom.collectBtn.disabled = true;
        dom.downloadBtn.disabled = true;

        for (const chapter of selectedChapters) {
            try {
                const chapterIndex = chapterList.findIndex(c => c.id === chapter.id) + 1;
                dom.statusDiv.textContent = `正在下载: 第${chapterIndex}章 ${chapter.title}(${completed+1}/${total})`;

                const { chapterTitle, content } = await downloadChapter(chapter);
                if (content) {
                    allContent += `=== 第${chapterIndex}章 ${chapterTitle} ===\n${content}\n\n`;
                }

                completed++;
                dom.progressFill.style.width = `${(completed / total) * 100}%`;

                if (completed < total) {
                    await new Promise(resolve => setTimeout(resolve, CONFIG.downloadInterval));
                }
            } catch (e) {
                if (!confirm(`章节 ${chapter.title} 下载失败,是否继续?`)) {
                    dom.statusDiv.textContent = '下载已取消';
                    dom.collectBtn.disabled = false;
                    dom.downloadBtn.disabled = false;
                    return;
                }
            }
        }

        const bookTitle = document.querySelector('h1').textContent || `book_${bookId}`;
        const startIndex = chapterList.findIndex(c => c.id === startId) + 1;
        const endIndex = chapterList.findIndex(c => c.id === endId) + 1;
        const blob = new Blob([allContent], { type: 'text/plain;charset=utf-8' });
        const url = URL.createObjectURL(blob);

        GM_download({
            url: url,
            name: `${bookTitle}_第${startIndex}-${endIndex}章.txt`,
            onload: () => {
                dom.statusDiv.textContent = `下载完成!共${total}个章节`;
                dom.progressFill.style.width = '100%';
                dom.collectBtn.disabled = false;
                dom.downloadBtn.disabled = false;
                URL.revokeObjectURL(url);
            }
        });
    }

    window.addEventListener('load', init);
})();