Back to Home
Gistify Snippet
kyiov
2h ago·1 file(s)
Public

Al-Quran Indonesia API & Surah Scraper

Complete Al-Quran digital scraper supporting all 114 Surahs, Ayah translations, Latin transliterations, and MP3 audio recitations.

#quran#islam#api#indonesia#javascript#tools
0
Raw
al-quran.jsjavascript
287 lines
1// al-quran.js — bundle standalone. npm deps: cheerio, node:url
2
3/**
4 * Scraper: al-quran.js
5 * Source : kyioapi/al-quran.js
6 *
7 * Input : Function parameters / CLI args (e.g. query: string, url: string, options: object)
8 * Output : Promise<Object | Array | Buffer> (JSON formatted scraping response / stream URL / media metadata)
9 *
10 * CLI Run:
11 * node kyioapi/al-quran.js [functionName] [args...]
12 * node cli.js kyioapi/al-quran.js [functionName] [args...]
13 */
14
15import * as __ns_12 from 'cheerio'
16const cheerio = __ns_12
17import * as __ns_13 from 'node:url'
18const pathToFileURL = __ns_13.pathToFileURL
19
20
21
22/**
23 * Base Scraper Class
24 * Centralizes request logic, headers, and common scraping utilities.
25 * Refactored to use native fetch for better performance in Node.js 18+.
26 */
27class BaseScraper {
28 constructor(baseURL, options = {}) {
29 this.baseURL = baseURL;
30 this.options = {
31 timeout: options.timeout || 30000,
32 headers: {
33 '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',
34 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
35 'Accept-Language': 'en-US,en;q=0.9,id;q=0.8',
36 ...options.headers
37 },
38 withCookies: options.withCookies || false
39 };
40 this.cookies = new Map();
41 }
42
43 /**
44 * Internal fetch wrapper with timeout and cookie handling
45 */
46 async _request(url, config = {}) {
47 const fullUrl = url.startsWith('http') ? url : `${this.baseURL}${url}`;
48 const method = config.method || 'GET';
49 const headers = { ...this.options.headers, ...config.headers };
50
51 if (this.options.withCookies && this.cookies.size > 0) {
52 headers['cookie'] = Array.from(this.cookies.entries())
53 .map(([k, v]) => `${k}=${v}`)
54 .join('; ');
55 }
56
57 const controller = new AbortController();
58 const id = setTimeout(() => controller.abort(), config.timeout || this.options.timeout);
59
60 try {
61 const response = await fetch(fullUrl, {
62 method,
63 headers,
64 body: config.data ? (typeof config.data === 'object' ? JSON.stringify(config.data) : config.data) : undefined,
65 signal: controller.signal,
66 ...config.fetchOptions
67 });
68
69 clearTimeout(id);
70
71 if (this.options.withCookies) {
72 const setCookie = response.headers.get('set-cookie');
73 if (setCookie) {
74 setCookie.split(',').forEach(cookie => {
75 const [pair] = cookie.split(';');
76 const [key, value] = pair.split('=');
77 if (key && value) this.cookies.set(key.trim(), value.trim());
78 });
79 }
80 }
81
82 return response;
83 } catch (error) {
84 clearTimeout(id);
85 throw error;
86 }
87 }
88
89 /**
90 * Fetches raw text
91 */
92 async getText(url, config = {}) {
93 try {
94 const response = await this._request(url, config);
95 return await response.text();
96 } catch (error) {
97 console.error(`Scraper Text Error [GET ${url}]:`, error.message);
98 throw error;
99 }
100 }
101
102 /**
103 * Fetches HTML and returns a Cheerio object
104 */
105 async getHTML(url, config = {}) {
106 try {
107 const data = await this.getText(url, config);
108 return cheerio.load(data);
109 } catch (error) {
110 throw error;
111 }
112 }
113
114 /**
115 * Fetches JSON data
116 */
117 async getJSON(url, config = {}) {
118 try {
119 const response = await this._request(url, config);
120 const text = await response.text();
121 try {
122 return JSON.parse(text);
123 } catch (e) {
124 if (text.trim().startsWith('<!DOCTYPE') || text.trim().startsWith('<html')) {
125 throw new Error(`Expected JSON but received HTML. This might be a Cloudflare challenge or an error page. [GET ${url}]`);
126 }
127 throw new Error(`Failed to parse JSON response: ${e.message}. [GET ${url}]`);
128 }
129 } catch (error) {
130 console.error(`Scraper JSON Error [GET ${url}]:`, error.message);
131 throw error;
132 }
133 }
134
135 /**
136 * Post JSON data
137 */
138 async postJSON(url, body, config = {}) {
139 try {
140 const response = await this._request(url, {
141 method: 'POST',
142 headers: { 'Content-Type': 'application/json', ...config.headers },
143 data: body,
144 ...config
145 });
146 const text = await response.text();
147 try {
148 return JSON.parse(text);
149 } catch (e) {
150 if (text.trim().startsWith('<!DOCTYPE') || text.trim().startsWith('<html')) {
151 throw new Error(`Expected JSON but received HTML. This might be a Cloudflare challenge or an error page. [POST ${url}]`);
152 }
153 if (config.allowTextResponse) return text;
154 throw new Error(`Failed to parse JSON response: ${e.message}. [POST ${url}]`);
155 }
156 } catch (error) {
157 console.error(`Scraper POST Error [POST ${url}]:`, error.message);
158 throw error;
159 }
160 }
161}
162
163
164
165/**
166 * Al-Quran Indonesia Scraper (V2 - npoint.io)
167 * Data Source: https://api.npoint.io/99c279bb173a6e28359c
168 */
169class AlQuran extends BaseScraper {
170 constructor() {
171 super('https://api.npoint.io/99c279bb173a6e28359c', {
172 headers: {
173 'Accept': 'application/json'
174 }
175 });
176 this.listCache = null;
177 }
178
179 /**
180 * Get list of all Surahs
181 */
182 async getSurahList() {
183 if (this.listCache) return this.listCache;
184 try {
185 this.listCache = await this.getJSON('/data');
186 return this.listCache;
187 } catch (error) {
188 throw new Error(`Gagal mengambil daftar surah: ${error.message}`);
189 }
190 }
191
192 /**
193 * Get specific Surah by number
194 * Merges surah info with ayahs from separate endpoint
195 * @param {string|number} surahNumber
196 */
197 async getSurah(surahNumber) {
198 try {
199 const list = await this.getSurahList();
200 const surahInfo = list.find(s => s.nomor == surahNumber);
201 if (!surahInfo) throw new Error(`Surah nomor ${surahNumber} tidak ditemukan.`);
202
203 const ayahs = await this.getJSON(`/surat/${surahNumber}`);
204
205 return {
206 ...surahInfo,
207 ayat: ayahs
208 };
209 } catch (error) {
210 throw new Error(`Gagal mengambil surah ${surahNumber}: ${error.message}`);
211 }
212 }
213
214 /**
215 * Get specific Ayah (Verse)
216 * @param {string|number} surahNumber
217 * @param {string|number} ayahNumber
218 */
219 async getAyah(surahNumber, ayahNumber) {
220 try {
221 const surah = await this.getSurah(surahNumber);
222 const ayah = surah.ayat.find(a => a.nomor == ayahNumber);
223 if (!ayah) throw new Error(`Ayat nomor ${ayahNumber} tidak ditemukan di Surah ${surah.nama}.`);
224
225 return {
226 surah: {
227 nomor: surah.nomor,
228 nama: surah.nama,
229 asma: surah.asma
230 },
231 ...ayah
232 };
233 } catch (error) {
234 throw new Error(`Gagal mengambil ayat: ${error.message}`);
235 }
236 }
237
238 /**
239 * Search surah by name
240 * @param {string} query
241 */
242 async searchSurah(query) {
243 const data = await this.getSurahList();
244 return data.filter(s =>
245 s.nama.toLowerCase().includes(query.toLowerCase()) ||
246 s.arti.toLowerCase().includes(query.toLowerCase())
247 );
248 }
249}
250
251const alQuran = new AlQuran();
252export { AlQuran, alQuran };
253export default alQuran;
254
255// ─────────────────────────────────────────────────────────────────────
256// Self-test: jalankan langsung dengan `node al-quran.js [args...]`
257// ─────────────────────────────────────────────────────────────────────
258if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
259 const __exports = { AlQuran: AlQuran, alQuran: alQuran }
260 const __fns = Object.fromEntries(Object.entries(__exports).filter(([, v]) => typeof v === 'function'))
261 const __names = Object.keys(__fns)
262 if (__names.length === 0) {
263 console.log('Tidak ada fungsi yang bisa di-demo. Import modul ini dari kode lain.')
264 } else {
265 const __search = __names.find((n) => /search|cari/i.test(n))
266 const __name = __search || __names[0]
267 const __fn = __fns[__name]
268 const __isClass = /^class\s/.test(Function.prototype.toString.call(__fn))
269 let __args = process.argv.slice(2).map((a) => { try { return JSON.parse(a) } catch { return a } })
270 if (__args.length === 0 && __search) __args.push('naruto')
271 if (__args.length === 0 && !__search && !__isClass && __fn.length > 0) {
272 console.log('Fungsi: ' + __names.join(', '))
273 console.log('Contoh: node ' + "al-quran.js" + ' ' + __name + ' <arg>')
274 } else {
275 ;(async () => {
276 try {
277 const r = __isClass ? new __fn(...__args) : await __fn(...__args)
278 console.log(JSON.stringify(r, null, 2))
279 } catch (e) {
280 console.error('Error: ' + (e && e.message ? e.message : e))
281 process.exitCode = 1
282 }
283 })()
284 }
285 }
286}
287
Discussion & Comments0

No comments yet — be the first to leave feedback or start a code discussion.

Sign in with your GitHub account to write comments and join the discussion.