const fs = require("fs"); const { pipeline } = require("stream/promises"); const { Readable } = require("stream"); const BASE = "https://snapdouyin.app"; const UA = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"; function calculateHash(url, salt) { return Buffer.from(url).toString("base64") + (url.length + 1000) + Buffer.from(salt).toString("base64"); } function parseCookies(setCookieHeaders) { if (!setCookieHeaders) return ""; return setCookieHeaders.map((c) => c.split(";")[0]).join("; "); } async function getSessionAndToken() { const res = await fetch(BASE + "/", { headers: { "User-Agent": UA } }); const html = await res.text(); const setCookie = res.headers.getSetCookie ? res.headers.getSetCookie() : []; const cookie = parseCookies(setCookie); const match = html.match(/id="token"[^>]*value="([^"]+)"/); if (!match) throw new Error("Token tidak ditemukan di halaman"); return { cookie, token: match[1] }; } async function getVideoData(videoUrl, retries = 2) { for (let attempt = 0; attempt <= retries; attempt++) { try { const { cookie: initialCookie, token } = await getSessionAndToken(); const hash = calculateHash(videoUrl, "aio-dl"); const body = new URLSearchParams(); body.append("url", videoUrl); body.append("token", token); body.append("hash", hash); const res = await fetch(BASE + "/wp-json/mx-downloader/video-data/", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Origin: BASE, Referer: BASE + "/", Cookie: initialCookie, "User-Agent": UA, }, body, }); if (!res.ok) throw new Error(`video-data gagal dengan status ${res.status}`); const newSetCookie = res.headers.getSetCookie ? res.headers.getSetCookie() : []; const newCookie = parseCookies(newSetCookie); const cookie = newCookie || initialCookie; const data = await res.json(); if (data.error) throw new Error(data.error); data._cookie = cookie; return data; } catch (err) { if (attempt === retries) throw err; } } } async function downloadMedia(mediaUrl, cookie, outputPath) { const res = await fetch(mediaUrl + "&bandwidth_saving=1", { headers: { accept: "*/*", "accept-language": "id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", Cookie: cookie, "User-Agent": UA, }, signal: AbortSignal.timeout(30000), }); if (!res.ok) throw new Error(`download gagal dengan status ${res.status}`); if (!res.body) throw new Error("Response tidak punya body"); await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(outputPath)); return outputPath; } async function main() { const url = process.argv[2]; const qualityArg = process.argv[3]; if (!url) { console.error("Gunakan: node snapdouyin.js [index_kualitas]"); process.exit(1); } const data = await getVideoData(url); if (qualityArg === undefined) { console.log(JSON.stringify(data, null, 2)); return; } const media = data.medias[Number(qualityArg)]; if (!media) throw new Error("Index kualitas tidak valid"); const filename = `douyin-${media.quality.replace(/\s/g, "")}-${Date.now()}.${media.extension}`; await downloadMedia(media.url, data._cookie, filename); console.log(JSON.stringify({ url: data.url, title: data.title, thumbnail: data.thumbnail, duration: data.duration, source: data.source, downloaded: { saved: filename, quality: media.quality, extension: media.extension, size: media.formattedSize, }, }, null, 2)); } main().catch((err) => { console.error("Error:", err.message); if (err.cause) console.error("Cause:", err.cause.message || err.cause); process.exit(1); });