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