您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
备份豆瓣个人广播并保存为Markdown文件,支持批量备份和断点续传
当前为
// ==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 += `\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 += `> \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或关注我们的公众号极客氢云获取最新地址