Greasy Fork

Greasy Fork is available in English.

抖音主页视频图文下载

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

目前为 2025-02-10 提交的版本,查看 最新版本

// ==UserScript==
// @name         抖音主页视频图文下载
// @namespace    douyin-homepage-download
// @version      1.1.0
// @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://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// ==/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 #progressBar {
            width: 100%;
            height: 10px;
            background-color: #e0e0e0;
            margin-top: 10px;
        }
        #videoTableContainer #progress {
            width: 0%;
            height: 100%;
            background-color: #76c7c0;
        }
        #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);
      console.log("格式化后的视频列表:", formattedVideos);

      // 缓存视频列表信息
      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())); // 更新缓存
    console.log("视频列表已缓存:", Array.from(cachedData.values()));
  }

  // 显示视频列表信息
  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>
            </div>
            <p id="downloadStatus"></p>
            <div id="progressBar"><div id="progress"></div></div>
            <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;
      });
    });

    // 初始化时设置按钮状态
    const downloadBtn = document.getElementById("downloadSelected");
    const clearBtn = document.getElementById("clearSelected");
    const authorFilter = document.getElementById("authorFilter").value;
    const typeFilter = document.getElementById("typeFilter").value;
    const isFilterEmpty = !authorFilter && !typeFilter;
    downloadBtn.disabled = isFilterEmpty;
    clearBtn.disabled = isFilterEmpty;
  }

  // 过滤表单(改为动态生成表格内容)
  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";
    }

    // 新增:按钮禁用逻辑
    const downloadBtn = document.getElementById("downloadSelected");
    const clearBtn = document.getElementById("clearSelected");
    const isFilterEmpty = !authorFilter && !typeFilter;
    downloadBtn.disabled = isFilterEmpty;
    clearBtn.disabled = isFilterEmpty;
    // 重新生成表格内容
    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"));
    });
    const totalCount = selectedVideos.length;

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

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

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

    // 如果只选中一个视频,直接下载
    if (totalCount === 1 && firstType === "视频") {
      const video = selectedVideos[0];
      try {
        statusElement.textContent = "正在下载视频...";
        progressBar.style.width = "50%";

        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);

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

    // 多个文件时使用 ZIP 压缩
    const zip = new JSZip();
    let downloadedCount = 0;
    let failedItems = [];

    statusElement.textContent = "准备下载...";
    progressBar.style.width = "0%";

    for (const video of selectedVideos) {
      try {
        statusElement.textContent = `正在下载 ${firstType}... (${downloadedCount + 1}/${totalCount})`;
        await downloadAndAddToZip(zip, video, firstType);
        downloadedCount++;
        progressBar.style.width = `${(downloadedCount / totalCount) * 90}%`; // 留10%给压缩过程
      } catch (error) {
        failedItems.push(video.item_title || video.desc);
        console.error(`下载失败: ${video.item_title}`, error);
      }
    }

    if (downloadedCount > 0) {
      try {
        statusElement.textContent = "正在生成压缩包...";
        progressBar.style.width = "95%";

        const content = await zip.generateAsync(
          {
            type: "blob",
            compression: "DEFLATE",
            compressionOptions: { level: 6 },
          },
          metadata => {
            statusElement.textContent = `正在压缩... ${Math.round(metadata.percent)}%`;
          },
        );

        saveAs(content, `[${firstType}]${selectedVideos[0]?.nickname}.zip`);
        progressBar.style.width = "100%";

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

  // 下载单个项目并添加到 ZIP 文件
  async function downloadAndAddToZip(zip, video, type) {
    try {
      if (type === "视频") {
        const response = await fetch(video.url);
        const blob = await response.blob();
        zip.file(`${getAwemeName(video)}.mp4`, blob);
      } else if (type === "图文") {
        const folder = zip.folder(getAwemeName(video));
        for (let j = 0; j < video.images.length; j++) {
          const imgResponse = await fetch(video.images[j]);
          const imgBlob = await imgResponse.blob();
          folder.file(`image_${j + 1}.jpg`, imgBlob);
        }
      }
    } 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("抖音主页视频图文下载脚本已加载!");
})();