cd /news/ai-tools/tiktok-downloader-scraper-node-js-es… · home topics ai-tools article
[ARTICLE · art-19193] src=gist.github.com pub= topic=ai-tools verified=true sentiment=· neutral

TikTok Downloader & Scraper (Node.js/ES Modules) - Auto-detect and download regular videos, MP3 audio, or photo slides. Features automatic slide-to-MP4 conversion via WebSocket.

A developer has built a Node.js/ES Modules-based TikTok downloader and scraper that can automatically detect and download regular videos, MP3 audio, or photo slides. The tool features automatic slide-to-MP4 conversion via WebSocket, using cheerio for HTML parsing and the savetik.io API to extract content. It parses TikTok URLs to identify content type and retrieves download links for videos, audio, and individual slide images.

read7 min publishedMay 31, 2026

| import * as cheerio from 'cheerio'; | | import fs from 'fs/promises'; | | import WebSocket from 'ws'; | | import { createWriteStream } from 'fs'; | | | | const decodeHtmlEntities = (str) => { | | if (!str) return str; | | return str.replace(/+/gi, '+') | | .replace(/=/gi, '=') | | .replace(/&/gi, '&') | | .replace(/&/g, '&'); | | }; | | | | const downloadTikTok = async (tiktokUrl) => { | | const url = 'https://savetik.io/api/ajaxSearch'; | | | | const headers = { | | 'Accept': '/', | | 'Accept-Encoding': 'gzip, deflate, br, zstd', | | 'Accept-Language': 'id-ID', | | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', | | 'Origin': 'https://savetik.io', | | 'Referer': 'https://savetik.io/en', | | 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Mobile Safari/537.36', | | }; | | | | const formData = new URLSearchParams({ | | q: tiktokUrl, | | cursor: '0', | | page: '0', | | lang: 'en' | | }); | | | | try { | | const response = await fetch(url, { | | method: 'POST', | | headers: headers, | | body: formData.toString() | | }); | | | | if (!response.ok) { | | throw new Error(HTTP error! Status: ${response.status}); | | } | | | | const result = await response.json(); | | | | await fs.writeFile('hasil_data.txt', JSON.stringify(result, null, 2), 'utf-8'); | | | | if (result.status !== 'ok' || !result.data) { | | throw new Error('Gagal mendapatkan data HTML yang valid dari server.'); | | } | | | | const $ = cheerio.load(result.data); | | const parsedData = {}; | | | | parsedData.title = $('h3').first().text().trim() || null; | | parsedData.thumbnail = $('.thumbnail .image-tik img').attr('src') || | | $('.thumbnail img').first().attr('src') || | | null; | | | | parsedData.videos = []; | | $('.dl-action a').each((i, el) => { | | const text = $(el).text().trim(); | | const href = $(el).attr('href'); | | if (text.includes('Download MP4') || text.includes('Download Video')) { | | parsedData.videos.push({ | | label: text.replace(/\s+/g, ' '), | | url: href | | }); | | } | | }); | | parsedData.videoUrl = parsedData.videos[0]?.url || null; | | | | let mp3Link = null; | | $('.dl-action a').each((i, el) => { | | const text = $(el).text().trim(); | | if (text.includes('Download MP3')) { | | mp3Link = $(el).attr('href'); | | return false; | | } | | }); | | parsedData.mp3Url = mp3Link; | | | | parsedData.photos = []; | | $('.download-box li, .download-items').each((index, element) => { | | const thumb = $(element).find('.download-items__thumb img').attr('src'); | | const downloadLink = $(element).find('.download-items__btn a').attr('href'); | | if (downloadLink) { | | parsedData.photos.push({ | | index: index + 1, | | thumbnail: thumb || null, | | downloadUrl: downloadLink | | }); | | } | | }); | | | | const renderElem = $('#ConvertToVideo').length ? $('#ConvertToVideo') : $('[data-audiourl]'); | | let rawAudioUrl = renderElem.attr('data-audiourl'); | | let rawImageData = renderElem.attr('data-imagedata'); | | | | parsedData.audioUrl = rawAudioUrl ? decodeHtmlEntities(rawAudioUrl) : null; | | parsedData.imageDataUrl = rawImageData ? decodeHtmlEntities(rawImageData) : null; | | parsedData.contentType = parsedData.photos.length > 0 ? 'slide' : 'video'; | | parsedData.tiktokId = $('#TikTokId').val() || null; | | | | const scriptText = $('script').text(); | | parsedData.k_exp = scriptText.match(/k_exp\s*=\s*"([^"]+)"/)?.[1] || null; | | parsedData.k_token = scriptText.match(/k_token\s*=\s*"([^"]+)"/)?.[1] || null; | | parsedData.k_url_convert = scriptText.match(/k_url_convert\s*=\s*"([^"]+)"/)?.[1] || null; | | | | await fs.writeFile('hasil_clean.txt', JSON.stringify(parsedData, null, 2), 'utf-8'); | | | | return parsedData; | | | | } catch (error) { | | console.error('Gagal memproses downloadTikTok:', error.message); | | throw error; | | } | | }; | | | | const convertSlideToVideo = async (parsedData) => { | | if (!parsedData || parsedData.contentType !== 'slide') { | | console.log('[-] Batalkan konversi: Konten ini bukan bertipe slide/foto.'); | | return null; | | } | | | | if (!parsedData.audioUrl || !parsedData.imageDataUrl) { | | console.log('[-] Batalkan konversi: audioUrl atau imageDataUrl tidak ditemukan.'); | | return null; | | } | | | | const url = parsedData.k_url_convert || 'https://s3.tik-cdn.com/api/json/convert'; | | | | const headers = { | | 'Accept': '/', | | 'Accept-Encoding': 'gzip, deflate, br, zstd', | | 'Accept-Language': 'id-ID', | | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', | | 'Origin': 'https://savetik.io', | | 'Referer': 'https://savetik.io/', | | 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Mobile Safari/537.36', | | 'Sec-Ch-Ua': '"Chromium";v="127", "Not)A;Brand";v="99", "Microsoft Edge Simulate";v="127", "Lemur";v="127"', | | 'Sec-Ch-Ua-Mobile': '?0', | | 'Sec-Ch-Ua-Platform': '"Android"', | | 'Sec-Fetch-Dest': 'empty', | | 'Sec-Fetch-Mode': 'cors', | | 'Sec-Fetch-Site': 'cross-site' | | }; | | | | const formData = new URLSearchParams({ | | ftype: 'mp4', | | v_id: parsedData.tiktokId, | | audioUrl: parsedData.audioUrl, | | audioType: 'audio/mp3', | | imageUrl: parsedData.imageDataUrl, | | fquality: '1080p', | | fname: 'SaveTik.io', | | exp: parsedData.k_exp, | | token: parsedData.k_token | | }); | | | | try { | | console.log('[+] Mengirim permintaan konversi slide ke MP4...'); | | const response = await fetch(url, { | | method: 'POST', | | headers: headers, | | body: formData.toString() | | }); | | | | if (!response.ok) { | | throw new Error(Gagal menghubungi server convert. Status: ${response.status}); | | } | | | | const convertResult = await response.json(); | | console.log('[+] Hasil Respon Convert:', convertResult); | | | | await fs.writeFile('hasil_convert.txt', JSON.stringify(convertResult, null, 2), 'utf-8'); | | console.log('[+] Sukses! Log convert disimpan di "hasil_convert.txt"'); | | | | return convertResult; | | | | } catch (error) { | | console.error('[-] Terjadi kesalahan saat mengonversi slide:', error.message); | | throw error; | | } | | }; | | | | const downloadConvertedVideo = (jobId, outputFilename = 'converted_video.mp4') => { | | return new Promise((resolve, reject) => { | | const wsUrl = wss://s3.tik-cdn.com/sub/${jobId}?fname=SaveTik.io; | | console.log([+] Menghubungi WebSocket: ${wsUrl}); | | | | const headers = { | | 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Mobile Safari/537.36', | | 'Origin': 'https://savetik.io', | | 'Cache-Control': 'no-cache', | | 'Pragma': 'no-cache', | | 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits' | | }; | | | | const ws = new WebSocket(wsUrl, { headers }); | | const writeStream = createWriteStream(outputFilename); | | let receivedBytes = 0; | | let timeoutId; | | | | ws.on('open', () => { | | console.log('[+] WebSocket terbuka, menunggu data video...'); | | timeoutId = setTimeout(() => { | | ws.terminate(); | | reject(new Error('Timeout: tidak ada data dari WebSocket dalam 60 detik')); | | }, 60000); | | }); | | | | ws.on('message', (data, isBinary) => { | | if (isBinary) { | | receivedBytes += data.length; | | writeStream.write(Buffer.from(data)); | | console.log([+] Menerima chunk binary: ${data.length} bytes (total: ${receivedBytes})); | | } else { | | const msg = data.toString(); | | console.log([+] Pesan teks: ${msg}); | | if (msg.includes('end') || msg.includes('complete')) { | | ws.close(); | | } | | } | | }); | | | | ws.on('close', (code, reason) => { | | clearTimeout(timeoutId); | | writeStream.end(); | | console.log([+] WebSocket ditutup: ${code} - ${reason}); | | if (receivedBytes === 0) { | | reject(new Error('Tidak ada data video yang diterima')); | | } else { | | console.log([+] Video berhasil disimpan ke ${outputFilename} (${receivedBytes} bytes)); | | resolve(outputFilename); | | } | | }); | | | | ws.on('error', (err) => { | | clearTimeout(timeoutId); | | writeStream.end(); | | reject(err); | | }); | | }); | | }; | | | | const init = async () => { | | const targetUrl = 'https://vm.tiktok.com/ZS926oGFNUuYw-uz16y/'; | | | | console.log('[+] Memulai Scrape data dari Savetik...'); | | const data = await downloadTikTok(targetUrl); | | console.log('[+] Tipe Konten Terdeteksi:', data.contentType); | | | | if (data.contentType === 'slide') { | | const convertResult = await convertSlideToVideo(data); | | if (convertResult && convertResult.status === 'success' && convertResult.jobId) { | | const jobId = convertResult.jobId; | | console.log([+] Job ID diterima: ${jobId}); | | await new Promise(resolve => setTimeout(resolve, 3000)); | | await downloadConvertedVideo(jobId, 'slide_to_video.mp4'); | | } else { | | console.log('[-] Gagal mendapatkan jobId dari server konversi.'); | | } | | } else { | | console.log('[+] Link tersebut adalah Video reguler. Opsi MP4 langsung:', data.videos); | | } | | }; | | | | init().catch(console.error); |

── more in #ai-tools 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/tiktok-downloader-sc…] indexed:0 read:7min 2026-05-31 ·