// ==UserScript== // @name 小红书无水印图片/视频/Live图下载器 (状态记忆版) // @namespace http://tampermonkey.net/ // @version 2.2 // @description 小红书图片和视频下载器。采用底层网络请求拦截技术,100% 抓取纯视频和 Live 图高清源文件;加入UI轮询保活机制;新增下载状态记忆功能,下载后按钮变为绿色打勾,二次进入不迷路。 // @author 你的名字 (Original author: pleia) // @match https://www.xiaohongshu.com/* // @grant GM_download // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant unsafeWindow // @run-at document-start // @license MIT // @connect * // @downloadURL https://update.greasyfork.icu/scripts/569534/%E5%B0%8F%E7%BA%A2%E4%B9%A6%E6%97%A0%E6%B0%B4%E5%8D%B0%E5%9B%BE%E7%89%87%E8%A7%86%E9%A2%91Live%E5%9B%BE%E4%B8%8B%E8%BD%BD%E5%99%A8%20%28%E7%8A%B6%E6%80%81%E8%AE%B0%E5%BF%86%E7%89%88%29.user.js // @updateURL https://update.greasyfork.icu/scripts/569534/%E5%B0%8F%E7%BA%A2%E4%B9%A6%E6%97%A0%E6%B0%B4%E5%8D%B0%E5%9B%BE%E7%89%87%E8%A7%86%E9%A2%91Live%E5%9B%BE%E4%B8%8B%E8%BD%BD%E5%99%A8%20%28%E7%8A%B6%E6%80%81%E8%AE%B0%E5%BF%86%E7%89%88%29.meta.js // ==/UserScript== (function() { 'use strict'; // ========================================== // 核心黑科技:网络请求拦截器 // ========================================== const interceptedData = {}; // 存储抓取到的最新鲜的笔记数据 // 1. 拦截 Fetch 请求 const originalFetch = unsafeWindow.fetch || window.fetch; unsafeWindow.fetch = async function(...args) { const response = await originalFetch.apply(this, args); try { const url = args[0] instanceof Request ? args[0].url : args[0]; if (url && (url.includes('/api/sns/web/v1/feed') || url.includes('/api/sns/web/v1/note/detail'))) { const clone = response.clone(); clone.json().then(data => { extractNotesFromApi(data); }).catch(e => console.error("解析Fetch JSON失败", e)); } } catch(e) {} return response; }; // 2. 拦截 XHR 请求 (兜底) const originalXHR = unsafeWindow.XMLHttpRequest.prototype.open; unsafeWindow.XMLHttpRequest.prototype.open = function(method, url, ...rest) { this.addEventListener('load', function() { if (url && typeof url === 'string' && (url.includes('/api/sns/web/v1/feed') || url.includes('/api/sns/web/v1/note/detail'))) { try { const data = JSON.parse(this.responseText); extractNotesFromApi(data); } catch(e) {} } }); return originalXHR.call(this, method, url, ...rest); }; // 提取并保存API返回的笔记数据 function extractNotesFromApi(data) { if (!data || !data.data) return; let items = data.data.items || (Array.isArray(data.data) ? data.data : [data.data]); items.forEach(item => { let note = item.note_card || item; let id = note.id || note.note_id; if (id) { interceptedData[id] = note; } }); } // ========================================== // UI 及下载逻辑 (防消失 + 状态记忆版) // ========================================== let cssInjected = false; // 轮询检查UI是否存在并更新状态 function ensureUI() { if (!document.body) return; // 网页body还没加载完,先不急 // 只注入一次CSS样式 if (!cssInjected) { GM_addStyle(` .xhs-download-btn { position: fixed; bottom: 50px; right: 50px; background: linear-gradient(135deg, #ff2442 0%, #ff768a 100%); color: white; border: none; border-radius: 50%; width: 60px; height: 60px; font-size: 18px; cursor: pointer; box-shadow: 0 4px 20px rgba(255, 36, 66, 0.4); display: flex; align-items: center; justify-content: center; z-index: 9999; transition: all 0.3s ease; animation: pulse 2s infinite; } .xhs-download-btn:hover { transform: scale(1.15); box-shadow: 0 6px 25px rgba(255, 36, 66, 0.5); animation: none; } .xhs-download-btn:active { transform: scale(0.95); box-shadow: 0 2px 10px rgba(255, 36, 66, 0.3); } /* 下载完成后的绿色打勾样式 */ .xhs-download-btn.downloaded { background: linear-gradient(135deg, #00b09b 0%, #96c93d 100%); animation: none; box-shadow: 0 4px 20px rgba(0, 176, 155, 0.4); } .xhs-download-btn.downloaded:hover { transform: scale(1.15); box-shadow: 0 6px 25px rgba(0, 176, 155, 0.5); } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(255, 36, 66, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(255, 36, 66, 0); } 100% { box-shadow: 0 0 0 0 rgba(255, 36, 66, 0); } } .download-progress { position: fixed; bottom: 120px; right: 50px; background-color: rgba(0, 0, 0, 0.8); color: white; padding: 10px 15px; border-radius: 25px; font-size: 14px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); opacity: 0; transition: opacity 0.3s ease; z-index: 9998; pointer-events: none; } .download-progress.show { opacity: 1; } /* 默认下载图标 */ .download-icon { width: 24px; height: 24px; position: relative; } .download-icon::before { content: ''; position: absolute; top: 0; left: 50%; transform: translateX(-50%); width: 16px; height: 16px; border: 2px solid white; border-radius: 2px; } .download-icon::after { content: ''; position: absolute; top: 10px; left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 8px solid white; } .download-icon span { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); width: 12px; height: 2px; background-color: white; } /* 打勾成功图标 */ .success-icon { width: 24px; height: 24px; position: relative; } .success-icon::after { content: ''; position: absolute; top: 4px; left: 8px; width: 6px; height: 12px; border: solid white; border-width: 0 3px 3px 0; transform: rotate(45deg); } `); cssInjected = true; } let downloadBtn = document.querySelector('.xhs-download-btn'); let currentNoteId = getActiveNoteId(); // 1. 如果按钮不存在,创建按钮 if (!downloadBtn) { downloadBtn = document.createElement('button'); downloadBtn.className = 'xhs-download-btn'; downloadBtn.innerHTML = '
'; document.body.appendChild(downloadBtn); const progressIndicator = document.createElement('div'); progressIndicator.className = 'download-progress'; progressIndicator.id = 'xhs-progress-indicator'; document.body.appendChild(progressIndicator); // 绑定点击事件 downloadBtn.addEventListener('click', async function() { const indicator = document.getElementById('xhs-progress-indicator'); const pageTitle = getSafeFileName(document.title); let noteId = getActiveNoteId(); indicator.textContent = '正在读取笔记源数据...'; indicator.classList.add('show'); let mediaPairs = getMediaFromState(); if (!mediaPairs || mediaPairs.length === 0) { alert('未找到任何图片或视频!\n\n【重要提醒】如果是刚安装脚本,请按 F5 刷新一次当前网页!'); indicator.classList.remove('show'); return; } try { await downloadPairs(mediaPairs, pageTitle, indicator); indicator.textContent = '所有文件下载完成!'; // 下载成功后,记录状态并更新按钮 UI if (noteId) { GM_setValue(`xhs_downloaded_${noteId}`, true); downloadBtn.classList.add('downloaded'); downloadBtn.innerHTML = ''; } } catch (error) { console.error('下载失败:', error); indicator.textContent = `下载失败: ${error.message}`; alert(`下载过程中出现错误: ${error.message}`); } setTimeout(() => { indicator.classList.remove('show'); }, 3000); }); } // 2. 监测页面跳转:如果进入了新笔记或旧笔记,刷新按钮状态 if (downloadBtn && currentNoteId && downloadBtn.dataset.noteId !== currentNoteId) { downloadBtn.dataset.noteId = currentNoteId; // 记录当前绑定的笔记ID // 查询本地是否下载过 let hasDownloaded = GM_getValue(`xhs_downloaded_${currentNoteId}`, false); if (hasDownloaded) { downloadBtn.classList.add('downloaded'); downloadBtn.innerHTML = ''; } else { downloadBtn.classList.remove('downloaded'); downloadBtn.innerHTML = ''; } } } // 每隔 1 秒检查一次按钮及状态 setInterval(ensureUI, 1000); // ========================================== // 数据解析器 // ========================================== function extractVideoFromObject(obj) { if (!obj) return null; let urls = []; let stringified = JSON.stringify(obj); let regex = /"(?:masterUrl|videoUrl|url|backupUrl)":"(https?:\/\/[^"]+)"/g; let match; while ((match = regex.exec(stringified)) !== null) { let url = match[1]; url = url.replace(/\\u([\d\w]{4})/gi, function (m, grp) { return String.fromCharCode(parseInt(grp, 16)); }); url = url.replace(/\\\//g, '/'); if (!url.includes('.jpg') && !url.includes('.png') && !url.includes('.webp') && !url.includes('.jpeg') && !url.includes('image')) { urls.push(url); } } let mp4Urls = urls.filter(u => !u.includes('.m3u8')); if (mp4Urls.length > 0) return mp4Urls[0]; if (urls.length > 0) return urls[0]; return null; } function getMediaFromState() { let noteId = getActiveNoteId(); let noteDetail = null; if (noteId && interceptedData[noteId]) { noteDetail = interceptedData[noteId]; } else { const state = unsafeWindow.__INITIAL_STATE__ || window.__INITIAL_STATE__; if (state?.note?.noteDetailMap) { noteDetail = state.note.noteDetailMap[noteId]?.note; if (!noteDetail) { const keys = Object.keys(state.note.noteDetailMap); if (keys.length > 0) noteDetail = state.note.noteDetailMap[keys[0]]?.note; } } } if (!noteDetail) return null; const results = []; if (noteDetail.type === 'video' || noteDetail.video) { let videoUrl = extractVideoFromObject(noteDetail.video || noteDetail); if (videoUrl) { results.push({ index: 1, imgUrl: null, videoUrl: fixUrl(videoUrl) }); return results; } } if (noteDetail.imageList && noteDetail.imageList.length > 0) { noteDetail.imageList.forEach((item, index) => { let imgUrl = item.urlDefault || item.url || item.livePhotoFileUrl || item.infoList?.[0]?.url; let videoUrl = extractVideoFromObject(item.stream); if (imgUrl || videoUrl) { results.push({ index: index + 1, imgUrl: imgUrl ? fixUrl(imgUrl) : null, videoUrl: videoUrl ? fixUrl(videoUrl) : null }); } }); return results; } return results.length > 0 ? results : null; } // ========================================== // 执行与工具函数 // ========================================== async function downloadPairs(pairs, pageTitle, indicator) { let totalFiles = 0; pairs.forEach(p => { if (p.imgUrl) totalFiles++; if (p.videoUrl) totalFiles++; }); let currentFile = 0; for (let i = 0; i < pairs.length; i++) { const pair = pairs[i]; const baseName = pairs.length === 1 ? pageTitle : `${pageTitle}_${pair.index}`; if (pair.imgUrl) { currentFile++; indicator.textContent = `正在下载第 ${currentFile}/${totalFiles} 个文件 (图片)...`; const ext = getExtension(pair.imgUrl, 'jpg'); await downloadFile(pair.imgUrl, `${baseName}.${ext}`); await sleep(300); } if (pair.videoUrl) { currentFile++; indicator.textContent = `正在下载第 ${currentFile}/${totalFiles} 个文件 (纯视频)...`; let ext = getExtension(pair.videoUrl, 'mp4'); if (ext !== 'mp4' && ext !== 'webm' && ext !== 'mov') ext = 'mp4'; await downloadFile(pair.videoUrl, `${baseName}.${ext}`); await sleep(400); } } } function downloadFile(url, fileName) { return new Promise((resolve, reject) => { GM_download({ url: url, name: fileName, onerror: reject, onload: resolve }); }); } function getActiveNoteId() { const path = window.location.pathname; const match = path.match(/\/(explore|discovery\/item)\/([a-zA-Z0-9]+)/); return match ? match[2] : null; } function fixUrl(url) { if (!url) return null; if (url.startsWith('//')) return 'https:' + url; if (url.startsWith('/')) return window.location.origin + url; return url; } function getExtension(url, defaultExt) { try { const urlWithoutQuery = url.split('?')[0]; const parts = urlWithoutQuery.split('.'); if (parts.length > 1) { const ext = parts[parts.length - 1].toLowerCase(); if (ext.length <= 5 && /^[a-z0-9]+$/.test(ext)) return ext; } } catch (e) {} return defaultExt; } function getSafeFileName(name) { return name.replace(/[<>:"/\\|?*]/g, '_').replace(/\s+/g, '_').substring(0, 80).trim(); } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } })();