Created
March 3, 2025 07:57
-
-
Save caocuong2404/2707b98706fe5cd65c5647a0d8b6367d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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(); | |
console.log(`Saved ${videoData.length} videos with metadata to douyin-video-data.json`); | |
console.log(`Also saved ${urls.length} video URLs to douyin-video-links.txt for backward compatibility`); | |
} | |
} | |
// Main Downloader | |
class DouyinDownloader { | |
constructor() { | |
this.validateEnvironment(); | |
const secUserId = this.extractSecUserId(); | |
this.apiClient = new DouyinApiClient(secUserId); | |
} | |
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; | |
} | |
async downloadAllVideos() { | |
try { | |
console.log("Starting video data collection..."); | |
const allVideoData = []; | |
let hasMore = true; | |
let maxCursor = 0; | |
while (hasMore) { | |
console.log(`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; | |
console.clear(); | |
console.log(`Found: ${allVideoData.length} videos`); | |
await sleep(CONFIG.REQUEST_DELAY_MS); | |
} | |
if (allVideoData.length > 0) { | |
console.log(`Saving ${allVideoData.length} videos with metadata...`); | |
FileHandler.saveVideoUrls(allVideoData); | |
console.log("Download complete!"); | |
} else { | |
console.log("No videos found."); | |
} | |
} catch (error) { | |
console.error("Error downloading videos:", error); | |
throw error; | |
} | |
} | |
} | |
// Script initialization | |
const run = async () => { | |
try { | |
const downloader = new DouyinDownloader(); | |
await downloader.downloadAllVideos(); | |
} catch (error) { | |
console.error("Critical error:", error); | |
alert(`An error occurred: ${error.message}`); | |
} | |
}; | |
run(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment