/** * @fileoverview Modul Downloader & Scraper TikTok Tanpa Watermark (TikWM API). * * Mengimplementasikan antarmuka klien API TikWM untuk mengekstrak metadata, video tanpa watermark (HD/SD), * format gambar multi-slide, serta audio latar (MP3) dari video TikTok. * Didukung fitur pencarian video berbasis kata kunci dan feed trending global. * * @module scrapers/tiktok * @version 2.0.0 * @author kyiov * @license MIT */ import { pathToFileURL } from 'node:url' /** * @typedef {Object} TikTokAuthor * @property {string} id - Pengenal unik pembuat konten * @property {string} unique_id - Username handle TikTok (@username) * @property {string} nickname - Nama tampilan profil * @property {string} avatar - Tautan foto profil */ /** * @typedef {Object} TikTokMedia * @property {'video'|'image'} type - Jenis media konten * @property {number} [duration] - Durasi video dalam detik * @property {string} [play] - URL unduhan video tanpa watermark * @property {string} [hdplay] - URL unduhan video kualitas HD tanpa watermark * @property {string} [wmplay] - URL unduhan video dengan watermark asli * @property {string[]} [images] - Daftar tautan gambar untuk post slide */ /** * @typedef {Object} TikTokDownloadResult * @property {string} id - ID unik video TikTok * @property {string} title - Judul atau takarir (caption) video * @property {string} cover - URL gambar sampul (thumbnail) * @property {TikTokMedia} media - Objek aset media (video/gambar) * @property {TikTokAuthor} author - Informasi pembuat konten * @property {Object} music - Metadata musik/audio latar * @property {number} views - Jumlah penayangan * @property {number} likes - Jumlah suka * @property {number} comments - Jumlah komentar * @property {number} shares - Jumlah pembagian */ export class TikWMScraper { /** * Menginisialisasi klien TikWM dengan base URL dan konfigurasi header default. */ constructor() { this.baseUrl = 'https://tikwm.com' this.headers = { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 'Accept': 'application/json, text/javascript, */*; q=0.01' } } /** * Mengunduh metadata video TikTok beserta tautan media tanpa watermark. * * @param {string} url - Tautan URL postingan TikTok lengkap atau format pendek (vt.tiktok.com) * @returns {Promise} Metadata lengkap dan tautan download media * @throws {Error} Jika URL tidak valid atau video gagal diekstrak */ async download(url) { if (!url) throw new Error('Parameter URL video TikTok wajib disertakan.') const params = new URLSearchParams({ url, hd: '1' }) const res = await fetch(`${this.baseUrl}/api/`, { method: 'POST', headers: this.headers, body: params }) if (!res.ok) throw new Error(`HTTP Error: ${res.status}`) const json = await res.json() if (json.code !== 0 || !json.data) { throw new Error(json.msg || 'Gagal mengekstrak data video dari URL tersebut.') } const data = json.data const isImage = data.duration === 0 && Array.isArray(data.images) && data.images.length > 0 return { id: data.id, title: data.title || '', cover: data.cover || '', media: isImage ? { type: 'image', images: data.images, image_count: data.images.length } : { type: 'video', duration: data.duration, play: data.play, hdplay: data.hdplay || data.play, wmplay: data.wmplay, size: data.size, hd_size: data.hd_size }, author: { id: data.author?.id, unique_id: data.author?.unique_id, nickname: data.author?.nickname, avatar: data.author?.avatar }, music: data.music_info || {}, views: data.play_count || 0, likes: data.digg_count || 0, comments: data.comment_count || 0, shares: data.share_count || 0, created_at: data.create_time } } /** * Mencari video TikTok berdasarkan kata kunci tertentu. * * @param {string} keywords - Kata kunci penelusuran video * @param {number} [count=10] - Jumlah video maksimum yang ingin diambil * @returns {Promise>} Daftar video yang cocok dengan kata kunci * @throws {Error} Jika kata kunci kosong atau penelusuran gagal */ async search(keywords, count = 10) { if (!keywords) throw new Error('Kata kunci pencarian wajib disertakan.') const params = new URLSearchParams({ keywords, count: String(count), cursor: '0', hd: '1' }) const res = await fetch(`${this.baseUrl}/api/feed/search/`, { method: 'POST', headers: this.headers, body: params }) if (!res.ok) throw new Error(`HTTP Error: ${res.status}`) const json = await res.json() if (json.code !== 0 || !json.data?.videos) { throw new Error(json.msg || 'Tidak ditemukan video yang sesuai dengan kata kunci.') } return json.data.videos.map((item) => ({ id: item.video_id, title: item.title, cover: item.cover, play: item.play, duration: item.duration, author: { id: item.author?.id, unique_id: item.author?.unique_id, nickname: item.author?.nickname, avatar: item.author?.avatar }, views: item.play_count, likes: item.digg_count })) } } export const tikwm = new TikWMScraper() export default tikwm // Eksekusi mandiri untuk pengujian langsung tanpa argumen CLI if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { try { console.log('Menguji pencarian video TikTok...') const searchResults = await tikwm.search('nature', 2) console.log('Hasil Pencarian:', JSON.stringify(searchResults, null, 2)) if (searchResults.length > 0) { const first = searchResults[0] const videoUrl = `https://www.tiktok.com/@${first.author.unique_id}/video/${first.id}` console.log(`Menguji ekstraksi video langsung: ${videoUrl}`) const downloadData = await tikwm.download(videoUrl) console.log('Hasil Ekstraksi Video:', { id: downloadData.id, title: downloadData.title, play: downloadData.media.play, author: downloadData.author.unique_id }) } } catch (err) { console.error('Galat eksekusi TikWM:', err.message || err) } }