/***
@ 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 };
}
};