// ==UserScript== // @name Douyin User Video Downloader // @namespace http://tampermonkey.net/ // @version 1.2 // @description Extract video, audio, links and metadata from Douyin user profiles // @author CaoCuong2404 // @match https://www.douyin.com/user/* // @icon https://www.google.com/s2/favicons?sz=64&domain=douyin.com // @grant none // @run-at document-end // @downloadURL none // ==/UserScript== (function () { "use strict"; // Add Tailwind CSS const tailwindCDN = document.createElement("script"); tailwindCDN.src = "https://cdn.tailwindcss.com"; document.head.appendChild(tailwindCDN); // Global state const state = { videos: [], selectedVideos: new Set(), isFetching: false, fetchedCount: 0, totalFound: 0, }; function createMainUI() { const container = document.createElement("div"); container.className = "fixed top-4 right-4 w-[900px] bg-white rounded-lg shadow-lg p-4 z-50 max-h-[90vh] flex flex-col"; container.id = "douyin-downloader"; container.innerHTML = `
Douyin

Douyin Downloader

Select No. Cover Title Date Actions
`; return container; } function createVideoRow(video, index) { const row = document.createElement("tr"); row.className = "hover:bg-gray-50"; const date = new Date(video.createTime); const formattedDate = date.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric", }); row.innerHTML = ` ${index + 1}
${video.title}
${video.title}
${formattedDate}
Video ${ video.audioUrl ? ` | Audio ` : "" }
`; return row; } function updateUI() { const selectedCount = state.selectedVideos.size; const totalCount = state.videos.length; // Update counts document.getElementById("selected-count").textContent = selectedCount; document.getElementById("total-count").textContent = totalCount; // Update select all checkbox const selectAllCheckbox = document.getElementById("select-all"); selectAllCheckbox.checked = selectedCount === totalCount && totalCount > 0; // Update download button const downloadBtn = document.getElementById("download-btn"); downloadBtn.disabled = selectedCount === 0; } function setupEventListeners() { // Fetch videos button document.getElementById("fetch-videos").addEventListener("click", async () => { if (state.isFetching) return; state.isFetching = true; state.fetchedCount = 0; state.videos = []; state.selectedVideos.clear(); const button = document.getElementById("fetch-videos"); const statusEl = document.getElementById("fetch-status"); const tableBody = document.getElementById("videos-table-body"); tableBody.innerHTML = ""; button.disabled = true; button.innerHTML = ` Fetching... `; try { const downloader = new DouyinDownloader(); await downloader.fetchAllVideos((newVideos) => { // Sort new videos by date (latest first) newVideos.sort((a, b) => new Date(b.createTime) - new Date(a.createTime)); // Add new videos to state state.videos.push(...newVideos); state.fetchedCount += newVideos.length; // Update table state.videos.forEach((video, index) => { const existingRow = document.querySelector(`[data-video-id="${video.id}"]`)?.closest("tr"); if (!existingRow) { tableBody.appendChild(createVideoRow(video, index)); } }); // Update status statusEl.textContent = `Fetched ${state.fetchedCount} videos`; updateUI(); }); setupTableEventListeners(); } catch (error) { console.error("Error fetching videos:", error); statusEl.textContent = "Error: " + error.message; } finally { state.isFetching = false; button.disabled = false; button.innerHTML = "Fetch Videos"; } }); // Download dropdown const downloadBtn = document.getElementById("download-btn"); const dropdownMenu = document.getElementById("dropdown-menu"); downloadBtn.addEventListener("click", () => { dropdownMenu.classList.toggle("hidden"); }); // Close dropdown when clicking outside document.addEventListener("click", (e) => { if (!downloadBtn.contains(e.target)) { dropdownMenu.classList.add("hidden"); } }); // Download actions dropdownMenu.addEventListener("click", (e) => { const action = e.target.dataset.action; if (!action) return; const selectedVideos = state.videos.filter((v) => state.selectedVideos.has(v.id)); switch (action) { case "audio": selectedVideos.forEach((video) => { if (video.audioUrl) window.open(video.audioUrl, "_blank"); }); break; case "video": selectedVideos.forEach((video) => { if (video.videoUrl) window.open(video.videoUrl, "_blank"); }); break; case "json": FileHandler.saveVideoUrls(selectedVideos, { downloadJson: true, downloadTxt: false }); break; case "txt": FileHandler.saveVideoUrls(selectedVideos, { downloadJson: false, downloadTxt: true }); break; } dropdownMenu.classList.add("hidden"); }); } function setupTableEventListeners() { // Select all checkbox document.getElementById("select-all").addEventListener("change", (e) => { const checkboxes = document.querySelectorAll(".video-checkbox"); checkboxes.forEach((checkbox) => { checkbox.checked = e.target.checked; const videoId = checkbox.dataset.videoId; if (e.target.checked) { state.selectedVideos.add(videoId); } else { state.selectedVideos.delete(videoId); } }); updateUI(); }); // Individual video checkboxes document.querySelectorAll(".video-checkbox").forEach((checkbox) => { checkbox.addEventListener("change", (e) => { const videoId = e.target.dataset.videoId; if (e.target.checked) { state.selectedVideos.add(videoId); } else { state.selectedVideos.delete(videoId); } updateUI(); }); }); } // 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, options = { downloadJson: true, downloadTxt: true }) { if (!videoData || videoData.length === 0) { console.warn("No video data to save"); return { savedCount: 0 }; } const now = new Date(); const timestamp = now.toISOString().replace(/[:.]/g, "-"); let savedCount = 0; // Save complete JSON data if option is enabled if (options.downloadJson) { const jsonContent = JSON.stringify(videoData, null, 2); const jsonBlob = new Blob([jsonContent], { type: "application/json" }); const jsonUrl = URL.createObjectURL(jsonBlob); const jsonLink = document.createElement("a"); jsonLink.href = jsonUrl; jsonLink.download = `douyin-video-data-${timestamp}.json`; jsonLink.style.display = "none"; document.body.appendChild(jsonLink); jsonLink.click(); document.body.removeChild(jsonLink); console.log(`Saved ${videoData.length} videos with metadata to JSON file`); } // Save plain URLs list if option is enabled if (options.downloadTxt) { // Create a list of video URLs const urlList = videoData.map((video) => video.videoUrl).join("\n"); const txtBlob = new Blob([urlList], { type: "text/plain" }); const txtUrl = URL.createObjectURL(txtBlob); const txtLink = document.createElement("a"); txtLink.href = txtUrl; txtLink.download = `douyin-video-links-${timestamp}.txt`; txtLink.style.display = "none"; document.body.appendChild(txtLink); txtLink.click(); document.body.removeChild(txtLink); console.log(`Saved ${videoData.length} video URLs to text file`); } savedCount = videoData.length; return { savedCount }; } } // 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 fetchAllVideos(onProgress) { let hasMore = true; let maxCursor = 0; while (hasMore) { const data = await retryWithDelay(() => this.apiClient.fetchVideos(maxCursor)); const { videoData, hasMore: more, maxCursor: newCursor } = VideoDataProcessor.processVideoData(data); if (onProgress) { onProgress(videoData); } hasMore = more; maxCursor = newCursor; await sleep(CONFIG.REQUEST_DELAY_MS); } } } // Initialize the UI function initializeUI() { const container = createMainUI(); document.body.appendChild(container); setupEventListeners(); } // Start the script if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initializeUI); } else { initializeUI(); } })();