#!/usr/bin/env node // cse.js — cari di Google Programmable Search Engine (element API, pure JS) const CX = '006516753008110874046:1ugcdt3vo7z' // download/direct-link search 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 BASE = 'https://cse.google.com' let jar = {} // cookie jar: name -> value async function get(url, referer) { const r = await fetch(url, { headers: { 'User-Agent': UA, 'Accept': '*/*', 'Accept-Language': 'en-US,en;q=0.9', ...(referer ? { 'Referer': referer } : {}), ...(Object.keys(jar).length ? { 'Cookie': Object.entries(jar).map(([k, v]) => `${k}=${v}`).join('; ') } : {}) } }) const raw = await r.text() // simpan cookie baru dari response let sc if (typeof r.headers.getSetCookie === 'function') sc = r.headers.getSetCookie() else if (r.headers.get('set-cookie')) sc = [r.headers.get('set-cookie')] if (sc) for (const c of sc) { const [pair] = c.split(';') const eq = pair.indexOf('=') if (eq > 0) jar[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim() } return { status: r.status, raw } } // ambil sca_esv dari halaman utama async function mainPage(cx) { const { raw } = await get(`${BASE}/cse?cx=${encodeURIComponent(cx)}`) const m = /sca_esv\\x3d([a-f0-9]+)/.exec(raw) || /sca_esv=([a-f0-9]+)/.exec(raw) return m ? m[1] : null } // ambil cse_token dari cse.js async function cseTok(sca, cx) { const { raw } = await get(`${BASE}/cse.js?sca_esv=${sca}&hpg=1&cx=${encodeURIComponent(cx)}`, `${BASE}/cse?cx=${encodeURIComponent(cx)}`) const m = /"cse_token":\s*"([^"]+)"/.exec(raw) if (!m) throw new Error('cse_token tidak ditemukan') return m[1] } // ambil hasil async function search(cx, q, start, num) { const sca = await mainPage(cx) const tok = await cseTok(sca, cx) const cb = `google.search.cse.api${Date.now() % 100000}` const params = new URLSearchParams({ rsz: 'filtered_cse', num: String(num), hl: 'en', source: 'gcsc', cselibv: '93e57cb47c499c53', cx, q, safe: 'off', cse_tok: tok, filter: '1', sort: '', exp: 'cc,esbfa', fexp: '121991397,121991398,121877338,121877336', oq: q, gs_l: 'partner-generic.3...3113.3113.0.3141.1.1.0.0.0.0.0.0..0.0.csems,nrl=10...0.....34.partner-generic..1.0.0.', cseclient: 'hosted-page-client', callback: cb, rurl: `${BASE}/cse?cx=${encodeURIComponent(cx)}` }) if (start) params.set('start', String(start)) const { status, raw } = await get(`${BASE}/cse/element/v1?${params}`, `${BASE}/cse?cx=${encodeURIComponent(cx)}`) if (status !== 200) throw new Error(`HTTP ${status}`) const i = raw.indexOf('(') const j = raw.lastIndexOf(')') return JSON.parse(raw.slice(i + 1, j)) } function clean(s) { if (s == null) return '' return String(s).replace(/<[^>]+>/g, '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").trim() } function fmt(d) { return (d.results || []).map(r => ({ title: clean(r.title), link: r.unescapedUrl || r.url || '', domain: r.visibleUrl || '', snippet: clean(r.content) })) } async function run() { const args = process.argv.slice(2) const asJson = args.includes('--json') const outIdx = args.indexOf('--out') const outFile = outIdx >= 0 ? (args[outIdx + 1] && !args[outIdx + 1].startsWith('--') ? args[outIdx + 1] : 'output/cse.json') : null let n = 10, start = 0 const ni = args.indexOf('--n') if (ni >= 0) n = parseInt(args[ni + 1], 10) || 10 const pi = args.indexOf('--page') if (pi >= 0) start = (parseInt(args[pi + 1], 10) || 1) * 10 const q = args.find(a => !a.startsWith('--')) if (!q) { console.error('usage: node cse.js [--n 30] [--page 2] [--json] [--out f]') process.exit(1) } // kunjungan pertama ke cse.google.com untuk dapat NID await get(`${BASE}/cse?cx=${encodeURIComponent(CX)}`) const all = [] let total = 0 for (let s = start; s < start + n && all.length < n; s += 10) { const d = await search(CX, q, s, Math.min(10, n - all.length)) if (s === start) total = parseInt((d.cursor || {}).estimatedResultCount, 10) || 0 const items = fmt(d) if (!items.length) break all.push(...items) if (items.length < 10) break } const out = { query: q, total, shown: all.length, results: all } if (asJson) { console.log(JSON.stringify(out, null, 2)) } else { console.log(`${q}: ${total} hasil (${all.length} ditampilkan)`) all.forEach((r, i) => { console.log(`\n[${i + 1}] ${r.title}`) if (r.domain) console.log(` ${r.domain}`) console.log(` ${r.link}`) if (r.snippet) console.log(` ${r.snippet.slice(0, 160)}`) }) } if (outFile) { const { mkdir, writeFile } = await import('node:fs/promises') const { dirname } = await import('node:path') await mkdir(dirname(outFile), { recursive: true }) await writeFile(outFile, JSON.stringify(out, null, 2)) console.error(`\nsaved: ${outFile}`) } } run().catch(e => { console.error('error:', e.message); process.exit(1) })