Back to Home
Gistify Snippet
kyiov
3d ago·1 file(s)
Public

NontonAnimeID Scraper with Dynamic Nonce Extraction

Mengimplementasikan web scraper untuk NontonAnimeID menggunakan Cheerio, secara dinamis mengekstrak nonce dan URL AJAX dari skrip base64 untuk navigasi dan pengambilan data yang akurat. Memproses kartu anime untuk judul, tautan, dan gambar.

#javascript#scraper#cheerio#nodejs#anime#web-scraping
7
Raw
nontonanime.jsjavascript
1121 lines
1import * as cheerio from 'cheerio';
2import { pathToFileURL } from 'node:url';
3import dns from 'node:dns';
4
5dns.setDefaultResultOrder('ipv4first');
6
7class NontonAnimeIDScraper {
8 constructor(baseUrl = process.env.NONTONANIME_URL || 'https://nontonanimeid.my.id') {
9 this.baseUrl = baseUrl.replace(/\/$/, '');
10 this.headers = {
11 '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',
12 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
13 'Accept-Language': 'id,en-US;q=0.7,en;q=0.3'
14 };
15 this.lastNonce = null;
16 this.lastAjaxUrl = null;
17 this.lastUrl = null;
18 }
19
20 async _getSoup(url, params = {}) {
21 this.lastUrl = url;
22 let finalUrl = url;
23 if (Object.keys(params).length > 0) {
24 const q = new URLSearchParams();
25 for (const [k, v] of Object.entries(params)) {
26 if (Array.isArray(v)) {
27 v.forEach(val => q.append(`${k}[]`, val));
28 } else {
29 q.append(k, v);
30 }
31 }
32 finalUrl = `${url}?${q.toString()}`;
33 }
34
35 try {
36 const response = await fetch(finalUrl, {
37 headers: {
38 ...this.headers,
39 'Referer': this.baseUrl
40 },
41 signal: AbortSignal.timeout(20000)
42 });
43 if (!response.ok) {
44 throw new Error(`HTTP error! status: ${response.status}`);
45 }
46 const html = await response.text();
47 const $ = cheerio.load(html);
48 this._extractNonceAndAjaxUrl($);
49 return $;
50 } catch (error) {
51 console.warn(`[NontonAnimeID] Fetch failed (${finalUrl}):`, error?.message || error);
52 return null;
53 }
54 }
55
56 _extractNonceAndAjaxUrl($) {
57 const scraper = this;
58 $('script').each((i, el) => {
59 const src = $(el).attr('src') || '';
60 if (src.startsWith('data:text/javascript;base64,')) {
61 try {
62 const b64Data = src.split('base64,')[1];
63 const decoded = Buffer.from(b64Data, 'base64').toString('utf-8');
64 const nonceMatch = decoded.match(/"nonce"\s*:\s*"([^"]+)"/);
65 const urlMatch = decoded.match(/"url"\s*:\s*"([^"]+)"/);
66 if (nonceMatch) scraper.lastNonce = nonceMatch[1];
67 if (urlMatch) scraper.lastAjaxUrl = urlMatch[1].replace(/\\/g, '');
68 } catch (e) {
69
70 }
71 }
72 });
73 }
74
75 _parseAnimeCard($, cardEl) {
76 const card = $(cardEl);
77 const aTag = card.is('a') ? card : card.find('a').first();
78 const link = aTag.attr('href') || card.attr('href') || '';
79
80 const imgTag = card.find('img').first();
81 const image = imgTag.attr('src') || imgTag.attr('data-src') || '';
82
83 let title = '';
84 const titleTag = card.find('[class*="title"], .tt');
85 if (aTag.attr('title')) {
86 title = aTag.attr('title');
87 } else if (titleTag.length > 0) {
88 const span = titleTag.find('span');
89 if (span.length > 0) {
90 title = span.attr('data-title-default') || span.text().trim();
91 } else {
92 title = titleTag.text().trim();
93 }
94 } else if (imgTag.length > 0) {
95 title = imgTag.attr('alt') || '';
96 }
97 title = title.trim();
98
99 const ratingTag = card.find('.rating, .kotakscore, .as-rating');
100 let rating = '';
101 if (ratingTag.length > 0) {
102 rating = ratingTag.text().replace('⭐', '').trim();
103 if (!rating && ratingTag.hasClass('kotakscore')) {
104 rating = ratingTag.text().replace(/\n/g, '').trim();
105 }
106 }
107
108 const typeTag = card.find('.type, .as-type');
109 const typeVal = typeTag.length > 0 ? typeTag.text().replace('📺', '').trim() : '';
110
111 const seasonTag = card.find('.season, .as-season');
112 const season = seasonTag.length > 0 ? seasonTag.text().replace('📅', '').trim() : '';
113
114 const synopsisTag = card.find('.synopsis, .as-synopsis');
115 const synopsis = synopsisTag.length > 0 ? synopsisTag.text().trim() : '';
116
117 let genres = [];
118 const genresContainer = card.find('[class*="genres"]');
119 if (genresContainer.length > 0) {
120 genresContainer.find('.genre-tag, .genre-pill, .as-genre-tag').each((i, el) => {
121 genres.push($(el).text().trim());
122 });
123 } else {
124 card.find('.genre-tag, .genre-pill, .as-genre-tag').each((i, el) => {
125 genres.push($(el).text().trim());
126 });
127 }
128
129 return {
130 title,
131 link,
132 image,
133 rating,
134 type: typeVal,
135 season,
136 synopsis,
137 genres
138 };
139 }
140
141 async getHome() {
142 const $ = await this._getSoup(this.baseUrl);
143 if (!$) return {};
144
145 const data = {
146 episode_terbaru: [],
147 series_terbaru_movie: [],
148 series_terbaru_tv: [],
149 popular_series_semua: [],
150 popular_genre: [],
151 top_rating_anime: [],
152 series_popular_summer: []
153 };
154
155
156 $('#postbaru article.animeseries').each((i, el) => {
157 const article = $(el);
158 const aTag = article.find('a');
159 if (aTag.length > 0) {
160 const link = aTag.attr('href') || '';
161 const imgTag = aTag.find('img');
162 const image = imgTag.attr('src') || '';
163
164 const titleSpan = aTag.find('h3.title span');
165 let title = titleSpan.length > 0 ? (titleSpan.attr('data-title-default') || titleSpan.text().trim()) : '';
166 if (!title && imgTag.length > 0) {
167 title = imgTag.attr('alt') || '';
168 }
169 title = title.trim();
170
171 const epSpan = aTag.find('span.types.episodes');
172 const episode = epSpan.length > 0 ? epSpan.text().trim() : '';
173
174 const statusSpan = aTag.find('span.types.status');
175 const status = statusSpan.length > 0 ? statusSpan.text().trim() : '';
176
177 data.episode_terbaru.push({
178 title,
179 link,
180 image,
181 episode,
182 status
183 });
184 }
185 });
186
187 const parseTabContent = (tabId) => {
188 const items = [];
189 $(`#${tabId} div.animeseries`).each((i, el) => {
190 const article = $(el);
191 const aTag = article.find('a');
192 if (aTag.length > 0) {
193 const link = aTag.attr('href') || '';
194 const imgTag = aTag.find('img');
195 const image = imgTag.attr('src') || '';
196
197 const titleDiv = aTag.find('div.title');
198 let title = '';
199 if (titleDiv.length > 0) {
200 const titleSpan = titleDiv.find('span');
201 title = titleSpan.length > 0 ? (titleSpan.attr('data-title-default') || titleSpan.text().trim()) : titleDiv.text().trim();
202 }
203 if (!title && imgTag.length > 0) {
204 title = imgTag.attr('alt') || '';
205 }
206 title = title.trim();
207
208 const scoreSpan = aTag.find('span.kotakscore');
209 const score = scoreSpan.length > 0 ? scoreSpan.text().replace(/\n/g, '').replace(/ /g, '').replace('⭐', '').trim() : '';
210
211 items.push({
212 title,
213 link,
214 image,
215 score
216 });
217 }
218 });
219 return items;
220 };
221
222 data.series_terbaru_movie = parseTabContent('tab-7');
223 data.series_terbaru_tv = parseTabContent('tab-8');
224 data.popular_series_semua = parseTabContent('tab-9');
225 data.popular_genre = parseTabContent('tab-10');
226
227
228 const sidebar = $('#sidebar_right').length > 0 ? $('#sidebar_right') : $('body');
229 let topRatingHeader = null;
230 sidebar.find('h3, h2').each((i, el) => {
231 if ($(el).text().includes('Top Rating Anime')) {
232 topRatingHeader = $(el);
233 }
234 });
235 if (topRatingHeader) {
236 const ul = $(topRatingHeader).nextAll('ul.latestepisodes').first();
237 if (ul.length > 0) {
238 ul.find('li').each((i, el) => {
239 const li = $(el);
240 const aTag = li.find('a');
241 if (aTag.length > 0) {
242 const link = aTag.attr('href') || '';
243 const lefts = aTag.find('div.lefts');
244 const rights = aTag.find('div.rights');
245
246 const title = lefts.length > 0 ? lefts.text().trim() : '';
247 const videoNum = rights.length > 0 ? rights.find('span.video').text().trim() : '';
248
249 data.top_rating_anime.push({
250 title,
251 link,
252 episodes_count: videoNum
253 });
254 }
255 });
256 }
257 }
258
259
260 let popularSummerHeader = null;
261 sidebar.find('h3, h2').each((i, el) => {
262 if ($(el).text().includes('Series Popular Summer')) {
263 popularSummerHeader = $(el);
264 }
265 });
266 if (popularSummerHeader) {
267 const kotakbatas = $(popularSummerHeader).nextAll('div.kotakbatas').first();
268 if (kotakbatas.length > 0) {
269 kotakbatas.find('div.bor').each((i, el) => {
270 const aTag = $(el).find('a.popseries');
271 if (aTag.length > 0) {
272 const link = aTag.attr('href') || '';
273 const imgTag = aTag.find('img');
274 const image = imgTag.attr('src') || '';
275 const title = imgTag.attr('alt') || '';
276
277 data.series_popular_summer.push({
278 title: title.trim(),
279 link,
280 image
281 });
282 }
283 });
284 }
285 }
286
287 return data;
288 }
289
290 async getAnimeList(page = 1, filters = {}) {
291 const url = page > 1 ? `${this.baseUrl}/anime/page/${page}/` : `${this.baseUrl}/anime/`;
292 const $ = await this._getSoup(url, filters);
293 if (!$) return [];
294
295 const results = [];
296 const gridContainer = $('div.result');
297 if (gridContainer.length > 0) {
298 const cards = gridContainer.find('a.as-anime-card');
299 if (cards.length > 0) {
300 const scraper = this;
301 cards.each((i, el) => {
302 results.push(scraper._parseAnimeCard($, el));
303 });
304 } else {
305 const listItems = gridContainer.find('div.animeseries');
306 const scraper = this;
307 listItems.each((i, el) => {
308 const aTag = $(el).find('a');
309 if (aTag.length > 0) {
310 results.push(scraper._parseAnimeCard($, aTag));
311 }
312 });
313 }
314 }
315 return results;
316 }
317
318 async searchAnime(query, page = 1) {
319 const url = page > 1 ? `${this.baseUrl}/page/${page}/` : `${this.baseUrl}/`;
320 const params = { s: query };
321 const $ = await this._getSoup(url, params);
322 if (!$) return [];
323
324 const results = [];
325 const gridContainer = $('div.result');
326 if (gridContainer.length > 0) {
327 const cards = gridContainer.find('a.as-anime-card');
328 if (cards.length > 0) {
329 const scraper = this;
330 cards.each((i, el) => {
331 results.push(scraper._parseAnimeCard($, el));
332 });
333 } else {
334 const listItems = gridContainer.find('div.animeseries');
335 const scraper = this;
336 listItems.each((i, el) => {
337 const aTag = $(el).find('a');
338 if (aTag.length > 0) {
339 results.push(scraper._parseAnimeCard($, aTag));
340 }
341 });
342 }
343 }
344
345 if (results.length === 0) {
346 $('article.bs').each((i, el) => {
347 const a = $(el).find('a.tip').first();
348 const href = a.attr('href') || '';
349 const title = (a.attr('title') || a.text().trim() || '').replace(/\s+/g, ' ');
350 if (!href || !title) return;
351 results.push({
352 title,
353 link: href,
354 image: $(el).find('img').first().attr('src') || '',
355 rating: $(el).find('.rating').first().text().trim() || '',
356 type: $(el).find('.typez').first().text().trim() || '',
357 status: $(el).find('.epx, .status').first().text().trim() || ''
358 });
359 });
360 }
361
362 return results;
363 }
364
365 async getOngoingList(page = 1, sort = 'date') {
366 const url = page > 1 ? `${this.baseUrl}/ongoing-list/page/${page}/` : `${this.baseUrl}/ongoing-list/`;
367 const params = { sort, mode: 'sort' };
368 const $ = await this._getSoup(url, params);
369 if (!$) return [];
370
371 const results = [];
372 const gachaGrid = $('div.gacha-grid');
373 if (gachaGrid.length > 0) {
374 gachaGrid.find('a.gacha-card').each((i, el) => {
375 const card = $(el);
376 const link = card.attr('href') || '';
377
378 const imgTag = card.find('img');
379 const image = imgTag.attr('src') || '';
380
381 const titleTag = card.find('h3.title');
382 let title = titleTag.length > 0 ? titleTag.text().trim() : '';
383 if (!title && imgTag.length > 0) {
384 title = imgTag.attr('alt') || '';
385 }
386 title = title.trim();
387
388 const currentEp = card.find('span.current-ep');
389 const currentEpText = currentEp.length > 0 ? currentEp.text().trim() : '';
390
391 const totalEp = card.find('span.total-ep');
392 const totalEpText = totalEp.length > 0 ? totalEp.text().trim() : '';
393
394 const ratingSpan = card.find('span.skor-angka');
395 const rating = ratingSpan.length > 0 ? ratingSpan.text().replace('(', '').replace(')', '').trim() : '';
396
397 const hotTag = card.find('div.hot-tag');
398 const isHot = hotTag.length > 0;
399
400 let rarity = '';
401 const classes = card.attr('class') || '';
402 const match = classes.match(/rarity-(\d+)/);
403 if (match) rarity = match[1];
404
405 results.push({
406 title,
407 link,
408 image,
409 current_episode: currentEpText,
410 total_episodes: totalEpText,
411 rating,
412 hot: isHot,
413 rarity
414 });
415 });
416 }
417 return results;
418 }
419
420 async getPopularSeries(page = 1) {
421 const url = page > 1 ? `${this.baseUrl}/popular-series/page/${page}/` : `${this.baseUrl}/popular-series/`;
422 const $ = await this._getSoup(url);
423 if (!$) return {};
424
425 const data = {
426 tabs: {},
427 overall_rank: []
428 };
429
430 const tabsNav = $('ul.tabs');
431 if (tabsNav.length > 0) {
432 const scraper = this;
433 tabsNav.find('li.tab-link').each((i, el) => {
434 const linkLi = $(el);
435 const tabName = linkLi.text().trim();
436 const tabId = linkLi.attr('data-tab');
437 if (tabName && tabId) {
438 data.tabs[tabName] = [];
439 const tabContent = $(`#${tabId}`);
440 if (tabContent.length > 0) {
441 tabContent.find('div.animeseries').each((j, art) => {
442 const aTag = $(art).find('a');
443 if (aTag.length > 0) {
444 data.tabs[tabName].push(scraper._parseAnimeCard($, aTag));
445 }
446 });
447 }
448 }
449 });
450 }
451
452 const rankList = $('ul.rank');
453 if (rankList.length > 0) {
454 rankList.find('li').each((i, el) => {
455 const li = $(el);
456 const aTag = li.find('a');
457 if (aTag.length > 0) {
458 const link = aTag.attr('href') || '';
459 const imgTag = aTag.find('img');
460 const image = imgTag.attr('src') || '';
461
462 const mid = aTag.find('div.mid');
463 let title = '';
464 let synopsis = '';
465 let genres = [];
466 if (mid.length > 0) {
467 const h2Tag = mid.find('h2');
468 title = h2Tag.length > 0 ? h2Tag.text().trim() : '';
469 if (!title && imgTag.length > 0) {
470 title = imgTag.attr('alt') || '';
471 }
472 title = title.trim();
473
474 const pTag = mid.find('p');
475 synopsis = pTag.length > 0 ? pTag.text().trim() : '';
476
477 const viewerDiv = mid.find('div.viewer');
478 if (viewerDiv.length > 0) {
479 const viewerText = viewerDiv.text().replace('Genre :', '').trim();
480 genres = viewerText.split(',').map(g => g.trim()).filter(g => g);
481 }
482 }
483
484 data.overall_rank.push({
485 title,
486 link,
487 image,
488 synopsis,
489 genres
490 });
491 }
492 });
493 }
494
495 return data;
496 }
497
498 async getJadwalRilis() {
499 const url = `${this.baseUrl}/jadwal-rilis/`;
500 const $ = await this._getSoup(url);
501 if (!$) return {};
502
503 const data = {
504 pengumuman_libur: [],
505 perlu_diperhatikan: [],
506 perkiraan_rilis_mendatang: [],
507 kalender_rilis: {}
508 };
509
510 const liburBox = $('div.as-delay-announcements');
511 if (liburBox.length > 0) {
512 liburBox.find('li').each((i, el) => {
513 data.pengumuman_libur.push($(el).text().trim());
514 });
515 }
516
517 const notesBox = $('div.as-important-notes');
518 if (notesBox.length > 0) {
519 notesBox.find('li').each((i, el) => {
520 data.perlu_diperhatikan.push($(el).text().trim());
521 });
522 }
523
524 const upcomingBox = $('div.jr-upcoming-box');
525 if (upcomingBox.length > 0) {
526 upcomingBox.find('div.jr-upcoming-item').each((i, el) => {
527 const item = $(el);
528 const imgTag = item.find('img');
529 const image = imgTag.attr('src') || '';
530
531 const titleSpan = item.find('span.jr-upcoming-title');
532 const title = titleSpan.length > 0 ? titleSpan.text().trim() : '';
533
534 const epSpan = item.find('span.jr-upcoming-ep');
535 const episodeTime = epSpan.length > 0 ? epSpan.text().trim() : '';
536
537 const timeSpan = item.find('div.jr-upcoming-time');
538 const timeLeft = timeSpan.length > 0 ? timeSpan.text().trim() : '';
539
540 data.perkiraan_rilis_mendatang.push({
541 title,
542 image,
543 episode_time: episodeTime,
544 time_left: timeLeft
545 });
546 });
547 }
548
549 const days = ['senin', 'selasa', 'rabu', 'kamis', 'jumat', 'sabtu', 'minggu'];
550 days.forEach(day => {
551 const tabContent = $(`#${day}`);
552 if (tabContent.length > 0) {
553 const dateText = tabContent.attr('data-date-text') || '';
554 data.kalender_rilis[day] = {
555 date_text: dateText.trim(),
556 series: []
557 };
558
559 tabContent.find('a.as-anime-card').each((i, el) => {
560 const card = $(el);
561 const link = card.attr('href') || '';
562 const imgTag = card.find('img');
563 const image = imgTag.attr('src') || '';
564
565 const titleTag = card.find('h3.as-anime-title');
566 const title = titleTag.length > 0 ? titleTag.text().trim() : '';
567
568 const epText = card.find('span.jr-ep-text');
569 const episode = epText.length > 0 ? epText.text().trim() : '';
570
571 const typeBadge = card.find('span.jr-type-badge');
572 const typeVal = typeBadge.length > 0 ? typeBadge.text().trim() : '';
573
574 const timeSpan = card.find('span.time-text');
575 const timeVal = timeSpan.length > 0 ? timeSpan.text().replace('⏰', '').trim() : '';
576
577 const ratingSpan = card.find('span.rating-text');
578 const rating = ratingSpan.length > 0 ? ratingSpan.text().replace('⭐', '').trim() : '';
579
580 const membersSpan = card.find('span.members-text');
581 const members = membersSpan.length > 0 ? membersSpan.text().replace('👤', '').trim() : '';
582
583 let genresList = [];
584 card.find('span.jr-genre-pill').each((j, gEl) => {
585 genresList.push($(gEl).text().trim());
586 });
587
588 data.kalender_rilis[day].series.push({
589 title,
590 link,
591 image,
592 episode,
593 type: typeVal,
594 time: timeVal,
595 rating,
596 members,
597 genres: genresList
598 });
599 });
600 }
601 });
602
603 return data;
604 }
605
606 async getGenresList(sort = 'az') {
607 const url = `${this.baseUrl}/genres/`;
608 const params = { sort, mode: 'sort' };
609 const $ = await this._getSoup(url, params);
610 if (!$) return [];
611
612 const results = [];
613 const gridContainer = $('div.genre-grid-container');
614 if (gridContainer.length > 0) {
615 gridContainer.find('a.genre-grid-card').each((i, el) => {
616 const card = $(el);
617 const link = card.attr('href') || '';
618 const slug = link.replace(/\/$/, '').split('/').pop() || '';
619
620 const imgTag = card.find('img');
621 const image = imgTag.attr('src') || '';
622
623 const titleTag = card.find('h3.genre-name');
624 const name = titleTag.length > 0 ? titleTag.text().trim() : '';
625
626 let totalSeries = '';
627 const countSpan = card.find('span[class*="count"]');
628 if (countSpan.length > 0) {
629 totalSeries = countSpan.text().trim();
630 }
631
632 let ongoingSeries = '';
633 const ongoingSpan = card.find('span[class*="ongoing"]');
634 if (ongoingSpan.length > 0) {
635 ongoingSeries = ongoingSpan.text().trim();
636 }
637
638 results.push({
639 name,
640 link,
641 slug,
642 image,
643 total_series: totalSeries,
644 ongoing_series: ongoingSeries
645 });
646 });
647 }
648 return results;
649 }
650
651 async getAnimeDetail(animeUrlOrSlug) {
652 const url = animeUrlOrSlug.startsWith('http') ? animeUrlOrSlug : `${this.baseUrl}/anime/${animeUrlOrSlug}/`;
653 const $ = await this._getSoup(url);
654 if (!$) return {};
655
656 const titleH1 = $('h1.entry-title');
657 let title = '';
658 if (titleH1.length > 0) {
659 const span = titleH1.find('span');
660 title = span.length > 0 ? (span.attr('data-title-default') || span.text().trim()) : titleH1.text().replace('Nonton', '').replace('Sub Indo', '').trim();
661 }
662
663 let poster = '';
664 let score = '';
665 let typeVal = '';
666 let trailer = '';
667 const animeCard = $('div.anime-card');
668 if (animeCard.length > 0) {
669 const sidebar = animeCard.find('div.anime-card__sidebar');
670 if (sidebar.length > 0) {
671 const imgTag = sidebar.find('img');
672 poster = imgTag.attr('src') || '';
673
674 const scoreDiv = sidebar.find('div.anime-card__score');
675 if (scoreDiv.length > 0) {
676 const valSpan = scoreDiv.find('span.value');
677 score = valSpan.length > 0 ? valSpan.text().trim() : '';
678 const typeSpan = scoreDiv.find('span.type');
679 typeVal = typeSpan.length > 0 ? typeSpan.text().trim() : '';
680 }
681
682 const trailerA = sidebar.find('a.trailerbutton');
683 trailer = trailerA.attr('href') || '';
684 }
685 }
686
687 let details = {};
688 let synopsis = '';
689 let genres = [];
690 if (animeCard.length > 0) {
691 const mainInfo = animeCard.find('div.anime-card__main');
692 if (mainInfo.length > 0) {
693 const detailsUl = mainInfo.find('ul.details-list');
694 if (detailsUl.length > 0) {
695 detailsUl.find('li').each((i, el) => {
696 const li = $(el);
697 const labelTag = li.find('strong, span.detail-label');
698 if (labelTag.length > 0) {
699 const label = labelTag.text().replace(':', '').trim();
700 const valText = li.text().replace(labelTag.text(), '').trim();
701 details[label] = valText;
702 }
703 });
704 }
705
706 const genresDiv = mainInfo.find('div.anime-card__genres');
707 if (genresDiv.length > 0) {
708 genresDiv.find('a').each((i, el) => {
709 genres.push({
710 name: $(el).text().trim(),
711 link: $(el).attr('href') || ''
712 });
713 });
714 }
715
716 const synDiv = mainInfo.find('div#tab-synopsis');
717 if (synDiv.length > 0) {
718 synopsis = synDiv.text().trim();
719 }
720 }
721 }
722
723 let status = '';
724 let totalEpisodes = '';
725 let episodeDuration = '';
726 let season = '';
727 let seasonLink = '';
728 const quickInfo = $('div.anime-card__quick-info');
729 if (quickInfo.length > 0) {
730 const statusSpan = quickInfo.find('span[class*="status"]');
731 status = statusSpan.length > 0 ? statusSpan.text().trim() : '';
732
733 quickInfo.find('span.info-item').each((i, el) => {
734 const text = $(el).text();
735 if (text.toLowerCase().includes('episodes')) {
736 totalEpisodes = text.trim();
737 } else if (text.includes('min') || text.includes('menit')) {
738 episodeDuration = text.trim();
739 }
740 });
741
742 const seasonA = quickInfo.find('span.season a');
743 if (seasonA.length > 0) {
744 season = seasonA.text().trim();
745 seasonLink = seasonA.attr('href') || '';
746 }
747 }
748
749 let episodes = [];
750 const epSection = $('section.anime-card__episode-list-section');
751 if (epSection.length > 0) {
752 const epItems = epSection.find('div.episode-list-items');
753 if (epItems.length > 0) {
754 epItems.find('a.episode-item').each((i, el) => {
755 const aItem = $(el);
756 const link = aItem.attr('href') || '';
757 const titleSpan = aItem.find('span.ep-title');
758 const epTitle = titleSpan.length > 0 ? titleSpan.text().trim() : '';
759 const dateSpan = aItem.find('span.ep-date');
760 const epDate = dateSpan.length > 0 ? dateSpan.text().trim() : '';
761
762 episodes.push({
763 title: epTitle,
764 link,
765 date: epDate
766 });
767 });
768 }
769 }
770
771 let recommended = [];
772 const relatedDiv = $('div.related');
773 if (relatedDiv.length > 0) {
774 const scraper = this;
775 relatedDiv.find('a.as-anime-card').each((i, el) => {
776 recommended.push(scraper._parseAnimeCard($, el));
777 });
778 }
779 // Themesia layout fallback
780 if (!poster) {
781 poster = $('.thumb img, .single-info .thumb img, .info-left img, .infox img').first().attr('src') || '';
782 }
783 if (!title) {
784 title = $('h1.entry-title, h1').first().text().trim();
785 }
786 if (Object.keys(details).length === 0) {
787 $('.spe span').each((i, el) => {
788 const b = $(el).find('b').text().replace(':', '').trim();
789 const full = $(el).text().replace($(el).find('b').text(), '').trim();
790 if (b) details[b] = full;
791 if (/status/i.test(b)) status = full;
792 if (/episode/i.test(b)) totalEpisodes = full;
793 if (/durasi|duration/i.test(b)) episodeDuration = full;
794 if (/tipe|type/i.test(b)) typeVal = full;
795 });
796 }
797 if (genres.length === 0) {
798 $('.genxed a, a[href*="/genres/"]').each((i, el) => {
799 const name = $(el).text().trim();
800 const link = $(el).attr('href') || '';
801 if (name) genres.push({ name, link });
802 });
803 }
804 if (!synopsis) {
805 synopsis = $('.desc, .entry-content, .sinopc').first().text().trim();
806 }
807 if (episodes.length === 0) {
808 $('.eplister ul li').each((i, el) => {
809 const a = $(el).find('a').first();
810 const link = a.attr('href') || '';
811 const epNum = $(el).find('.epl-num').text().trim();
812 const epTitle = $(el).find('.epl-title').text().trim() || a.text().trim();
813 const epDate = $(el).find('.epl-date').text().trim();
814 if (link) {
815 episodes.push({
816 episode: epNum,
817 title: epTitle,
818 link,
819 date: epDate
820 });
821 }
822 });
823 }
824
825 return {
826 title,
827 poster,
828 score,
829 type: typeVal,
830 trailer,
831 synopsis,
832 genres,
833 details,
834 status,
835 total_episodes: totalEpisodes,
836 episode_duration: episodeDuration,
837 season,
838 season_link: seasonLink,
839 episodes,
840 recommended_series: recommended
841 };
842 }
843
844 async getStreamingDetail(episodeUrlOrSlug) {
845 const url = episodeUrlOrSlug.startsWith('http') ? episodeUrlOrSlug : `${this.baseUrl}/${episodeUrlOrSlug}/`;
846 const $ = await this._getSoup(url);
847 if (!$) return {};
848
849 const titleH1 = $('h1.entry-title');
850 const title = titleH1.length > 0 ? titleH1.text().trim() : '';
851
852 let animeTitle = '';
853 let animeLink = '';
854 const breadcrumbs = $('nav.breadcrumbs');
855 if (breadcrumbs.length > 0) {
856 const links = breadcrumbs.find('a').filter((i, el) => $(el).attr('href'));
857 if (links.length >= 3) {
858 const lastLink = links.last();
859 animeTitle = lastLink.text().trim();
860 animeLink = lastLink.attr('href') || '';
861 } else if (links.length === 2) {
862 const lastLink = links.last();
863 animeTitle = lastLink.text().trim();
864 animeLink = lastLink.attr('href') || '';
865 }
866 }
867
868 let prevLink = null;
869 let nextLink = null;
870 let allEpsLink = null;
871 const naveps = $('div.naveps');
872 if (naveps.length > 0) {
873 naveps.find('div.nvs').each((i, el) => {
874 const nvs = $(el);
875 const aTag = nvs.find('a');
876 if (aTag.length > 0) {
877 const href = aTag.attr('href') || '';
878 const label = aTag.text().toLowerCase();
879 if (label.includes('prev')) {
880 prevLink = href;
881 } else if (label.includes('next')) {
882 nextLink = href;
883 } else if (label.includes('all') || label.includes('episode')) {
884 allEpsLink = href;
885 }
886 } else if (nvs.hasClass('nvsc')) {
887 const aTagC = nvs.find('a');
888 if (aTagC.length > 0) {
889 allEpsLink = aTagC.attr('href') || '';
890 }
891 }
892 });
893 }
894
895 let defaultVideoUrl = '';
896 const videoku = $('div#videoku');
897 if (videoku.length > 0) {
898 const iframe = videoku.find('iframe');
899 if (iframe.length > 0) {
900 defaultVideoUrl = iframe.attr('src') || iframe.attr('data-src') || '';
901 }
902 }
903
904 let videoServers = [];
905 const playerTabs = $('ul.player');
906 if (playerTabs.length > 0) {
907 playerTabs.find('li.serverplayer').each((i, el) => {
908 const li = $(el);
909 const serverName = li.text().trim();
910 const postId = li.attr('data-post') || '';
911 const serverType = li.attr('data-type') || '';
912 const nume = li.attr('data-nume') || '';
913 const isActive = li.hasClass('on');
914
915 videoServers.push({
916 server_name: serverName,
917 post_id: postId,
918 server_type: serverType,
919 nume: nume,
920 is_active: isActive
921 });
922 });
923 }
924
925 let downloadLinks = [];
926 const downloadArea = $('div#download_area');
927 if (downloadArea.length > 0) {
928 const arealinker = downloadArea.find('div#arealinker');
929 if (arealinker.length > 0) {
930 arealinker.find('div.listlink').each((i, el) => {
931 const listlink = $(el);
932 const spanTag = listlink.find('span');
933 const formatName = spanTag.length > 0 ? spanTag.text().trim() : 'Unknown';
934
935 let links = [];
936 listlink.find('a').each((j, aEl) => {
937 const aLink = $(aEl);
938 links.push({
939 label: aLink.text().trim(),
940 url: aLink.attr('href') || ''
941 });
942 });
943
944 downloadLinks.push({
945 format: formatName,
946 links
947 });
948 });
949 }
950 }
951
952 let episodeTerbaru = [];
953 const sidebar = $('#sidebar_right').length > 0 ? $('#sidebar_right') : $('body');
954 let sidebarLatestHeader = null;
955 sidebar.find('h3, h2').each((i, el) => {
956 if ($(el).text().includes('Episode Terbaru')) {
957 sidebarLatestHeader = $(el);
958 }
959 });
960 if (sidebarLatestHeader) {
961 const ul = $(sidebarLatestHeader).nextAll('ul.latestepisodes').first();
962 if (ul.length > 0) {
963 ul.find('li').each((i, el) => {
964 const li = $(el);
965 const aTag = li.find('a');
966 if (aTag.length > 0) {
967 const link = aTag.attr('href') || '';
968 const lefts = aTag.find('div.lefts');
969 const rights = aTag.find('div.rights');
970
971 const epTitle = lefts.length > 0 ? lefts.text().trim() : '';
972 const videoNum = rights.length > 0 ? rights.find('span.video').text().trim() : '';
973
974 episodeTerbaru.push({
975 title: epTitle,
976 link,
977 episode: videoNum
978 });
979 }
980 });
981 }
982 }
983
984 let seriesPopularSummer = [];
985 let sidebarPopHeader = null;
986 sidebar.find('h3, h2').each((i, el) => {
987 if ($(el).text().includes('Series Popular Summer')) {
988 sidebarPopHeader = $(el);
989 }
990 });
991 if (sidebarPopHeader) {
992 const relatedGrid = $(sidebarPopHeader).nextAll('div.related').first();
993 if (relatedGrid.length > 0) {
994 const scraper = this;
995 relatedGrid.find('a.as-anime-card').each((i, el) => {
996 seriesPopularSummer.push(scraper._parseAnimeCard($, el));
997 });
998 }
999 }
1000
1001 return {
1002 title,
1003 anime_title: animeTitle,
1004 anime_link: animeLink,
1005 prev_episode_link: prevLink,
1006 next_episode_link: nextLink,
1007 all_episodes_link: allEpsLink,
1008 default_video_url: defaultVideoUrl,
1009 video_servers: videoServers,
1010 download_links: downloadLinks,
1011 episode_terbaru_sidebar: episodeTerbaru,
1012 series_popular_summer_sidebar: seriesPopularSummer,
1013 nonce: this.lastNonce,
1014 ajax_url: this.lastAjaxUrl
1015 };
1016 }
1017
1018 async getVideoIframe(postId, nume, serverName, nonce = null, ajaxUrl = null) {
1019 const finalNonce = nonce || this.lastNonce;
1020 const finalAjaxUrl = ajaxUrl || this.lastAjaxUrl || `${this.baseUrl}/wp-admin/admin-ajax.php`;
1021
1022 if (!finalNonce) {
1023 throw new Error("Nonce is required. Scrape a streaming page first.");
1024 }
1025
1026 const body = new URLSearchParams();
1027 body.append('action', 'player_ajax');
1028 body.append('post', postId);
1029 body.append('nume', nume);
1030 body.append('serverName', serverName);
1031 body.append('nonce', finalNonce);
1032
1033 const headers = {
1034 ...this.headers,
1035 'Content-Type': 'application/x-www-form-urlencoded',
1036 'X-Requested-With': 'XMLHttpRequest',
1037 'Origin': this.baseUrl
1038 };
1039 if (this.lastUrl) {
1040 headers['Referer'] = this.lastUrl;
1041 }
1042
1043 try {
1044 const response = await fetch(finalAjaxUrl, {
1045 method: 'POST',
1046 headers,
1047 body
1048 });
1049 if (!response.ok) {
1050 throw new Error(`HTTP error! status: ${response.status}`);
1051 }
1052 const html = await response.text();
1053 const $ = cheerio.load(html);
1054 const iframe = $('iframe');
1055 if (iframe.length > 0) {
1056 return iframe.attr('src') || iframe.attr('data-src') || '';
1057 }
1058 return '';
1059 } catch (error) {
1060 console.error(`Error resolving video iframe for ${serverName}:`, error);
1061 return '';
1062 }
1063 }
1064}
1065
1066export default NontonAnimeIDScraper;
1067
1068// ─────────────────────────────────────────────────────────────────────
1069// Direct Execution / CLI Runner
1070// ─────────────────────────────────────────────────────────────────────
1071if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
1072 const scraper = new NontonAnimeIDScraper();
1073 const args = process.argv.slice(2);
1074
1075 ;(async () => {
1076 try {
1077 if (args.length === 0) {
1078 console.log('[+] Menjalankan default search: "one piece"...\n');
1079 const res = await scraper.searchAnime('one piece');
1080 console.log(JSON.stringify(res, null, 2));
1081 return;
1082 }
1083
1084 const cmd = args[0].toLowerCase();
1085
1086 if (cmd === 'search' || cmd === 'cari') {
1087 const query = args.slice(1).join(' ') || 'one piece';
1088 console.log(`[+] Mencari: "${query}"...`);
1089 const res = await scraper.searchAnime(query);
1090 console.log(JSON.stringify(res, null, 2));
1091 } else if (cmd === 'detail') {
1092 const url = args[1];
1093 console.log(`[+] Detail: ${url}`);
1094 const res = await scraper.getAnimeDetail(url);
1095 console.log(JSON.stringify(res, null, 2));
1096 } else if (cmd === 'stream' || cmd === 'episode') {
1097 const url = args[1];
1098 console.log(`[+] Stream: ${url}`);
1099 const res = await scraper.getStreamingDetail(url);
1100 console.log(JSON.stringify(res, null, 2));
1101 } else if (cmd === 'ongoing') {
1102 console.log('[+] Mengambil ongoing anime...');
1103 const res = await scraper.getOngoingList();
1104 console.log(JSON.stringify(res, null, 2));
1105 } else if (cmd === 'popular') {
1106 console.log('[+] Mengambil popular series...');
1107 const res = await scraper.getPopularSeries();
1108 console.log(JSON.stringify(res, null, 2));
1109 } else {
1110 const query = args.join(' ');
1111 console.log(`[+] Mencari: "${query}"...`);
1112 const res = await scraper.searchAnime(query);
1113 console.log(JSON.stringify(res, null, 2));
1114 }
1115 } catch (err) {
1116 console.error('Error:', err.message || err);
1117 process.exitCode = 1;
1118 }
1119 })();
1120}
1121
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.