tiktok.mjs

Tiktok downloader with complete metadata.

#downloader#tools#utility
18
26 Des 2025, 10:05
RawEdit
javascript0 lines
/***
  @ Base: https://www.tiktok.com/
  @ Author: Shannz
  @ Note: Tiktok downloader with complete metadata.
***/

import axios from 'axios';
import * as cheerio from 'cheerio';
import fs from 'fs';
import path from 'path';
import mime from 'mime-types';
import { CookieJar } from 'tough-cookie';
import { wrapper } from 'axios-cookiejar-support';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

async function uploadToCloud(filePath, customFileName) {
    try {
        const stats = fs.statSync(filePath);
        const originalName = customFileName || path.basename(filePath);
        const ext = path.extname(originalName);
        const nameWithoutExt = path.basename(originalName, ext);
        const fileKey = `${nameWithoutExt}-${Date.now()}${ext}`;
        
        const contentType = mime.lookup(filePath) || 'application/octet-stream';
        const { data } = await axios.post('https://api.cloudsky.biz.id/get-upload-url', {
            fileKey: fileKey,
            contentType: contentType,
            fileSize: stats.size
        });

        await axios.put(data.uploadUrl, fs.readFileSync(filePath), {
            headers: { 
                'Content-Type': contentType, 
                'Content-Length': stats.size,
                'x-amz-server-side-encryption': 'AES256' 
            },
            maxBodyLength: Infinity,
            maxContentLength: Infinity
        });

        return `https://api.cloudsky.biz.id/file?key=${encodeURIComponent(fileKey)}`;
    } catch (e) {
        console.error(`[Upload Cloud Error] ${customFileName}:`, e.message);
        return null;
    }
}

async function downloadVideo(url, outputPath, apiInstance) {
    try {
        const response = await apiInstance.get(url, {
            responseType: 'arraybuffer',
            headers: {
                'Referer': 'https://www.tiktok.com/',
                'Range': 'bytes=0-' // Essential for resume/stream support
            }
        });
        
        fs.writeFileSync(outputPath, Buffer.from(response.data));
        return true;
    } catch (e) {
        console.error(`[Download Error]:`, e.message);
        return false;
    }
}

export async function tiktok(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(`Please Wait...`);
        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; // Fallback

        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 wmPath = path.join(__dirname, `wm_${videoId}.mp4`);
            if (await downloadVideo(watermarkUrl, wmPath, api)) {
                const cloudLink = await uploadToCloud(wmPath, `tiktok_wm_${videoId}.mp4`);
                if (cloudLink) {
                    finalResult.cloudUrl.watermark = cloudLink;
                }
                if (fs.existsSync(wmPath)) fs.unlinkSync(wmPath);
            }
        }

        if (hdNoWatermarkUrl) {
            const hdPath = path.join(__dirname, `hd_${videoId}.mp4`);
            if (await downloadVideo(hdNoWatermarkUrl, hdPath, api)) {
                const cloudLink = await uploadToCloud(hdPath, `tiktok_hd_${videoId}.mp4`);
                if (cloudLink) {
                    finalResult.cloudUrl.hd_nonwatermark = cloudLink;
                }
                if (fs.existsSync(hdPath)) fs.unlinkSync(hdPath);
            }
        }

        return {
            status: true,
            result: finalResult
        };

    } catch (error) {
        console.error('Error Main Process:', error.message);
        return { status: false, error: error.message };
    }
};