#!/usr/bin/env node 'use strict'; // WM: Avril pemula // saluran : https://whatsapp.com/channel/0029VbCnMvaEgGfOncmyD02j const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const WM = 'Avril pemula'; const CHANNEL = 'https://whatsapp.com/channel/0029VbCnMvaEgGfOncmyD02j'; const BASE_URL = 'https://snaptik.app'; const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'; const jar = new Map(); function parseSetCookie(headers) { const raw = headers.getSetCookie ? headers.getSetCookie() : (headers.get('set-cookie') ? [headers.get('set-cookie')] : []); for (const c of raw) { const [pair] = c.split(';'); const i = pair.indexOf('='); if (i > 0) jar.set(pair.slice(0, i).trim(), pair.slice(i + 1).trim()); } } const baseHeaders = () => { const h = { 'User-Agent': UA, 'Accept': 'application/json, text/plain, */*', 'Accept-Language': 'en-US,en;q=0.9', 'Origin': BASE_URL, 'Referer': BASE_URL + '/' }; if (jar.size) h.Cookie = [...jar].map(([k, v]) => `${k}=${v}`).join('; '); return h; }; async function initSession() { const res = await fetch(BASE_URL + '/', { headers: baseHeaders() }); parseSetCookie(res.headers); } async function fetchToken() { const res = await fetch(`${BASE_URL}/api/token`, { method: 'POST', headers: { ...baseHeaders(), 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json' } }); parseSetCookie(res.headers); if (!res.ok) throw new Error(`token ${res.status}`); return res.json(); } function decryptPuzzle(id, b64) { const raw = Buffer.from(b64, 'base64'); const iv = raw.subarray(0, 16); const ct = raw.subarray(16); const key = crypto.createHash('sha256') .update(`sn4pt1k_v3r1fy2026:${id}`) .digest(); const d = crypto.createDecipheriv('aes-256-cbc', key, iv); return JSON.parse(Buffer.concat([d.update(ct), d.final()]).toString('utf8')); } function solve(p) { switch (p.t) { case 'b': return ((p.a ^ p.b) >> p.s) & 255; case 'r': return p.n.reduce((x, v) => x + v, 0) * 2 + 1; case 'c': return p.w.charCodeAt(p.i) * p.m; case 'm': return ((p.a + p.b) % 100) * p.c; case 'n': return p.a * p.b + p.b * p.c + p.c * p.a - p.a; default: throw new Error('unknown challenge ' + p.t); } } async function buildVerify() { const t = await fetchToken(); const p = decryptPuzzle(t.id, t.p); return `${t.id}:${solve(p)}:${p._e}:${p._h}`; } async function extract(url, retry = true) { const verify = await buildVerify(); const res = await fetch(`${BASE_URL}/api/extract?url=${encodeURIComponent(url)}`, { headers: { ...baseHeaders(), 'X-Requested-With': 'XMLHttpRequest', 'X-Verify': verify } }); parseSetCookie(res.headers); if (res.status === 403 && retry) { console.log(`[${WM}] 403 → retry token baru...`); return extract(url, false); } if (!res.ok) throw new Error(`extract ${res.status}`); return res.json(); } // ← FIX: terima downloadUrl, kirim sebagai query param &url= async function resolveHD(hdPath, downloadUrl) { const verify = await buildVerify(); let url = hdPath.startsWith('http') ? hdPath : BASE_URL + hdPath; if (downloadUrl) { url += `&url=${encodeURIComponent(downloadUrl)}`; } const res = await fetch(url, { headers: { ...baseHeaders(), 'X-Requested-With': 'XMLHttpRequest', 'X-Verify': verify } }); parseSetCookie(res.headers); if (!res.ok) return null; const j = await res.json(); return j.url || j.downloadUrl || j.hd || null; } async function download(url, dest) { const res = await fetch(url, { headers: baseHeaders() }); if (!res.ok) throw new Error(`download ${res.status}`); const buf = Buffer.from(await res.arrayBuffer()); fs.writeFileSync(dest, buf); return buf.length; } const sanitize = (s) => String(s).replace(/[^\w\-.]+/g, '_').slice(0, 80); (async () => { const input = process.argv[2]; const out = { watermark: WM, saluran: CHANNEL, input: input || null, success: false, type: null, id: null, title: null, author: null, stats: null, source: { downloadUrl: null, hdDownloadUrl: null, hdResolved: null }, files: [], error: null }; if (!input) { console.error('Usage: node snaptik.js ""'); console.log(JSON.stringify(out, null, 2)); process.exit(1); } try { console.log(`[${WM}] scraping: ${input}`); await initSession(); const raw = await extract(input); const d = raw.data || raw; out.id = d.id || null; out.title = d.title || null; out.author = d.author || null; out.stats = d.stats || null; out.source.downloadUrl = d.downloadUrl || null; out.source.hdDownloadUrl = d.hdDownloadUrl || null; const slide = d.type === 'image' || d.type === 'slideshow' || (Array.isArray(d.images) && d.images.length > 0); out.type = slide ? 'image' : 'video'; const base = sanitize(`${d.author?.username || 'tiktok'}_${d.id || Date.now()}`); const cwd = process.cwd(); if (!slide) { let dl = d.downloadUrl; let q = 'SD'; if (d.hdDownloadUrl) { try { const hd = await resolveHD(d.hdDownloadUrl, d.downloadUrl); if (hd) { dl = hd; q = 'HD'; out.source.hdResolved = hd; } } catch (e) { console.log(`[${WM}] HD gagal (${e.message}) → SD`); } } if (!dl) throw new Error('no downloadUrl'); const f = path.join(cwd, `${base}.mp4`); const size = await download(dl, f); out.success = true; out.files.push({ type: 'video', quality: q, path: f, size }); console.log(`[${WM}] ✓ ${q} → ${path.basename(f)} (${(size / 1048576).toFixed(2)} MB)`); } else { const imgs = d.images || []; const audio = d.music?.playUrl || d.music?.url || d.audioUrl || d.sound?.playUrl || null; for (let i = 0; i < imgs.length; i++) { const im = imgs[i]; const u = typeof im === 'string' ? im : (im.url || im.downloadUrl || im.thumbnail); if (!u) continue; const f = path.join(cwd, `${base}_${String(i + 1).padStart(2, '0')}.jpg`); const size = await download(u, f); out.files.push({ type: 'image', index: i + 1, path: f, size }); } if (audio) { const f = path.join(cwd, `${base}_audio.mp3`); const size = await download(audio, f); out.files.push({ type: 'audio', path: f, size }); } out.success = out.files.length > 0; console.log(`[${WM}] ✓ slideshow → ${out.files.length} file`); } } catch (e) { out.error = e.message; console.error(`[${WM}] ✗ ${e.message}`); } console.log(JSON.stringify(out, null, 2)); process.exit(out.success ? 0 : 1); })();