// ==UserScript==
// @name XHS-Downloader
// @namespace https://github.com/JoeanAmier/XHS-Downloader
// @version 1.8.7
// @description 提取小红书作品/用户链接,下载小红书无水印图文/视频作品文件
// @author JoeanAmier
// @match http*://xhslink.com/*
// @match http*://www.xiaohongshu.com/explore*
// @match http*://www.xiaohongshu.com/user/profile/*
// @match http*://www.xiaohongshu.com/search_result*
// @match http*://www.xiaohongshu.com/board/*
// @icon data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMjAwMTA5MDQvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvVFIvMjAwMS9SRUMtU1ZHLTIwMDEwOTA0L0RURC9zdmcxMC5kdGQiPjxzdmcgdmVyc2lvbj0iMS4wIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNTYuMDAwMDAwcHQiIGhlaWdodD0iMjU2LjAwMDAwMHB0IiB2aWV3Qm94PSIwIDAgMjU2LjAwMDAwMCAyNTYuMDAwMDAwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBtZWV0Ij48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLjAwMDAwMCwyNTYuMDAwMDAwKSBzY2FsZSgwLjEwMDAwMCwtMC4xMDAwMDApIiBmaWxsPSIjZTgzYjM1IiBzdHJva2U9Im5vbmUiPjxwYXRoIGQ9Ik0xMTI1IDI1NDMgYy02MyAtNyAtMjIxIC00NiAtMjgyIC02OSAtMTc3IC02NyAtMzIxIC0xNjAgLTQ1OSAtMjk4IC0xOTQgLTE5NCAtMzEzIC00MjIgLTM1OSAtNjgzIC0xOSAtMTExIC0xOSAtMzI2IDAgLTQzOSA3NCAtNDIzIDM3OCAtNzk4IDc3NiAtOTU4IDQ4OSAtMTk3IDEwMzUgLTgwIDEzOTkgMzAwIDEzOCAxNDUgMjM2IDMwNyAyOTQgNDg3IDg1IDI2MCA4MSA1NjYgLTEwIDgxOSAtMTU2IDQzNCAtNTM4IDc1NCAtOTkzIDgzMiAtODYgMTQgLTI4MiAyMCAtMzY2IDl6IG01MCAtODEwIGw4MCAtMzAgMyAtMzg5IDIgLTM5MCAtODcgMjkgYy0xMzIgNDQgLTMwMiA0OCAtNDQwIDExIGwtMjMgLTYgMCAzOTEgYzAgMjE1IDMgMzkxIDggMzkxIDQgMSAzMCA3IDU3IDE1IDM2IDExIDg5IDE0IDE4NSAxMSAxMjEgLTIgMTQzIC02IDIxNSAtMzN6IG02MzggMTggbDM3IC0xMiAwIC0zODkgYzAgLTIxNSAtMiAtMzkwIC00IC0zOTAgLTMgMCAtMzUgNyAtNzMgMTYgLTEwOCAyNSAtMjc0IDE1IC0zODUgLTIzIGwtODggLTMxIDAgMzg4IDAgMzg3IDM5IDE4IGMxMDAgNDQgMTYwIDU1IDMwMSA1MSA3NCAtMiAxNTIgLTkgMTczIC0xNXogbS0xMTQzIC00NTggYzAgLTE5NyA0IC0zNjQgOCAtMzcxIDcgLTkgMjUgLTggODcgOCAxNDUgMzcgMzE1IDI0IDQ1NiAtMzUgbDYxIC0yNiA3MSAzMCBjNDAgMTYgMTAwIDM1IDEzNSA0MSA4MSAxNiAyNzEgOCAzMzIgLTE0IDI0IC05IDUwIC0xNCA1NyAtMTIgMTAgNCAxMyA4MSAxMyAzNzEgbDAgMzY1IDQ1IDAgNDUgMCAwIC00MjkgYzAgLTIzNyAtMyAtNDMyIC04IC00MzUgLTQgLTIgLTU5IDExIC0xMjEgMzAgLTYzIDE5IC0xNDYgMzcgLTE4NSA0MCAtODcgOCAtMjE0IC0xNCAtMjkzIC01MSAtNzEgLTMyIC0xMDUgLTMyIC0xOTYgNSAtMTUzIDYxIC0yODEgNjMgLTQ3MCA1IC02MCAtMTkgLTExNCAtMzIgLTExOSAtMjkgLTQgMyAtOCAxOTggLTggNDM1IGwwIDQyOSA0NSAwIDQ1IDAgMCAtMzU3eiIvPjxwYXRoIGQ9Ik04NTcgMTYwMyBjLTI0IC0yMyAtMSAtMzEgMTA2IC0zNiA2MSAtNCAxMjMgLTggMTM3IC05IDQ1IC01IDI5IDI2IC0yMCAzOSAtNDkgMTIgLTIxMiAxNyAtMjIzIDZ6Ii8+PHBhdGggZD0iTTg1NyAxNDYzIGMtMjQgLTIzIC0yIC0zMSAxMDEgLTM3IDU5IC0zIDEyMSAtNyAxMzcgLTggMjEgLTIgMzAgMSAzMCAxMiAwIDEwIC0xOCAxOSAtNTAgMjYgLTU1IDEzIC0yMDggMTggLTIxOCA3eiIvPjxwYXRoIGQ9Ik04NjMgMTMyNCBjLTcgLTMgLTEzIC0xMSAtMTMgLTE5IDAgLTkgMzAgLTE0IDEwOCAtMTkgNTkgLTMgMTE4IC03IDEzMiAtOSAzNiAtMyAzOSAxNiA2IDMxIC0yOCAxNCAtMjExIDI2IC0yMzMgMTZ6Ii8+PHBhdGggZD0iTTg2OCAxMTgzIGMtMTAgLTIgLTE4IC0xMSAtMTggLTE5IDAgLTExIDE3IC0xNCA4MSAtMTQgNDQgMCAxMDAgLTUgMTI0IC0xMSA1MiAtMTMgODMgLTYgNjYgMTUgLTIyIDI2IC0xOTEgNDYgLTI1MyAyOXoiLz48cGF0aCBkPSJNMTQ4OCAxNTk4IGMtMzggLTggLTQ4IC0xNCAtNDggLTMwIDAgLTE3IDQgLTE5IDI4IC0xMyAxNSAzIDc2IDkgMTM1IDEyIDkzIDUgMTA4IDggMTA1IDIyIC0zIDEzIC0xOCAxNiAtODggMTcgLTQ3IDEgLTEwNiAtMyAtMTMyIC04eiIvPjxwYXRoIGQ9Ik0xNDkwIDE0NTcgYy0zMCAtOCAtNDYgLTE4IC00OCAtMzEgLTMgLTE2IDAgLTE4IDI1IC0xMiAxNSA0IDc3IDEwIDEzNiAxMyA5MyA1IDEwOCA4IDEwNSAyMiAtMyAxMyAtMTggMTYgLTg4IDE4IC00NyAxIC0xMDUgLTQgLTEzMCAtMTB6Ii8+PHBhdGggZD0iTTE0OTUgMTMxNiBjLTM1IC04IC01MSAtMTcgLTUzIC0zMCAtMyAtMTYgMCAtMTggMjUgLTEyIDE1IDQgNzYgMTAgMTM2IDEzIDc2IDQgMTA3IDkgMTA3IDE4IDAgNyAtNyAxNiAtMTYgMTkgLTI1IDEwIC0xNDQgNSAtMTk5IC04eiIvPjxwYXRoIGQ9Ik0xNTAwIDExNzcgYy00MCAtOSAtNTYgLTE3IC01OCAtMzEgLTMgLTE2IDAgLTE4IDMwIC0xMiAxOCAzIDc2IDkgMTI4IDEyIDExOSA3IDEzMiAxMCAxMDYgMzAgLTI0IDE3IC0xMjYgMTcgLTIwNiAxeiIvPjwvZz48L3N2Zz4=
// @grant GM_getValue
// @grant GM_setValue
// @grant unsafeWindow
// @grant GM_setClipboard
// @grant GM_registerMenuCommand
// @license GNU General Public License v3.0
// @run-at document-end
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.9.1/jszip.min.js
// ==/UserScript==
(function () {
let disclaimer = GM_getValue("disclaimer", false);
const readme = () => {
const instructions = `
XHS-Downloader 用户脚本 功能清单:
1. 下载小红书无水印作品文件
2. 提取发现页面作品链接
3. 提取账号发布作品链接
4. 提取账号收藏作品链接
5. 提取账号专辑作品链接
6. 提取账号点赞作品链接
7. 提取搜索结果作品链接
8. 提取搜索结果用户链接
XHS-Downloader 用户脚本 详细说明:
1. 下载小红书无水印作品文件时,脚本需要花费时间处理文件,请等待片刻,切勿多次点击下载按钮
2. 无水印作品文件较大,可能需要较长的时间处理,页面跳转可能会导致下载失败
3. 提取账号发布、收藏、点赞、专辑作品链接时,脚本可以自动滚动页面直至加载全部作品,默认滚动检测间隔:2.5 秒
4. 提取发现作品链接、搜索作品、用户链接时,脚本可以自动滚动页面加载更多内容,默认滚动页面次数:10 次
5. 自动滚动页面功能默认关闭;用户可以自由开启,并修改滚动检测间隔、滚动页面次数,修改后立即生效
6. 如果未开启自动滚动页面功能,用户需要手动滚动页面以便加载更多内容后再进行其他操作
7. 支持作品文件打包下载;该功能默认开启,多个文件的作品将会以压缩包格式下载
项目开源地址:https://github.com/JoeanAmier/XHS-Downloader
`
const disclaimer_content = `
关于 XHS-Downloader 的 免责声明:
1. 使用者对本项目的使用由使用者自行决定,并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。
2. 本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者尽力确保代码的正确性和安全性,但不保证代码完全没有错误或缺陷。
3. 使用者在使用本项目时必须严格遵守 GNU General Public License v3.0 的要求,并在适当的地方注明使用了 GNU General Public License v3.0 的代码。
4. 使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来,或要求其对使用者使用本项目所产生的任何损失或损害负责。
5. 使用者在使用本项目的代码和功能时,必须自行研究相关法律法规,并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险,均由使用者自行承担。
6. 本项目的作者不会提供 XHS-Downloader 项目的付费版本,也不会提供与 XHS-Downloader 项目相关的任何商业服务。
7. 基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关,原创作者不承担与二次开发行为或其结果相关的任何责任,使用者应自行对因二次开发可能带来的各种情况负全部责任。
在使用本项目的代码和功能之前,请您认真考虑并接受以上免责声明。如果您对上述声明有任何疑问或不同意,请不要使用本项目的代码和功能。如果您使用了本项目的代码和功能,则视为您已完全理解并接受上述免责声明,并自愿承担使用本项目的一切风险和后果。
是否已阅读 XHS-Downloader 功能说明与免责声明(YES/NO)
`
alert(instructions);
alert(`自动滚动页面功能代码已重构,该功能默认关闭!
启用该功能可能会被小红书检测为自动化操作,从而导致账号受到风控或封禁!
该功能在使用过程中遇到任何问题请及时向开发者反馈!
`);
if (!disclaimer) {
const answer = prompt(disclaimer_content, "");
if (answer === null) {
GM_setValue("disclaimer", false);
disclaimer = false;
} else {
GM_setValue("disclaimer", answer.toUpperCase() === "YES");
disclaimer = GM_getValue("disclaimer");
location.reload();
}
}
};
if (!disclaimer) {
readme();
}
GM_registerMenuCommand("关于 XHS-Downloader", function () {
readme();
});
let packageDownloadFiles = GM_getValue("packageDownloadFiles", true);
GM_registerMenuCommand(`文件打包下载功能 ${packageDownloadFiles ? '✔️' : '❌'}`, function () {
packageDownloadFiles = !packageDownloadFiles;
GM_setValue("packageDownloadFiles", packageDownloadFiles);
alert('修改文件打包下载功能成功!');
});
let autoScrollSwitch = GM_getValue("autoScrollSwitch", false);
GM_registerMenuCommand(`自动滚动页面功能 ${autoScrollSwitch ? '✔️' : '❌'}`, function () {
autoScrollSwitch = !autoScrollSwitch;
GM_setValue("autoScrollSwitch", autoScrollSwitch);
alert('修改自动滚动页面功能成功!');
});
let scrollCheckTime = GM_getValue("scrollCheckTime", 2500);
GM_registerMenuCommand("修改滚动检测间隔", function () {
let data;
data = prompt("请输入自动滚动页面检测间隔:\n如果网络环境不佳导致脚本未能加载全部作品,可以设置较大的检测间隔!", scrollCheckTime / 1000);
if (data === null) {
return
}
data = parseFloat(data) || 2.5
scrollCheckTime = data * 1000;
GM_setValue("scrollCheckTime", scrollCheckTime);
alert(`修改自动滚动页面检测间隔成功,当前值:${data} 秒`);
});
let maxScrollCount = GM_getValue("maxScrollCount", 10);
GM_registerMenuCommand("修改滚动页面次数", function () {
let data;
data = prompt("请输入自动滚动页面次数:\n仅对提取发现作品、搜索作品、搜索用户链接生效!", maxScrollCount);
if (data === null) {
return
}
maxScrollCount = parseInt(data) || 10;
GM_setValue("maxScrollCount", maxScrollCount);
alert(`修改自动滚动页面次数成功,当前值:${maxScrollCount} 次`);
});
const icon = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMjAwMTA5MDQvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvVFIvMjAwMS9SRUMtU1ZHLTIwMDEwOTA0L0RURC9zdmcxMC5kdGQiPjxzdmcgdmVyc2lvbj0iMS4wIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNTYuMDAwMDAwcHQiIGhlaWdodD0iMjU2LjAwMDAwMHB0IiB2aWV3Qm94PSIwIDAgMjU2LjAwMDAwMCAyNTYuMDAwMDAwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBtZWV0Ij48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLjAwMDAwMCwyNTYuMDAwMDAwKSBzY2FsZSgwLjEwMDAwMCwtMC4xMDAwMDApIiBmaWxsPSIjZTgzYjM1IiBzdHJva2U9Im5vbmUiPjxwYXRoIGQ9Ik0xMTI1IDI1NDMgYy02MyAtNyAtMjIxIC00NiAtMjgyIC02OSAtMTc3IC02NyAtMzIxIC0xNjAgLTQ1OSAtMjk4IC0xOTQgLTE5NCAtMzEzIC00MjIgLTM1OSAtNjgzIC0xOSAtMTExIC0xOSAtMzI2IDAgLTQzOSA3NCAtNDIzIDM3OCAtNzk4IDc3NiAtOTU4IDQ4OSAtMTk3IDEwMzUgLTgwIDEzOTkgMzAwIDEzOCAxNDUgMjM2IDMwNyAyOTQgNDg3IDg1IDI2MCA4MSA1NjYgLTEwIDgxOSAtMTU2IDQzNCAtNTM4IDc1NCAtOTkzIDgzMiAtODYgMTQgLTI4MiAyMCAtMzY2IDl6IG01MCAtODEwIGw4MCAtMzAgMyAtMzg5IDIgLTM5MCAtODcgMjkgYy0xMzIgNDQgLTMwMiA0OCAtNDQwIDExIGwtMjMgLTYgMCAzOTEgYzAgMjE1IDMgMzkxIDggMzkxIDQgMSAzMCA3IDU3IDE1IDM2IDExIDg5IDE0IDE4NSAxMSAxMjEgLTIgMTQzIC02IDIxNSAtMzN6IG02MzggMTggbDM3IC0xMiAwIC0zODkgYzAgLTIxNSAtMiAtMzkwIC00IC0zOTAgLTMgMCAtMzUgNyAtNzMgMTYgLTEwOCAyNSAtMjc0IDE1IC0zODUgLTIzIGwtODggLTMxIDAgMzg4IDAgMzg3IDM5IDE4IGMxMDAgNDQgMTYwIDU1IDMwMSA1MSA3NCAtMiAxNTIgLTkgMTczIC0xNXogbS0xMTQzIC00NTggYzAgLTE5NyA0IC0zNjQgOCAtMzcxIDcgLTkgMjUgLTggODcgOCAxNDUgMzcgMzE1IDI0IDQ1NiAtMzUgbDYxIC0yNiA3MSAzMCBjNDAgMTYgMTAwIDM1IDEzNSA0MSA4MSAxNiAyNzEgOCAzMzIgLTE0IDI0IC05IDUwIC0xNCA1NyAtMTIgMTAgNCAxMyA4MSAxMyAzNzEgbDAgMzY1IDQ1IDAgNDUgMCAwIC00MjkgYzAgLTIzNyAtMyAtNDMyIC04IC00MzUgLTQgLTIgLTU5IDExIC0xMjEgMzAgLTYzIDE5IC0xNDYgMzcgLTE4NSA0MCAtODcgOCAtMjE0IC0xNCAtMjkzIC01MSAtNzEgLTMyIC0xMDUgLTMyIC0xOTYgNSAtMTUzIDYxIC0yODEgNjMgLTQ3MCA1IC02MCAtMTkgLTExNCAtMzIgLTExOSAtMjkgLTQgMyAtOCAxOTggLTggNDM1IGwwIDQyOSA0NSAwIDQ1IDAgMCAtMzU3eiIvPjxwYXRoIGQ9Ik04NTcgMTYwMyBjLTI0IC0yMyAtMSAtMzEgMTA2IC0zNiA2MSAtNCAxMjMgLTggMTM3IC05IDQ1IC01IDI5IDI2IC0yMCAzOSAtNDkgMTIgLTIxMiAxNyAtMjIzIDZ6Ii8+PHBhdGggZD0iTTg1NyAxNDYzIGMtMjQgLTIzIC0yIC0zMSAxMDEgLTM3IDU5IC0zIDEyMSAtNyAxMzcgLTggMjEgLTIgMzAgMSAzMCAxMiAwIDEwIC0xOCAxOSAtNTAgMjYgLTU1IDEzIC0yMDggMTggLTIxOCA3eiIvPjxwYXRoIGQ9Ik04NjMgMTMyNCBjLTcgLTMgLTEzIC0xMSAtMTMgLTE5IDAgLTkgMzAgLTE0IDEwOCAtMTkgNTkgLTMgMTE4IC03IDEzMiAtOSAzNiAtMyAzOSAxNiA2IDMxIC0yOCAxNCAtMjExIDI2IC0yMzMgMTZ6Ii8+PHBhdGggZD0iTTg2OCAxMTgzIGMtMTAgLTIgLTE4IC0xMSAtMTggLTE5IDAgLTExIDE3IC0xNCA4MSAtMTQgNDQgMCAxMDAgLTUgMTI0IC0xMSA1MiAtMTMgODMgLTYgNjYgMTUgLTIyIDI2IC0xOTEgNDYgLTI1MyAyOXoiLz48cGF0aCBkPSJNMTQ4OCAxNTk4IGMtMzggLTggLTQ4IC0xNCAtNDggLTMwIDAgLTE3IDQgLTE5IDI4IC0xMyAxNSAzIDc2IDkgMTM1IDEyIDkzIDUgMTA4IDggMTA1IDIyIC0zIDEzIC0xOCAxNiAtODggMTcgLTQ3IDEgLTEwNiAtMyAtMTMyIC04eiIvPjxwYXRoIGQ9Ik0xNDkwIDE0NTcgYy0zMCAtOCAtNDYgLTE4IC00OCAtMzEgLTMgLTE2IDAgLTE4IDI1IC0xMiAxNSA0IDc3IDEwIDEzNiAxMyA5MyA1IDEwOCA4IDEwNSAyMiAtMyAxMyAtMTggMTYgLTg4IDE4IC00NyAxIC0xMDUgLTQgLTEzMCAtMTB6Ii8+PHBhdGggZD0iTTE0OTUgMTMxNiBjLTM1IC04IC01MSAtMTcgLTUzIC0zMCAtMyAtMTYgMCAtMTggMjUgLTEyIDE1IDQgNzYgMTAgMTM2IDEzIDc2IDQgMTA3IDkgMTA3IDE4IDAgNyAtNyAxNiAtMTYgMTkgLTI1IDEwIC0xNDQgNSAtMTk5IC04eiIvPjxwYXRoIGQ9Ik0xNTAwIDExNzcgYy00MCAtOSAtNTYgLTE3IC01OCAtMzEgLTMgLTE2IDAgLTE4IDMwIC0xMiAxOCAzIDc2IDkgMTI4IDEyIDExOSA3IDEzMiAxMCAxMDYgMzAgLTI0IDE3IC0xMjYgMTcgLTIwNiAxeiIvPjwvZz48L3N2Zz4=";
const about = () => {
window.open('https://github.com/JoeanAmier/XHS-Downloader', '_blank');
}
const abnormal = () => {
alert("下载无水印作品文件失败!请向作者反馈!\n项目地址:https://github.com/JoeanAmier/XHS-Downloader");
};
const generateVideoUrl = note => {
try {
return [`https://sns-video-bd.xhscdn.com/${note.video.consumer.originVideoKey}`];
} catch (error) {
console.error("Error generating video URL:", error);
return [];
}
};
const generateImageUrl = note => {
let images = note.imageList;
const regex = /http:\/\/sns-webpic-qc\.xhscdn.com\/\d+\/[0-9a-z]+\/(\S+)!/;
let urls = [];
try {
images.forEach((item) => {
let match = item.urlDefault.match(regex);
if (match && match[1]) {
urls.push(`https://ci.xiaohongshu.com/${match[1]}?imageView2/format/png`);
}
})
return urls
} catch (error) {
console.error("Error generating image URLs:", error);
return [];
}
};
const download = async (urls, type_) => {
const name = extractName();
console.info(`文件名称 ${name}`);
if (type_ === "video") {
await downloadVideo(urls[0], name);
} else {
await downloadImage(urls, name);
}
};
const exploreDeal = async note => {
try {
let links;
if (note.type === "normal") {
links = generateImageUrl(note);
} else {
links = generateVideoUrl(note);
}
if (links.length > 0) {
console.info("下载链接", links);
await download(links, note.type);
} else {
abnormal()
}
} catch (error) {
console.error("Error in exploreDeal function:", error);
abnormal();
}
};
const extractNoteInfo = () => {
const regex = /\/explore\/([^?]+)/;
const match = currentUrl.match(regex);
if (match) {
return unsafeWindow.__INITIAL_STATE__.note.noteDetailMap[match[1]]
} else {
console.error("从链接提取作品 ID 失败", currentUrl,);
}
};
const extractDownloadLinks = async () => {
if (currentUrl.includes("https://www.xiaohongshu.com/explore/")) {
let note = extractNoteInfo();
if (note.note) {
await exploreDeal(note.note);
} else {
abnormal();
}
}
};
const triggerDownload = (name, blob) => {
// 创建 Blob 对象的 URL
const blobUrl = URL.createObjectURL(blob);
// 创建一个临时链接元素
const tempLink = document.createElement("a");
tempLink.href = blobUrl;
tempLink.download = name;
// 将链接添加到 DOM 并模拟点击
document.body.appendChild(tempLink); // 避免某些浏览器安全限制
tempLink.click();
// 清理临时链接元素
document.body.removeChild(tempLink); // 从 DOM 中移除临时链接
URL.revokeObjectURL(blobUrl); // 释放 URL
console.info(`文件已成功下载: ${name}`);
}
const downloadFile = async (link, name, trigger = true, retries = 5) => {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
// 使用 fetch 获取文件数据
const response = await fetch(link, {method: "GET"});
// 检查响应状态码
if (!response.ok) {
console.error(`下载失败,状态码: ${response.status},URL: ${link},尝试次数: ${attempt}`);
continue; // 继续下一次尝试
}
const blob = await response.blob();
if (trigger) {
triggerDownload(name, blob);
return true;
} else {
return blob;
}
} catch (error) {
console.error(`下载失败 (${name}),错误信息:`, error, `尝试次数: ${attempt}`);
if (attempt === retries) {
return false; // 如果达到最大重试次数,返回失败
}
}
}
return false; // 如果所有尝试都失败,返回失败
};
const downloadFiles = async (urls, name,) => {
const downloadResults = []; // 用于存储下载结果
const downloadPromises = urls.map(async (url, index) => {
const fileName = `${name}_${index + 1}.png`; // 根据索引生成文件名
const result = await downloadFile(url, fileName, false); // 调用单个文件下载方法
if (result) {
downloadResults.push({name: fileName, file: result});
return true; // 成功
} else {
return false; // 失败
}
});
// 等待所有下载操作完成
const results = await Promise.all(downloadPromises);
if (results.every(result => result === true)) {
try {
const zip = new JSZip();
downloadResults.forEach((item) => {
zip.file(item.name, item.file);
});
const content = await zip.generateAsync({type: "blob", compression: "STORE"});
triggerDownload(`${name}.zip`, content,)
return true;
} catch (error) {
console.error('生成 ZIP 文件或保存失败,错误信息:', error);
return false;
}
} else {
return false;
}
};
const truncateString = (str, maxLength) => {
if (str.length > maxLength) {
const halfLength = Math.floor(maxLength / 2) - 1; // 减去 1 留出省略号的空间
return str.slice(0, halfLength) + '...' + str.slice(-halfLength);
}
return str;
};
const extractName = () => {
let name = document.title.replace(/ - 小红书$/, "").replace(/[^\u4e00-\u9fa5a-zA-Z0-9 ~!@#$%&()_\-+=\[\];"',.!()【】:“”,。《》?]/g, "");
name = truncateString(name, 64,);
let match = currentUrl.match(/\/([^\/]+)$/);
let id = match ? match[1] : null;
return name === "" ? id : name
};
const downloadVideo = async (url, name) => {
if (!await downloadFile(url, `${name}.mp4`)) {
abnormal();
}
};
const downloadImage = async (urls, name) => {
let success;
if (!packageDownloadFiles) {
let result = [];
for (const [index, url] of urls.entries()) {
result.push(await downloadFile(url, `${name}_${index + 1}.png`));
}
success = result.every(item => item === true);
} else if (urls.length > 1) {
success = await downloadFiles(urls, name,);
} else {
success = await downloadFile(urls[0], `${name}.png`);
}
if (!success) {
abnormal();
}
};
const window_scrollBy = (x, y,) => {
window.scrollBy(x, y,);
}
// 随机整数生成函数
const getRandomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
// 判断是否需要暂停,模拟用户的停顿行为
const shouldPause = () => Math.random() < 0.2; // 20%几率停顿
// 执行一次增量滚动
const scrollOnce = () => {
const scrollDistanceMin = 100; // 最小滚动距离
const scrollDistanceMax = 300; // 最大滚动距离
const scrollDistance = getRandomInt(scrollDistanceMin, scrollDistanceMax);
window_scrollBy(0, scrollDistance); // 增量滚动
};
// 检查是否已经滚动到底部
const isAtBottom = () => {
const docHeight = document.documentElement.scrollHeight;
const winHeight = window.innerHeight;
const scrollPos = window.scrollY;
return (docHeight - winHeight - scrollPos <= 10); // 如果距离底部小于10px,认为滚动到底部
};
// 自动滚动主函数
const scrollScreen = (callback, endless = false, scrollCount = 0,) => {
const timeoutMin = 250; // 最小滚动间隔
const timeoutMax = 500; // 最大滚动间隔
const scrollInterval = setInterval(() => {
if (shouldPause()) {
// 停顿,模拟用户的休息
clearInterval(scrollInterval);
setTimeout(() => {
scrollScreen(callback, endless, scrollCount,); // 重新启动滚动
}, getRandomInt(timeoutMin, timeoutMax,)); // 随机停顿时间
} else if (endless) {
// 无限滚动至底部模式
if (!isAtBottom()) {
scrollOnce(); // 执行一次滚动
} else {
// 到达底部,停止滚动
clearInterval(scrollInterval);
callback(); // 调用回调函数
}
} else if (scrollCount < maxScrollCount && !isAtBottom()) {
scrollOnce(); // 执行一次滚动
scrollCount++;
} else {
// 如果到达底部或滚动次数已满,停止滚动
clearInterval(scrollInterval);
callback(); // 调用回调函数
}
}, getRandomInt(timeoutMin, timeoutMax)); // 随机滚动间隔
};
const scrollScreenEvent = (callback, endless = false) => {
if (autoScrollSwitch) {
scrollScreen(callback, endless,);
} else {
callback();
}
};
const extractNotesInfo = order => {
const notesRawValue = unsafeWindow.__INITIAL_STATE__.user.notes._rawValue[order];
return notesRawValue.map(item => [item.id, item.xsecToken,]);
};
const extractBoardInfo = () => {
// 定义正则表达式来匹配 URL 中的 ID
const regex = /\/board\/([a-z0-9]+)\?/;
// 使用 exec 方法执行正则表达式
const match = regex.exec(currentUrl);
// 检查是否有匹配
if (match) {
// 提取 ID
const id = match[1]; // match[0] 是整个匹配的字符串,match[1] 是第一个括号内的匹配
const notesRawValue = unsafeWindow.__INITIAL_STATE__.board.boardFeedsMap._rawValue[id].notes;
return notesRawValue.map(item => [item.noteId, item.xsecToken,]);
} else {
console.error("从链接提取专辑 ID 失败", currentUrl,);
return [];
}
};
const extractFeedInfo = () => {
const notesRawValue = unsafeWindow.__INITIAL_STATE__.feed.feeds._rawValue;
return notesRawValue.map(item => [item.id, item.xsecToken,]);
};
const extractSearchNotes = () => {
const notesRawValue = unsafeWindow.__INITIAL_STATE__.search.feeds._rawValue;
return notesRawValue.map(item => [item.id, item.xsecToken,]);
}
const extractSearchUsers = () => {
const notesRawValue = unsafeWindow.__INITIAL_STATE__.search.userLists._rawValue;
return notesRawValue.map(item => item.id);
}
const generateNoteUrls = data => data.map(([id, token,]) => `https://www.xiaohongshu.com/discovery/item/${id}?source=webshare&xhsshare=pc_web&xsec_token=${token}&xsec_source=pc_share`).join(" ");
const generateUserUrls = data => data.map(id => `https://www.xiaohongshu.com/user/profile/${id}`).join(" ");
const extractAllLinks = (callback, order) => {
scrollScreenEvent(() => {
let data;
if (order >= 0 && order <= 2) {
data = extractNotesInfo(order);
} else if (order === 3) {
data = extractSearchNotes();
} else if (order === 4) {
data = extractSearchUsers();
} else if (order === -1) {
data = extractFeedInfo()
} else if (order === 5) {
data = extractBoardInfo()
} else {
data = [];
}
let urlsString = order !== 4 ? generateNoteUrls(data) : generateUserUrls(data);
callback(urlsString);
}, [0, 1, 2, 5].includes(order))
};
const extractAllLinksEvent = (order = 0) => {
extractAllLinks(urlsString => {
if (urlsString) {
GM_setClipboard(urlsString, "text", () => {
alert('作品/用户链接已复制到剪贴板!');
});
} else {
alert("未提取到任何作品/用户链接!")
}
}, order);
};
const createContainer = () => {
let container = document.createElement('div');
container.id = 'xhsFunctionContainer';
let imgTextContainer = document.createElement('div');
imgTextContainer.id = 'xhsImgTextContainer';
let img = new Image(48, 48); // 确保 icon 变量已定义
img.src = icon;
img.style.borderRadius = '50%';
img.style.objectFit = 'cover';
let textDiv = document.createElement('div');
textDiv.id = 'xhsImgTextContainer__text'
textDiv.textContent = 'XHS-Downloader';
imgTextContainer.appendChild(img);
imgTextContainer.appendChild(textDiv);
container.appendChild(imgTextContainer);
document.body.appendChild(container);
return container;
};
const createButton = (id, text, onClick, ...args) => {
let button = document.createElement('button');
button.id = id;
button.textContent = text;
button.addEventListener('click', () => onClick(...args));
return button;
};
const exclusionButton = ["xhsImgTextContainer", "About"];
const updateContainer = buttons => {
let container = document.getElementById('xhsFunctionContainer');
if (!container) {
container = createContainer();
}
// 移除除了 imgTextContainer 以外的所有子元素
Array.from(container.children).forEach(child => {
if (!exclusionButton.includes(child.id)) {
child.remove();
}
});
// 添加有效按钮
buttons.forEach(button => {
container.appendChild(button);
});
};
const buttons = [createButton("Download", "下载无水印作品文件", extractDownloadLinks), createButton("Post", "提取发布作品链接", extractAllLinksEvent, 0), createButton("Collection", "提取收藏作品链接", extractAllLinksEvent, 1), createButton("Favorite", "提取点赞作品链接", extractAllLinksEvent, 2), createButton("Feed", "提取发现作品链接", extractAllLinksEvent, -1), createButton("Search", "提取搜索作品链接", extractAllLinksEvent, 3), createButton("User", "提取搜索用户链接", extractAllLinksEvent, 4), createButton("Board", "提取专辑作品链接", extractAllLinksEvent, 5), createButton("Disclaimer", "脚本说明及免责声明", readme,), createButton("About", "关于 XHS-Downloader", about,),];
const run = url => {
setTimeout(function () {
if (!disclaimer) {
} else if (url === "https://www.xiaohongshu.com/explore" || url.includes("https://www.xiaohongshu.com/explore?")) {
updateContainer(buttons.slice(4, 5));
} else if (url.includes("https://www.xiaohongshu.com/explore/")) {
updateContainer(buttons.slice(0, 1));
} else if (url.includes("https://www.xiaohongshu.com/user/profile/")) {
updateContainer(buttons.slice(1, 4));
} else if (url.includes("https://www.xiaohongshu.com/search_result")) {
updateContainer(buttons.slice(5, 7));
} else if (url.includes("https://www.xiaohongshu.com/board/")) {
updateContainer(buttons.slice(7, 8));
}
}, 500)
}
let currentUrl = window.location.href;
updateContainer(buttons.slice(8));
// 初始化容器
run(currentUrl)
// 设置 MutationObserver 来监听 URL 变化
let observer
if (disclaimer) {
observer = new MutationObserver(function () {
if (currentUrl !== window.location.href) {
currentUrl = window.location.href;
run(currentUrl);
}
});
const config = {childList: true, subtree: true};
observer.observe(document.body, config);
}
const buttonStyle = `
#xhsFunctionContainer {
position: fixed;
bottom: 15%;
background-color: #fff;
color: #2f3542;
padding: 5px 10px;
border-radius: 0 32px 32px 0;
box-shadow: 0 3.2px 12px #00000014, 0 5px 24px #0000000a;
transition: width 0.25s ease-in-out, border-radius 0.25s ease-in-out, height 0.25s ease-in-out;
overflow: hidden;
white-space: nowrap;
width: 65px; /* 初始宽度 */
height: 60px;
text-align: center;
font-size: 16px;
display: flex;
flex-direction: column-reverse;
z-index: 99999;
}
#xhsFunctionContainer:hover {
padding: 10px 10px 5px 10px;
width: 210px; /* hover时的宽度 */
height: auto;
}
#xhsFunctionContainer button {
cursor: pointer;
height: 48px;
color: #ff4757;
font-size: 14px;
font-weight: 600;
border-radius: 32px;
margin-bottom: 14px;
border: 3px #ff4757 solid;
}
#xhsFunctionContainer button:active {
background-color: #ff4757; /* 点击时的背景颜色 */
}
#xhsImgTextContainer {
display: flex;
align-items: center;
gap: 14px;
}
#xhsImgTextContainer__text {
font-size: 14px;
font-weight: 600;
}
`;
const head = document.head || document.getElementsByTagName('head')[0];
const style = document.createElement('style');
head.appendChild(style);
style.type = 'text/css';
style.appendChild(document.createTextNode(buttonStyle));
console.info("用户接受 XHS-Downloader 免责声明", disclaimer)
if (typeof JSZip === 'undefined') {
alert("XHS-Downloader 用户脚本依赖库 JSZip 加载失败,作品文件打包下载功能无法使用,请尝试刷新网页或者向作者反馈!");
}
})();