🏠 返回首頁 

抖音归档

easy way to archive tiktok or douyin's video


Install this script?
// ==UserScript==
// @name         抖音归档
// @namespace    douyin_archive
// @version      1.1
// @description  easy way to archive tiktok or douyin's video
// @author       邪不压正
// @license      AGPL License
// @run-at       document-start
// @match        https://www.douyin.com/user/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=douyin.com
// @grant        unsafeWindow
// @grant        GM_openInTab
// @grant        GM_xmlhttpRequest
// ==/UserScript==
(function () {
'use strict';
//put your code here ?
const realSend = XMLHttpRequest.prototype.send;
logme(" replaced")
XMLHttpRequest.prototype.send = function () {
const xhr = this;
//注册监听是慢的,直接替换是快的
this.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
// 请求已完成且状态码为200
const url = xhr.responseURL;
if (url.includes('aweme/v1/web/aweme/post/')) {
const response = JSON.parse(xhr.responseText)
const awemeList = response.aweme_list
awemeList.forEach(item => {
if (item.media_type === 4) {
mergedVideoList.push(item);
} else if (item.media_type === 2) {
mergedPicsList.push(item);
// item.images.forEach((image, index) => {
//     const url = image.url_list[0];
//     mergedPicListAlone.push(url)
// });
}
})
mergedList = mergedList.concat(awemeList)
hasMore = response.has_more
logme("-------- catch!--------" + awemeList.length + awemeList[0].desc)
updateTotalCount()
setTimeout(() => {
updateVideoBannerList()
}, 200)
// audioUrl = awemeList[0].music.play_url.uri
}
}
}
realSend.apply(this, arguments);
};
logme("---------------------------archive---------------------------")
let mergedVideoList = []
let mergedList = []
let mergedPicsList = []
let mergedPicListAlone = []
let hasMore = 1
//去水印下载
if (window.location.host !== "www.douyin.com") {
return;
}
window.addEventListener('load', function () {
logme("Window onLOAD")
if (!window.location.href.match(/https:\/\/www\.douyin\.com\/user\/.*?/))
return
logme("into page")
async function downloader() {
try {
logme("into await allDownBtn")
//延迟加载等到第一次视频加载完成
let videoTop = await awaitQuery(".sDAMysaM")
videoTop.style.height = '88px'
//附加下载按钮
let downText = document.createElement('div')
downText.innerText = '已加载';
downText.style.display = 'inline-block';
downText.style.whiteSpace = 'nowrap';
downText.style.textAlign = 'center'
downText.style.fontSize = '13px'
downText.style.color = 'white'
downText.id = "downText"
videoTop.append(downText)
let picsDownBtn = document.createElement('button')
picsDownBtn.innerText = '图文'
picsDownBtn.classList.add('B10aL8VQ')
picsDownBtn.classList.add('s6mStVxD')
picsDownBtn.classList.add('vMQD6aai')
picsDownBtn.classList.add('vk7WaOg_')
picsDownBtn.classList.add('a2I1sBCL')
picsDownBtn.classList.add('tAofAbwG')
picsDownBtn.style.marginLeft = '10px'
videoTop.append(picsDownBtn)
let videoDownBtn = document.createElement('button')
videoDownBtn.innerText = '视频'
videoDownBtn.classList.add('B10aL8VQ')
videoDownBtn.classList.add('s6mStVxD')
videoDownBtn.classList.add('vMQD6aai')
videoDownBtn.classList.add('vk7WaOg_')
videoDownBtn.classList.add('a2I1sBCL')
videoDownBtn.classList.add('tAofAbwG')
videoDownBtn.style.marginLeft = '10px'
videoTop.append(videoDownBtn)
let allDownBtn = document.createElement('button')
allDownBtn.innerText = '全部'
allDownBtn.classList.add('B10aL8VQ')
allDownBtn.classList.add('s6mStVxD')
allDownBtn.classList.add('vMQD6aai')
allDownBtn.classList.add('vk7WaOg_')
allDownBtn.classList.add('a2I1sBCL')
allDownBtn.classList.add('tAofAbwG')
allDownBtn.style.marginLeft = '10px'
videoTop.append(allDownBtn)
updateTotalCount()
picsDownBtn.addEventListener("click", async (e) => {
for (const pics of mergedPicsList) {
await downloadPics(pics);
const awemeId = pics.aweme_id;
localStorage.setItem('downloaded_' + awemeId, true);
}
});
videoDownBtn.addEventListener("click",  async (e) => {
for (const video of mergedVideoList) {
const name = generateVideoFilename(video); // 生成视频的文件名
await downloadVideo(convertHttpToHttps(getVideoUrl(video)), name); // 调用下载函数下载视频
// 给每个视频设置下载标记
const awemeId = video.aweme_id;
localStorage.setItem('downloaded_' + awemeId, true);
}
});
allDownBtn.addEventListener("click", (e) => {
alert("懒得写了 分别下吧 ")
});
} catch (e) {
}
}
downloader().then(r => {
});
// window.addEventListener("click", downloader);
});
//其他方法
function awaitQuery(selectors, delay = 200) {
return new Promise(resolve => {
let totalDelay = 0;
let elementInterval = setInterval(() => {
if (totalDelay >= 2500) {
clearInterval(elementInterval);
}
let element = document.querySelector(selectors);
if (element) {
resolve(element);
clearInterval(elementInterval);
} else {
totalDelay += delay;
}
}, delay);
})
}
async function downloadVideo(url, name) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Download failed for ${url}.`);
}
const blob = await response.blob();
const a = document.createElement("a");
const objectUrl = window.URL.createObjectURL(blob);
a.download = name;
a.href = objectUrl;
a.click();
window.URL.revokeObjectURL(objectUrl);
a.remove();
} catch (error) {
console.error(error);
}
}
async function downloadPics(imageInfo) {
for (let i = 0; i < imageInfo.images.length; i++) {
const image = imageInfo.images[i];
const url = image.url_list[0];
const numberedName = `${generatePicsFilename(imageInfo)}_${i + 1}`; // 添加编号
await downloadImage(url, numberedName);
}
}
async function downloadImage(url, name) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Download failed for ${url}.`);
}
const blob = await response.blob();
const a = document.createElement("a");
const objectUrl = window.URL.createObjectURL(blob);
a.download = name;
a.href = objectUrl;
a.click();
window.URL.revokeObjectURL(objectUrl);
a.remove();
} catch (error) {
console.error(error);
}
}
function convertHttpToHttps(url) {
if (url.startsWith('http://')) {
return 'https://' + url.substring('http://'.length);
}
return url;
}
function updateTotalCount() {
const loadedText = document.getElementById("downText");
if (loadedText) {
let loadedVideoCount = mergedVideoList.length.toString()
logme(" loadedVideoCount  " + loadedVideoCount)
if (hasMore === 1) {
loadedText.textContent = `已加载 图文:${mergedPicsList.length}个 视频${loadedVideoCount}/${mergedVideoList[0].author.aweme_count}个 还有视频未加载`;
} else if (loadedVideoCount < mergedVideoList[0].author.aweme_count) {
loadedText.textContent = `已加载 图文:${mergedPicsList.length}个 视频${loadedVideoCount}/${mergedVideoList[0].author.aweme_count}个 误差大是加载时序紊乱,请刷新 / 误差小是有视频被隐藏`;
const refresh = document.createElement('button');
refresh.innerText = "刷新"
refresh.style.marginLeft = '8px'
refresh.classList = "B10aL8VQ s6mStVxD vMQD6aai vk7WaOg_ a2I1sBCL tAofAbwG"
refresh.addEventListener("click", () => {
location.reload()
})
loadedText.append(refresh)
} else {
loadedText.textContent = `已加载 图文:${mergedPicsList.length}个 视频${loadedVideoCount}/${mergedVideoList[0].author.aweme_count}个 全部加载完成`;
}
}
}
function formatTimestamp(timestamp) {
const date = new Date(timestamp * 1000); // 将秒转换为毫秒
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
return `${year}-${month}-${day}-${hours}`;
}
function updateVideoBannerList() {
let list = document.querySelector('.UFuuTZ1P')
let ul = list.querySelector("ul");
let liList = ul.querySelectorAll("li");
liList.forEach(li => {
// 检查是否已经存在下载按钮
const existingDownloadButton = li.querySelector('.download-button');
if (existingDownloadButton) return
const awemeId = li.childNodes[0].children[0].getAttribute('href').split('/').pop();
let matchingData = mergedVideoList.find(data => data.aweme_id === awemeId) || mergedPicsList.find(data => data.aweme_id === awemeId);
if (matchingData) {
// 添加自定义属性 data-aweme-id 到 <li> 元素
li.setAttribute('data-aweme-id', matchingData.aweme_id);
// 在匹配的 <li> 元素中添加下载按钮
const buttonContainer = document.createElement('div');
const downloadButton = document.createElement('button');
// downloadButton.classList = "B10aL8VQ s6mStVxD vMQD6aai vk7WaOg_ a2I1sBCL tAofAbwG"
buttonContainer.style.position = 'absolute';
buttonContainer.style.top = '85%';
buttonContainer.style.right = '0';
downloadButton.style.fontSize = '10px'
downloadButton.style.color = 'white'
downloadButton.style.background = 'grey'
downloadButton.style.padding = '4px 8px'
downloadButton.style.borderRadius = '4px'
const isDownloaded = localStorage.getItem('downloaded_' + awemeId);
if (isDownloaded) {
downloadButton.innerText = "下载 ✔"
} else {
downloadButton.innerText = "下载"
}
downloadButton.classList.add('download-button'); // 添加适当的类名或 ID
downloadButton.addEventListener("click", (e) => {
if (matchingData.media_type === 4) {
let videoUrl = getVideoUrl(matchingData)
if (videoUrl) {
downloadVideo(convertHttpToHttps(videoUrl), generateVideoFilename(matchingData)).then()
localStorage.setItem('downloaded_' + awemeId, true);
downloadButton.innerText = "下载 ✔"
} else {
alert("未找到下载地址 " + matchingData.desc)
}
} else if (matchingData.media_type === 2) {
downloadPics(matchingData).then(r => {
})
localStorage.setItem('downloaded_' + awemeId, true);
downloadButton.innerText = "下载 ✔"
}
})
buttonContainer.appendChild(downloadButton);
li.appendChild(buttonContainer);
}
});
}
function getVideoUrl(matchingData) {
let videoUrl = null;
if (matchingData.video.play_addr_h264 && matchingData.video.play_addr_h264.url_list.length > 0) {
videoUrl = matchingData.video.play_addr_h264.url_list[0];
} else if (matchingData.video.play_addr && matchingData.video.play_addr.url_list.length > 0) {
videoUrl = matchingData.video.play_addr.url_list[0];
} else if (matchingData.video.play_addr_265 && matchingData.video.play_addr_265.url_list.length > 0) {
videoUrl = matchingData.video.play_addr_265.url_list[0];
}
return videoUrl;
}
function generateVideoFilename(videoInfo) {
const {author, create_time, desc, author_user_id} = videoInfo;
const formattedTimestamp = formatTimestamp(create_time);
return `${author.nickname}_${formattedTimestamp}_video_${desc}_${author_user_id}.mp4`;
}
function generatePicsFilename(picsInfo) {
const {author, create_time, desc, author_user_id} = picsInfo;
const formattedTimestamp = formatTimestamp(create_time);
return `${author.nickname}_${formattedTimestamp}_pics_${desc}_${author_user_id}`;
}
function logme(string) {
console.log("|||" + string)
}
})();