Bilibili动态预览图片下载

在B站个人空间的投稿 - 图文界面,提供右键直接下载动态中的图片

当前为 2025-01-30 提交的版本,查看 最新版本

// ==UserScript==
// @name         Bilibili动态预览图片下载
// @namespace    BilibiliDynamicPreviewDownload
// @license      MIT
// @version      0.9
// @description  在B站个人空间的投稿 - 图文界面,提供右键直接下载动态中的图片
// @author       Kaesinol
// @match        https://space.bilibili.com/*
// @grant        GM_download
// @require      https://update.gf.qytechs.cn/scripts/473358/1237031/JSZip.js
// ==/UserScript==

(function () {
  "use strict";

  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:")
        ) || [];
      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);
      downloadFiles(pictures, fileName);
    } catch (error) {
      console.error("请求或解析失败:", error);
    }
  };

  const downloadFiles = async (urls, fileName) => {
    if (urls.length > 1) {
      await createZipAndDownload(urls, fileName);
    } else {
      const promises = urls.map((url, index) =>
        downloadFile(url, index, fileName)
      );
      await Promise.all(promises);
    }
  };

  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) => {
          // Get the file extension dynamically using the lambda
          const extensionMatch = getFileExtensionFromUrl(url);
          const extension = extensionMatch[1]; // No error handling, must have an extension
          const fileNameWithIndex = `${fileName} - ${index + 1}.${extension}`;
          zip.file(fileNameWithIndex, blob);
        })
        .catch((error) => {
          console.error("下载文件失败:", error);
        });
    });

    await Promise.all(promises);

    // Generate and download the ZIP file
    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]; // No error handling, must have an extension
      const fileDownloadName = `${fileName} - ${index + 1}.${extension}`;

      // Use GM_download to download the file
      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.href.match(/\/(\d+)\??/);
      if (match && match[1]) {
        const dynamicId = match[1];
        fetchJsonData(dynamicId);
      } else {
        console.warn("未匹配到动态ID:", targetElement.href);
      }
    }
  };

  const observer = new MutationObserver(() => {
    let targetElements = document.querySelectorAll(
      "div.opus-body > div > div > div > div > div > div a"
    );
    targetElements = targetElements.length
      ? targetElements
      : document.querySelectorAll(
          "#page-article .main-content .article-card a"
        );
    targetElements.forEach((targetElement) => {
      if (!targetElement.hasAttribute("data-listener")) {
        targetElement.addEventListener(
          "contextmenu",
          (event) => handleEvent(event, targetElement),
          true
        ); // 捕获阶段绑定
        targetElement.setAttribute("data-listener", "true"); // 防止重复绑定
      }
    });
  });

  // 配置 MutationObserver
  observer.observe(document.body, {
    childList: true,
    subtree: true,
  });

  // 初次匹配页面上已经存在的元素
  let initialTargetElements = document.querySelectorAll(
    "div.opus-body > div > div > div > div > div > div a"
  );
  initialTargetElements = initialTargetElements.length
    ? initialTargetElements
    : document.querySelectorAll("#page-article .main-content .article-card a");
  initialTargetElements.forEach((targetElement) => {
    targetElement.addEventListener(
      "contextmenu",
      (event) => handleEvent(event, targetElement),
      true
    ); // 捕获阶段绑定
  });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址