Bilibili评论区图片批量下载

批量下载B站评论区中的图片(暂仅支持动态和视频评论区)

// ==UserScript==
// @name         Bilibili评论区图片批量下载
// @namespace    BilibiliCommentImageDownloader
// @version      0.3
// @description  批量下载B站评论区中的图片(暂仅支持动态和视频评论区)
// @author       Kaesinol
// @license      MIT
// @match        https://t.bilibili.com/*
// @match        https://*.bilibili.com/opus/*
// @match        https://www.bilibili.com/video/*
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function () {
  "use strict";

  // 当前页码
  let currentPage = 1;

  // 创建下载菜单区域
  function createDownloadMenu() {
    const menuContainer = document.createElement("div");
    menuContainer.id = "bili-img-download-menu";
    menuContainer.style.cssText = `
            position: fixed;
            top: 70px;
            right: 20px;
            width: 400px;
            max-height: 600px;
            overflow-y: auto;
            background-color: #fff;
            border: 1px solid #ccc;
            border-radius: 5px;
            padding: 10px;
            z-index: 9999;
            box-shadow: 0 0 10px rgba(0,0,0,0.2);
            display: none;
        `;

    const menuHeader = document.createElement("div");
    menuHeader.innerHTML = "<h3>评论图片下载</h3>";
    menuHeader.style.cssText = `
            margin-bottom: 10px;
            padding-bottom: 5px;
            border-bottom: 1px solid #eee;
            display: flex;
            justify-content: space-between;
        `;

    const closeButton = document.createElement("span");
    closeButton.innerHTML = "×";
    closeButton.style.cssText = `
            cursor: pointer;
            font-size: 18px;
            font-weight: bold;
        `;
    closeButton.onclick = function () {
      menuContainer.style.display = "none";
    };

    menuHeader.appendChild(closeButton);
    menuContainer.appendChild(menuHeader);

    const menuContent = document.createElement("div");
    menuContent.id = "bili-img-download-content";
    menuContainer.appendChild(menuContent);

    // 添加分页控制区域
    const paginationDiv = document.createElement("div");
    paginationDiv.id = "bili-img-pagination";
    paginationDiv.style.cssText = `
            display: flex;
            justify-content: space-between;
            margin-top: 10px;
            padding-top: 10px;
            border-top: 1px solid #eee;
        `;

    const prevButton = document.createElement("button");
    prevButton.textContent = "上一页";
    prevButton.id = "bili-prev-page";
    prevButton.style.cssText = `
            padding: 5px 10px;
            background-color: #00a1d6;
            color: white;
            border: none;
            border-radius: 3px;
            cursor: pointer;
        `;
    prevButton.disabled = true;

    const pageInfo = document.createElement("span");
    pageInfo.id = "bili-page-info";
    pageInfo.textContent = "第1页";
    pageInfo.style.cssText = `
            line-height: 30px;
        `;

    const nextButton = document.createElement("button");
    nextButton.textContent = "下一页";
    nextButton.id = "bili-next-page";
    nextButton.style.cssText = `
            padding: 5px 10px;
            background-color: #00a1d6;
            color: white;
            border: none;
            border-radius: 3px;
            cursor: pointer;
        `;

    paginationDiv.appendChild(prevButton);
    paginationDiv.appendChild(pageInfo);
    paginationDiv.appendChild(nextButton);
    menuContainer.appendChild(paginationDiv);

    document.body.appendChild(menuContainer);
    return menuContainer;
  }

  // 获取OID,统一从 #bili-comments 元素的 data-params 属性中提取
  function getOid() {
    const commentEl = document.querySelector("bili-comments[data-params]");
    if (commentEl) {
      const params = commentEl.getAttribute("data-params");
      const oidMatch = params && params.match(/\d{4,}/);
      return oidMatch ? oidMatch[0] : null;
    }
    return null;
  }

  // 从API获取数据
  function fetchCommentData(oid, page = 1) {
    return new Promise((resolve, reject) => {
      // 根据当前页面类型选择 type
      let initialType = 11;
      if (
        window.location.href.indexOf("https://www.bilibili.com/video/") === 0
      ) {
        initialType = 1;
      }
      const fetchWithType = (type) => {
        const apiUrl = `https://api.bilibili.com/x/v2/reply?type=${type}&oid=${oid}&pn=${page}`;
        GM_xmlhttpRequest({
          method: "GET",
          url: apiUrl,
          onload: function (response) {
            try {
              const data = JSON.parse(response.responseText);
              if (data && data.code === 0) {
                resolve(data.data);
              } else if (type === 11) {
                console.warn("Type 11 failed, retrying with Type 17...");
                fetchWithType(17);
              } else {
                reject("获取数据失败: " + (data.message || "未知错误"));
              }
            } catch (e) {
              reject("解析数据失败: " + e.message);
            }
          },
          onerror: function (error) {
            reject("网络请求失败: " + error);
          },
        });
      };

      fetchWithType(initialType);
    });
  }

  // 处理获取到的数据
  function processData(data, page) {
    const replies =
      page === 1
        ? [...(data.top_replies || []), ...(data.replies || [])]
        : data.replies || [];
    const processedData = [];

    for (const reply of replies) {
      if (!reply.member || !reply.content) continue;

      const pictures = reply.content.pictures || [];
      if (pictures.length === 0) continue;

      const message = reply.content.message || "";
      // 储存完整消息和截断消息
      const truncatedMessage =
        message.length > 10 ? message.substring(0, 10) + "..." : message;
      // 硬截断为20个字符
      const hardTruncatedMessage =
        message.length > 20 ? message.substring(0, 20) + "..." : message;

      const displayText = `${
        reply.member.uname
      } - ${truncatedMessage} - ${formatTimestamp(reply.ctime)}`;

      const imageData = pictures.map((pic, index) => {
        const originalUrl = pic.img_src;
        const fileExtension = originalUrl.split(".").pop().split("?")[0];

        // 处理biz_scene,移除opus_前缀
        let bizScene = reply.reply_control?.biz_scene || "unknown";
        bizScene = bizScene.replace("opus_", "");

        // 新的命名格式
        return {
          url: originalUrl,
          fileName: `${reply.member.uname} - ${reply.member.mid} - ${bizScene} - ${index}.${fileExtension}`,
        };
      });

      processedData.push({
        displayText,
        fullMessage: message,
        truncatedMessage: hardTruncatedMessage,
        username: reply.member.uname,
        timestamp: formatTimestamp(reply.ctime),
        images: imageData,
      });
    }

    return processedData;
  }

  // 格式化时间戳
  function formatTimestamp(timestamp) {
    const date = new Date(timestamp * 1000);
    return `${date.getFullYear()}-${padZero(date.getMonth() + 1)}-${padZero(
      date.getDate()
    )} ${padZero(date.getHours())}:${padZero(date.getMinutes())}`;
  }

  // 数字补零
  function padZero(num) {
    return num < 10 ? "0" + num : num;
  }

  // 创建下载选项
  function createDownloadOptions(processedData, menuContent) {
    menuContent.innerHTML = "";

    if (processedData.length === 0) {
      menuContent.innerHTML = "<p>没有找到包含图片的评论</p>";
      return;
    }

    for (let i = 0; i < processedData.length; i++) {
      const item = processedData[i];

      const downloadOption = document.createElement("div");
      downloadOption.className = "download-option";
      downloadOption.style.cssText = `
                padding: 8px;
                margin: 5px 0;
                border: 1px solid #eee;
                border-radius: 3px;
                cursor: pointer;
                transition: background-color 0.2s;
            `;

      const downloadOptionContent = document.createElement("div");
      downloadOptionContent.style.cssText = `
                display: flex;
                justify-content: space-between;
                align-items: center;
            `;

      const infoDiv = document.createElement("div");
      infoDiv.style.cssText = `
                display: flex;
                flex-wrap: nowrap;
                align-items: center;
                overflow: hidden;
                flex: 1;
            `;

      const usernameSpan = document.createElement("span");
      usernameSpan.textContent = item.username;
      usernameSpan.style.cssText = `
                font-weight: bold;
                margin-right: 5px;
                white-space: nowrap;
            `;

      const messageSpan = document.createElement("span");
      messageSpan.textContent = item.truncatedMessage;
      messageSpan.title = item.fullMessage; // 添加tooltip显示完整消息内容
      messageSpan.style.cssText = `
                margin: 0 5px;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
                flex: 1;
            `;

      const timeSpan = document.createElement("span");
      timeSpan.textContent = item.timestamp;
      timeSpan.style.cssText = `
                font-size: 11px;
                color: #999;
                white-space: nowrap;
                margin-left: 5px;
            `;

      const countSpan = document.createElement("span");
      countSpan.style.cssText = `
                color: #00a1d6;
                white-space: nowrap;
                margin-left: 10px;
            `;
      countSpan.textContent = `[${item.images.length}张]`;

      infoDiv.appendChild(usernameSpan);
      infoDiv.appendChild(messageSpan);
      infoDiv.appendChild(timeSpan);

      downloadOptionContent.appendChild(infoDiv);
      downloadOptionContent.appendChild(countSpan);
      downloadOption.appendChild(downloadOptionContent);

      downloadOption.addEventListener("mouseover", function () {
        this.style.backgroundColor = "#f5f5f5";
      });

      downloadOption.addEventListener("mouseout", function () {
        this.style.backgroundColor = "transparent";
      });

      downloadOption.addEventListener("click", function () {
        downloadImages(item.images);
      });

      menuContent.appendChild(downloadOption);
    }
  }

  // 下载图片
  function downloadImages(images) {
    let downloaded = 0;

    console.log("开始下载", `准备下载 ${images.length} 张图片...`);

    for (const image of images) {
      GM_download({
        url: image.url,
        name: image.fileName,
        onload: function () {
          downloaded++;
          if (downloaded === images.length) {
            console.log(`成功下载 ${downloaded} 张图片...`);
          }
        },
        onerror: function (error) {
          console.log(`图片 ${image.fileName} 下载失败`, error);
        },
      });
    }
  }

  // 加载数据并显示
  async function loadAndDisplayData(page = 1) {
    const menuContainer =
      document.getElementById("bili-img-download-menu") || createDownloadMenu();
    const menuContent = document.getElementById("bili-img-download-content");
    const pageInfo = document.getElementById("bili-page-info");
    const prevButton = document.getElementById("bili-prev-page");

    menuContainer.style.display = "block";
    menuContent.innerHTML = "<p>正在加载数据...</p>";

    try {
      const oid = getOid();
      if (!oid) {
        menuContent.innerHTML = "<p>错误: 无法获取OID,请确保在正确的页面</p>";
        return;
      }

      const data = await fetchCommentData(oid, page);
      const processedData = processData(data, page);
      createDownloadOptions(processedData, menuContent);

      // 更新分页信息
      currentPage = page;
      pageInfo.textContent = `第${page}页`;
      prevButton.disabled = page <= 1;

      // 如果没有数据,禁用下一页按钮
      const nextButton = document.getElementById("bili-next-page");
      if (processedData.length === 0) {
        nextButton.disabled = true;
      } else {
        nextButton.disabled = false;
      }
    } catch (error) {
      menuContent.innerHTML = `<p>错误: ${error}</p>`;
      console.error("Error:", error);
    }
  }

  // 添加导航按钮
  function addNavButton() {
    const navContainer = document.querySelector(".bili-tabs__nav__items");
    if (!navContainer) {
      // 如果找不到导航容器,稍后再试
      setTimeout(addNavButton, 1000);
      return;
    }

    const navItem = document.createElement("div");
    navItem.className = "bili-tabs__nav__item";
    navItem.textContent = "解析评论区图片";
    navItem.style.cssText = `
            cursor: pointer;
        `;

    navItem.addEventListener("click", function () {
      loadAndDisplayData(1);
    });

    navContainer.appendChild(navItem);
  }

  // 设置分页事件监听
  function setupPaginationEvents() {
    document.addEventListener("click", function (e) {
      if (e.target.id === "bili-prev-page" && !e.target.disabled) {
        if (currentPage > 1) {
          loadAndDisplayData(currentPage - 1);
        }
      } else if (e.target.id === "bili-next-page" && !e.target.disabled) {
        loadAndDisplayData(currentPage + 1);
      }
    });
  }

  // 主函数
  function main() {
    // 创建下载菜单但不显示
    createDownloadMenu();

    // 添加导航按钮
    addNavButton();

    // 设置分页事件
    setupPaginationEvents();

    // 添加油猴脚本菜单命令,点击后弹出下载界面
    GM_registerMenuCommand("显示下载界面", function () {
      loadAndDisplayData(1);
    });

    // 点击其他地方关闭菜单
    document.addEventListener("click", function (e) {
      const menuContainer = document.getElementById("bili-img-download-menu");
      if (
        menuContainer &&
        menuContainer.style.display === "block" &&
        !menuContainer.contains(e.target) &&
        !e.target.matches(".bili-tabs__nav__item")
      ) {
        menuContainer.style.display = "none";
      }
    });
  }

  // 页面加载完成后执行
  window.addEventListener("load", main);
})();

QingJ © 2025

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