const BASE = "https://p.savenow.to"; const DEFAULT_APIKEY = "dfcb6d76f2f6a9894gjkege8a4ab232222"; const REFERER = "https://en.loader.to/"; const FORMATS = ["mp3", "m4a", "webm", "aac", "flac", "opus", "ogg", "wav", "144", "240", "360", "480", "720", "1080", "1440", "4k"]; const baseHeaders = { "accept": "*/*", "accept-language": "id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7", "cache-control": "no-cache", "pragma": "no-cache", "Referer": REFERER }; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function health() { try { await fetch(`${BASE}/api/health`, { method: "HEAD", headers: baseHeaders }); } catch (_) {} } async function requestDownload(sourceUrl, format, apikey, addInfo) { const params = new URLSearchParams({ format, url: sourceUrl, apikey }); if (addInfo) params.set("add_info", "1"); const res = await fetch(`${BASE}/api/v2/download?${params.toString()}`, { headers: baseHeaders }); const data = await res.json(); if (!data.success) throw new Error(data.error || "permintaan download ditolak oleh server"); return data; } async function checkProgress(id) { const res = await fetch(`${BASE}/api/progress?id=${encodeURIComponent(id)}&_=${Date.now()}`, { headers: baseHeaders }); return res.json(); } function isFailureStatus(progress) { if (progress.error) return true; const text = (progress.text || "").toLowerCase(); return text.includes("fail") || text.includes("error") || text.includes("gagal"); } async function waitForResult(id, { intervalMs = 1500, maxAttempts = 60 } = {}) { for (let attempt = 0; attempt < maxAttempts; attempt++) { const progress = await checkProgress(id); if (progress.download_url) return progress; if (isFailureStatus(progress)) throw new Error(progress.text || progress.error || "proses download gagal"); await sleep(intervalMs); } throw new Error("timeout menunggu hasil download"); } async function probe(url) { try { const res = await fetch(url, { method: "HEAD", headers: { Referer: REFERER } }); return { ok: res.ok, status: res.status, contentType: res.headers.get("content-type") || null, contentLength: res.headers.get("content-length") ? Number(res.headers.get("content-length")) : null }; } catch (_) { return null; } } async function download(sourceUrl, format, options = {}) { if (!sourceUrl) throw new Error("url wajib diisi"); if (!FORMATS.includes(format)) throw new Error(`format tidak dikenal, pilih salah satu: ${FORMATS.join(", ")}`); const apikey = options.apikey || DEFAULT_APIKEY; const addInfo = options.addInfo !== false; await health(); const initial = await requestDownload(sourceUrl, format, apikey, addInfo); let final = initial; if (!initial.url && !initial.download_url) { final = await waitForResult(initial.id, options); } const downloadUrl = final.download_url || final.url || initial.url; const fileInfo = options.probe === false ? null : await probe(downloadUrl); return { tool: "loaderto-wrapper by ShanMolvyr", success: true, sourceUrl, format, id: initial.id, title: initial.info ? initial.info.title || null : null, thumbnail: initial.info ? initial.info.image || null : null, downloadUrl, filename: final.filename || null, expiresIn: final.expires_in || null, file: fileInfo }; } async function main() { const [sourceUrl, format = "mp3", apikey] = process.argv.slice(2); try { const result = await download(sourceUrl, format, { apikey }); console.log(JSON.stringify(result, null, 2)); } catch (err) { console.log(JSON.stringify({ tool: "loaderto-wrapper by ShanMolvyr", success: false, error: err.message }, null, 2)); process.exitCode = 1; } } if (require.main === module) { main(); } module.exports = { download, FORMATS };