// ==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 // @downloadURL none // ==/UserScript== (function () { "use strict"; // 新增:作者信息展示区域 function createAuthorInfoBox() { const authorInfoBox = document.createElement("div"); authorInfoBox.id = "authorInfoBox"; authorInfoBox.innerHTML = `

作者信息

昵称:-
粉丝数:-
获赞数:-
作品数:-
IP 属地:-
签名:-
`; 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 = `
${createAuthorInfoBox().outerHTML}

视频列表

${Array.from(videos.values()) .map( video => ` `, ) .join("")}
封面 标题 描述 类型 标签 发布时间 点赞数 评论数 分享数 收藏数 时长 作者
封面 ${ video.item_title } ${video.desc} ${video.type} ${video.tag} ${video.date} ${video.diggCount || 0} ${video.commentCount || 0} ${video.shareCount || 0} ${video.collectCount || 0} ${video.duration} ${video.nickname}
`; 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 => ` 封面 ${video.item_title} ${ video.desc } ${video.type} ${video.tag} ${video.date} ${video.diggCount || 0} ${video.commentCount || 0} ${video.shareCount || 0} ${video.collectCount || 0} ${video.duration} ${video.nickname} `, ) .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("抖音主页视频图文下载脚本已加载!"); })();