tiktok.mjs

javascriptCreated 26 Des 2025, 10:05290 views
Tiktok video and image slide downloader with complete metadata.
#downloader#tools#utility
javascript
/***
  @ Base: https://www.tiktok.com/
  @ Author: Shannz
  @ Note: Tiktok video and image slide downloader with complete metadata.
***/

import axios from 'axios';
import * as cheerio from 'cheerio';
import { CookieJar } from 'tough-cookie';
import { wrapper } from 'axios-cookiejar-support';
import { uploadFile } from 'cloudsky-storage';

async function getBuffer(url, apiInstance) {
    try {
        const response = await apiInstance.get(url, {
            responseType: 'arraybuffer',
            headers: {
                'Referer': 'https://www.tiktok.com/',
                'Range': 'bytes=0-'
            }
        });
        return Buffer.from(response.data);
    } catch (e) {
        console.error(`[Download Error]:`, e.message);
        return null;
    }
}

export const tiktok = {
    video: async (url) => {
        const jar = new CookieJar();
        const api = axios.create({
            jar: jar,
            withCredentials: true,
            headers: {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0',
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
            }
        });
        wrapper(api);
    
        try {
            console.log(`Downloading Video...`);
            const htmlResponse = await api.get(url);
            const $ = cheerio.load(htmlResponse.data);
            
            let scriptContent = $('#__UNIVERSAL_DATA_FOR_REHYDRATION__').html() || $('#SIGI_STATE').html();
            if (!scriptContent) throw new Error('Script tag data tidak ditemukan (Captcha/IP Blocked).');
            
            const jsonData = JSON.parse(scriptContent);
            
            const defaultScope = jsonData?.__DEFAULT_SCOPE__;
            const itemStruct = defaultScope?.["webapp.video-detail"]?.itemInfo?.itemStruct 
                               || Object.values(jsonData.ItemModule || {})[0];
    
            if (!itemStruct) throw new Error('Struct video tidak ditemukan dalam JSON.');
    
            const videoId = itemStruct.id;
            const videoData = itemStruct.video;
            const watermarkUrl = videoData.downloadAddr || videoData.playAddr;
            let hdNoWatermarkUrl = null;
            let bitrateLabel = 0;
            let qualityLabel = 'Original';
    
            if (videoData.bitrateInfo && Array.isArray(videoData.bitrateInfo)) {
                const bestQuality = videoData.bitrateInfo.sort((a, b) => b.Bitrate - a.Bitrate)[0];
                
                if (bestQuality) {
                    bitrateLabel = bestQuality.Bitrate;
                    qualityLabel = bestQuality.QualityType;
                    const urlList = bestQuality.PlayAddr?.UrlList || [];
                    hdNoWatermarkUrl = urlList.find(u => u.includes('aweme/v1/play')) || urlList[urlList.length - 1];
                }
            }
            if (!hdNoWatermarkUrl) hdNoWatermarkUrl = videoData.playAddr;
    
            const finalResult = {
                metadata: {
                    id: videoId,
                    description: itemStruct.desc,
                    createTime: new Date(itemStruct.createTime * 1000).toLocaleString(),
                    region: itemStruct.locationCreated || 'N/A',
                    hashtags: itemStruct.challenges?.map(tag => ({
                        id: tag.id,
                        name: tag.title
                    })) || []
                },
                originalUrl: {
                    watermark: watermarkUrl,
                    hd_nonwatermark: hdNoWatermarkUrl
                },
                cloudUrl: {
                    watermark: null,
                    hd_nonwatermark: null
                },
                videoInfo: {
                    duration: videoData?.duration,
                    resolution: `${videoData?.width}x${videoData?.height}`,
                    format: videoData?.format,
                    codec: videoData?.codecType,
                    bitrate: bitrateLabel,
                    quality: qualityLabel,
                    cover: {
                        static: videoData?.cover,
                        dynamic: videoData?.dynamicCover,
                        origin: videoData?.originCover
                    }
                },
                author: {
                    id: itemStruct.author?.id,
                    uniqueId: itemStruct.author?.uniqueId,
                    nickname: itemStruct.author?.nickname,
                    signature: itemStruct.author?.signature,
                    avatar: itemStruct.author?.avatarLarger || itemStruct.author?.avatarThumb,
                    verified: itemStruct.author?.verified
                },
                music: {
                    id: itemStruct.music?.id,
                    title: itemStruct.music?.title,
                    author: itemStruct.music?.authorName,
                    cover: itemStruct.music?.coverLarge,
                    playUrl: itemStruct.music?.playUrl, 
                    isOriginal: itemStruct.music?.original
                },
                stats: {
                    views: itemStruct.statsV2?.playCount || itemStruct.stats?.playCount,
                    likes: itemStruct.statsV2?.diggCount || itemStruct.stats?.diggCount,
                    comments: itemStruct.statsV2?.commentCount || itemStruct.stats?.commentCount,
                    shares: itemStruct.statsV2?.shareCount || itemStruct.stats?.shareCount,
                    saves: itemStruct.statsV2?.collectCount || itemStruct.stats?.collectCount
                }
            };

            if (watermarkUrl) {
                const wmBuffer = await getBuffer(watermarkUrl, api);
                if (wmBuffer) {
                    try {
                        const upload = await uploadFile(wmBuffer, `tiktok_wm_${videoId}.mp4`);
                        if (upload.success) {
                            finalResult.cloudUrl.watermark = upload.data.url;
                        }
                    } catch (e) {
                        console.error(`[Upload WM Error]:`, e.message);
                    }
                }
            }
    
            if (hdNoWatermarkUrl) {
                const hdBuffer = await getBuffer(hdNoWatermarkUrl, api);
                if (hdBuffer) {
                    try {
                        const upload = await uploadFile(hdBuffer, `tiktok_hd_${videoId}.mp4`);
                        if (upload.success) {
                            finalResult.cloudUrl.hd_nonwatermark = upload.data.url;
                        }
                    } catch (e) {
                        console.error(`[Upload HD Error]:`, e.message);
                    }
                }
            }
    
            return {
                status: true,
                result: finalResult
            };
    
        } catch (error) {
            console.error('Error Main Process:', error.message);
            return { status: false, error: error.message };
        }
    },

    slide: async (url) => {
        const jar = new CookieJar();
        const api = axios.create({
            jar: jar,
            withCredentials: true,
            headers: {
                'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Mobile Safari/537.36',
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
            }
        });
        wrapper(api);
    
        try {
            console.log(`Downloading Image Slide...`);
            const htmlResponse = await api.get(url);
            const $ = cheerio.load(htmlResponse.data);
            
            let scriptContent = $('#__UNIVERSAL_DATA_FOR_REHYDRATION__').html() || $('#SIGI_STATE').html();
            if (!scriptContent) throw new Error('Script tag data tidak ditemukan (Captcha/IP Blocked).');
            
            const jsonData = JSON.parse(scriptContent);
            const defaultScope = jsonData?.__DEFAULT_SCOPE__;
            const itemStruct = defaultScope?.["webapp.reflow.video.detail"]?.itemInfo?.itemStruct 
                               || defaultScope?.["webapp.video-detail"]?.itemInfo?.itemStruct;
    
            if (!itemStruct) throw new Error('Struct video tidak ditemukan dalam JSON.');
            if (!itemStruct.imagePost) throw new Error('Konten ini bukan berupa image slide.');
    
            const videoId = itemStruct.id;
            const imagesList = itemStruct.imagePost.images.map(img => img.imageURL.urlList[0]);
            const audioUrl = itemStruct.music?.playUrl;
    
            const finalResult = {
                metadata: {
                    id: videoId,
                    type: 'image_slide',
                    description: itemStruct.desc,
                    createTime: new Date(itemStruct.createTime * 1000).toLocaleString(),
                    region: itemStruct.locationCreated || 'N/A',
                    hashtags: itemStruct.challenges?.map(tag => ({
                        id: tag.id,
                        name: tag.title
                    })) || []
                },
                originalUrl: {
                    images: imagesList,
                    audio: audioUrl
                },
                author: {
                    id: itemStruct.author?.id,
                    uniqueId: itemStruct.author?.uniqueId,
                    nickname: itemStruct.author?.nickname,
                    signature: itemStruct.author?.signature,
                    avatar: itemStruct.author?.avatarLarger || itemStruct.author?.avatarThumb,
                    verified: itemStruct.author?.verified
                },
                music: {
                    id: itemStruct.music?.id,
                    title: itemStruct.music?.title,
                    author: itemStruct.music?.authorName,
                    cover: itemStruct.music?.coverLarge,
                    playUrl: audioUrl,
                    isOriginal: itemStruct.music?.original
                },
                stats: {
                    views: itemStruct.statsV2?.playCount || itemStruct.stats?.playCount,
                    likes: itemStruct.statsV2?.diggCount || itemStruct.stats?.diggCount,
                    comments: itemStruct.statsV2?.commentCount || itemStruct.stats?.commentCount,
                    shares: itemStruct.statsV2?.shareCount || itemStruct.stats?.shareCount,
                    saves: itemStruct.statsV2?.collectCount || itemStruct.stats?.collectCount
                }
            };
    
            return {
                status: true,
                result: finalResult
            };
    
        } catch (error) {
            console.error('Error Slide Process:', error.message);
            return { status: false, error: error.message };
        }
    }
}

//tiktok.slide('https://vm.tiktok.com/ZP8bbmHn6/').then(a => console.log(JSON.stringify(a, null, 2)))
//tiktok.video('https://vm.tiktok.com/ZP8bbXdyf/').then(a => console.log(JSON.stringify(a, null, 2)))