QQ阅读Markdown下载器

自动解析QQ阅读页面内容并下载为Markdown文件

// ==UserScript==
// @name         QQ阅读Markdown下载器
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  自动解析QQ阅读页面内容并下载为Markdown文件
// @author       Ningest
// @match        https://book.qq.com/book-read/*
// @grant        GM_download
// @grant        GM_setClipboard
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/turndown.js
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // 全局变量
    let downloadButton = null;
    let isProcessing = false;

    // 主函数
    function init() {
        console.log('QQ阅读Markdown下载器: 开始初始化');
        
        if (!isQQReadPage()) {
            console.log('QQ阅读Markdown下载器: 不是QQ阅读页面或未找到article元素');
            return;
        }

        if (!checkCompatibility()) {
            console.warn('QQ阅读Markdown下载器: 浏览器兼容性检查失败');
            return;
        }

        // 等待页面加载完成
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', createUI);
        } else {
            createUI();
        }
    }

    // 检测是否为QQ阅读章节页面
    function isQQReadPage() {
        const isQQRead = window.location.href.includes('book.qq.com/book-read/');
        const hasArticle = document.getElementById('article') !== null;
        console.log('QQ阅读Markdown下载器: 页面检测结果:', { isQQRead, hasArticle });
        return isQQRead && hasArticle;
    }

    // 兼容性检查
    function checkCompatibility() {
        const requiredFeatures = [
            'Blob'
        ];
        
        // 检查URL API
        const hasURLAPI = typeof window.URL !== 'undefined' && 
                         typeof window.URL.createObjectURL === 'function' && 
                         typeof window.URL.revokeObjectURL === 'function';
        
        const missingFeatures = requiredFeatures.filter(feature => !window[feature]);
        
        if (missingFeatures.length > 0) {
            console.warn('QQ阅读Markdown下载器: 浏览器不支持以下功能:', missingFeatures);
            return false;
        }
        
        if (!hasURLAPI) {
            console.warn('QQ阅读Markdown下载器: 浏览器不支持URL API,将使用备用下载方案');
            // 不返回false,而是继续使用备用方案
        }
        
        return true;
    }

    // 创建用户界面
    function createUI() {
        console.log('QQ阅读Markdown下载器: 开始创建UI');
        
        if (downloadButton) {
            console.log('QQ阅读Markdown下载器: 按钮已存在,跳过创建');
            return; // 避免重复创建
        }

        downloadButton = createDownloadButton();
        document.body.appendChild(downloadButton);
        console.log('QQ阅读Markdown下载器: 按钮已添加到页面');

        // 添加CSS动画
        addCSS();
    }

    // 创建下载按钮
    function createDownloadButton() {
        const button = document.createElement('button');
        button.textContent = '下载Markdown';
        button.id = 'qq-read-markdown-downloader-btn';
        button.style.cssText = `
            position: fixed !important;
            top: 20px !important;
            right: 20px !important;
            z-index: 999999 !important;
            background: #007bff !important;
            color: white !important;
            border: none !important;
            padding: 10px 20px !important;
            border-radius: 5px !important;
            cursor: pointer !important;
            font-size: 14px !important;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2) !important;
            transition: all 0.3s ease !important;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
            display: block !important;
            visibility: visible !important;
            opacity: 1 !important;
        `;
        
        button.addEventListener('click', handleDownload);
        button.addEventListener('mouseenter', function() {
            this.style.background = '#0056b3 !important';
            this.style.transform = 'translateY(-2px)';
        });
        button.addEventListener('mouseleave', function() {
            this.style.background = '#007bff !important';
            this.style.transform = 'translateY(0)';
        });
        
        return button;
    }

    // 添加CSS样式
    function addCSS() {
        const style = document.createElement('style');
        style.textContent = `
            @keyframes slideIn {
                from {
                    opacity: 0;
                    transform: translateX(100%);
                }
                to {
                    opacity: 1;
                    transform: translateX(0);
                }
            }
            
            @keyframes fadeIn {
                from {
                    opacity: 0;
                }
                to {
                    opacity: 1;
                }
            }
            
            #qq-read-markdown-downloader-btn:hover {
                background: #0056b3 !important;
                transform: translateY(-2px) !important;
            }
        `;
        document.head.appendChild(style);
    }

    // 处理下载
    async function handleDownload() {
        if (isProcessing) {
            showNotification('正在处理中,请稍候...', 'info');
            return;
        }

        isProcessing = true;
        updateButtonState(true);

        try {
            const content = parseContent();
            if (!content) {
                throw new Error('无法解析页面内容');
            }

            const markdownContent = convertToMarkdown(content);
            const filename = generateFilename(content.metadata, content.title);
            
            downloadMarkdown(markdownContent, filename);
            showNotification('Markdown文件下载成功!', 'success');
            
        } catch (error) {
            handleError(error, 'handleDownload');
        } finally {
            isProcessing = false;
            updateButtonState(false);
        }
    }

    // 更新按钮状态
    function updateButtonState(processing) {
        if (downloadButton) {
            downloadButton.textContent = processing ? '处理中...' : '下载Markdown';
            downloadButton.disabled = processing;
            downloadButton.style.opacity = processing ? '0.7' : '1';
        }
    }

    // 解析页面内容
    function parseContent() {
        const article = document.getElementById('article');
        if (!article) {
            throw new Error('未找到文章内容容器');
        }
        
        return {
            title: extractTitle(),
            content: extractContent(article),
            metadata: extractMetadata()
        };
    }

    // 提取章节标题
    function extractTitle() {
        // 优先从h2.bt2提取(新版本)
        let titleElement = document.querySelector('#article h2.bt2');
        if (titleElement) {
            // 移除图片元素,只保留文本
            const clone = titleElement.cloneNode(true);
            const images = clone.querySelectorAll('img');
            images.forEach(img => img.remove());
            return clone.textContent.trim();
        }
        
        // 备用方案:从h3.bt3提取(旧版本)
        titleElement = document.querySelector('#article h3.bt3');
        if (titleElement) {
            return titleElement.textContent.trim();
        }
        
        // 最后备用方案:从页面标题提取
        const pageTitle = document.title;
        const match = pageTitle.match(/^(.+?)_(.+?)在线阅读/);
        return match ? match[2] : pageTitle;
    }

    // 提取正文内容
    function extractContent(articleElement) {
        // 克隆元素避免修改原DOM
        const clone = articleElement.cloneNode(true);
        
        // 移除不需要的元素
        const elementsToRemove = clone.querySelectorAll('script, style, .ad, .recommend, .footer, .rec-book');
        elementsToRemove.forEach(el => el.remove());
        
        return clone.innerHTML;
    }

    // 提取元数据
    function extractMetadata() {
        const metadata = {};
        
        // 提取书名
        const bookTitle = document.querySelector('.book-title');
        if (bookTitle) {
            metadata.bookTitle = bookTitle.textContent.trim();
        }
        
        // 提取作者(支持新版本的a标签和旧版本的span标签)
        let author = document.querySelector('.info li:nth-child(2) a');
        if (!author) {
            author = document.querySelector('.info li:nth-child(2) span');
        }
        if (author) {
            metadata.author = author.textContent.trim();
        }
        
        // 提取字数
        const wordCount = document.querySelector('.info li:nth-child(3) span');
        if (wordCount) {
            metadata.wordCount = wordCount.textContent.trim();
        }
        
        // 提取更新时间
        const updateTime = document.querySelector('.updateTime');
        if (updateTime) {
            metadata.updateTime = updateTime.textContent.trim();
        }
        
        return metadata;
    }

    // 转换为Markdown
    function convertToMarkdown(content) {
        const turndownService = new TurndownService({
            headingStyle: 'atx',
            codeBlockStyle: 'fenced',
            bulletListMarker: '-'
        });

        // 设置自定义规则
        setupTurndownRules(turndownService);

        // 转换HTML内容
        let markdown = turndownService.turndown(content.content);

        // 添加标题和元数据
        const fullMarkdown = generateFullMarkdown(content.title, content.metadata, markdown);

        return fullMarkdown;
    }

    // 设置Turndown规则
    function setupTurndownRules(turndownService) {
        // 代码块规则
        turndownService.addRule('codeBlocks', {
            filter: ['pre'],
            replacement: function(content, node) {
                const code = node.querySelector('code');
                if (!code) return content;
                
                const language = detectLanguage(code.textContent);
                return `\n\`\`\`${language}\n${code.textContent}\n\`\`\`\n`;
            }
        });

        // 代码清单标题规则
        turndownService.addRule('codeListTitle', {
            filter: function(node) {
                return node.nodeType === 1 && 
                       node.className === 'biaoti' && 
                       node.textContent.includes('代码清单');
            },
            replacement: function(content) {
                return `\n**${content}**\n`;
            }
        });

        // 正文段落规则
        turndownService.addRule('paragraphs', {
            filter: ['p'],
            replacement: function(content, node) {
                if (node.className === 'zw') {
                    return `\n${content}\n`;
                }
                return content;
            }
        });

        // 标题规则(支持h2.bt2和h3.bt3)
        turndownService.addRule('headings', {
            filter: ['h2', 'h3'],
            replacement: function(content, node) {
                if (node.className === 'bt2' || node.className === 'bt3') {
                    return `\n# ${content}\n`;
                }
                return `\n### ${content}\n`;
            }
        });
    }

    // 检测代码语言
    function detectLanguage(codeContent) {
        const firstLine = codeContent.split('\n')[0].toLowerCase();
        
        if (firstLine.includes('package') || firstLine.includes('import') || firstLine.includes('public class')) {
            return 'java';
        }
        if (firstLine.includes('create table') || firstLine.includes('select') || firstLine.includes('insert')) {
            return 'sql';
        }
        if (firstLine.includes('<?xml') || firstLine.includes('<!DOCTYPE')) {
            return 'xml';
        }
        if (firstLine.includes('function') || firstLine.includes('var ') || firstLine.includes('const ')) {
            return 'javascript';
        }
        if (firstLine.includes('import ') && firstLine.includes('from ')) {
            return 'javascript';
        }
        if (firstLine.includes('class ') && firstLine.includes('{')) {
            return 'java';
        }
        
        return '';
    }

    // 生成完整的Markdown内容
    function generateFullMarkdown(title, metadata, content) {
        let markdown = `# ${title}\n\n`;

        // 添加书籍信息
        if (metadata.bookTitle || metadata.author || metadata.wordCount || metadata.updateTime) {
            markdown += '**书籍信息:**\n';
            if (metadata.bookTitle) {
                markdown += `- 书名:${metadata.bookTitle}\n`;
            }
            if (metadata.author) {
                markdown += `- 作者:${metadata.author}\n`;
            }
            if (metadata.wordCount) {
                markdown += `- 字数:${metadata.wordCount}\n`;
            }
            if (metadata.updateTime) {
                markdown += `- 更新时间:${metadata.updateTime}\n`;
            }
            markdown += '\n---\n\n';
        }

        // 添加正文内容
        markdown += '## 正文内容\n\n';
        markdown += content;

        return markdown;
    }

    // 生成文件名
    function generateFilename(metadata, title) {
        const bookTitle = metadata.bookTitle || '未知书籍';
        const chapterTitle = title || '未知章节';
        const timestamp = new Date().toISOString().slice(0, 10);
        
        // 清理文件名中的非法字符
        const cleanBookTitle = bookTitle.replace(/[<>:"/\\|?*]/g, '');
        const cleanChapterTitle = chapterTitle.replace(/[<>:"/\\|?*]/g, '');
        
        return `${cleanBookTitle}_${cleanChapterTitle}_${timestamp}.md`;
    }

    // 下载Markdown文件
    function downloadMarkdown(markdownContent, filename) {
        // 检查是否支持URL API
        const hasURLAPI = typeof window.URL !== 'undefined' && 
                         typeof window.URL.createObjectURL === 'function' && 
                         typeof window.URL.revokeObjectURL === 'function';
        
        if (hasURLAPI) {
            // 使用URL API下载
            const blob = new Blob([markdownContent], { type: 'text/markdown;charset=utf-8' });
            const url = URL.createObjectURL(blob);
            
            const a = document.createElement('a');
            a.href = url;
            a.download = filename;
            a.style.display = 'none';
            
            document.body.appendChild(a);
            a.click();
            
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        } else {
            // 备用方案:使用data URL
            try {
                const dataUrl = 'data:text/markdown;charset=utf-8,' + encodeURIComponent(markdownContent);
                const a = document.createElement('a');
                a.href = dataUrl;
                a.download = filename;
                a.style.display = 'none';
                
                document.body.appendChild(a);
                a.click();
                
                document.body.removeChild(a);
            } catch (error) {
                // 最后的备用方案:复制到剪贴板并提示用户
                console.warn('QQ阅读Markdown下载器: 无法下载文件,将复制内容到剪贴板');
                if (typeof GM_setClipboard !== 'undefined') {
                    GM_setClipboard(markdownContent);
                    showNotification('内容已复制到剪贴板,请手动保存为.md文件', 'info');
                } else {
                    // 如果连GM_setClipboard都不可用,则显示内容让用户手动复制
                    showNotification('无法下载文件,请查看控制台输出', 'error');
                    console.log('Markdown内容:', markdownContent);
                }
            }
        }
    }

    // 显示通知
    function showNotification(message, type = 'info') {
        // 移除已存在的通知
        const existingNotification = document.querySelector('.qq-read-notification');
        if (existingNotification) {
            existingNotification.remove();
        }

        const notification = document.createElement('div');
        notification.className = 'qq-read-notification';
        notification.textContent = message;
        notification.style.cssText = `
            position: fixed;
            top: 80px;
            right: 20px;
            z-index: 10001;
            background: ${type === 'success' ? '#28a745' : type === 'error' ? '#dc3545' : '#007bff'};
            color: white;
            padding: 12px 20px;
            border-radius: 5px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            animation: slideIn 0.3s ease;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            font-size: 14px;
            max-width: 300px;
            word-wrap: break-word;
        `;
        
        document.body.appendChild(notification);
        
        setTimeout(() => {
            if (notification.parentNode) {
                notification.style.animation = 'fadeOut 0.3s ease';
                setTimeout(() => {
                    if (notification.parentNode) {
                        notification.remove();
                    }
                }, 300);
            }
        }, 3000);
    }

    // 错误处理
    function handleError(error, context) {
        console.error(`QQ阅读Markdown下载器错误 [${context}]:`, error);
        showNotification(`下载失败: ${error.message}`, 'error');
    }

    // 安全执行函数
    function safeExecute(func, context) {
        try {
            return func();
        } catch (error) {
            handleError(error, context);
            return null;
        }
    }

    // 启动插件
    init();

})();