// al-quran.js — bundle standalone. npm deps: cheerio, node:url /** * Scraper: al-quran.js * Source : kyioapi/al-quran.js * * Input : Function parameters / CLI args (e.g. query: string, url: string, options: object) * Output : Promise (JSON formatted scraping response / stream URL / media metadata) * * CLI Run: * node kyioapi/al-quran.js [functionName] [args...] * node cli.js kyioapi/al-quran.js [functionName] [args...] */ import * as __ns_12 from 'cheerio' const cheerio = __ns_12 import * as __ns_13 from 'node:url' const pathToFileURL = __ns_13.pathToFileURL /** * Base Scraper Class * Centralizes request logic, headers, and common scraping utilities. * Refactored to use native fetch for better performance in Node.js 18+. */ class BaseScraper { constructor(baseURL, options = {}) { this.baseURL = baseURL; this.options = { timeout: options.timeout || 30000, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9,id;q=0.8', ...options.headers }, withCookies: options.withCookies || false }; this.cookies = new Map(); } /** * Internal fetch wrapper with timeout and cookie handling */ async _request(url, config = {}) { const fullUrl = url.startsWith('http') ? url : `${this.baseURL}${url}`; const method = config.method || 'GET'; const headers = { ...this.options.headers, ...config.headers }; if (this.options.withCookies && this.cookies.size > 0) { headers['cookie'] = Array.from(this.cookies.entries()) .map(([k, v]) => `${k}=${v}`) .join('; '); } const controller = new AbortController(); const id = setTimeout(() => controller.abort(), config.timeout || this.options.timeout); try { const response = await fetch(fullUrl, { method, headers, body: config.data ? (typeof config.data === 'object' ? JSON.stringify(config.data) : config.data) : undefined, signal: controller.signal, ...config.fetchOptions }); clearTimeout(id); if (this.options.withCookies) { const setCookie = response.headers.get('set-cookie'); if (setCookie) { setCookie.split(',').forEach(cookie => { const [pair] = cookie.split(';'); const [key, value] = pair.split('='); if (key && value) this.cookies.set(key.trim(), value.trim()); }); } } return response; } catch (error) { clearTimeout(id); throw error; } } /** * Fetches raw text */ async getText(url, config = {}) { try { const response = await this._request(url, config); return await response.text(); } catch (error) { console.error(`Scraper Text Error [GET ${url}]:`, error.message); throw error; } } /** * Fetches HTML and returns a Cheerio object */ async getHTML(url, config = {}) { try { const data = await this.getText(url, config); return cheerio.load(data); } catch (error) { throw error; } } /** * Fetches JSON data */ async getJSON(url, config = {}) { try { const response = await this._request(url, config); const text = await response.text(); try { return JSON.parse(text); } catch (e) { if (text.trim().startsWith(' s.nomor == surahNumber); if (!surahInfo) throw new Error(`Surah nomor ${surahNumber} tidak ditemukan.`); const ayahs = await this.getJSON(`/surat/${surahNumber}`); return { ...surahInfo, ayat: ayahs }; } catch (error) { throw new Error(`Gagal mengambil surah ${surahNumber}: ${error.message}`); } } /** * Get specific Ayah (Verse) * @param {string|number} surahNumber * @param {string|number} ayahNumber */ async getAyah(surahNumber, ayahNumber) { try { const surah = await this.getSurah(surahNumber); const ayah = surah.ayat.find(a => a.nomor == ayahNumber); if (!ayah) throw new Error(`Ayat nomor ${ayahNumber} tidak ditemukan di Surah ${surah.nama}.`); return { surah: { nomor: surah.nomor, nama: surah.nama, asma: surah.asma }, ...ayah }; } catch (error) { throw new Error(`Gagal mengambil ayat: ${error.message}`); } } /** * Search surah by name * @param {string} query */ async searchSurah(query) { const data = await this.getSurahList(); return data.filter(s => s.nama.toLowerCase().includes(query.toLowerCase()) || s.arti.toLowerCase().includes(query.toLowerCase()) ); } } const alQuran = new AlQuran(); export { AlQuran, alQuran }; export default alQuran; // ───────────────────────────────────────────────────────────────────── // Self-test: jalankan langsung dengan `node al-quran.js [args...]` // ───────────────────────────────────────────────────────────────────── if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const __exports = { AlQuran: AlQuran, alQuran: alQuran } const __fns = Object.fromEntries(Object.entries(__exports).filter(([, v]) => typeof v === 'function')) const __names = Object.keys(__fns) if (__names.length === 0) { console.log('Tidak ada fungsi yang bisa di-demo. Import modul ini dari kode lain.') } else { const __search = __names.find((n) => /search|cari/i.test(n)) const __name = __search || __names[0] const __fn = __fns[__name] const __isClass = /^class\s/.test(Function.prototype.toString.call(__fn)) let __args = process.argv.slice(2).map((a) => { try { return JSON.parse(a) } catch { return a } }) if (__args.length === 0 && __search) __args.push('naruto') if (__args.length === 0 && !__search && !__isClass && __fn.length > 0) { console.log('Fungsi: ' + __names.join(', ')) console.log('Contoh: node ' + "al-quran.js" + ' ' + __name + ' ') } else { ;(async () => { try { const r = __isClass ? new __fn(...__args) : await __fn(...__args) console.log(JSON.stringify(r, null, 2)) } catch (e) { console.error('Error: ' + (e && e.message ? e.message : e)) process.exitCode = 1 } })() } } }