// ==UserScript== // @name 抖音主页视频图文下载 // @namespace douyin-homepage-download // @version 1.0.2 // @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.style.marginBottom = '10px'; authorInfoBox.style.padding = '10px'; authorInfoBox.style.backgroundColor = '#f9f9f9'; authorInfoBox.style.border = '1px solid #ddd'; authorInfoBox.style.borderRadius = '4px'; authorInfoBox.style.display = 'none'; // 默认隐藏 authorInfoBox.innerHTML = `

作者信息

昵称:-
粉丝数:-
获赞数:-
作品数:-
IP 属地:-
`; return authorInfoBox; } // 新增:友好提示函数 function showFriendlyMessage(message, isSuccess = true) { const msgBox = document.createElement('div'); msgBox.style.position = 'fixed'; msgBox.style.top = '20px'; msgBox.style.left = '50%'; msgBox.style.transform = 'translateX(-50%)'; msgBox.style.padding = '10px 20px'; msgBox.style.backgroundColor = isSuccess ? '#4CAF50' : '#f44336'; msgBox.style.color = 'white'; msgBox.style.borderRadius = '4px'; msgBox.style.zIndex = '100000'; msgBox.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'; 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; padding: 8px; 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; } `); // 获取 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 = GM_getValue('cachedUserInfo', {}); // 获取缓存 cachedData[userInfo.uid] = userInfo; // 按 UID 存储 GM_setValue('cachedUserInfo', cachedData); // 更新缓存 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); } // 过滤表单(改为动态生成表格内容) 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 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'); if (authorFilter) { const selectedAuthor = Array.from(videos.values()).find(video => video.nickname === authorFilter); if (selectedAuthor) { authorNickname.textContent = selectedAuthor.nickname; authorFollowers.textContent = selectedAuthor.mplatform_followers_count || '-'; authorLikes.textContent = selectedAuthor.total_favorited || '-'; authorWorks.textContent = selectedAuthor.aweme_count || '-'; authorIP.textContent = selectedAuthor.ip_location || '-'; authorInfoBox.style.display = 'block'; // 显示作者信息 } } 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 authorFilter = document.getElementById('authorFilter').value; const typeFilter = document.getElementById('typeFilter').value; const videosArray = GM_getValue('cachedVideoList', []); const videos = new Map(videosArray); // 新增:更新作者信息 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'); if (authorFilter) { const selectedAuthor = Array.from(videos.values()).find(video => video.nickname === authorFilter); if (selectedAuthor) { authorNickname.textContent = selectedAuthor.nickname; authorFollowers.textContent = selectedAuthor.mplatform_followers_count || '-'; authorLikes.textContent = selectedAuthor.total_favorited || '-'; authorWorks.textContent = selectedAuthor.aweme_count || '-'; authorIP.textContent = selectedAuthor.ip_location || '-'; authorInfoBox.style.display = 'block'; // 显示作者信息 } } else { authorInfoBox.style.display = 'none'; // 隐藏作者信息 } const selectedCheckboxes = document.querySelectorAll('.videoCheckbox:checked'); const selectedVideos = Array.from(selectedCheckboxes).map(cb => 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'); statusElement.textContent = `正在下载... 已下载 0/${totalCount} 项`; document.getElementById('progress').style.width = '0%'; // 如果只选中一个视频,直接下载 if (totalCount === 1 && firstType === '视频') { const video = selectedVideos[0]; try { 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 = '下载完成!'; alert('下载完成!'); } catch (error) { console.error('下载失败:', error); statusElement.textContent = '下载失败,请重试。'; } return; } // 多个文件时使用 ZIP 压缩 const zip = new JSZip(); let downloadedCount = 0; for (const video of selectedVideos) { await downloadAndAddToZip(zip, video, firstType); downloadedCount++; statusElement.textContent = `正在下载... 已下载 ${downloadedCount}/${totalCount} 项`; document.getElementById('progress').style.width = `${(downloadedCount / totalCount) * 100}%`; } const content = await zip.generateAsync({ type: 'blob' }); saveAs(content, `[${firstType}]${selectedVideos[0]?.nickname}.zip`); showFriendlyMessage('🎉 下载完成!'); statusElement.textContent = '下载完成!'; } // 下载单个项目并添加到 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('🗑️ 已清除选中内容!'); } // 创建按钮 const button = document.createElement('button'); button.innerText = '显示数据列表'; button.style.position = 'fixed'; button.style.bottom = '20px'; button.style.right = '20px'; button.style.zIndex = '10001'; button.onclick = displayVideoList; document.body.appendChild(button); console.log('抖音主页视频图文下载脚本已加载!'); })();