// ==UserScript==
// @name Douyin Video Downloader with Metadata
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Extract video links and metadata from Douyin user profiles
// @author You
// @match https://www.douyin.com/user/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=douyin.com
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
"use strict";
// Add UI elements
function addUI() {
const container = document.createElement("div");
container.style.position = "fixed";
container.style.bottom = "20px";
container.style.right = "20px";
container.style.zIndex = "9999";
container.style.backgroundColor = "rgba(0, 0, 0, 0.7)";
container.style.padding = "15px";
container.style.borderRadius = "5px";
container.style.boxShadow = "0 0 10px rgba(0, 0, 0, 0.5)";
container.style.color = "white";
container.style.fontFamily = "Arial, sans-serif";
container.style.fontSize = "14px";
container.style.display = "flex";
container.style.flexDirection = "column";
container.style.gap = "10px";
const title = document.createElement("div");
title.textContent = "Douyin Downloader";
title.style.fontWeight = "bold";
title.style.fontSize = "16px";
container.appendChild(title);
const downloadButton = document.createElement("button");
downloadButton.textContent = "Download All Videos with Metadata";
downloadButton.style.padding = "8px 12px";
downloadButton.style.backgroundColor = "#FE2C55"; // Douyin red color
downloadButton.style.color = "white";
downloadButton.style.border = "none";
downloadButton.style.borderRadius = "4px";
downloadButton.style.cursor = "pointer";
downloadButton.style.fontWeight = "bold";
downloadButton.onclick = () => {
downloadButton.disabled = true;
downloadButton.textContent = "Downloading...";
statusText.textContent = "Starting download...";
run()
.then(() => {
downloadButton.disabled = false;
downloadButton.textContent = "Download All Videos with Metadata";
})
.catch((error) => {
statusText.textContent = `Error: ${error.message}`;
downloadButton.disabled = false;
downloadButton.textContent = "Try Again";
});
};
container.appendChild(downloadButton);
const statusText = document.createElement("div");
statusText.textContent = "Ready";
statusText.style.fontSize = "12px";
statusText.style.color = "#ccc";
container.appendChild(statusText);
document.body.appendChild(container);
return { statusText };
}
// Configuration
const CONFIG = {
API_BASE_URL: "https://www.douyin.com/aweme/v1/web/aweme/post/",
DEFAULT_HEADERS: {
accept: "application/json, text/plain, */*",
"accept-language": "vi",
"sec-ch-ua": '"Not?A_Brand";v="8", "Chromium";v="118", "Microsoft Edge";v="118"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"user-agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.0.0",
},
RETRY_DELAY_MS: 2000,
MAX_RETRIES: 5,
REQUEST_DELAY_MS: 1000,
};
// Utility functions
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const retryWithDelay = async (fn, retries = CONFIG.MAX_RETRIES) => {
let lastError;
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
console.log(`Attempt ${i + 1} failed:`, error);
await sleep(CONFIG.RETRY_DELAY_MS);
}
}
throw lastError;
};
// API Client
class DouyinApiClient {
constructor(secUserId) {
this.secUserId = secUserId;
}
async fetchVideos(maxCursor) {
const url = new URL(CONFIG.API_BASE_URL);
const params = {
device_platform: "webapp",
aid: "6383",
channel: "channel_pc_web",
sec_user_id: this.secUserId,
max_cursor: maxCursor,
count: "20",
version_code: "170400",
version_name: "17.4.0",
};
Object.entries(params).forEach(([key, value]) => url.searchParams.append(key, value));
const response = await fetch(url, {
headers: {
...CONFIG.DEFAULT_HEADERS,
referrer: `https://www.douyin.com/user/${this.secUserId}`,
},
credentials: "include",
});
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return response.json();
}
}
// Data Processing
class VideoDataProcessor {
static extractVideoMetadata(video) {
if (!video) return null;
// Initialize the metadata object
const metadata = {
id: video.aweme_id || "",
desc: video.desc || "",
title: video.desc || "", // Using desc as the title since title field isn't directly available
createTime: video.create_time ? new Date(video.create_time * 1000).toISOString() : "",
videoUrl: "",
audioUrl: "",
coverUrl: "",
dynamicCoverUrl: "",
};
// Extract video URL
if (video.video?.play_addr) {
metadata.videoUrl = video.video.play_addr.url_list[0];
if (metadata.videoUrl && !metadata.videoUrl.startsWith("https")) {
metadata.videoUrl = metadata.videoUrl.replace("http", "https");
}
} else if (video.video?.download_addr) {
metadata.videoUrl = video.video.download_addr.url_list[0];
if (metadata.videoUrl && !metadata.videoUrl.startsWith("https")) {
metadata.videoUrl = metadata.videoUrl.replace("http", "https");
}
}
// Extract audio URL
if (video.music?.play_url) {
metadata.audioUrl = video.music.play_url.url_list[0];
}
// Extract cover URL (static thumbnail)
if (video.video?.cover) {
metadata.coverUrl = video.video.cover.url_list[0];
} else if (video.cover) {
metadata.coverUrl = video.cover.url_list[0];
}
// Extract dynamic cover URL (animated thumbnail)
if (video.video?.dynamic_cover) {
metadata.dynamicCoverUrl = video.video.dynamic_cover.url_list[0];
} else if (video.dynamic_cover) {
metadata.dynamicCoverUrl = video.dynamic_cover.url_list[0];
}
return metadata;
}
static processVideoData(data) {
if (!data?.aweme_list) {
return { videoData: [], hasMore: false, maxCursor: 0 };
}
const videoData = data.aweme_list.map((video) => this.extractVideoMetadata(video)).filter((item) => item && item.videoUrl);
return {
videoData,
hasMore: data.has_more,
maxCursor: data.max_cursor,
};
}
}
// File Handler
class FileHandler {
static saveVideoUrls(videoData) {
if (!videoData.length) {
throw new Error("No video data to save");
}
// Save full JSON data for comprehensive metadata
const jsonBlob = new Blob([JSON.stringify(videoData, null, 2)], { type: "application/json" });
const jsonLink = document.createElement("a");
jsonLink.href = window.URL.createObjectURL(jsonBlob);
jsonLink.download = "douyin-video-data.json";
jsonLink.click();
// Also save plain URLs for backward compatibility
const urls = videoData.map((item) => item.videoUrl);
const txtBlob = new Blob([urls.join("\n")], { type: "text/plain" });
const txtLink = document.createElement("a");
txtLink.href = window.URL.createObjectURL(txtBlob);
txtLink.download = "douyin-video-links.txt";
txtLink.click();
return {
jsonCount: videoData.length,
urlCount: urls.length,
};
}
}
// Main Downloader
class DouyinDownloader {
constructor(statusElement) {
this.validateEnvironment();
const secUserId = this.extractSecUserId();
this.apiClient = new DouyinApiClient(secUserId);
this.statusElement = statusElement;
}
validateEnvironment() {
if (typeof window === "undefined" || !window.location) {
throw new Error("Script must be run in a browser environment");
}
}
extractSecUserId() {
const secUserId = location.pathname.replace("/user/", "");
if (!secUserId || location.pathname.indexOf("/user/") === -1) {
throw new Error("Please run this script on a DouYin user profile page!");
}
return secUserId;
}
updateStatus(message) {
if (this.statusElement) {
this.statusElement.textContent = message;
}
console.log(message);
}
async downloadAllVideos() {
try {
this.updateStatus("Starting video data collection...");
const allVideoData = [];
let hasMore = true;
let maxCursor = 0;
while (hasMore) {
this.updateStatus(`Fetching videos with cursor: ${maxCursor}`);
const data = await retryWithDelay(() => this.apiClient.fetchVideos(maxCursor));
const { videoData, hasMore: more, maxCursor: newCursor } = VideoDataProcessor.processVideoData(data);
allVideoData.push(...videoData);
hasMore = more;
maxCursor = newCursor;
this.updateStatus(`Found: ${allVideoData.length} videos`);
await sleep(CONFIG.REQUEST_DELAY_MS);
}
if (allVideoData.length > 0) {
this.updateStatus(`Saving ${allVideoData.length} videos with metadata...`);
const result = FileHandler.saveVideoUrls(allVideoData);
this.updateStatus(
`Download complete! Saved ${result.jsonCount} videos with metadata to JSON and ${result.urlCount} URLs to TXT.`,
);
} else {
this.updateStatus("No videos found.");
}
} catch (error) {
this.updateStatus(`Error downloading videos: ${error.message}`);
throw error;
}
}
}
// Script initialization
async function run() {
try {
const ui = window.douyinDownloaderUI || addUI();
window.douyinDownloaderUI = ui;
const downloader = new DouyinDownloader(ui.statusText);
await downloader.downloadAllVideos();
} catch (error) {
console.error("Critical error:", error);
alert(`An error occurred: ${error.message}`);
}
}
// Add the UI to the page after it's loaded
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", addUI);
} else {
addUI();
}
})();