豆瓣广播备份工具

备份豆瓣个人广播并保存为Markdown文件,支持批量备份和断点续传

当前为 2025-03-14 提交的版本,查看 最新版本

// ==UserScript==
// @name         豆瓣广播备份工具
// @name:zh-CN   豆瓣广播备份工具
// @name:en      Douban Status Backup Tool
// @namespace    https://github.com/pieck42/DoubanStatusBackup
// @version      1.0.0
// @description  备份豆瓣个人广播并保存为Markdown文件,支持批量备份和断点续传
// @description:zh-CN  备份豆瓣个人广播并保存为Markdown文件,支持批量备份和断点续传
// @description:en  Backup Douban personal status to Markdown files, support batch backup and breakpoint resume
// @author       Pieck
// @license      MIT
// @match        https://www.douban.com/people/*/statuses*
// @match        https://www.douban.com/mine/statuses*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dayjs.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @grant        none
// @compatible   chrome
// @compatible   firefox
// @compatible   edge
// @supportURL   https://github.com/pieck42/DoubanStatusBackup/issues
// @homepageURL  https://github.com/pieck42/DoubanStatusBackup
// ==/UserScript==

(function() {
    'use strict';

    // 添加样式
    const style = document.createElement('style');
    style.textContent = `
        .douban-backup-btn {
            display: inline-block;
            padding: 8px 16px;
            background-color: #41ac52;
            color: white;
            border-radius: 4px;
            cursor: pointer;
            margin: 10px 0;
            border: none;
            font-size: 14px;
        }
        .douban-backup-btn:hover {
            background-color: #37964a;
        }
        .douban-backup-container {
            margin: 20px 0;
            padding: 15px;
            background-color: #f7f7f7;
            border-radius: 4px;
        }
        .douban-backup-progress {
            width: 100%;
            height: 20px;
            margin-top: 10px;
            border-radius: 4px;
        }
    `;
    document.head.appendChild(style);

    // 创建备份按钮和容器
    const container = document.createElement('div');
    container.className = 'douban-backup-container';
    container.innerHTML = `
        <h3>豆瓣广播备份工具</h3>
        <button id="backupStatusBtn" class="douban-backup-btn">备份当前页面广播</button>
        <div style="margin-top: 10px;">
            <button id="backupAllStatusBtn" class="douban-backup-btn">备份多页广播</button>
            <span style="margin-left: 10px;">从第</span>
            <input type="number" id="startPage" placeholder="起始页" min="1" style="width: 60px; padding: 5px;">
            <span>到第</span>
            <input type="number" id="endPage" placeholder="结束页" min="1" style="width: 60px; padding: 5px;">
            <span>页</span>
        </div>
        <div id="backupInfo" style="margin-top: 10px;"></div>
        <progress id="backupProgress" class="douban-backup-progress" value="0" max="100" style="display: none;"></progress>
    `;

    // 找到适合插入按钮的位置
    const insertPoint = document.querySelector('.stream-items') || document.querySelector('#wrapper');
    if (insertPoint) {
        insertPoint.parentNode.insertBefore(container, insertPoint);
    }

    // 增强调试功能
    function debugLog(message, isError = false) {
        console.log(`[豆瓣备份] ${message}`);
        // 在页面上显示调试信息
        const infoElement = document.getElementById('backupInfo');
        if (infoElement) {
            if (isError) {
                infoElement.innerHTML += `<div style="color: red">${message}</div>`;
            } else {
                infoElement.textContent = message;
            }
        }
    }

    // 提取广播内容的函数 - 添加超时保护
    async function extractStatuses(container) {
        debugLog("开始提取广播内容...");
        const statusItems = container.querySelectorAll('.status-item');
        debugLog(`找到 ${statusItems.length} 条广播`);
        
        const statuses = [];
        let processedCount = 0;

        for (const item of statusItems) {
            try {
                // 检查是否是被转发广播中的原广播内容
                const isOriginalInReshared = item.closest('.status-real-wrapper') && 
                                           item.closest('.status-reshared-wrapper');
                
                // 跳过被转发广播中的原广播内容
                if (isOriginalInReshared) {
                    continue;
                }
                
                // 添加超时保护
                const extractPromise = extractSingleStatus(item);
                const timeoutPromise = new Promise((_, reject) => 
                    setTimeout(() => reject(new Error("提取超时")), 5000)
                );
                
                const status = await Promise.race([extractPromise, timeoutPromise]);
                if (status) {
                    statuses.push(status);
                }
                
                processedCount++;
                debugLog(`已处理 ${processedCount}/${statusItems.length} 条广播`);
            } catch (error) {
                console.error("处理广播时出错:", error);
                debugLog(`处理广播时出错: ${error.message}`, true);
                // 继续处理下一条广播
                processedCount++;
            }
        }

        debugLog(`广播提取完成,共 ${statuses.length} 条`);
        return statuses;
    }

    // 修改 fetchComments 函数,添加对转发广播的检查
    async function fetchComments(item) {
        const comments = [];
        
        // 检查是否在转发广播的原广播内容中
        const isInResharedOriginal = item.closest('.status-real-wrapper') && 
                                    item.closest('.status-reshared-wrapper');
        
        // 如果是转发广播中的原广播,直接返回空评论
        if (isInResharedOriginal) {
            return comments;
        }
        
        // 获取评论按钮
        const commentBtn = item.querySelector('.btn-action-reply');
        if (!commentBtn) return comments;
        
        // 检查是否会导致页面跳转
        const willRedirect = !commentBtn.getAttribute('data-action-type') || 
                            commentBtn.getAttribute('data-action-type') !== 'showComments';
        
        // 如果会导致跳转,则不获取评论
        if (willRedirect) {
            return [{
                content: `[该广播有回应,请访问原文查看]`,
                author: {
                    name: '系统提示',
                    uid: '',
                    link: ''
                }
            }];
        }
        
        // 点击评论按钮显示评论
        commentBtn.click();
        
        // 等待评论加载
        await new Promise(resolve => setTimeout(resolve, 500));
        
        // 获取评论容器
        const commentsContainer = item.querySelector('.comments-items');
        if (!commentsContainer) return comments;
        
        // 获取所有评论项
        const commentItems = commentsContainer.querySelectorAll('.lite-comment-item');
        
        commentItems.forEach(commentItem => {
            const contentElement = commentItem.querySelector('.lite-comment-item-content');
            const authorElement = commentItem.querySelector('.lite-comment-item-author');
            
            if (contentElement && authorElement) {
                comments.push({
                    content: contentElement.textContent.trim(),
                    author: {
                        name: authorElement.textContent.trim(),
                        uid: authorElement.href ? authorElement.href.match(/people\/([^\/]+)/)?.[1] || '' : '',
                        link: authorElement.href || ''
                    }
                });
            }
        });
        
        return comments;
    }

    // 修改 extractSpecialContent 函数,增强对话题讨论类型的支持
    function extractSpecialContent(item) {
        // 检查是否是话题讨论类型
        const isTopicDiscussion = item.getAttribute('data-atype') === 'group/topic' || 
                                 item.getAttribute('data-atype') === 'personal/topic';
        
        // 如果是话题讨论,优先查找 blockquote 中的内容
        if (isTopicDiscussion) {
            const blockquote = item.querySelector('blockquote');
            if (blockquote) {
                const paragraph = blockquote.querySelector('p');
                if (paragraph) {
                    return paragraph.textContent.trim();
                }
                return blockquote.textContent.trim();
            }
        }
        
        // 尝试查找可能包含内容的所有元素
        const contentSelectors = [
            'blockquote p', 
            '.bd p', 
            '.content p',
            '.status-saying p',
            '.status-content p',
            '.text p',
            'p[style*="white-space"]', // 针对您截图中的特殊样式
            '.content blockquote p'    // 特别针对您的案例
        ];
        
        for (const selector of contentSelectors) {
            const elements = item.querySelectorAll(selector);
            if (elements.length > 0) {
                const texts = Array.from(elements).map(el => el.textContent.trim()).filter(text => text);
                if (texts.length > 0) {
                    return texts.join('\n\n');
                }
            }
        }
        
        return null;
    }

    // 修改 extractSingleStatus 函数,优先处理话题讨论类型
    async function extractSingleStatus(item) {
        // 检查是否已被删除
        const isDeleted = item.classList.contains('deleted') || item.classList.contains('hidden');
        
        // 检查是否是转发类型的广播
        const isReshared = item.classList.contains('status-reshared-wrapper') || 
                           item.parentNode.classList.contains('status-reshared-wrapper');
        
        // 获取广播ID
        const statusId = item.getAttribute('data-sid') || item.id.replace('status-', '');
        debugLog(`正在处理广播 ID: ${statusId}`);
        
        // 获取广播类型
        const statusType = item.getAttribute('data-atype') || '';
        
        // 获取时间
        const timeElement = item.querySelector('.created_at') || item.querySelector('.lnk-time');
        let timeText = timeElement ? timeElement.textContent.trim() : '';
        const timeHref = timeElement && timeElement.href ? timeElement.href : '';
        
        // 获取完整时间(包括时分秒)
        let fullTimeText = '';
        if (timeElement && timeElement.getAttribute('title')) {
            // 从title属性中获取完整时间
            fullTimeText = timeElement.getAttribute('title');
        } else if (timeElement && timeElement.getAttribute('data-time')) {
            // 从data-time属性中获取时间戳
            const timestamp = parseInt(timeElement.getAttribute('data-time'));
            if (!isNaN(timestamp)) {
                const date = new Date(timestamp * 1000);
                fullTimeText = date.toLocaleString('zh-CN');
            }
        }

        // 如果没有获取到完整时间,使用页面显示的时间
        if (!fullTimeText) {
            fullTimeText = timeText;
        }
        
        // 获取作者信息
        const authorElement = item.querySelector('.lnk-people') || item.querySelector('.user-name');
        const author = {
            name: authorElement ? authorElement.textContent.trim() : '',
            uid: authorElement && authorElement.href ? authorElement.href.match(/people\/([^\/]+)/)?.[1] || '' : '',
            link: authorElement && authorElement.href ? authorElement.href : ''
        };
        
        // 获取活动信息
        const activityElement = item.querySelector('.activity');
        let activity = activityElement ? activityElement.textContent.trim() : '';

        // 如果没有找到活动元素,尝试从文本中提取活动类型
        if (!activity) {
            // 检查是否包含"看过"、"读过"、"在看"等标识
            const textDiv = item.querySelector('.text');
            if (textDiv) {
                const textContent = textDiv.textContent.trim();
                if (textContent.includes('看过')) {
                    activity = '看过';
                } else if (textContent.includes('读过')) {
                    activity = '读过';
                } else if (textContent.includes('在看')) {
                    activity = '在看';
                } else if (textContent.includes('想看')) {
                    activity = '想看';
                } else if (textContent.includes('想读')) {
                    activity = '想读';
                }
            }
        }
        
        // 获取评分信息
        let rating = '';
        const ratingElement = item.querySelector('.rating-stars');
        if (ratingElement) {
            rating = ratingElement.textContent.trim();
        }
        
        // 获取正文内容 - 针对话题讨论类型特殊处理
        let contentText = '';
        
        // 如果是话题讨论类型,优先使用特殊提取方法
        if (statusType === 'group/topic' || statusType === 'personal/topic') {
            const specialContent = extractSpecialContent(item);
            if (specialContent) {
                contentText = specialContent;
            }
        }
        
        // 如果特殊提取没有结果,继续使用常规方法
        if (!contentText) {
            // 获取广播内容 - 增强版
            const statusSaying = item.querySelector('.status-saying');
            if (statusSaying) {
                // 移除多余的空行和空格
                contentText = statusSaying.textContent.replace(/\s+/g, ' ').trim();
                
                // 如果内容包含"转发:",则正确格式化
                if (contentText.includes('转发:')) {
                    contentText = contentText.replace(/转发:\s+/, '转发:');
                }
                
                // 移除JavaScript图片代码
                contentText = removePhotoScript(contentText);
            } else {
                // 如果没有 .status-saying,尝试其他选择器
                const contentElement = item.querySelector('.status-content') || 
                                      item.querySelector('.text') || 
                                      item.querySelector('.content') ||
                                      item.querySelector('p') ||  // 添加对普通段落的支持
                                      item.querySelector('blockquote p'); // 添加对引用块中段落的支持
                
                if (contentElement) {
                    contentText = contentElement.textContent.replace(/\s+/g, ' ').trim();
                    // 移除JavaScript图片代码
                    contentText = removePhotoScript(contentText);
                }
            }
        }

        // 如果内容仍然为空,尝试从其他元素获取
        if (!contentText) {
            // 尝试从.bd元素获取所有文本内容
            const bdElement = item.querySelector('.bd');
            if (bdElement) {
                const statusSayingInBd = bdElement.querySelector('.status-saying');
                if (statusSayingInBd) {
                    const blockquote = statusSayingInBd.querySelector('blockquote');
                    if (blockquote) {
                        const paragraph = blockquote.querySelector('p');
                        if (paragraph) {
                            contentText = paragraph.textContent.trim();
                        } else {
                            contentText = blockquote.textContent.trim();
                        }
                    } else {
                        contentText = statusSayingInBd.textContent.trim();
                    }
                    // 移除JavaScript图片代码
                    contentText = removePhotoScript(contentText);
                }
            }
        }

        // 如果仍然为空,尝试获取所有可见文本,包括嵌套在深层的内容
        if (!contentText) {
            // 添加对嵌套内容的支持
            const allTextElements = item.querySelectorAll('p, div.text, div.content, blockquote p, .content p');
            const allTexts = [];
            allTextElements.forEach(el => {
                if (el.textContent.trim() && !el.querySelector('a, .created_at, .actions')) {
                    allTexts.push(el.textContent.trim());
                }
            });
            contentText = allTexts.join('\n\n');
            // 移除JavaScript图片代码
            contentText = removePhotoScript(contentText);
        }
        
        // 获取话题信息 - 增强版
        let topic = null;
        
        // 如果是话题讨论类型,优先从 data-aid 和 data-atypecn 获取信息
        if (statusType === 'group/topic' || statusType === 'personal/topic') {
            const topicId = item.getAttribute('data-aid');
            const topicType = item.getAttribute('data-atypecn') || '话题讨论';
            
            if (topicId) {
                // 尝试从内容区域找到话题链接
                const contentDiv = item.querySelector('.content');
                let topicUrl = '';
                let topicTitle = topicType;
                
                if (contentDiv) {
                    const topicLink = contentDiv.querySelector('a[href*="/topic/"]');
                    if (topicLink) {
                        topicUrl = topicLink.href;
                        topicTitle = topicLink.textContent.trim() || topicType;
                    } else {
                        // 如果找不到链接,构造一个可能的URL
                        topicUrl = `https://www.douban.com/topic/${topicId}/`;
                    }
                } else {
                    // 如果找不到内容区域,构造一个可能的URL
                    topicUrl = `https://www.douban.com/topic/${topicId}/`;
                }
                
                topic = {
                    title: topicTitle,
                    url: topicUrl
                };
            }
        }
        
        // 如果上面的方法没有找到话题,使用常规方法
        if (!topic) {
            const topicElement = item.querySelector('.title a') || item.querySelector('a[href*="/topic/"]');
            topic = topicElement ? {
                title: topicElement.textContent.trim() || '话题讨论',
                url: topicElement.href
            } : null;
        }
        
        // 获取图片
        const imageElements = item.querySelectorAll('.status-images img, .topic-pics img, .pics-wrapper img');
        const images = Array.from(imageElements).map(img => ({
            small: { url: img.src },
            large: { url: img.src.replace('/small/', '/large/').replace('/medium/', '/large/') },
            alt: img.alt || '图片'
        }));
        
        // 获取卡片信息(推荐内容)
        const cardElement = item.querySelector('.card') || item.querySelector('.subject-card');
        let card = null;
        if (cardElement) {
            const cardTitleElement = cardElement.querySelector('.title a') || cardElement.querySelector('a');
            card = {
                title: cardTitleElement ? cardTitleElement.textContent.trim() : '',
                url: cardTitleElement && cardTitleElement.href ? cardTitleElement.href : '',
                description: cardElement.querySelector('.card-summary') ? 
                            cardElement.querySelector('.card-summary').textContent.trim() : ''
            };
        }
        
        // 获取评论数和评论内容
        let comments = [];
        let commentCount = 0;
        
        // 检查是否在转发广播的原广播内容中
        const isInResharedOriginal = item.closest('.status-real-wrapper') && 
                                    item.closest('.status-reshared-wrapper');
        
        // 只处理非转发广播原文的评论
        if (!isInResharedOriginal) {
            const commentCountElement = item.querySelector('.btn-action-reply');
            commentCount = commentCountElement ? 
                parseInt(commentCountElement.getAttribute('data-count')) || 
                parseInt(commentCountElement.textContent.match(/\d+/)?.[0]) || 0 : 0;
            
            if (commentCount > 0 && commentCountElement) {
                const willRedirect = !commentCountElement.getAttribute('data-action-type') || 
                                   commentCountElement.getAttribute('data-action-type') !== 'showComments';
                
                if (!willRedirect) {
                    try {
                        comments = await fetchComments(item);
                    } catch (error) {
                        console.error('获取评论失败:', error);
                        comments = [{
                            content: `[该广播有 ${commentCount} 条回应,但获取失败]`,
                            author: {
                                name: '系统提示',
                                uid: '',
                                link: ''
                            }
                        }];
                    }
                } else {
                    comments = [{
                        content: `[该广播有 ${commentCount} 条回应,请访问原文查看]`,
                        author: {
                            name: '系统提示',
                            uid: '',
                            link: ''
                        }
                    }];
                }
            }
        }
        
        // 获取点赞数
        const likeCountElement = item.querySelector('.like-count');
        const likeCount = likeCountElement ? 
            parseInt(likeCountElement.textContent.match(/\d+/)?.[0]) || 0 : 0;
        
        // 获取转发信息 - 重要修改
        let resharedStatus = null;
        
        // 检查是否有转发内容
        const statusRealWrapper = item.querySelector('.status-real-wrapper') || 
                                 (item.parentNode && item.parentNode.querySelector('.status-real-wrapper'));
        
        if (statusRealWrapper) {
            // 获取被转发广播的ID
            const resharedId = statusRealWrapper.getAttribute('data-sid') || '';
            
            // 获取被转发广播的作者
            const resharedAuthorElement = statusRealWrapper.querySelector('.lnk-people') || 
                                         statusRealWrapper.querySelector('.user-name');
            
            // 获取被转发广播的内容
            const resharedContentElement = statusRealWrapper.querySelector('.status-saying') || 
                                          statusRealWrapper.querySelector('.status-content') || 
                                          statusRealWrapper.querySelector('.text') || 
                                          statusRealWrapper.querySelector('.content');
            
            let resharedText = '';
            if (resharedContentElement) {
                // 尝试获取blockquote中的内容
                const blockquote = resharedContentElement.querySelector('blockquote');
                if (blockquote) {
                    resharedText = blockquote.textContent.trim();
                } else {
                    resharedText = resharedContentElement.textContent.trim();
                }
            }
            
            // 获取被转发广播的图片
            const resharedImageElements = statusRealWrapper.querySelectorAll('.status-images img, .topic-pics img, .pics-wrapper img');
            const resharedImages = Array.from(resharedImageElements).map(img => ({
                small: { url: img.src },
                large: { url: img.src.replace('/small/', '/large/').replace('/medium/', '/large/') },
                alt: img.alt || '图片'
            }));
            
            // 获取被转发广播的卡片
            const resharedCardElement = statusRealWrapper.querySelector('.card') || 
                                       statusRealWrapper.querySelector('.subject-card');
            let resharedCard = null;
            if (resharedCardElement) {
                const cardTitleElement = resharedCardElement.querySelector('.title a') || 
                                        resharedCardElement.querySelector('a');
                resharedCard = {
                    title: cardTitleElement ? cardTitleElement.textContent.trim() : '',
                    url: cardTitleElement && cardTitleElement.href ? cardTitleElement.href : '',
                    description: resharedCardElement.querySelector('.card-summary') ? 
                                resharedCardElement.querySelector('.card-summary').textContent.trim() : ''
                };
            }
            
            // 构建被转发广播的信息
            resharedStatus = {
                id: resharedId,
                author: {
                    name: resharedAuthorElement ? resharedAuthorElement.textContent.trim() : '',
                    uid: resharedAuthorElement && resharedAuthorElement.href ? 
                         resharedAuthorElement.href.match(/people\/([^\/]+)/)?.[1] || '' : '',
                    link: resharedAuthorElement && resharedAuthorElement.href ? resharedAuthorElement.href : ''
                },
                text: resharedText,
                images: resharedImages,
                card: resharedCard
            };
        }
        
        // 返回提取的广播信息
        return {
            id: statusId,
            type: statusType,
            create_time: timeText,
            full_time: fullTimeText,
            sharing_url: timeHref,
            original_url: timeHref || `https://www.douban.com/people/${author.uid}/status/${statusId}/`,
            author: author,
            activity: activity,
            rating: rating,
            text: contentText,
            topic: topic,
            images: images,
            card: card,
            reshared_status: resharedStatus,
            comment_count: commentCount,
            like_count: likeCount,
            deleted: isDeleted,
            hidden: isDeleted,
            comments: comments
        };
    }

    // 添加一个新函数来移除JavaScript图片代码
    function removePhotoScript(text) {
        // 匹配 (function() {...})() 这种模式的代码,同时也匹配后面可能跟随的"长图"等描述
        return text.replace(/\(function\s*\(\)\s*\{[\s\S]*?CREATE_HONRIZONTAL_PHOTOS[\s\S]*?\}\s*\)\(\)\s*(?:长图|小图)?/g, '').trim();
    }

    // 将广播转换为Markdown格式
    function statusesToMarkdown(statuses, userName) {
        const now = new Date();
        const dateStr = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2,'0')}-${now.getDate().toString().padStart(2,'0')}`;
        
        let md = `# 豆瓣用户 ${userName} 的广播备份\n\n`;
        md += `*备份时间:${dateStr}*\n\n`;
        md += `*共备份 ${statuses.length} 条广播*\n\n`;
        md += `---\n\n`;

        // 格式化广播内容的函数
        function formatStatus(status, isReshared = false) {
            let result = '';
            
            if (!isReshared) {
                result += `## 广播 ${status.id}\n\n`;
                
                // 添加完整时间和原始地址
                if (status.full_time) {
                    result += `**时间**:${status.full_time}\n\n`;
                } else {
                    result += `**时间**:${status.create_time}\n\n`;
                }
                
                // 添加原始地址
                if (status.original_url) {
                    result += `**原始地址**:[${status.original_url}](${status.original_url})\n\n`;
                }
                
                if (status.type) {
                    result += `**类型**:${status.type}\n\n`;
                }
            }
            
            // 添加作者信息和活动
            if (isReshared) {
                result += `**原作者**:[${status.author.name}](${status.author.link}) (@${status.author.uid})\n\n`;
            } else if (status.activity) {
                // 如果是特定活动类型,使用更友好的格式
                if (['看过', '读过', '在看', '想看', '想读'].includes(status.activity)) {
                    result += `**动态**:[${status.author.name}](${status.author.link}) (@${status.author.uid}) ${status.activity}`;
                    
                    // 如果有评分,添加评分信息
                    if (status.rating) {
                        result += ` **评分**:${status.rating}`;
                    }
                    
                    result += `\n\n`;
                } else {
                    result += `**动态**:[${status.author.name}](${status.author.link}) (@${status.author.uid}) ${status.activity}\n\n`;
                }
            }
            
            // 添加内容
            if (status.text) {
                result += `**内容**:${status.text}\n\n`;
            }
            
            // 添加话题
            if (status.topic) {
                result += `**话题**:[${status.topic.title}](${status.topic.url})\n\n`;
            }
            
            // 添加卡片(推荐内容)
            if (status.card) {
                if (status.type === 'movie' || status.text.includes('看过')) {
                    result += `**电影**:[${status.card.title}](${status.card.url})`;
                    if (status.card.rating) {
                        result += ` - 评分:${status.card.rating}`;
                    }
                    result += `\n\n`;
                } else if (status.type === 'book' || status.text.includes('读过')) {
                    result += `**图书**:[${status.card.title}](${status.card.url})`;
                    if (status.card.rating) {
                        result += ` - 评分:${status.card.rating}`;
                    }
                    result += `\n\n`;
                } else if (status.text.includes('分享电视剧')) {
                    result += `**电视剧**:[${status.card.title}](${status.card.url})`;
                    if (status.card.rating) {
                        result += ` - 评分:${status.card.rating}`;
                    }
                    result += `\n\n`;
                } else if (status.text.includes('分享网页')) {
                    result += `**网页**:[${status.card.title}](${status.card.url})\n\n`;
                } else {
                    result += `**推荐**:[${status.card.title}](${status.card.url})\n\n`;
                }
                
                if (status.card.description) {
                    result += `**描述**:${status.card.description}\n\n`;
                }
            }
            
            // 添加图片
            if (status.images && status.images.length > 0) {
                result += `**图片**:\n\n`;
                status.images.forEach(img => {
                    result += `![${img.alt}](${img.large.url})\n\n`;
                });
            }
            
            // 添加播客单集信息
            if (status.podcast_episode) {
                result += `**播客单集**:[${status.podcast_episode.title}](${status.podcast_episode.url})\n\n`;
                
                if (status.podcast_episode.podcast.title) {
                    result += `**播客**:[${status.podcast_episode.podcast.title}](${status.podcast_episode.podcast.url})\n\n`;
                }
                
                if (status.podcast_episode.description) {
                    result += `**描述**:${status.podcast_episode.description}\n\n`;
                }
                
                if (status.podcast_episode.duration) {
                    result += `**时长**:${status.podcast_episode.duration}\n\n`;
                }
            }
            
            // 添加影评信息
            if (status.review) {
                result += `**影评**:[${status.review.title}](${status.review.url})\n\n`;
                
                if (status.review.subject) {
                    result += `**影片**:[${status.review.subject.title}](${status.review.subject.url})\n\n`;
                }
                
                if (status.review.content) {
                    result += `**内容摘要**:${status.review.content}\n\n`;
                }
            }
            
            return result;
        }

        statuses.forEach(status => {
            let statusMd = formatStatus(status);
            
            // 修改转发信息的处理方式
            if (status.reshared_status) {
                // 使用更清晰的格式显示转发内容
                statusMd += `**转发内容**:\n\n`;
                statusMd += `> **原作者**:[${status.reshared_status.author.name}](${status.reshared_status.author.link}) (@${status.reshared_status.author.uid})\n>\n`;
                statusMd += `> **内容**:${status.reshared_status.text}\n>\n`;
                
                // 添加转发的图片
                if (status.reshared_status.images && status.reshared_status.images.length > 0) {
                    statusMd += `> **图片**:\n>\n`;
                    status.reshared_status.images.forEach(img => {
                        statusMd += `> ![${img.alt}](${img.large.url})\n>\n`;
                    });
                }
                
                // 添加转发的卡片信息
                if (status.reshared_status.card) {
                    statusMd += `> **推荐**:[${status.reshared_status.card.title}](${status.reshared_status.card.url})\n>\n`;
                    if (status.reshared_status.card.description) {
                        statusMd += `> **描述**:${status.reshared_status.card.description}\n>\n`;
                    }
                }
            }
            
            // 添加互动数据
            statusMd += `**互动**:${status.like_count} 人赞 · ${status.comment_count} 条回应\n\n`;
            
            // 添加评论/回应
            if (status.comments && status.comments.length > 0) {
                statusMd += `**回应**:\n\n`;
                status.comments.forEach((comment, i) => {
                    statusMd += `${i+1}. **[${comment.author.name}](${comment.author.link})** (@${comment.author.uid}): ${comment.content}\n`;
                });
                statusMd += `\n`;
            } else if (status.comment_count > 0) {
                statusMd += `**回应**:共 ${status.comment_count} 条\n\n`;
            }
            
            // 添加分隔线
            statusMd += `---\n\n`;
            
            md += statusMd;
        });

        return md;
    }

    // 添加JSZip库
    function loadJSZip() {
        return new Promise((resolve, reject) => {
            if (window.JSZip) {
                resolve(window.JSZip);
                return;
            }
            
            const script = document.createElement('script');
            script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js';
            script.onload = () => resolve(window.JSZip);
            script.onerror = () => reject(new Error('Failed to load JSZip'));
            document.head.appendChild(script);
        });
    }

    // 保存为Markdown文件并返回Promise
    function saveAsMarkdownAsync(content, fileName) {
        return new Promise((resolve) => {
            const blob = new Blob([content], {type: 'text/markdown;charset=utf-8'});
            const link = document.createElement('a');
            link.href = URL.createObjectURL(blob);
            link.download = fileName;
            link.click();
            URL.revokeObjectURL(link.href);
            
            // 给用户一些时间保存文件
            setTimeout(() => {
                resolve();
            }, 1000);
        });
    }

    // 创建并下载ZIP文件
    async function createAndDownloadZip(files, zipName) {
        try {
            const JSZip = await loadJSZip();
            const zip = new JSZip();
            
            // 添加所有文件到zip
            files.forEach(file => {
                zip.file(file.name, file.content);
            });
            
            // 生成zip
            const zipContent = await zip.generateAsync({type: 'blob'});
            
            // 下载zip
            const link = document.createElement('a');
            link.href = URL.createObjectURL(zipContent);
            link.download = zipName;
            link.click();
            URL.revokeObjectURL(link.href);
            
            return true;
        } catch (error) {
            console.error('创建ZIP文件失败:', error);
            return false;
        }
    }

    // 获取当前页码
    function getCurrentPage() {
        const urlParams = new URLSearchParams(window.location.search);
        return parseInt(urlParams.get('p')) || 1;
    }

    // 获取总页数(修改版)
    function getTotalPages() {
        const paginator = document.querySelector('.paginator');
        let totalPages = 1;
        
        if (paginator) {
            // 首先尝试从分页器中找到最大页码
            const pageLinks = paginator.querySelectorAll('a');
            for (const link of pageLinks) {
                if (/\d+/.test(link.textContent)) {
                    const pageNum = parseInt(link.textContent);
                    if (!isNaN(pageNum) && pageNum > totalPages) {
                        totalPages = pageNum;
                    }
                }
            }
            
            // 检查是否有"后页"链接,如果有,说明还有更多页
            const nextPageLink = paginator.querySelector('a.next');
            if (nextPageLink) {
                // 豆瓣通常每页显示10条广播,尝试估算总页数
                const statusItems = document.querySelectorAll('.status-item');
                if (statusItems.length > 0) {
                    // 假设每页最多显示10条广播
                    const itemsPerPage = 10;
                    // 尝试从URL或其他地方获取总条目数
                    // 如果无法获取,至少将总页数设置为一个较大的值,如100
                    totalPages = Math.max(totalPages, 100);
                }
            }
        }
        
        return totalPages;
    }

    // 获取用户名
    function getUserName() {
        // 尝试从页面中获取用户名
        const userNameElement = document.querySelector('h1') || document.querySelector('.info h1');
        if (userNameElement) {
            return userNameElement.textContent.trim();
        }
        
        // 尝试从URL获取用户ID
        const urlMatch = location.href.match(/\/people\/([^\/]+)/);
        if (urlMatch && urlMatch[1]) {
            return urlMatch[1];
        }
        
        return '豆瓣用户';
    }

    // 创建Markdown文件对象的辅助函数
    function createMarkdownFile(content, fileName) {
        return {
            name: fileName,
            content: content
        };
    }

    // 备份当前页面广播 - 添加超时保护
    document.getElementById('backupStatusBtn').addEventListener('click', async () => {
        const infoElement = document.getElementById('backupInfo');
        infoElement.textContent = '正在备份当前页面广播...';
        
        try {
            const userName = getUserName();
            const currentPage = getCurrentPage(); // 获取当前页码
            const statusContainer = document.querySelector('.stream-items') || document.getElementById('wrapper');
            
            // 设置总体超时
            const extractionPromise = extractStatuses(statusContainer);
            const timeoutPromise = new Promise((_, reject) => 
                setTimeout(() => reject(new Error("整体提取超时,可能是页面结构复杂")), 30000)
            );
            
            const statuses = await Promise.race([extractionPromise, timeoutPromise]);
            
            if (statuses.length === 0) {
                infoElement.textContent = '未找到任何广播内容!';
                return;
            }
            
            infoElement.textContent = '正在生成Markdown文件...';
            const markdownContent = statusesToMarkdown(statuses, userName);
            // 修改文件名,添加当前页码
            const fileName = `豆瓣广播_${userName}_第${currentPage}页_${new Date().toISOString().split('T')[0]}.md`;
            
            infoElement.textContent = '正在保存文件...';
            await saveAsMarkdownAsync(markdownContent, fileName);
            
            infoElement.textContent = `成功备份第 ${currentPage} 页,共 ${statuses.length} 条广播!`;
        } catch (error) {
            console.error('备份过程中出错:', error);
            infoElement.textContent = `备份失败: ${error.message}`;
        }
    });

    // 备份多页广播(修复版 - 确保跨页面连续运行)
    document.getElementById('backupAllStatusBtn').addEventListener('click', async () => {
        initBackup();
    });

    // 初始化备份过程
    function initBackup() {
        const infoElement = document.getElementById('backupInfo');
        const progressBar = document.getElementById('backupProgress');
        progressBar.style.display = 'block';
        
        // 首次点击按钮时,初始化备份状态
        if (!localStorage.getItem('doubanBackupState')) {
            // 获取起始页和结束页
            const startPageInput = document.getElementById('startPage');
            const endPageInput = document.getElementById('endPage');
            
            let startPage = parseInt(startPageInput.value) || 1;
            let endPage = parseInt(endPageInput.value) || 0;
            
            if (startPage < 1) startPage = 1;
            
            const userName = getUserName();
            
            // 获取估计的总页数
            const estimatedTotalPages = getTotalPages();
            
            // 如果没有设置结束页,使用估计的总页数
            if (endPage <= 0) {
                endPage = estimatedTotalPages;
            }
            
            // 如果起始页大于结束页,交换它们
            if (startPage > endPage) {
                [startPage, endPage] = [endPage, startPage];
            }
            
            // 保存备份状态到localStorage
            const state = {
                startPage: startPage,
                endPage: endPage,
                currentPage: startPage,
                userName: userName,
                originalPage: getCurrentPage(), // 记录初始页面
                status: 'running',
                timestamp: Date.now(),
                processed: [] // 保存已处理页面
            };
            
            localStorage.setItem('doubanBackupState', JSON.stringify(state));
            console.log("初始化备份状态:", state);
        }
        
        // 处理当前页面
        processCurrrentPage();
    }

    // 处理当前页面内容
    async function processCurrrentPage() {
        if (!localStorage.getItem('doubanBackupState')) {
            console.log("没有进行中的备份");
            return;
        }
        
        const infoElement = document.getElementById('backupInfo');
        const progressBar = document.getElementById('backupProgress');
        progressBar.style.display = 'block';
        
        const state = JSON.parse(localStorage.getItem('doubanBackupState'));
        console.log("当前备份状态:", state);
        
        const { startPage, endPage, currentPage, userName, originalPage, processed } = state;
        
        // 当前页码
        const actualCurrentPage = getCurrentPage();
        console.log(`准备处理页面: 目标页=${currentPage}, 当前页=${actualCurrentPage}`);
        
        // 显示当前进度
        infoElement.textContent = `正在备份第 ${currentPage} 页 (总进度: ${processed.length+1}/${endPage-startPage+1})`;
        const progress = ((processed.length) / (endPage - startPage + 1)) * 100;
        progressBar.value = progress;
        
        // 如果当前页面不是目标处理页,跳转到目标页
        if (actualCurrentPage !== currentPage) {
            infoElement.textContent = `当前是第 ${actualCurrentPage} 页,正在跳转到目标备份页 ${currentPage}...`;
            
            setTimeout(() => {
                const targetUrl = new URL(window.location.href);
                targetUrl.searchParams.set('p', currentPage);
                window.location.href = targetUrl.toString();
            }, 1000);
            return;
        }
        
        // 处理当前页面
        try {
            infoElement.textContent = `正在提取第 ${currentPage} 页数据...`;
            const statusContainer = document.querySelector('.stream-items') || document.getElementById('wrapper');
            
            if (!statusContainer) {
                throw new Error("找不到广播容器");
            }
            
            const statuses = await extractStatuses(statusContainer);
            
            if (statuses.length > 0) {
                infoElement.textContent = `正在生成第 ${currentPage} 页的Markdown...`;
                const markdownContent = statusesToMarkdown(statuses, userName);
                const fileName = `豆瓣广播_${userName}_第${currentPage}页_${new Date().toISOString().split('T')[0]}.md`;
                
                // 保存文件
                await saveAsMarkdownAsync(markdownContent, fileName);
                
                infoElement.textContent = `已备份第 ${currentPage} 页,共 ${statuses.length} 条广播`;
                
                // 记录已处理的页面
                if (!processed.includes(currentPage)) {
                    processed.push(currentPage);
                }
            } else {
                infoElement.textContent = `第 ${currentPage} 页没有广播内容`;
            }
            
            // 更新状态
            state.processed = processed;
            
            // 检查是否已完成所有页面
            if (currentPage >= endPage || processed.length >= (endPage - startPage + 1)) {
                // 备份完成
                infoElement.textContent = `备份完成!已备份从第 ${startPage} 页到第 ${endPage} 页的内容`;
                progressBar.style.display = 'none';
                
                // 清除备份状态
                localStorage.removeItem('doubanBackupState');
                
                // 返回到原始页面
                if (getCurrentPage() !== originalPage) {
                    infoElement.textContent = `备份完成!正在返回第 ${originalPage} 页...`;
                    
                    setTimeout(() => {
                        const finalUrl = new URL(window.location.href);
                        finalUrl.searchParams.set('p', originalPage);
                        window.location.href = finalUrl.toString();
                    }, 2000);
                }
            } else {
                // 更新状态到下一页
                state.currentPage = currentPage + 1;
                localStorage.setItem('doubanBackupState', JSON.stringify(state));
                
                // 随机延迟后加载下一页
                const delay = 2000 + Math.floor(Math.random() * 3000);
                infoElement.textContent = `准备加载第 ${state.currentPage} 页 (${delay/1000}秒后)...`;
                
                setTimeout(() => {
                    // 导航到下一页
                    const nextPageUrl = new URL(window.location.href);
                    nextPageUrl.searchParams.set('p', state.currentPage);
                    window.location.href = nextPageUrl.toString();
                }, delay);
            }
        } catch (error) {
            console.error(`备份第 ${currentPage} 页时出错:`, error);
            infoElement.textContent = `备份第 ${currentPage} 页出错: ${error.message}`;
            
            // 错误处理 - 可以选择重试或跳过
            if (currentPage < endPage) {
                infoElement.textContent = `跳过第 ${currentPage} 页,准备加载下一页...`;
                
                // 记录当前页(跳过的页)
                if (!processed.includes(currentPage)) {
                    processed.push(currentPage);
                }
                
                state.processed = processed;
                state.currentPage = currentPage + 1;
                localStorage.setItem('doubanBackupState', JSON.stringify(state));
                
                setTimeout(() => {
                    const nextPageUrl = new URL(window.location.href);
                    nextPageUrl.searchParams.set('p', state.currentPage);
                    window.location.href = nextPageUrl.toString();
                }, 3000);
            } else {
                localStorage.removeItem('doubanBackupState');
                progressBar.style.display = 'none';
            }
        }
    }

    // 添加一个页面初始化的函数,确保在页面加载完成后自动继续备份
    function initPageCheck() {
        console.log("页面检查初始化");
        const backupState = localStorage.getItem('doubanBackupState');
        
        // 添加取消备份按钮
        addCancelButton(!!backupState);
        
        if (backupState) {
            console.log("检测到有未完成的备份");
            try {
                const state = JSON.parse(backupState);
                // 检查备份是否过期(12小时前的备份视为过期)
                const isExpired = Date.now() - state.timestamp > 12 * 60 * 60 * 1000;
                
                if (isExpired) {
                    console.log("备份已过期,清除状态");
                    localStorage.removeItem('doubanBackupState');
                    document.getElementById('backupInfo').textContent = '检测到过期的备份状态,已清除';
                } else {
                    console.log("继续执行未完成的备份");
                    // 确保界面元素已加载
                    if (document.getElementById('backupInfo')) {
                        document.getElementById('backupInfo').textContent = `检测到未完成的备份,即将继续...`;
                        
                        // 短暂延迟后继续处理当前页面
                        setTimeout(() => {
                            processCurrrentPage();
                        }, 1500);
                    } else {
                        console.error("备份界面元素尚未加载");
                    }
                }
            } catch (error) {
                console.error("解析备份状态出错:", error);
                localStorage.removeItem('doubanBackupState');
            }
        }
    }

    // 添加取消备份的按钮
    function addCancelButton(isVisible = false) {
        // 检查按钮是否已存在
        if (document.getElementById('cancelBackupBtn')) {
            document.getElementById('cancelBackupBtn').style.display = isVisible ? 'inline-block' : 'none';
            return;
        }
        
        // 创建按钮
        const cancelBtn = document.createElement('button');
        cancelBtn.id = 'cancelBackupBtn';
        cancelBtn.className = 'douban-backup-btn';
        cancelBtn.style.backgroundColor = '#e74c3c';
        cancelBtn.textContent = '取消备份';
        cancelBtn.style.display = isVisible ? 'inline-block' : 'none';
        cancelBtn.style.marginLeft = '10px';
        
        // 添加点击事件
        cancelBtn.addEventListener('click', () => {
            if (confirm('确定要取消当前备份吗?')) {
                const state = JSON.parse(localStorage.getItem('doubanBackupState') || '{}');
                localStorage.removeItem('doubanBackupState');
                
                document.getElementById('backupInfo').textContent = '备份已取消';
                document.getElementById('backupProgress').style.display = 'none';
                cancelBtn.style.display = 'none';
                
                // 询问是否返回初始页面
                if (state.originalPage && state.originalPage !== getCurrentPage()) {
                    if (confirm('是否返回初始页面?')) {
                        const originalUrl = new URL(window.location.href);
                        originalUrl.searchParams.set('p', state.originalPage);
                        window.location.href = originalUrl.toString();
                    }
                }
            }
        });
        
        // 添加到DOM
        const container = document.querySelector('.douban-backup-container');
        if (container && container.querySelector('div')) {
            container.querySelector('div').appendChild(cancelBtn);
        }
    }

    // 使用更可靠的方式在页面准备就绪后执行初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initPageCheck);
    } else {
        // 如果DOMContentLoaded已经触发,直接初始化
        setTimeout(initPageCheck, 500);
    }
})(); 

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址