抖音主页视频图文下载

拦截抖音主页接口,获取用户信息和视频列表数据,于视频、图文下载

// ==UserScript==
// @name         抖音主页视频图文下载
// @namespace    douyin-homepage-download
// @version      1.1.3
// @description  拦截抖音主页接口,获取用户信息和视频列表数据,于视频、图文下载
// @author       chrngfu
// @match        https://www.douyin.com/*
// @license      MIT
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @require      https://unpkg.com/[email protected]
// ==/UserScript==

(function () {
  "use strict";

  // 新增:作者信息展示区域
  function createAuthorInfoBox() {
    const authorInfoBox = document.createElement("div");
    authorInfoBox.id = "authorInfoBox";
    authorInfoBox.innerHTML = `
      <div class="header">
          <h4>作者信息</h4>
          <button id="deleteAuthorBtn">删除作者数据</button>
      </div>
      <div class="info-grid">
          <div><strong>昵称:</strong><span id="authorNickname">-</span></div>
          <div><strong>粉丝数:</strong><span id="authorFollowers">-</span></div>
          <div><strong>获赞数:</strong><span id="authorLikes">-</span></div>
          <div><strong>作品数:</strong><span id="authorWorks">-</span></div>
          <div><strong>IP 属地:</strong><span id="authorIP">-</span></div>
          <div><strong>签名:</strong><span id="authorSignature">-</span></div>
      </div>
  `;
    return authorInfoBox;
  }

  // 新增:友好提示函数
  function showFriendlyMessage(message, isSuccess = true) {
    const msgBox = document.createElement("div");
    msgBox.className = `friendly-message ${isSuccess ? "success" : "error"}`;
    msgBox.textContent = message;
    document.body.appendChild(msgBox);

    setTimeout(() => {
      document.body.removeChild(msgBox);
    }, 3000);
  }

  // 使用 GM_addStyle 添加 CSS 样式
  GM_addStyle(`
      /* 新增禁用按钮样式 */
      button:disabled {
          opacity: 0.6;
          cursor: not-allowed;
      }
      #videoTableContainer {
          width: 90%;
          height: 80%;
          position: fixed;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
          background-color: #fff;
          padding: 20px;
          z-index: 10000;
          border: 1px solid #ccc;
          box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
          overflow: hidden;
          display: flex;
          flex-direction: column;
      }
      #videoTableContainer h3 {
          margin: 0 0 10px 0;
      }
      #videoTableContainer table {
          width: 100%;
          border-collapse: collapse;
          table-layout: fixed;
      }
      #videoTableContainer table th,
      #videoTableContainer table td {
          border: 1px solid #ddd;
          font-size: 14px;
          padding: 4px 6px;
          text-align: left;
          vertical-align: middle; /* 上下居中 */
      }
      #videoTableContainer table th {
          text-align: center;
          background-color: #f2f2f2;
          font-weight: bold;
      }
      #videoTableContainer table tr {
          height: 50px; /* 固定每行高度 */
      }
      #videoTableContainer table tr:nth-child(even) {
          background-color: #f9f9f9;
      }
      #videoTableContainer table tr:hover {
          background-color: #f1f1f1;
      }
      #videoTableContainer table td.center {
          text-align: center; /* 左右居中 */
      }
      #videoTableContainer .cover-image {
          max-width: 100px;
          max-height: 50px;
          display: block;
          margin: 0 auto;
      }
      #videoTableContainer .filters {
          margin-bottom: 10px;
      }
      #videoTableContainer .filters select,
      #videoTableContainer .filters input {
          margin-right: 10px;
      }
      #videoTableContainer .actions {
          margin-bottom: 10px;
      }
      #videoTableContainer .actions button {
          margin-right: 10px;
      }
      #videoTableContainer #videoTableWrapper {
          flex: 1;
          overflow-y: auto;
      }
      /* 新增样式 */
      #closeButton {
          position: absolute;
          top: 10px;
          right: 10px;
          background-color: #f44336;
          color: white;
          border: none;
          padding: 5px 10px;
          cursor: pointer;
      }
      #authorInfoBox {
          margin-bottom: 10px;
          padding: 10px;
          background-color: #f9f9f9;
          border: 1px solid #ddd;
          border-radius: 4px;
          display: none;
      }
      #authorInfoBox .header {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-bottom: 10px;
      }
      #authorInfoBox h4 {
          margin: 0;
      }
      #deleteAuthorBtn {
          background-color: #f44336;
          color: white;
          border: none;
          padding: 5px 10px;
          border-radius: 4px;
          cursor: pointer;
      }
      #authorInfoBox .info-grid {
          display: flex;
          flex-wrap: wrap;
          gap: 10px;
      }
      .friendly-message {
          position: fixed;
          top: 20px;
          left: 50%;
          transform: translateX(-50%);
          padding: 10px 20px;
          color: white;
          border-radius: 4px;
          z-index: 100000;
          box-shadow: 0 2px 5px rgba(0,0,0,0.2);
      }
      .friendly-message.success {
          background-color: #4CAF50;
      }
      .friendly-message.error {
          background-color: #f44336;
      }
      #videoTable td {
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
      }
      #showDataButton {
          position: fixed;
          bottom: 20px;
          right: 20px;
          z-index: 10001;
      }
      /* 图片预览相关样式 */
      .preview-overlay {
          position: fixed;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          background-color: rgba(0, 0, 0, 0.8);
          display: flex;
          justify-content: center;
          align-items: center;
          z-index: 100001;
          cursor: pointer;
      }

      .preview-image {
          max-width: 90%;
          max-height: 90vh;
          object-fit: contain;
          border-radius: 4px;
          box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
      }

      .cover-image {
          max-width: 100px;
          max-height: 50px;
          display: block;
          margin: 0 auto;
          cursor: pointer;
          transition: transform 0.2s;
      }

      .cover-image:hover {
          transform: scale(1.05);
      }
  `);

  // 获取 Aweme 名称
  function getAwemeName(aweme) {
    let name = aweme.item_title ? aweme.item_title : aweme.caption;
    if (!name) name = aweme.desc ? aweme.desc : aweme.awemeId;
    return (
      (aweme.date ? `【${aweme.date.slice(0, 10)}】` : "") +
      name
        .replace(/[\/:*?"<>|\s]+/g, "")
        .slice(0, 27)
        .replace(/\.\d+$/g, "")
    );
  }

  // 拦截 XHR 请求
  const originalOpen = XMLHttpRequest.prototype.open;
  const originalSend = XMLHttpRequest.prototype.send;

  XMLHttpRequest.prototype.open = function (method, url) {
    this._url = url; // 保存请求的 URL
    return originalOpen.apply(this, arguments);
  };

  XMLHttpRequest.prototype.send = function (body) {
    // 监听请求完成事件
    this.addEventListener("load", function () {
      if (this._url.includes("/aweme/v1/web/user/profile/other")) {
        // 用户主页信息
        const userProfile = JSON.parse(this.responseText);
        console.log("原始用户主页信息:", userProfile);

        // 格式化用户信息
        const formattedUserInfo = formatUserData(userProfile.user || {});
        console.log("格式化后的用户信息:", formattedUserInfo);

        // 缓存用户信息
        cacheUserInfo(formattedUserInfo);
      } else if (this._url.includes("/aweme/v1/web/aweme/post/")) {
        // 主页视频列表信息
        const videoList = JSON.parse(this.responseText);
        console.log("主页视频列表信息:", videoList);
        processVideoList(videoList);
      }
    });

    return originalSend.apply(this, arguments);
  };

  // 格式化用户信息
  function formatUserData(userInfo) {
    for (let key in userInfo) {
      if (!userInfo[key]) userInfo[key] = ""; // 确保每个字段都有值
    }
    return {
      uid: userInfo.uid,
      nickname: userInfo.nickname,
      following_count: userInfo.following_count,
      mplatform_followers_count: userInfo.mplatform_followers_count,
      total_favorited: userInfo.total_favorited,
      unique_id: userInfo.unique_id ? userInfo.unique_id : userInfo.short_id,
      ip_location: userInfo.ip_location ? userInfo.ip_location.replace("IP属地:", "") : "",
      gender: userInfo.gender ? "男女".charAt(userInfo.gender).trim() : "",
      city: [userInfo.province, userInfo.city, userInfo.district].filter(x => x).join("·"), // 合并城市信息
      signature: userInfo.signature,
      aweme_count: userInfo.aweme_count,
      create_time: Date.now(),
    };
  }

  // 格式化日期
  function formatDate(date, fmt) {
    date = new Date(date * 1000);
    let o = {
      "M+": date.getMonth() + 1, //月份
      "d+": date.getDate(), //日
      "H+": date.getHours(), //小时
      "m+": date.getMinutes(), //分
      "s+": date.getSeconds(), //秒
      "q+": Math.floor((date.getMonth() + 3) / 3), //季度
      S: date.getMilliseconds(), //毫秒
    };
    if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
    for (let k in o)
      if (new RegExp("(" + k + ")").test(fmt))
        fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length));
    return fmt;
  }

  // 格式化秒数为时间字符串
  function formatSeconds(value) {
    let secondTime = parseInt(value);
    let minuteTime = 0;
    let hourTime = 0;
    if (secondTime > 60) {
      minuteTime = parseInt(secondTime / 60);
      secondTime = parseInt(secondTime % 60);
      if (minuteTime >= 60) {
        hourTime = parseInt(minuteTime / 60);
        minuteTime = parseInt(minuteTime % 60);
      }
    }
    let result = "" + parseInt(secondTime) + "秒";
    if (minuteTime > 0) {
      result = "" + parseInt(minuteTime) + "分钟" + result;
    }
    if (hourTime > 0) {
      result = "" + parseInt(hourTime) + "小时" + result;
    }
    return result;
  }

  // 缓存用户信息
  function cacheUserInfo(userInfo) {
    const cachedData = new Map(GM_getValue("cachedUserInfo", [])); // 改为 Map 形式
    cachedData.set(userInfo.uid, userInfo); // 使用 uid 作为 key
    GM_setValue("cachedUserInfo", Array.from(cachedData.entries())); // 保存为数组形式
    console.log("用户信息已缓存:", userInfo);
  }

  // 处理视频列表数据
  function processVideoList(videoList) {
    if (videoList.aweme_list) {
      const formattedVideos = videoList.aweme_list.map(formatDouyinAwemeData);
      // 缓存视频列表信息
      cacheVideoList(new Map(formattedVideos.map(video => [video.awemeId, video])));
    }
  }

  // 格式化 Douyin 视频数据
  function formatDouyinAwemeData(item) {
    return {
      awemeId: item.aweme_id,
      item_title: item.item_title || "",
      caption: item.caption || "",
      desc: item.desc || "",
      type: item.images ? "图文" : "视频",
      tag: (item.text_extra || [])
        .map(tag => tag.hashtag_name)
        .filter(tag => tag)
        .join("#"),
      video_tag: (item.video_tag || [])
        .map(tag => tag.tag_name)
        .filter(tag => tag)
        .join("->"),
      date: formatDate(item.create_time, "yyyy-MM-dd HH:mm:ss"),
      create_time: item.create_time,
      ...(item.statistics && {
        diggCount: item.statistics.digg_count,
        commentCount: item.statistics.comment_count,
        collectCount: item.statistics.collect_count,
        shareCount: item.statistics.share_count,
      }),
      ...(item.video && {
        duration: formatSeconds(Math.round(item.video.duration / 1e3)),
        url: item.video.play_addr.url_list[0],
        cover: item.video.cover.url_list[0],
        images: item.images ? item.images.map(row => row.url_list.pop()) : null,
      }),
      ...(item.author && {
        uid: item.author.uid,
        nickname: item.author.nickname,
      }),
    };
  }

  // 缓存视频列表信息
  function cacheVideoList(videos) {
    const cachedData = new Map(GM_getValue("cachedVideoList", [])); // 获取缓存并转换为 Map

    videos.forEach((video, awemeId) => {
      cachedData.set(awemeId, video); // 设置新视频
    });

    GM_setValue("cachedVideoList", Array.from(cachedData.entries())); // 更新缓存
  }

  // 显示视频列表信息
  function displayVideoList() {
    // 先移除旧的表格容器
    const oldTableContainer = document.getElementById("videoTableContainer");
    if (oldTableContainer) document.body.removeChild(oldTableContainer);

    const videosArray = GM_getValue("cachedVideoList", []);
    const videos = new Map(videosArray);
    const authors = [...new Set(Array.from(videos.values()).map(video => video.nickname))];
    const types = ["视频", "图文"];

    const tableContainer = document.createElement("div");
    tableContainer.id = "videoTableContainer";
    tableContainer.innerHTML = `
          <button id="closeButton" style="position:absolute;top:10px;right:10px;background-color:#f44336;color:white;border:none;padding:5px 10px;cursor:pointer;">关闭</button>
          <div class="filters">
              <label for="authorFilter">作者:</label>
              <select id="authorFilter">
                  <option value="">全部</option>
                  ${authors.map(author => `<option value="${author}">${author}</option>`).join("")}
              </select>
              <label for="typeFilter">类型:</label>
              <select id="typeFilter">
                  <option value="">全部</option>
                  ${types.map(type => `<option value="${type}">${type}</option>`).join("")}
              </select>
          </div>
          <!-- 新增作者信息展示区域 -->
          ${createAuthorInfoBox().outerHTML}
          <div class="actions">
              <button id="downloadSelected">下载选中内容</button>
              <button id="clearSelected">清除选中内容</button>
              <span id="selectedCount" style="margin-left: 10px;">已选择: 0 个</span>
          </div>
          <p id="downloadStatus"></p>
          <h3>视频列表</h3>
          <div id="videoTableWrapper">
              <table id="videoTable">
                  <thead>
                      <tr>
                          <th style="width:55px;"><input type="checkbox" id="selectAll"></th>
                          <th style="width:100px;">封面</th>
                          <th style="width:180px;">标题</th>
                          <th>描述</th>
                          <th style="width:100px;">类型</th>
                          <th>标签</th>
                          <th style="width:200px;">发布时间</th>
                          <th style="width:100px;">点赞数</th>
                          <th style="width:100px;">评论数</th>
                          <th style="width:100px;">分享数</th>
                          <th style="width:100px;">收藏数</th>
                          <th style="width:100px;">时长</th>
                          <th style="width:100px;">作者</th>
                      </tr>
                  </thead>
                  <tbody>
                      ${Array.from(videos.values())
                        .map(
                          video => `
                          <tr>
                              <td class="center"><input type="checkbox" class="videoCheckbox" data-id="${
                                video.awemeId
                              }"></td>
                              <td class="center">
                                  <img 
                                      src="${video.cover || (video.images ? video.images[0] : "")}" 
                                      class="cover-image" 
                                      data-preview="true"
                                      alt="封面"
                                  />
                              </td>
                              <td title="${
                                video.item_title
                              }" style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${
                            video.item_title
                          }</td>
                              <td title="${
                                video.desc
                              }" style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${video.desc}</td>
                              <td class="center">${video.type}</td>
                              <td title="${video.tag}">${video.tag}</td>
                              <td class="center">${video.date}</td>
                              <td class="center">${video.diggCount || 0}</td>
                              <td class="center">${video.commentCount || 0}</td>
                              <td class="center">${video.shareCount || 0}</td>
                              <td class="center">${video.collectCount || 0}</td>
                              <td class="center">${video.duration}</td>
                              <td class="center">${video.nickname}</td>
                          </tr>
                      `,
                        )
                        .join("")}
                  </tbody>
              </table>
          </div>
      `;
    document.body.appendChild(tableContainer);

    // 绑定关闭按钮事件
    document.getElementById("closeButton").addEventListener("click", () => {
      document.body.removeChild(tableContainer);
    });

    // 绑定筛选条件变化事件
    document.getElementById("authorFilter").addEventListener("change", filterTable);
    document.getElementById("typeFilter").addEventListener("change", filterTable);

    // 添加表格点击事件监听
    const videoTable = document.getElementById("videoTable");
    videoTable.addEventListener("click", e => {
      const target = e.target;
      if (target.matches("img.cover-image[data-preview]")) {
        showImagePreview(target.src);
      }
    });

    // 绑定下载和清除按钮事件
    document.getElementById("downloadSelected").addEventListener("click", downloadSelectedItems);
    document.getElementById("clearSelected").addEventListener("click", clearSelectedItems);

    // 绑定全选复选框事件
    document.getElementById("selectAll").addEventListener("change", e => {
      const checkboxes = document.querySelectorAll(".videoCheckbox");
      checkboxes.forEach(checkbox => {
        checkbox.checked = e.target.checked;
      });
    });

    // 更新选中数量的函数
    function updateSelectedCount() {
      const selectedCount = document.querySelectorAll(".videoCheckbox:checked").length;
      const selectedCountElement = document.getElementById("selectedCount");
      selectedCountElement.textContent = `已选择: ${selectedCount} 个`;

      // 同时更新下载和清除按钮的状态
      const downloadBtn = document.getElementById("downloadSelected");
      const clearBtn = document.getElementById("clearSelected");
      const hasSelection = selectedCount > 0;
      downloadBtn.disabled = !hasSelection;
      clearBtn.disabled = !hasSelection;
    }

    // 为所有复选框添加change事件监听
    document.querySelectorAll(".videoCheckbox").forEach(checkbox => {
      checkbox.addEventListener("change", updateSelectedCount);
    });

    // 修改全选复选框事件
    document.getElementById("selectAll").addEventListener("change", e => {
      const checkboxes = document.querySelectorAll(".videoCheckbox");
      checkboxes.forEach(checkbox => {
        checkbox.checked = e.target.checked;
      });
      updateSelectedCount();
    });

    // 初始化时设置按钮状态
    updateSelectedCount();
  }

  // 过滤表单(改为动态生成表格内容)
  function filterTable() {
    const authorFilter = document.getElementById("authorFilter").value;
    const typeFilter = document.getElementById("typeFilter").value;
    const videosArray = GM_getValue("cachedVideoList", []);
    const videos = new Map(videosArray);
    const userInfoArray = GM_getValue("cachedUserInfo", []);
    const userInfoMap = new Map(userInfoArray);

    // 更新作者信息
    const authorInfoBox = document.getElementById("authorInfoBox");
    const authorNickname = document.getElementById("authorNickname");
    const authorFollowers = document.getElementById("authorFollowers");
    const authorLikes = document.getElementById("authorLikes");
    const authorWorks = document.getElementById("authorWorks");
    const authorIP = document.getElementById("authorIP");
    const authorSignature = document.getElementById("authorSignature");
    const deleteAuthorBtn = document.getElementById("deleteAuthorBtn");

    if (authorFilter) {
      const selectedVideo = Array.from(videos.values()).find(video => video.nickname === authorFilter);
      if (selectedVideo) {
        const userInfo = userInfoMap.get(selectedVideo.uid);
        if (userInfo) {
          authorNickname.textContent = userInfo.nickname;
          authorFollowers.textContent = userInfo.mplatform_followers_count || "-";
          authorLikes.textContent = userInfo.total_favorited || "-";
          authorWorks.textContent = userInfo.aweme_count || "-";
          authorIP.textContent = userInfo.ip_location || "-";
          authorSignature.textContent = userInfo.signature || "-";
          deleteAuthorBtn.setAttribute("data-uid", userInfo.uid);
          authorInfoBox.style.display = "block";

          // 绑定删除按钮事件
          deleteAuthorBtn.onclick = () => deleteAuthorData(userInfo.uid);
        }
      }
    } else {
      authorInfoBox.style.display = "none";
    }

    // 重新绑定复选框事件
    document.querySelectorAll(".videoCheckbox").forEach(checkbox => {
      checkbox.addEventListener("change", () => {
        const selectedCount = document.querySelectorAll(".videoCheckbox:checked").length;
        const selectedCountElement = document.getElementById("selectedCount");
        selectedCountElement.textContent = `已选择: ${selectedCount} 个`;

        // 更新按钮状态
        const downloadBtn = document.getElementById("downloadSelected");
        const clearBtn = document.getElementById("clearSelected");
        const hasSelection = selectedCount > 0;
        downloadBtn.disabled = !hasSelection;
        clearBtn.disabled = !hasSelection;
      });
    });

    // 更新选中数量显示
    const selectedCount = document.querySelectorAll(".videoCheckbox:checked").length;
    const selectedCountElement = document.getElementById("selectedCount");
    selectedCountElement.textContent = `已选择: ${selectedCount} 个`;

    // 重新生成表格内容
    const tbody = document.querySelector("#videoTable tbody");
    tbody.innerHTML = Array.from(videos.values())
      .filter(video => {
        const matchAuthor = !authorFilter || video.nickname === authorFilter;
        const matchType = !typeFilter || video.type === typeFilter;
        return matchAuthor && matchType;
      })
      .map(
        video => `
              <tr>
                  <td class="center"><input type="checkbox" class="videoCheckbox" data-id="${video.awemeId}"></td>
                  <td class="center">
                      <img 
                          src="${video.cover || (video.images ? video.images[0] : "")}" 
                          class="cover-image" 
                          data-preview="true"
                          alt="封面"
                      />
                  </td>
                  <td title="${video.item_title}" style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${
          video.item_title
        }</td>
                  <td title="${video.desc}" style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${
          video.desc
        }</td>
                  <td class="center">${video.type}</td>
                  <td title="${video.tag}">${video.tag}</td>
                  <td class="center">${video.date}</td>
                  <td class="center">${video.diggCount || 0}</td>
                  <td class="center">${video.commentCount || 0}</td>
                  <td class="center">${video.shareCount || 0}</td>
                  <td class="center">${video.collectCount || 0}</td>
                  <td class="center">${video.duration}</td>
                  <td class="center">${video.nickname}</td>
              </tr>
          `,
      )
      .join("");
  }

  // 修改下载选中的项目函数
  async function downloadSelectedItems() {
    const selectedCheckboxes = document.querySelectorAll(".videoCheckbox:checked");
    const selectedVideos = Array.from(selectedCheckboxes).map(cb => {
      const videosArray = GM_getValue("cachedVideoList", []);
      const videos = new Map(videosArray);
      return videos.get(cb.getAttribute("data-id"));
    });

    if (selectedVideos.length === 0) {
      alert("请选择要下载的内容。");
      return;
    }

    const firstType = selectedVideos[0].type;
    if (selectedVideos.some(video => video.type !== firstType)) {
      alert("只能选择同一种类型的项目进行下载。");
      return;
    }

    const statusElement = document.getElementById("downloadStatus");

    // 如果只选中一个视频,直接下载
    if (selectedVideos.length === 1 && firstType === "视频") {
      const video = selectedVideos[0];
      try {
        statusElement.textContent = "正在下载视频...";
        const response = await fetch(video.url);
        const blob = await response.blob();
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = `${getAwemeName(video)}.mp4`;
        a.click();
        URL.revokeObjectURL(url);

        statusElement.textContent = "下载完成!";
        showFriendlyMessage("✅ 下载完成!");
      } catch (error) {
        console.error("下载失败:", error);
        statusElement.textContent = "下载失败,请重试。";
        showFriendlyMessage("❌ 下载失败,请重试", false);
      }
      return;
    }

    // 多个文件时使用 fflate 压缩
    let failedItems = [];
    const zipObj = {};
    const totalItems = selectedVideos.length;
    let completedItems = 0;

    statusElement.textContent = `准备下载 ${selectedVideos.length} 个${firstType}...`;

    // 并行下载所有文件
    const downloadPromises = selectedVideos.map(async video => {
      try {
        await downloadAndAddToZipObj(zipObj, video, firstType);
        completedItems++;
        statusElement.textContent = `下载中(${completedItems}/${totalItems})`;
      } catch (error) {
        failedItems.push(video.item_title || video.desc);
        console.error(`下载失败: ${video.item_title}`, error);
      }
    });

    // 等待所有文件下载完成
    await Promise.all(downloadPromises);
    if (Object.keys(zipObj).length > 0) {
      try {
        // 计算所有文件的总大小
        let totalSize = 0;
        for (const key in zipObj) {
          totalSize += zipObj[key].length;
        }

        // 如果总大小超过100MB,进行分块压缩
        if (totalSize > 100 * 1024 * 1024) {
          const CHUNK_SIZE = 100 * 1024 * 1024; // 100MB
          const chunks = {};
          let currentChunk = {};
          let currentSize = 0;
          let chunkIndex = 1;

          // 将文件分配到不同的块
          for (const key in zipObj) {
            if (currentSize + zipObj[key].length > CHUNK_SIZE) {
              chunks[chunkIndex] = currentChunk;
              currentChunk = {};
              currentSize = 0;
              chunkIndex++;
            }
            currentChunk[key] = zipObj[key];
            currentSize += zipObj[key].length;
          }
          if (Object.keys(currentChunk).length > 0) {
            chunks[chunkIndex] = currentChunk;
          }

          // 逐个压缩和下载每个块
          for (let i = 1; i <= chunkIndex; i++) {
            let dots = 0;
            statusElement.textContent = `压缩第 ${i}/${chunkIndex} 个文件包`;
            const compressInterval = setInterval(() => {
              dots = (dots + 1) % 4;
              statusElement.textContent = `压缩第 ${i}/${chunkIndex} 个文件包${"".padEnd(dots, "。")}`;
            }, 200);

            try {
              const zipData = await new Promise((resolve, reject) => {
                fflate.zip(
                  chunks[i],
                  {
                    level: 6,
                    mem: 8,
                  },
                  (err, data) => {
                    if (err) reject(err);
                    else resolve(data);
                  },
                );
              });

              clearInterval(compressInterval);
              statusElement.textContent = `下载第 ${i}/${chunkIndex} 个文件包...`;

              // 下载当前块
              const blob = new Blob([zipData], { type: "application/zip" });
              const url = URL.createObjectURL(blob);
              const a = document.createElement("a");
              a.href = url;
              a.download = `[${firstType}]${selectedVideos[0]?.nickname}_part${i}.zip`;
              a.click();
              URL.revokeObjectURL(url);

              // 等待一段时间再开始下一个块的处理
              await new Promise(resolve => setTimeout(resolve, 1000));
            } catch (error) {
              clearInterval(compressInterval);
              throw error;
            }
          }

          if (failedItems.length > 0) {
            statusElement.textContent = `完成!成功: ${completedItems}个,失败: ${failedItems.length}个`;
            showFriendlyMessage(`⚠️ 部分下载成功,${failedItems.length}个项目失败`, false);
          } else {
            statusElement.textContent = `全部完成!成功下载 ${completedItems} 个文件(共 ${chunkIndex} 个压缩包)`;
            showFriendlyMessage("✅ 下载完成!");
          }
        } else {
          // 原有的单个压缩包逻辑
          let dots = 0;
          statusElement.textContent = "压缩中";
          const compressInterval = setInterval(() => {
            dots = (dots + 1) % 4;
            statusElement.textContent = `压缩中${"".padEnd(dots, "。")}`;
          }, 200);

          // 使用异步压缩
          const zipData = await new Promise((resolve, reject) => {
            try {
              fflate.zip(
                zipObj,
                {
                  level: 6,
                  mem: 8,
                },
                (err, data) => {
                  if (err) reject(err);
                  else resolve(data);
                },
              );
            } catch (error) {
              reject(error);
            }
          });

          clearInterval(compressInterval);
          statusElement.textContent = "压缩完成,准备下载...";

          // 创建并下载压缩文件
          const blob = new Blob([zipData], { type: "application/zip" });
          const url = URL.createObjectURL(blob);
          const a = document.createElement("a");
          a.href = url;
          a.download = `[${firstType}]${selectedVideos[0]?.nickname}.zip`;
          a.click();
          URL.revokeObjectURL(url);

          if (failedItems.length > 0) {
            statusElement.textContent = `完成!成功: ${completedItems}个,失败: ${failedItems.length}个`;
            showFriendlyMessage(`⚠️ 部分下载成功,${failedItems.length}个项目失败`, false);
          } else {
            statusElement.textContent = `全部完成!成功下载 ${completedItems} 个文件`;
            showFriendlyMessage("✅ 下载完成!");
          }
        }
      } catch (error) {
        console.error("压缩失败:", error);
        statusElement.textContent = "压缩文件时出错,请重试。";
        showFriendlyMessage("❌ 压缩失败,请重试", false);
      }
    } else {
      statusElement.textContent = "所有项目下载失败。";
      showFriendlyMessage("❌ 下载失败,请重试", false);
    }
  }

  // 修改下载单个项目的函数
  async function downloadAndAddToZipObj(zipObj, video, type) {
    try {
      if (type === "视频") {
        const response = await fetch(video.url);
        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
        const arrayBuffer = await response.arrayBuffer();
        zipObj[`${getAwemeName(video)}.mp4`] = new Uint8Array(arrayBuffer);
      } else if (type === "图文") {
        const folderName = getAwemeName(video);
        const totalImages = video.images.length;

        for (let i = 0; i < totalImages; i++) {
          const imageUrl = video.images[i];
          try {
            const imgResponse = await fetch(imageUrl);
            if (!imgResponse.ok) throw new Error(`HTTP error! status: ${imgResponse.status}`);
            const arrayBuffer = await imgResponse.arrayBuffer();
            zipObj[`${folderName}/image_${i + 1}.jpg`] = new Uint8Array(arrayBuffer);
          } catch (error) {
            console.error(`图片 ${i + 1} 下载失败:`, error);
            throw error;
          }
        }
      }
    } catch (error) {
      console.error(`下载失败:`, error);
      throw error;
    }
  }

  // 清除选中的项目
  function clearSelectedItems() {
    const selectedCheckboxes = document.querySelectorAll(".videoCheckbox:checked");
    if (selectedCheckboxes.length === 0) {
      alert("请先选择要清除的内容。");
      return;
    }

    const videosArray = GM_getValue("cachedVideoList", []);
    const videos = new Map(videosArray);

    // 从缓存中删除选中的视频
    selectedCheckboxes.forEach(checkbox => {
      const awemeId = checkbox.getAttribute("data-id");
      videos.delete(awemeId); // 从 Map 中删除
    });

    // 更新缓存
    GM_setValue("cachedVideoList", Array.from(videos.entries()));
    console.log("已清除选中的内容:", Array.from(videos.values()));

    // 刷新表格
    displayVideoList();
    showFriendlyMessage("🗑️ 已清除选中内容!");
  }

  // 新增:删除作者数据的函数
  function deleteAuthorData(uid) {
    if (!confirm("确定要删除该作者的所有数据吗?此操作不可恢复。")) {
      return;
    }

    // 删除用户信息
    const userInfoArray = GM_getValue("cachedUserInfo", []);
    const userInfoMap = new Map(userInfoArray);
    userInfoMap.delete(uid);
    GM_setValue("cachedUserInfo", Array.from(userInfoMap.entries()));

    // 删除相关视频数据
    const videosArray = GM_getValue("cachedVideoList", []);
    const videos = new Map(videosArray);
    for (const [awemeId, video] of videos.entries()) {
      if (video.uid === uid) {
        videos.delete(awemeId);
      }
    }
    GM_setValue("cachedVideoList", Array.from(videos.entries()));

    // 刷新表格显示
    displayVideoList();
    showFriendlyMessage("✅ 作者数据已删除!");
  }

  // 添加预览图片功能
  function showImagePreview(imageUrl) {
    const overlay = document.createElement("div");
    overlay.className = "preview-overlay";

    const img = document.createElement("img");
    img.className = "preview-image";
    img.src = imageUrl;

    overlay.appendChild(img);
    document.body.appendChild(overlay);

    // 点击关闭预览
    overlay.onclick = () => {
      document.body.removeChild(overlay);
    };

    // 按ESC键关闭预览
    const escHandler = e => {
      if (e.key === "Escape") {
        document.body.removeChild(overlay);
        document.removeEventListener("keydown", escHandler);
      }
    };
    document.addEventListener("keydown", escHandler);
  }

  // 创建按钮
  const button = document.createElement("button");
  button.id = "showDataButton";
  button.innerText = "显示数据列表";
  button.onclick = displayVideoList;
  document.body.appendChild(button);

  console.log("抖音主页视频图文下载脚本已加载!");
})();