在B站个人空间的投稿 - 图文界面,提供右键直接下载动态中的图片,并记录已下载的动态ID,改变背景颜色来区别。(不支持旧版界面)
当前为
// ==UserScript==
// @name Bilibili动态预览图片下载
// @namespace BilibiliDynamicPreviewDownload
// @license MIT
// @version 1.0.3
// @description 在B站个人空间的投稿 - 图文界面,提供右键直接下载动态中的图片,并记录已下载的动态ID,改变背景颜色来区别。(不支持旧版界面)
// @author Kaesinol
// @match https://space.bilibili.com/*
// @grant GM_download
// @grant GM_getValue
// @grant GM_setValue
// @require https://update.greasyfork.org/scripts/473358/1237031/JSZip.js
// ==/UserScript==
(function () {
"use strict";
// 获取已下载的动态ID集合(初始为空)
let downloadedDynamicIds = GM_getValue("downloadedDynamicIds", {});
const fetchJsonData = async (dynamicId) => {
const apiUrl = `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/get_dynamic_detail?dynamic_id=${dynamicId}`;
try {
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const jsonData = await response.json();
const cardData = JSON.parse(jsonData.data.card.card);
const pictures =
cardData.item?.pictures?.map((p) =>
p.img_src.replace(/^http:/, "https:")
) ||
cardData.origin_image_urls ||
[];
const uname = jsonData.data.card.desc.user_profile.info.uname;
const uid = jsonData.data.card.desc.user_profile.info.uid;
const fileName = `${uname} - ${uid} - ${dynamicId}`;
console.log("提取的图片链接:", pictures);
if (pictures.length > 1) await createZipAndDownload(pictures, fileName);
else await downloadFile(pictures[0], 0, fileName);
downloadedDynamicIds[dynamicId] = true;
GM_setValue("downloadedDynamicIds", downloadedDynamicIds);
updateLinkColor(dynamicId);
} catch (error) {
console.error("请求或解析失败:", error);
}
};
const createZipAndDownload = async (urls, fileName) => {
const zip = new JSZip();
const promises = urls.map((url, index) => {
return fetch(url)
.then((response) => {
if (!response.ok) {
throw new Error(`Failed to fetch ${url}`);
}
return response.blob();
})
.then((blob) => {
const extensionMatch = getFileExtensionFromUrl(url);
const extension = extensionMatch[1];
const fileNameWithIndex = `${fileName} - ${index + 1}.${extension}`;
zip.file(fileNameWithIndex, blob);
})
.catch((error) => {
console.error("下载文件失败:", error);
});
});
await Promise.all(promises);
zip
.generateAsync({ type: "blob" })
.then((content) => {
GM_download({
url: URL.createObjectURL(content),
name: `${fileName}.zip`,
saveAs: false,
});
})
.catch((error) => {
console.error("ZIP生成失败:", error);
});
};
const getFileExtensionFromUrl = (url) => url.match(/\.([a-zA-Z0-9]+)$/);
const downloadFile = async (url, index, fileName) => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}`);
}
const blob = await response.blob();
const extensionMatch = getFileExtensionFromUrl(url);
const extension = extensionMatch[1];
const fileDownloadName = `${fileName} - ${index + 1}.${extension}`;
GM_download({
url: URL.createObjectURL(blob),
name: fileDownloadName,
saveAs: false,
});
} catch (error) {
console.error("下载文件失败:", error);
}
};
const handleEvent = (event, targetElement) => {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
if (event.type === "contextmenu") {
const match = targetElement.querySelector("a").href.match(/\/(\d+)\??/);
if (match && match[1]) {
const dynamicId = match[1];
fetchJsonData(dynamicId);
} else {
console.warn("未匹配到动态ID:", targetElement.href);
}
}
};
const updateLinkColor = (dynamicId) => {
const link = document.querySelector(`a[href*="${dynamicId}"]`);
if (link) {
link.parentElement.style.backgroundColor = "green";
}
};
const observer = new MutationObserver(() => {
let targetElements = document.querySelectorAll("div.opus-body div.item");
targetElements.forEach((targetElement) => {
if (!targetElement.hasAttribute("data-listener")) {
targetElement.addEventListener(
"contextmenu",
(event) => handleEvent(event, targetElement),
true
);
targetElement.setAttribute("data-listener", "true");
}
// 检查已下载的动态ID,并更新相应链接的颜色
const link = targetElement.querySelector("a");
const match = link.href.match(/\/(\d+)\??/);
if (match && downloadedDynamicIds[match[1]]) {
link.parentElement.style.backgroundColor = "green";
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
const initialTargetElements = document.querySelectorAll(
"div.opus-body div.item "
);
initialTargetElements.forEach((targetElement) => {
targetElement.addEventListener(
"contextmenu",
(event) => handleEvent(event, targetElement),
true
);
});
})();