/** * 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(/?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('