// ==UserScript==
// @name 小红书转发
// @namespace https://mundane.ink/redbook
// @version 3.5
// @description 在浏览小红书收藏和点赞时将数据转发到https://xhs.mundane.ink,方便收藏和点赞的管理和导出
// @match https://www.xiaohongshu.com/*
// @grant unsafeWindow
// @license MIT
// @icon https://www.xiaohongshu.com/favicon.ico
// ==/UserScript==
(function () {
"use strict";
console.log("小红书脚本生效了");
const baseUrl = "https://mundane.ink";
// const baseUrl = "http://localhost:8088";
document.body.addEventListener("click", (e) => {
if (
e.target.tagName === "A" &&
e.target.classList.value.includes("cover ld mask")
) {
setTimeout(() => {
const href = window.location.href;
const noteId = extractID(href);
if (noteId) {
createDownloadMdButton(noteId);
createMediaButton(noteId);
}
}, 1000);
}
});
function extractID(url) {
const regex = /explore\/([0-9a-fA-F]+)/;
const match = url.match(regex);
return match ? match[1] : null;
}
// 创建下载md按钮
function createDownloadMdButton(noteId) {
const mask = document.querySelector("div.note-detail-mask");
const button = document.createElement("button");
button.textContent = "下载md文件";
button.style.position = "fixed";
button.style.bottom = "65px";
button.style.right = "20px";
button.style.padding = "10px 20px";
button.style.border = "none";
button.style.backgroundColor = "#056b00";
button.style.color = "#fff";
button.style.fontFamily = "Arial, sans-serif";
button.style.fontSize = "16px";
button.style.fontWeight = "bold";
button.style.cursor = "pointer";
button.addEventListener("click", function () {
exportMd(noteId);
});
mask.appendChild(button);
}
// 创建下载图片和视频按钮
function createMediaButton(noteId) {
const mask = document.querySelector("div.note-detail-mask");
const button = document.createElement("button");
button.textContent = "下载图片和视频";
button.style.position = "fixed";
button.style.bottom = "20px";
button.style.right = "20px";
button.style.padding = "10px 20px";
button.style.border = "none";
button.style.backgroundColor = "#056b00";
button.style.color = "#fff";
button.style.fontFamily = "Arial, sans-serif";
button.style.fontSize = "16px";
button.style.fontWeight = "bold";
button.style.cursor = "pointer";
button.addEventListener("click", function () {
extractDownloadLinks();
});
mask.appendChild(button);
}
const extractDownloadLinks = async () => {
let note = extractNoteInfo();
if (note.note) {
await exploreDeal(note.note);
}
};
const extractNoteInfo = () => {
let note = Object.values(unsafeWindow.__INITIAL_STATE__.note.noteDetailMap);
return note[note.length - 1];
};
const exploreDeal = async (note) => {
try {
let links;
if (note.type === "normal") {
links = generateImageUrl(note);
} else {
links = generateVideoUrl(note);
}
if (links.length > 0) {
await download(links, note.type);
}
} catch (error) {
console.error("Error in deal function:", error);
}
};
const download = async (urls, type_) => {
const name = extractName();
if (type_ === "video") {
await downloadVideo(urls[0], name);
} else {
await downloadImage(urls, name);
}
};
const downloadVideo = async (url, name) => {
if (!(await downloadFile(url, `${name}.mp4`))) {
console.error("下载视频失败");
}
};
const downloadImage = async (urls, name) => {
let result = [];
for (const [index, url] of urls.entries()) {
result.push(await downloadFile(url, `${name}_${index + 1}.png`));
}
if (!result.every((item) => item === true)) {
console.error("下载图片失败");
}
};
const downloadFile = async (link, filename) => {
try {
// 使用 fetch 获取文件数据
let response = await fetch(link);
// 检查响应状态码
if (!response.ok) {
console.error(`请求失败,状态码: ${response.status}`, response.status);
return false;
}
let blob = await response.blob();
// 创建 Blob 对象的 URL
let blobUrl = window.URL.createObjectURL(blob);
// 创建一个临时链接元素
let tempLink = document.createElement("a");
tempLink.href = blobUrl;
tempLink.download = filename;
// 模拟点击链接
tempLink.click();
// 清理临时链接元素
window.URL.revokeObjectURL(blobUrl);
return true;
} catch (error) {
console.error(`下载失败 (${filename}):`, error);
return false;
}
};
const extractName = () => {
let name = document.title.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, "");
let match = window.location.href.match(/\/([^\/]+)$/);
let id = match ? match[1] : null;
return name === "" ? id : name;
};
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/2/w/format/png`
);
}
});
return urls;
} catch (error) {
console.error("Error generating image URLs:", error);
return [];
}
};
function getMedia(noteId) {
fetch(`${baseUrl}/mail/redbook/note/getMediaInfo`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ noteId }),
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// 解析响应数据为 JSON 格式
return response.json();
})
.then((resp) => {
if (resp.code === 200) {
exportMedia(resp.data);
}
})
.catch((error) => console.error(error));
}
function exportMedia(data) {
const { title, videoUrl, imageUrls } = data;
if (imageUrls.length <= 10) {
exportImages(title, imageUrls);
} else {
exportMoreImages(title, imageUrls);
}
if (videoUrl) {
exportVideo(title, videoUrl);
}
}
async function exportMoreImages(title, imageUrls) {
const imgUrls1 = imageUrls.slice(0, 10);
const imgUrls2 = imageUrls.slice(10);
exportImages(title, imgUrls1);
await pause();
exportImages10(title, imgUrls2);
}
// 暂停1s
function pause() {
return new Promise((resolve) => {
setTimeout(resolve, 1000);
});
}
function exportVideo(title, videoUrl) {
fetch(videoUrl)
.then((response) => {
return response.blob();
})
.then((blob) => {
// 创建一个下载链接
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = title + ".mp4";
document.body.appendChild(a);
// 模拟点击下载链接
a.click();
// 清理对象 URL
URL.revokeObjectURL(url);
})
.catch((error) => console.error(error));
}
async function exportImages10(title, imageUrls) {
for (let i = 0; i < imageUrls.length; i++) {
const imageUrl = imageUrls[i];
const response = await fetch(imageUrl);
const blob = await response.blob();
const imageURL = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = imageURL;
a.download = title + "-" + (i + 11) + ".png";
a.click();
URL.revokeObjectURL(imageURL);
}
}
async function exportImages(title, imageUrls) {
for (let i = 0; i < imageUrls.length; i++) {
const imageUrl = imageUrls[i];
const response = await fetch(imageUrl);
const blob = await response.blob();
const imageURL = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = imageURL;
a.download = title + "-" + (i + 1) + ".png";
a.click();
URL.revokeObjectURL(imageURL);
}
}
function exportMd(noteId) {
fetch(`${baseUrl}/mail/redbook/note/exportNoteMd`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ noteId }),
})
.then((response) => {
const contentDisposition = response.headers.get("Content-Disposition");
const filenameMatch = decodeURIComponent(
contentDisposition.match(/filename\=(.*)/)[1]
);
const filename = filenameMatch || "filename.md";
response.blob().then((blob) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
});
})
.catch((error) => console.error(error));
}
// 创建按钮元素
const btnScroll = document.createElement("button");
btnScroll.innerHTML = "自动滚动";
const btnJump = document.createElement("button");
btnJump.innerHTML = "去下载";
const btnTest = document.createElement("button");
btnTest.innerHTML = "测试";
// 设置按钮样式
btnScroll.style.position = "fixed";
btnScroll.style.top = "160px";
btnScroll.style.right = "20px";
btnScroll.style.backgroundColor = "#056b00";
btnScroll.style.color = "#fff";
btnScroll.style.padding = "8px";
btnScroll.style.borderRadius = "6px";
btnScroll.style.zIndex = "1000";
btnJump.style.position = "fixed";
btnJump.style.top = "210px";
btnJump.style.right = "20px";
btnJump.style.backgroundColor = "#056b00";
btnJump.style.color = "#fff";
btnJump.style.padding = "8px";
btnJump.style.borderRadius = "6px";
btnJump.style.zIndex = "1000";
btnTest.style.position = "fixed";
btnTest.style.top = "260px";
btnTest.style.right = "20px";
btnTest.style.backgroundColor = "#056b00";
btnTest.style.color = "#fff";
btnTest.style.padding = "8px";
btnTest.style.borderRadius = "6px";
btnTest.style.zIndex = "1000";
// 添加按钮到页面中
document.body.appendChild(btnScroll);
document.body.appendChild(btnJump);
// document.body.appendChild(btnTest);
let isScrolling = false;
let timerId;
function getUserId() {
const arr = window.location.href.match(/\/user\/profile\/(\w+)/);
if (!arr) {
return "";
}
if (arr.length < 2) {
return "";
}
return arr[1];
}
function simulateScroll() {
// window.scrollBy(0, 200);
window.scrollBy({ top: 200, left: 0, behavior: "smooth" });
}
function startScroll() {
if (isScrolling) {
return;
}
isScrolling = true;
btnScroll.innerHTML = "停止滚动";
btnScroll.style.backgroundColor = "#ff2442";
timerId = setInterval(simulateScroll, 200);
}
function cancelScroll() {
if (!isScrolling) {
return;
}
isScrolling = false;
btnScroll.style.backgroundColor = "#056b00";
btnScroll.innerHTML = "自动滚动";
if (timerId) {
clearInterval(timerId);
}
}
// 给按钮添加点击事件
btnScroll.addEventListener("click", function () {
if (isScrolling) {
cancelScroll();
} else {
startScroll();
}
});
btnJump.addEventListener("click", function () {
const userId = getUserId();
window.open(
`https://xhs.mundane.ink/manage/collect?userId=${userId}`,
"_blank"
);
});
btnTest.addEventListener("click", function () {
let tab = document.querySelectorAll(".tab-content-item")[1];
const elements = tab.querySelectorAll("a.cover.ld.mask");
elements[0].click();
let timeId = setInterval(function () {
let closeButton = document.querySelector("div.close-circle div.close");
if (closeButton) {
closeButton.click();
clearInterval(timeId);
}
}, 500);
});
const originOpen = XMLHttpRequest.prototype.open;
const collectUrl = "//edith.xiaohongshu.com/api/sns/web/v2/note/collect";
const feedUrl = "//edith.xiaohongshu.com/api/sns/web/v1/feed";
const likeUrl = "//edith.xiaohongshu.com/api/sns/web/v1/note/like";
let patchIndex = 0;
XMLHttpRequest.prototype.open = function (_, url) {
const xhr = this;
if (
url.startsWith(collectUrl) ||
url.startsWith(feedUrl) ||
url.startsWith(likeUrl)
) {
const getter = Object.getOwnPropertyDescriptor(
XMLHttpRequest.prototype,
"response"
).get;
Object.defineProperty(xhr, "responseText", {
get: () => {
let result = getter.call(xhr);
// console.log("result =", result);
let myUrl = "";
let requestData = "";
const userId = getUserId();
if (url.startsWith(collectUrl)) {
if (!userId) {
return result;
}
const params = new URLSearchParams(url.split("?")[1]);
const cursor = params.get("cursor");
if (!cursor) {
patchIndex = 0;
}
myUrl = `${baseUrl}/mail/redbook/collect/save`;
requestData = JSON.stringify({ result, userId, patchIndex });
} else if (url.startsWith(feedUrl)) {
myUrl = `${baseUrl}/mail/redbook/note/save`;
requestData = JSON.stringify({ result });
} else if (url.startsWith(likeUrl)) {
if (!userId) {
return result;
}
myUrl = `${baseUrl}/mail/redbook/like/save`;
const data = JSON.parse(result).data;
// 不要拦截点赞笔记的请求
if (!data.hasOwnProperty("notes")) {
return result;
}
requestData = JSON.stringify({ result, userId });
}
try {
// 将result发送到服务器
fetch(myUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: requestData,
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then((resp) => {
if (resp.code === 200) {
console.log("ok");
}
})
.catch((error) => console.error(error));
if (url.startsWith(collectUrl)) {
patchIndex++;
const obj = JSON.parse(result);
if (isScrolling && !obj.data.has_more) {
cancelScroll();
patchIndex = 0;
console.log("没有更多收藏数据了!!!");
alert("小红书收藏已发送完毕,没有更多了");
}
} else if (isScrolling && url.startsWith(likeUrl)) {
const obj = JSON.parse(result);
if (!obj.data.has_more) {
cancelScroll();
console.log("没有更多点赞数据了!!!");
alert("小红书点赞已发送完毕,没有更多了");
}
}
} catch (e) {
console.error(e);
}
return result;
},
});
}
originOpen.apply(this, arguments);
};
})();