Bilibili动态预览图片下载

在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
    );
  });
})();