// ==UserScript== // @name 小红书无水印下载 // @namespace https://github.com/meme-apps-2026/xhs-downloader // @version 0.1.8 // @description 一键下载小红书无水印原图/视频,右下角悬浮,直接下载原图 // @author meme // @match https://www.xiaohongshu.com/* // @match https://www.rednote.com/* // @icon https://www.xiaohongshu.com/favicon.ico // @grant GM_download // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant unsafeWindow // @grant GM_notification // @connect xhscdn.com // @connect *.xhscdn.com // @connect sns-img-bd.xhscdn.com // @connect sns-img-qc.xhscdn.com // @connect sns-webpic-qc.xhscdn.com // @connect sns-video-bd.xhscdn.com // @connect xiaohongshu.com // @connect *.xiaohongshu.com // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; const CSS = ` .xhs-dl-root{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif} .xhs-dl-float{position:fixed;right:20px;bottom:88px;z-index:999999;display:flex;flex-direction:column;align-items:flex-end;gap:10px} .xhs-dl-btn{display:inline-flex;align-items:center;gap:7px;padding:9px 14px;background:#fff;color:#111;border:1px solid #e5e5e5;border-radius:999px;font-size:13px;font-weight:600;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.12);transition:all .15s} .xhs-dl-btn:hover{background:#111;color:#fff;border-color:#111} .xhs-dl-btn svg{width:16px;height:16px} .xhs-dl-card{width:340px;background:#fff;border-radius:16px;box-shadow:0 16px 40px rgba(0,0,0,.18);overflow:hidden;border:1px solid #eee} .xhs-dl-card-head{padding:12px 14px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #f0f0f0} .xhs-dl-card-head strong{font-size:13px} .xhs-dl-card-head span{font-size:12px;color:#888;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .xhs-dl-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;padding:10px;max-height:320px;overflow:auto} .xhs-dl-thumb{position:relative;aspect-ratio:1;border-radius:10px;overflow:hidden;background:#f5f5f5;cursor:pointer;border:1px solid #eee} .xhs-dl-thumb img{width:100%;height:100%;object-fit:cover;display:block} .xhs-dl-thumb i{position:absolute;right:6px;bottom:6px;width:22px;height:22px;border-radius:999px;background:rgba(0,0,0,.6);color:#fff;display:grid;place-items:center} .xhs-dl-foot{padding:10px;display:flex;gap:8px;border-top:1px solid #f0f0f0} .xhs-dl-foot button{flex:1;padding:10px;border-radius:10px;border:1px solid #eee;background:#fff;font-size:13px;font-weight:600;cursor:pointer} .xhs-dl-foot button.primary{background:#111;color:#fff;border-color:#111} .xhs-dl-toast{position:fixed;left:50%;bottom:32px;transform:translateX(-50%);background:#111;color:#fff;padding:10px 16px;border-radius:999px;font-size:13px;z-index:999999} .xhs-dl-log{padding:8px 14px;font-size:12px;color:#666;background:#fafafa;border-top:1px solid #f0f0f0;max-height:80px;overflow:auto} `; if (typeof GM_addStyle !== 'undefined') GM_addStyle(CSS); else { const s = document.createElement('style'); s.textContent = CSS; document.documentElement.appendChild(s); } const $ = (s, r = document) => r.querySelector(s); function extractCurrentNoteId() { const m = location.href.match(/\/(?:explore|discovery\/item)\/([^/?#]+)/); return m ? m[1] : ""; } function safeExtract(obj, path, def = undefined) { try { return path.split('.').reduce((o, k) => o?.[k], obj) ?? def; } catch { return def; } } function extractNoteInfo() { const w = (typeof unsafeWindow !== 'undefined' && unsafeWindow.__INITIAL_STATE__) ? unsafeWindow : window; const st = w.__INITIAL_STATE__; if (!st) return null; const d1 = safeExtract(st, 'noteData.data.noteData'); if (d1 && (d1.imageList || d1.video)) return d1; const noteId = extractCurrentNoteId(); const map = safeExtract(st, 'note.noteDetailMap'); if (map) { if (noteId && map[noteId]?.note) return map[noteId].note; const vals = Object.values(map); if (vals.length) { const hit = vals.find(v => v.note?.noteId === noteId || v.note?.id === noteId); if (hit?.note) return hit.note; if (vals[vals.length - 1]?.note) return vals[vals.length - 1].note; } } const d3 = safeExtract(st, 'note.data'); if (d3 && (d3.imageList || d3.video)) return d3; return null; } // 统一 token 提取,兼容所有域名 function tokenFromUrl(url) { if (!url) return ""; if (url.includes("xhscdn.com/")) return url.split("xhscdn.com/")[1].split("!")[0].split("?")[0].replace(/^\/+/, ""); if (url.includes("ci.xiaohongshu.com/")) return url.split("ci.xiaohongshu.com/")[1].split("?")[0].replace(/^\/+/, ""); return url.split("!")[0].split("?")[0]; } function generateImageUrls(note) { const list = note.imageList || []; const out = []; for (const item of list) { const raw = item.urlDefault || item.url || item.urlPre || ""; const token = tokenFromUrl(raw); if (!token) continue; // 预览与下载都用 sns-img-bd,预览加 referrer 绕过,下载用 GM_xhr 也走同一链接 // 若 token 已含 1042g008/xxx 形态,直接拼 out.push(`https://sns-img-bd.xhscdn.com/${token}`); } return out; } function generateDisplayUrls(note) { // 预览用原始缩略图,确保一定能显示(即使高清链接被拦) const list = note.imageList || []; return list.map(it => (it.urlDefault || it.url || "").replace(/^http:/, "https:")); } function generateVideoUrl(note) { try { const key = note.video?.consumer?.originVideoKey; if (key) return [`https://sns-video-bd.xhscdn.com/${key}`]; const streams = note.video?.media?.stream; if (streams) { const all = Object.values(streams).flat(); if (all.length) { all.sort((a, b) => (b.height || 0) - (a.height || 0)); const best = all[0]; return [best.backupUrls?.[0] || best.masterUrl].filter(Boolean); } } } catch {} return []; } function showToast(msg, ms = 2500) { let t = $('.xhs-dl-toast'); if (t) t.remove(); t = document.createElement('div'); t.className = 'xhs-dl-toast'; t.textContent = msg; document.body.appendChild(t); setTimeout(() => t.remove(), ms); } function downloadUrl(url, filename) { // blob: 链接必须走原生 a.click,GM_download 在 ScriptCat 下对 blob 失效 if (url.startsWith('blob:')) { const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); return; } if (typeof GM_download !== 'undefined') { try { GM_download({ url, name: filename, onload: () => showToast('下载完成: ' + filename), onerror: (e) => { throw e; } }); return; } catch (e) {} } const a = document.createElement('a'); a.href = url; a.download = filename; a.target = '_blank'; a.rel = 'noopener'; document.body.appendChild(a); a.click(); a.remove(); } function downloadBlob(blob, filename) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 3000); } // 用 GM_xmlhttpRequest 绕 CORS,带 referer function fetchBlobViaGM(url) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'undefined') { GM_xmlhttpRequest({ method: 'GET', url, responseType: 'arraybuffer', timeout: 15000, headers: { 'Referer': 'https://www.xiaohongshu.com/', 'Accept': 'image/*,*/*' }, onload: (r) => { if (r.status >= 200 && r.status < 300) resolve(new Blob([r.response], { type: r.responseHeaders?.match(/Content-Type:\s*([^\r\n]+)/i)?.[1] || 'image/jpeg' })); else reject(new Error('http ' + r.status)); }, onerror: (e) => { console.error('[xhs-dl] GM_xhr error', url, e); reject(e.error || new Error('GM_xhr failed')); }, ontimeout: () => reject(new Error('timeout')), }); } else { fetch(url, { referrerPolicy: 'no-referrer' }).then(r => { if (!r.ok) throw new Error('http ' + r.status); return r.blob(); }).then(resolve).catch(reject); } }); } async function downloadAllDirect(images, displayUrls, title) { const safe = title.replace(/[\\/:*?"<>|]/g, '_').slice(0, 30) || 'xiaohongshu'; const card = $('#xhs-dl-card'); const log = card.querySelector('.xhs-dl-log') || (() => { const d = document.createElement('div'); d.className = 'xhs-dl-log'; card.appendChild(d); return d; })(); let ok = 0, fail = 0; log.textContent = `开始下载 ${images.length} 张...`; for (let i = 0; i < images.length; i++) { const url = images[i]; const disp = displayUrls[i] || url; log.textContent = `下载 ${i + 1}/${images.length} ...`; try { let blob; try { blob = await fetchBlobViaGM(url); } catch (e) { if (disp !== url) blob = await fetchBlobViaGM(disp); else throw e; } const ext = (blob.type.split('/')[1] || 'jpg').replace('jpeg', 'jpg').split(';')[0].split('+')[0] || 'jpg'; const filename = `${safe}_${String(i + 1).padStart(2, '0')}.${ext}`; downloadBlob(blob, filename); ok++; log.textContent = `已触发 ${ok}/${images.length}(失败 ${fail})`; // 轻微间隔避免浏览器拦截多文件下载 await new Promise(r => setTimeout(r, 300)); } catch (e) { fail++; console.error('[xhs-dl] fetch fail', i, url, e); log.textContent = `第 ${i + 1} 张失败: ${e.message},跳过(成功 ${ok} 失败 ${fail})`; await new Promise(r => setTimeout(r, 200)); } } if (ok === 0) { showToast('全部下载失败,请刷新后重试'); log.textContent = '下载失败,无可用图片'; return; } showToast(`已触发下载 ${ok}/${images.length} 张` + (fail ? `,${fail} 张失败` : '')); log.textContent = `完成:成功 ${ok} 张,失败 ${fail} 张,已直接下载`; } function ensureUI() { if ($('#xhs-dl-float')) return; const root = document.createElement('div'); root.id = 'xhs-dl-float'; root.className = 'xhs-dl-root xhs-dl-float'; root.innerHTML = ` `; document.body.appendChild(root); const btn = $('#xhs-dl-main'); const card = $('#xhs-dl-card'); // 点击空白自动收起 setTimeout(() => { document.addEventListener('click', (e) => { if (!card || card.style.display === 'none') return; if (card.contains(e.target) || btn.contains(e.target)) return; card.style.display = 'none'; }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && card && card.style.display !== 'none') card.style.display = 'none'; }); }, 100); btn.addEventListener('click', async (e) => { e.stopPropagation(); const note = extractNoteInfo(); if (!note) { showToast('未读到帖子数据,请先点进帖子详情并刷新'); return; } const title = (note.title || note.desc || document.title || 'xiaohongshu').trim().slice(0, 40); const isVideo = note.type === 'video' || !!note.video?.consumer || !!note.video?.media; const images = !isVideo ? generateImageUrls(note) : []; const displayUrls = !isVideo ? generateDisplayUrls(note) : []; const videos = isVideo ? generateVideoUrl(note) : []; if (isVideo && videos.length) { card.style.display = ''; card.innerHTML = `
视频 · 无水印${title.slice(0, 18)}
识别到 1 个视频,点击下载原画
视频地址已就绪
`; card.querySelector('#xhs-dl-go').onclick = () => { const name = title.replace(/[\\/:*?"<>|]/g, '_').slice(0, 30) + '.mp4'; downloadUrl(videos[0], name); }; card.querySelector('#xhs-dl-close').onclick = () => card.style.display = 'none'; return; } if (images.length) { card.style.display = ''; card.innerHTML = `
图文 · ${images.length} 张原图${title.slice(0, 18)}
${displayUrls.map((src, i) => `
`).join('')}
预览为缩略图,下载为无水印原图,逐张直接保存
`; card.querySelector('#xhs-dl-go').onclick = async () => { const b = card.querySelector('#xhs-dl-go'); b.disabled = true; b.textContent = '下载中...'; await downloadAllDirect(images, displayUrls, title); b.disabled = false; b.textContent = `一键下载 ${images.length} 张`; }; card.querySelector('#xhs-dl-close').onclick = () => card.style.display = 'none'; card.querySelectorAll('.xhs-dl-thumb').forEach(el => { el.onclick = async () => { const idx = parseInt(el.dataset.idx, 10); // 单张也走 GM_xhr 保证无水印 try { const blob = await fetchBlobViaGM(images[idx]); const url = URL.createObjectURL(blob); downloadUrl(url, `xhs_${String(idx + 1).padStart(2, '0')}.jpg`); setTimeout(() => URL.revokeObjectURL(url), 30000); } catch { downloadUrl(displayUrls[idx], `xhs_${String(idx + 1).padStart(2, '0')}.jpg`); } }; }); return; } showToast('该帖子无可下载的图片/视频'); }); } function shouldShow() { return /\/(explore|discovery\/item)\//.test(location.href); } function tick() { const has = !!$('#xhs-dl-float'); if (shouldShow() && !has) ensureUI(); if (!shouldShow() && has) { const el = $('#xhs-dl-float'); if (el) el.remove(); } } setInterval(tick, 800); if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', tick); else tick(); })();