Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /**
- * HOTELMURAH.COM - HOTEL/KERETA/PESAWAT
- * Base : https://www.hotelmurah.com
- * Author : Gienetic
- * Request : YOGIKID
- * Note : Orkut & Topup nya? BAYAR :V
- */
- const BASE = 'https://www.hotelmurah.com';
- const UA = 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Mobile Safari/537.36 Gienetic/KryzenTeam';
- const formatRupiah = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format;
- const HEADER_DASAR = {
- 'user-agent': UA,
- accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
- 'accept-language': 'id,en-US;q=0.9',
- 'x-requested-with': 'Gienetic-KryzenTeam',
- };
- let DAFTAR_STASIUN = [];
- let DAFTAR_BANDARA = [];
- const BANDARA_FALLBACK = [
- ['BPN', 4, 'Sepinggan - BalikPapan'],
- ['BDO', 6, 'Husein Sastranegara - Bandung'],
- ['BTH', 9, 'Hang Nadim - Batam'],
- ['DPS', 18, 'Ngurah Rai - Denpasar, Bali'],
- ['CGK', 24, 'Soekarno Hatta - Jakarta'],
- ['HLP', 25, 'Halim Perdanakusuma - Jakarta'],
- ['MDC', 42, 'Sam Ratulangi - Manado'],
- ['KNO', 45, 'Kuala Namu - Medan (Kuala Namu)'],
- ['UPG', 94, 'Sultan Hasanuddin - Ujungpandang, Makassar'],
- ['JOG', 99, 'Adi Sutjipto - Yogyakarta'],
- ].map(([kode, id, url]) => ({ kode, id, url, nama: url.split(' - ')[0], kota: url.split(' - ')[1] || '' }));
- // ─────────── Utilitas ───────────
- const tunggu = (ms) => new Promise((r) => setTimeout(r, ms));
- function dekodeEntitas(s = '') {
- return String(s)
- .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
- .replace(/"/g, '"').replace(/�?39;|'/g, "'").replace(/ /g, ' ')
- .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(+n));
- }
- const hapusTag = (s = '') => String(s).replace(/<[^>]+>/g, '');
- const bersihkan = (s = '') => dekodeEntitas(hapusTag(s)).replace(/\s+/g, ' ').trim();
- function hariIni() {
- const d = new Date();
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
- }
- function tambahHari(tgl, n) {
- const [y, m, d] = tgl.split('-').map(Number);
- const t = new Date(y, m - 1, d + n);
- return `${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, '0')}-${String(t.getDate()).padStart(2, '0')}`;
- }
- const tanggalValid = (s) => /^\d{4}-\d{2}-\d{2}$/.test(s) && !isNaN(new Date(s).getTime());
- const angka = (v) => { const n = parseInt(String(v).replace(/[^\d-]/g, ''), 10); return Number.isFinite(n) ? n : 0; };
- const tampilkan = (obj) => console.log(JSON.stringify(JSON.parse(JSON.stringify(obj)), null, 2));
- const diBlokir = (html) => /_cf_chl_opt|cf-browser-verification/.test(html);
- // ─────────── Lapisan HTTP ───────────
- async function permintaan(url, opsi = {}) {
- const r = await fetch(url, {
- method: opsi.method || 'GET',
- headers: { ...HEADER_DASAR, ...opsi.headers },
- body: opsi.body,
- signal: AbortSignal.timeout(opsi.timeout || 20000),
- });
- if (!r.ok) {
- const e = new Error(`HTTP ${r.status}`);
- e.status = r.status;
- throw e;
- }
- return r.text();
- }
- const ambil = (url, headers) => permintaan(url, { headers });
- const kirim = (url, body, headers) => permintaan(url, {
- method: 'POST', body,
- headers: { 'content-type': 'application/x-www-form-urlencoded', origin: BASE, 'x-requested-with': 'XMLHttpRequest', ...headers },
- });
- // ─────────── Stasiun & Bandara ───────────
- async function ambilDaftarStasiun() {
- if (DAFTAR_STASIUN.length) return DAFTAR_STASIUN;
- const html = await ambil(`${BASE}/tiketkeretaapi/`, { referer: `${BASE}/tiketkeretaapi/` });
- const seen = new Set();
- for (const m of html.matchAll(/ data-kode1="([^"]+)" data-kota1="([^"]+)" data-stasiun1="([^"]+)"/g)) {
- if (seen.has(m[1])) continue;
- seen.add(m[1]);
- DAFTAR_STASIUN.push({
- kode: m[1],
- kota: bersihkan(m[2]).replace(/^Kota /, ''),
- nama: bersihkan(m[3]).split(' (')[0],
- });
- }
- return DAFTAR_STASIUN;
- }
- async function ambilDaftarBandara() {
- if (DAFTAR_BANDARA.length) return DAFTAR_BANDARA;
- try {
- const html = await ambil(`${BASE}/tiketpesawat/`, { referer: `${BASE}/tiketpesawat/` });
- const map = new Map();
- for (const m of html.matchAll(/data-id="(\d+)"\s+data-tipe="airport"\s+data-url="([^"]+)"[^>]*>([^<]+)<\/div>/g)) {
- const kode = (m[3].match(/\(([A-Z]{3})\)/) || [])[1] || null;
- const url = dekodeEntitas(m[2]);
- map.set(`ID${m[1]}`, { kode, id: +m[1], url, nama: url.split(' - ')[0], kota: url.split(' - ')[1] || '' });
- }
- for (const m of html.matchAll(/[?&]ke=([^&"]+)[^"]*?[?&]kti=(\d+)/g)) {
- const id = +m[2];
- if (map.has(`ID${id}`)) continue;
- const url = decodeURIComponent(m[1]).replace(/\+/g, ' ');
- map.set(`ID${id}`, { kode: null, id, url, nama: url.split(' - ')[0], kota: url.split(' - ')[1] || '' });
- }
- DAFTAR_BANDARA = [...map.values()].sort((a, b) => a.id - b.id);
- if (!DAFTAR_BANDARA.length) DAFTAR_BANDARA = [...BANDARA_FALLBACK];
- } catch {
- DAFTAR_BANDARA = [...BANDARA_FALLBACK];
- }
- return DAFTAR_BANDARA;
- }
- function cariStasiun(q) {
- const u = q.toUpperCase();
- return DAFTAR_STASIUN.find((s) => s.kode === u || s.nama.toUpperCase().includes(u) || s.kota.toUpperCase().includes(u));
- }
- function cariBandara(q) {
- const u = q.toUpperCase();
- return DAFTAR_BANDARA.find((b) =>
- (b.kode && b.kode === u) ||
- b.nama.toUpperCase().includes(u) ||
- (b.kota && b.kota.toUpperCase().includes(u))
- );
- }
- const saranStasiun = (q) => {
- const u = q.toUpperCase();
- return DAFTAR_STASIUN
- .filter((s) => s.kode.startsWith(u) || s.nama.toUpperCase().includes(u) || s.kota.toUpperCase().includes(u))
- .slice(0, 5);
- };
- const saranBandara = (q) => {
- const u = q.toUpperCase();
- return DAFTAR_BANDARA
- .filter((b) => (b.kode && b.kode.startsWith(u)) || b.nama.toUpperCase().includes(u) || (b.kota && b.kota.toUpperCase().includes(u)))
- .slice(0, 5);
- };
- // ─────────── Hotel: Daftar ───────────
- function parseDaftarHotel(html) {
- const hotels = [];
- for (const blok of html.split('<div class="_Pd10">').slice(1)) {
- const kartu = blok.split('<!--')[0];
- const href = (kartu.match(/<a\s+href="([^"]+)"[^>]*class="box-hotel"/) || [])[1] || '';
- const po = kartu.match(/poclick\('(\d+)'\s*,\s*'([\s\S]*?)'\)/);
- const idM = kartu.match(/id="id-hotel-(\d+)"/);
- const namaM = kartu.match(/<div class="name-content"[^>]*>([\s\S]*?)<\/div>/);
- const id = idM?.[1] || po?.[1];
- const nama = namaM ? bersihkan(namaM[1]) : po ? dekodeEntitas(po[2]) : '';
- if (!id || !nama) continue;
- const path = dekodeEntitas(href).match(/\/hotel\/([^/]+)\/([^/?]+)\/?/);
- const kota = path?.[1] || '';
- let slug = path?.[2] || '';
- const kunci = (kartu.match(/id="([^"]+)-(?:pricetag|kupon)"/) || [])[1] || slug;
- if (!slug) slug = kunci;
- const alamatM = kartu.match(/<div class="name-address">([\s\S]*?)<\/div>/);
- let alamat = '';
- if (alamatM) {
- const t = bersihkan(alamatM[1]);
- alamat = t.includes('•') ? t.split('•').pop().trim() : t.trim();
- }
- hotels.push({
- id,
- nama,
- slug,
- kota,
- kunciHarga: kunci,
- bintang: (kartu.match(/<i class="fa fa-star"/g) || []).length,
- rating: parseFloat((kartu.match(/rating-review-score">([\d.]+)</) || [])[1]) || 0,
- alamat,
- gambar: (kartu.match(/<img\s+src="([^"]+)"\s+alt="/) || [])[1] || '',
- urlDetail: kota && slug ? `${BASE}/hotel/${kota}/${slug}/` : '',
- urlPemesanan: dekodeEntitas(href),
- });
- }
- return hotels;
- }
- async function ambilHarga(html, referer, paralel = 4) {
- const map = new Map();
- const m = html.match(/listHotel\s*=\s*'(\[[\s\S]*?\])'/);
- if (!m) return map;
- let payloads;
- try { payloads = JSON.parse(m[1]); } catch { return map; }
- for (let i = 0; i < payloads.length; i += paralel) {
- const batch = payloads.slice(i, i + paralel);
- const hasil = await Promise.allSettled(
- batch.map((p) => kirim(`${BASE}/bestprices`, 'data=' + encodeURIComponent(p), { referer }))
- );
- for (const r of hasil) {
- if (r.status !== 'fulfilled') continue;
- try {
- const o = JSON.parse(r.value);
- if (o?.kl) map.set(o.kl, o);
- } catch { /* lewati */ }
- }
- }
- return map;
- }
- function gabungHarga(hotel, data, mentah) {
- const harga = angka(data?.sellPrice);
- const hargaPajak = angka(data?.sellPriceWTax);
- const hargaPublik = angka(data?.webPrice);
- const habis = data?.soldout == 1;
- Object.assign(hotel, {
- status: habis ? 'soldout' : harga ? 'available' : 'unavailable',
- harga: harga || null,
- hargaTeks: harga ? formatRupiah(harga) : null,
- hargaTermasukPajak: hargaPajak || undefined,
- hargaTermasukPajakTeks: hargaPajak ? formatRupiah(hargaPajak) : undefined,
- pajak: hargaPajak > harga ? hargaPajak - harga : undefined,
- hargaPublik: hargaPublik || undefined,
- diskon: hargaPublik > harga && harga ? hargaPublik - harga : undefined,
- persenDiskon: hargaPublik > harga && harga ? Math.round((hargaPublik - harga) / hargaPublik * 100) : undefined,
- habis: habis || undefined,
- promo: data?.promo == 1 || undefined,
- tagPromo: data?.promoTag || undefined,
- favorit: data?.favorit == 1 || undefined,
- });
- if (mentah) hotel.mentah = data || null;
- return hotel;
- }
- function urutkan(arr, kunci) {
- const a = [...arr];
- if (kunci === 'harga') return a.sort((x, y) => (x.harga || Infinity) - (y.harga || Infinity));
- if (kunci === 'harga-desc') return a.sort((x, y) => (y.harga || 0) - (x.harga || 0));
- if (kunci === 'rating') return a.sort((x, y) => y.rating - x.rating);
- if (kunci === 'bintang') return a.sort((x, y) => y.bintang - x.bintang);
- if (kunci === 'nama') return a.sort((x, y) => x.nama.localeCompare(y.nama, 'id'));
- return a;
- }
- async function cariHotel(kota, cin, cout, opsi = {}) {
- const dewasa = opsi.dewasa ?? 1;
- const anak = opsi.anak ?? 1;
- const slugKota = encodeURIComponent(kota.toLowerCase());
- const referer = `${BASE}/hotel/${slugKota}/`;
- const lihat = new Set();
- const semua = [];
- const sumber = [];
- let halamanDiambil = 0;
- for (let h = 1; h <= (opsi.maxHalaman || 1); h++) {
- const params = new URLSearchParams({ cin, cout, a: dewasa, c: anak, mode: 'strict' });
- if (h > 1) params.set('per_page', String((h - 1) * 10));
- const url = `${BASE}/hotel/${slugKota}/?${params}`;
- sumber.push(url);
- const html = await ambil(url, { referer });
- if (diBlokir(html)) throw new Error('Diblokir Cloudflare. Coba lagi atau gunakan VPN.');
- const hotels = parseDaftarHotel(html);
- if (!hotels.length) break;
- halamanDiambil++;
- const peta = await ambilHarga(html, url, opsi.paralel);
- for (const x of hotels) {
- if (lihat.has(x.id)) continue;
- lihat.add(x.id);
- gabungHarga(x, peta.get(x.kunciHarga), opsi.mentah);
- semua.push(x);
- }
- if (hotels.length < 10) break;
- }
- return { hotels: semua, sumber, halamanDiambil };
- }
- function buatLaporanHotel(hotels, query, sumber, halaman, kunciSort) {
- const tersedia = hotels.filter((h) => h.harga > 0);
- const harga = tersedia.map((h) => h.harga).sort((a, b) => a - b);
- const rentangHarga = harga.length ? {
- min: harga[0],
- minTeks: formatRupiah(harga[0]),
- maks: harga[harga.length - 1],
- maksTeks: formatRupiah(harga[harga.length - 1]),
- rataRata: Math.round(harga.reduce((a, b) => a + b, 0) / harga.length),
- rataRataTeks: formatRupiah(Math.round(harga.reduce((a, b) => a + b, 0) / harga.length)),
- } : null;
- return {
- status: hotels.length ? 'success' : 'not_found',
- pesan: hotels.length ? undefined : `Tidak ada hotel di ${query.kota} pada tanggal tersebut.`,
- mataUang: 'IDR',
- query,
- paginasi: { halaman, perHalaman: 10 },
- urutan: kunciSort || 'default',
- ringkasan: {
- total: hotels.length,
- tersedia: tersedia.length,
- habis: hotels.filter((h) => h.habis).length,
- promo: hotels.filter((h) => h.promo).length,
- favorit: hotels.filter((h) => h.favorit).length,
- rentangHarga,
- },
- sumber,
- dibuatPada: new Date().toISOString(),
- hasil: hotels,
- };
- }
- // ─────────── Hotel: Detail ───────────
- async function detailHotel(input, cin, cout, opsi = {}) {
- const dewasa = opsi.dewasa ?? 1;
- const anak = opsi.anak ?? 1;
- const [kota, ...sisa] = input.split('/');
- const slug = sisa.length ? sisa.join('/') : kota;
- const url = sisa.length ? `${BASE}/hotel/${kota}/${slug}/` : `${BASE}/hotel/${slug}/`;
- const urlPenuh = `${url}?${new URLSearchParams({ cin, cout, a: dewasa, c: anak })}`;
- let html;
- try {
- html = await ambil(urlPenuh, { referer: `${BASE}/hotel/` });
- } catch (e) {
- if (e.status === 404) return { status: 'not_found', pesan: `Hotel "${input}" tidak ditemukan.` };
- throw e;
- }
- if (diBlokir(html)) throw new Error('Diblokir Cloudflare.');
- const namaM = html.match(/<h1 class="namehm-address">([^<]+)<\//);
- if (!namaM) return { status: 'not_found', pesan: `Hotel "${input}" tidak ditemukan.` };
- const bintang = (html.match(/<div class="rating-address">([\s\S]*?)<\/div>/)?.[1]?.match(/<i class="fas fa-star txkuning"/g) || []).length;
- const fasilitas = [...html.matchAll(/<div class="fass-slide">[\s\S]*?<div style="white-space: pre-line;">([^<]+)<\//g)].map((m) => bersihkan(m[1]));
- const galeri = [...new Set([...html.matchAll(/data-src="(https:\/\/(?:img\.hotelmurah\.com|q-xx\.bstatic\.com|pix8\.agoda\.net)[^"]+)"/gi)].map((m) => m[1]))];
- const ulasan = [...html.matchAll(/<div class="alg-review"[^>]*>[\s\S]*?<div class="tx-review">\s*"([^"]+)"[\s\S]*?<span>([^<]+)<\/span>[\s\S]*?<span>(\d+)\/10<\/span>/g)]
- .map((m) => ({ nama: bersihkan(m[2]), skor: +m[3], teks: bersihkan(m[1]) }));
- let harga = null;
- try {
- const peta = await ambilHarga(html, urlPenuh, opsi.paralel);
- const objs = [...peta.values()].filter((o) => angka(o.sellPrice) > 0).sort((a, b) => angka(a.sellPrice) - angka(b.sellPrice));
- if (objs.length) {
- const jual = angka(objs[0].sellPrice);
- const pajak = angka(objs[0].sellPriceWTax);
- harga = {
- hargaMulai: jual,
- hargaMulaiTeks: formatRupiah(jual),
- hargaMulaiTermasukPajak: pajak || undefined,
- hargaMulaiTermasukPajakTeks: pajak ? formatRupiah(pajak) : undefined,
- jumlahKamar: peta.size,
- };
- }
- } catch { /* lewati */ }
- const lat = html.match(/var latitude = '([^']+)'/);
- const lng = html.match(/var longitude = '([^']+)'/);
- return {
- status: 'success',
- nama: bersihkan(namaM[1]),
- slug,
- kota: sisa.length ? kota : undefined,
- bintang,
- rating: parseFloat(html.match(/<p class="box-rating">([\d.]+)<\//)?.[1]) || 0,
- labelRating: bersihkan(html.match(/<p class="rt-text">([^<]+)<\//)?.[1] || ''),
- jumlahUlasan: bersihkan(html.match(/<p class="rv-text">([^<]+)<\//)?.[1] || ''),
- alamat: bersihkan(html.match(/<span>([^<]+)<\/span>\s*<\/div>\s*<\/div>\s*<div class="deskripsi-hm"/)?.[1] || ''),
- lokasi: lat && lng ? { lat: parseFloat(lat[1]), lng: parseFloat(lng[1]) } : null,
- deskripsi: bersihkan(html.match(/<meta name="description" content="([^"]+)">/)?.[1] || ''),
- harga,
- fasilitas,
- galeri,
- ulasan,
- urlDetail: url,
- };
- }
- // ─────────── Kereta ───────────
- async function cariKereta(asal, tujuan, tanggal, opsi = {}) {
- const params = new URLSearchParams({
- dari: asal.kode,
- ke: tujuan.kode,
- checkin: tanggal,
- checkout: tambahHari(tanggal, 1),
- pp: opsi.pp || 0,
- dewasa: opsi.dewasa || 1,
- bayi: opsi.bayi || 0,
- stasiun_asal: `${asal.nama} (${asal.kode})`,
- stasiun_tujuan: `${tujuan.nama} (${tujuan.kode})`,
- });
- const html = await ambil(`${BASE}/tiketkeretaapi/kereta/list_kereta?${params}`, { referer: `${BASE}/tiketkeretaapi/` });
- if (diBlokir(html)) throw new Error('Diblokir Cloudflare.');
- const m = html.match(/var list_result_data = JSON\.parse\('({.*?})'\);/);
- const list = m ? JSON.parse(m[1]).pergi || [] : [];
- if (!list.length) {
- return {
- status: 'not_found',
- pesan: `Tidak ada kereta ${asal.kode} → ${tujuan.kode} pada ${tanggal}.`,
- query: { dari: asal, ke: tujuan, tanggal },
- };
- }
- const lihat = new Set();
- const jadwal = list
- .filter((k) => {
- const key = `${k.trainNo}|${k.classCategory}|${k.fare}`;
- if (lihat.has(key)) return false;
- lihat.add(key);
- return true;
- })
- .map((k) => ({
- namaKereta: bersihkan(k.trainName),
- nomorKereta: k.trainNo,
- kelas: k.classCategory,
- berangkat: k.departureTime,
- tiba: k.arrivalTime,
- kursiTersedia: +k.seatAvail || 0,
- habis: +k.seatAvail > 0 ? undefined : true,
- tarif: +k.fare || 0,
- tarifTeks: formatRupiah(+k.fare || 0),
- }));
- const tarif = jadwal.filter((j) => j.tarif > 0).map((j) => j.tarif).sort((a, b) => a - b);
- return {
- status: 'success',
- query: { dari: asal, ke: tujuan, tanggal },
- ringkasan: {
- total: jadwal.length,
- tersedia: jadwal.filter((j) => !j.habis).length,
- rentangTarif: tarif.length ? {
- min: tarif[0], minTeks: formatRupiah(tarif[0]),
- maks: tarif[tarif.length - 1], maksTeks: formatRupiah(tarif[tarif.length - 1]),
- } : null,
- },
- dibuatPada: new Date().toISOString(),
- hasil: jadwal,
- };
- }
- // ─────────── Pesawat ───────────
- async function cariPesawat(asal, tujuan, tanggal, opsi = {}) {
- const params = new URLSearchParams({
- dari: asal.url,
- ke: tujuan.url,
- tgl_berangkat: tanggal,
- tgl_kembali: 0,
- pp: 'no',
- dewasa: opsi.dewasa || 1,
- anak: opsi.anak || 0,
- bayi: opsi.bayi || 0,
- kai: asal.id,
- kti: tujuan.id,
- });
- const html = await ambil(`${BASE}/tiketpesawat/flight/result?${params}`, { referer: `${BASE}/tiketpesawat/` });
- if (diBlokir(html)) throw new Error('Diblokir Cloudflare.');
- const m = html.match(/var list_result_data = JSON\.parse\('({.*?})'\);/);
- const list = m ? JSON.parse(m[1]).berangkat || [] : [];
- if (!list.length) {
- return {
- status: 'not_found',
- pesan: `Tidak ada penerbangan ${asal.kode || asal.id} → ${tujuan.kode || tujuan.id} pada ${tanggal}.`,
- query: { dari: asal, ke: tujuan, tanggal },
- };
- }
- const lihat = new Set();
- const flight = list
- .filter((f) => {
- const k = `${f.airlinecode}|${f.flightno}|${f.fare}`;
- if (lihat.has(k)) return false;
- lihat.add(k);
- return true;
- })
- .map((f) => {
- const dep = f.penerbangan[0];
- const arr = f.penerbangan[f.penerbangan.length - 1];
- return {
- maskapai: bersihkan(f.maskapai),
- kodeMaskapai: f.airlinecode,
- nomorPenerbangan: f.flightno,
- kabin: f.cabin,
- dari: dep.from_airport,
- ke: arr.to_airport,
- berangkat: dep.departtime,
- tiba: arr.arrivetime,
- durasi: `${f.durationhour}j ${f.durationminute}m`,
- durasiMenit: (+f.durationhour || 0) * 60 + (+f.durationminute || 0),
- transit: +f.jml_transit || 0,
- bagasiKg: +f.baggagekilos || 0,
- tarif: +f.fare || 0,
- tarifTeks: formatRupiah(+f.fare || 0),
- diskonPromo: +f.diskon_promo || 0,
- diskonPromoTeks: +f.diskon_promo > 0 ? formatRupiah(+f.diskon_promo) : undefined,
- };
- });
- const tarif = flight.filter((f) => f.tarif > 0).map((f) => f.tarif).sort((a, b) => a - b);
- return {
- status: 'success',
- query: { dari: asal, ke: tujuan, tanggal },
- ringkasan: {
- total: flight.length,
- langsung: flight.filter((f) => !f.transit).length,
- denganTransit: flight.filter((f) => f.transit).length,
- rentangTarif: tarif.length ? {
- min: tarif[0], minTeks: formatRupiah(tarif[0]),
- maks: tarif[tarif.length - 1], maksTeks: formatRupiah(tarif[tarif.length - 1]),
- } : null,
- },
- dibuatPada: new Date().toISOString(),
- hasil: flight,
- };
- }
- // ─────────── CLI ───────────
- function parseOpsi(args) {
- const o = { _: [], maxHalaman: 1, paralel: 4 };
- const num = (v) => { const n = +v; return Number.isFinite(n) ? n : null; };
- for (let i = 0; i < args.length; i++) {
- const k = args[i];
- if (k === '--dewasa') o.dewasa = num(args[++i]);
- else if (k === '--anak') o.anak = num(args[++i]);
- else if (k === '--bayi') o.bayi = num(args[++i]);
- else if (k === '--pp') o.pp = ['ya', 'yes', '1', 'true'].includes(args[++i]?.toLowerCase()) ? 1 : 0;
- else if (k === '--max-halaman' || k === '--max-page') o.maxHalaman = Math.max(1, num(args[++i]) || 1);
- else if (k === '--limit') o.limit = Math.max(1, num(args[++i]) || 0);
- else if (k === '--urut' || k === '--sort') o.urut = args[++i]?.toLowerCase();
- else if (k === '--mentah' || k === '--raw') o.mentah = true;
- else if (k === '--paralel' || k === '--concurrency') o.paralel = Math.max(1, Math.min(8, num(args[++i]) || 4));
- else o._.push(k);
- }
- return o;
- }
- const BANTUAN = `
- hotelmurah.com — CLI
- PERINTAH
- hotel node hotelmurah.js hotel <kota> [cin] [cout] [opsi]
- detail node hotelmurah.js detail <kota/slug> [cin] [cout]
- kereta node hotelmurah.js kereta <dari> <ke> [tgl]
- pesawat node hotelmurah.js pesawat <dari> <ke> [tgl]
- bandara node hotelmurah.js --list-bandara
- stasiun node hotelmurah.js --list-stasiun
- OPSI TAMU
- --dewasa <n> jumlah dewasa (default 1)
- --anak <n> jumlah anak (hotel/pesawat)
- --bayi <n> jumlah bayi (kereta/pesawat)
- --pp ya|no pulang-pergi (kereta)
- OPSI HOTEL
- --max-halaman <n> halaman yang di-scrape (10 hotel/halaman, default 1)
- --limit <n> batasi jumlah hotel di output
- --urut <k> harga | harga-desc | rating | bintang | nama
- --mentah sertakan objek harga mentah dari server
- --paralel <1-8> permintaan harga paralel (default 4)
- TANGGAL
- Opsional. Default:
- hotel cin = besok, cout = besok+1
- kereta tgl = hari ini
- pesawat tgl = hari ini
- Format: YYYY-MM-DD
- CONTOH
- node hotelmurah.js hotel jakarta
- node hotelmurah.js hotel bandung 2026-06-20 2026-06-21 --max-halaman 3 --urut harga
- node hotelmurah.js detail jakarta/park-hyatt-jakarta-203-7
- node hotelmurah.js kereta GMR BD --dewasa 2 --bayi 1
- node hotelmurah.js pesawat CGK DPS --dewasa 2 --anak 1
- node hotelmurah.js --list-stasiun
- node hotelmurah.js --list-bandara
- `;
- function tentukanTanggalHotel(p) {
- let cin = p[2];
- let cout = p[3];
- if (cin && !tanggalValid(cin)) return { error: `Tanggal "${cin}" tidak valid (YYYY-MM-DD).` };
- if (cout && !tanggalValid(cout)) return { error: `Tanggal "${cout}" tidak valid (YYYY-MM-DD).` };
- cin = cin || tambahHari(hariIni(), 1);
- cout = cout || tambahHari(cin, 1);
- if (cout <= cin) return { error: 'Check-out harus setelah check-in.' };
- return { cin, cout };
- }
- async function utama() {
- const args = process.argv.slice(2);
- if (!args.length || ['-h', '--help', '--bantuan'].includes(args[0])) return console.log(BANTUAN);
- if (args[0] === '--list-bandara' || args[0] === '--list-pesawat') {
- await ambilDaftarBandara();
- DAFTAR_BANDARA.forEach((b) => console.log(`${(b.kode || '---').padEnd(4)} ${String(b.id).padStart(3)} ${b.url}`));
- return;
- }
- if (args[0] === '--list-stasiun' || args[0] === '--list-kereta') {
- await ambilDaftarStasiun();
- DAFTAR_STASIUN.forEach((s) => console.log(`${s.kode.padEnd(5)} ${s.nama} - ${s.kota}`));
- return;
- }
- const opsi = parseOpsi(args);
- const [mode, ...p] = opsi._;
- p.unshift(mode);
- try {
- if (mode === 'hotel') {
- if (!p[1]) return console.log(BANTUAN);
- const tgl = tentukanTanggalHotel(p);
- if (tgl.error) return tampilkan({ status: 'error', pesan: tgl.error });
- const hasil = await cariHotel(p[1], tgl.cin, tgl.cout, opsi);
- let hotels = urutkan(hasil.hotels, opsi.urut);
- if (opsi.limit) hotels = hotels.slice(0, opsi.limit);
- tampilkan(buatLaporanHotel(
- hotels,
- { kota: p[1], checkin: tgl.cin, checkout: tgl.cout, dewasa: opsi.dewasa ?? 1, anak: opsi.anak ?? 1 },
- hasil.sumber,
- hasil.halamanDiambil,
- opsi.urut,
- ));
- }
- else if (mode === 'detail') {
- if (!p[1]) return console.log(BANTUAN);
- const tgl = tentukanTanggalHotel(p);
- if (tgl.error) return tampilkan({ status: 'error', pesan: tgl.error });
- const data = await detailHotel(p[1], tgl.cin, tgl.cout, opsi);
- tampilkan({ ...data, query: { slug: p[1], checkin: tgl.cin, checkout: tgl.cout }, dibuatPada: new Date().toISOString() });
- }
- else if (mode === 'kereta') {
- if (!p[1] || !p[2]) return console.log(BANTUAN);
- const tgl = p[3] || hariIni();
- if (p[3] && !tanggalValid(tgl)) return tampilkan({ status: 'error', pesan: `Tanggal tidak valid.` });
- await ambilDaftarStasiun();
- const asal = cariStasiun(p[1]);
- const tujuan = cariStasiun(p[2]);
- if (!asal) return tampilkan({ status: 'not_found', pesan: `Stasiun "${p[1]}" tidak ditemukan.`, saran: saranStasiun(p[1]) });
- if (!tujuan) return tampilkan({ status: 'not_found', pesan: `Stasiun "${p[2]}" tidak ditemukan.`, saran: saranStasiun(p[2]) });
- tampilkan(await cariKereta(asal, tujuan, tgl, opsi));
- }
- else if (mode === 'pesawat') {
- if (!p[1] || !p[2]) return console.log(BANTUAN);
- const tgl = p[3] || hariIni();
- if (p[3] && !tanggalValid(tgl)) return tampilkan({ status: 'error', pesan: `Tanggal tidak valid.` });
- await ambilDaftarBandara();
- const asal = cariBandara(p[1]);
- const tujuan = cariBandara(p[2]);
- if (!asal) return tampilkan({ status: 'not_found', pesan: `Bandara "${p[1]}" tidak ditemukan.`, saran: saranBandara(p[1]) });
- if (!tujuan) return tampilkan({ status: 'not_found', pesan: `Bandara "${p[2]}" tidak ditemukan.`, saran: saranBandara(p[2]) });
- tampilkan(await cariPesawat(asal, tujuan, tgl, opsi));
- }
- else {
- console.log(BANTUAN);
- }
- } catch (e) {
- tampilkan({ status: 'error', pesan: e.message });
- process.exitCode = 1;
- }
- }
- if (require.main === module) utama();
- module.exports = {
- cariHotel, detailHotel, cariKereta, cariPesawat,
- parseDaftarHotel, ambilHarga, gabungHarga, urutkan, buatLaporanHotel,
- ambilDaftarStasiun, ambilDaftarBandara, cariStasiun, cariBandara,
- };
Add Comment
Please, Sign In to add comment