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

Samehadaku Anime Multi-Mirror Scraper

Mengimplementasikan scraper Samehadaku tangguh dengan `got-scraping` dan `cheerio`, dilengkapi mekanisme multi-mirror failover otomatis serta bypass deteksi bot Chrome untuk pencarian anime, detail, dan rilis terbaru.

#scraper#anime#samehadaku#got-scraping#cheerio#nodejs
7
Raw
samehadaku.jsjavascript
592 lines
1
2/**
3 * @fileoverview Modul Scraper Samehadaku dengan Arsitektur Multi-Mirror Failover.
4 *
5 * Mengimplementasikan web scraper tangguh untuk situs anime Samehadaku menggunakan `got-scraping`.
6 * Mengadopsi impersonasi TLS dan HTTP/2 fingerprinting browser Chrome untuk melewati deteksi bot,
7 * didukung mekanisme failover otomatis ke daftar mirror aktif saat domain utama mengalami kendala,
8 * serta parser terpadu untuk pencarian anime, detail serial, rilis terbaru, dan batch download.
9 *
10 * @module scrapers/samehadaku
11 * @version 2.1.0
12 * @author kyiov
13 * @license MIT
14 */
15
16import { gotScraping } from 'got-scraping';
17import * as cheerio from 'cheerio';
18import dns from 'node:dns';
19
20// Prioritaskan resolusi IPv4 untuk mencegah hambatan routing
21dns.setDefaultResultOrder('ipv4first');
22
23/** @constant {string[]} Daftar domain mirror Samehadaku aktif untuk mekanisme failover */
24const SAMEHADAKU_MIRRORS = [
25 'https://v2.samehadaku.how',
26 'https://samehadaku.email',
27 'https://samehadaku.care',
28 'https://samehadaku.win'
29];
30
31/** @constant {string} Domain mirror fallback utama */
32const DEFAULT_BASE_URL = SAMEHADAKU_MIRRORS[0];
33
34/**
35 * Klien scraper Samehadaku dengan dukungan mirror failover dinamis.
36 */
37export class SamehadakuScraper {
38 /**
39 * Menginisialisasi klien scraper Samehadaku.
40 *
41 * @param {Object} [options={}] - Opsi konfigurasi scraper
42 * @param {string} [options.baseUrl] - Kustomisasi URL domain basis awal
43 * @param {number} [options.timeout=20000] - Batas waktu request dalam milidetik
44 */
45 constructor(options = {}) {
46 this.mirrors = options.baseUrl ? [options.baseUrl, ...SAMEHADAKU_MIRRORS] : [...SAMEHADAKU_MIRRORS];
47 this.baseUrl = this.mirrors[0] || DEFAULT_BASE_URL;
48 this.timeout = options.timeout || 20000;
49 }
50
51 async fetchHtml(targetUrl) {
52 const fullUrl = targetUrl.startsWith('http') ? targetUrl : `${this.baseUrl}${targetUrl.startsWith('/') ? '' : '/'}${targetUrl}`;
53
54 let lastError = null;
55 try {
56 const response = await gotScraping.get(fullUrl, {
57 headers: {
58 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
59 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
60 'accept-language': 'id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7'
61 },
62 timeout: { request: this.timeout }
63 });
64 return response.body;
65 } catch (err) {
66 lastError = err;
67 for (const mirror of this.mirrors) {
68 if (mirror === this.baseUrl) continue;
69 try {
70 const fallbackUrl = targetUrl.startsWith('http') ? targetUrl : `${mirror}${targetUrl.startsWith('/') ? '' : '/'}${targetUrl}`;
71 const res = await gotScraping.get(fallbackUrl, {
72 headers: {
73 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
74 },
75 timeout: { request: this.timeout }
76 });
77 this.baseUrl = mirror;
78 return res.body;
79 } catch (e) {
80 lastError = e;
81 }
82 }
83 }
84
85 throw lastError || new Error('Gagal menghubungi semua mirror Samehadaku.');
86 }
87
88 async search(query, options = {}) {
89 if (!query) throw new Error('Kata kunci pencarian anime tidak boleh kosong.');
90 const limit = options.limit || 15;
91 const searchUrl = `${this.baseUrl}/?s=${encodeURIComponent(query)}`;
92 const html = await this.fetchHtml(searchUrl);
93 const $ = cheerio.load(html);
94 const results = [];
95
96 $('article, .animpost, .animepost, .relat, .post-item').each((_, el) => {
97 if (results.length >= limit) return;
98
99 const linkEl = $(el).find('a').first();
100 const href = $(el).find('.title a, h2 a, a[rel="bookmark"]').attr('href') || linkEl.attr('href');
101 let title = $(el).find('.title, h2, .entry-title, .tt').first().text().trim() || linkEl.attr('title') || '';
102 const thumb = $(el).find('img').attr('src') || $(el).find('img').attr('data-src');
103 const score = $(el).find('.score, .rating, .rating-star').first().text().trim();
104 let type = $(el).find('.type, .type-anime, .status').first().text().trim();
105
106 title = title.split('\n')[0].replace(/\s+/g, ' ').trim();
107 type = type.split('\n')[0].replace(/\s+/g, ' ').trim();
108
109 if (title && href && !results.some((r) => r.url === href)) {
110 results.push({
111 title,
112 url: href.startsWith('http') ? href : `${this.baseUrl}${href}`,
113 thumbnail: thumb || null,
114 image: thumb || null,
115 score: score || null,
116 type: type || null,
117 });
118 }
119 });
120
121 return {
122 success: true,
123 query,
124 total_found: results.length,
125 data: results,
126 results
127 };
128 }
129
130 async latest(page = 1) {
131 const pageUrl = page > 1 ? `${this.baseUrl}/page/${page}` : this.baseUrl;
132 const html = await this.fetchHtml(pageUrl);
133 const $ = cheerio.load(html);
134 const episodes = [];
135
136 $('.post-show ul li, .lstepsiode li, article, .animpost').each((_, el) => {
137 const linkEl = $(el).find('a').first();
138 const href = linkEl.attr('href') || $(el).find('.entry-title a').attr('href');
139 const title = $(el).find('.entry-title, .title, h2').first().text().trim() || linkEl.attr('title');
140 const thumb = $(el).find('img').attr('src') || $(el).find('img').attr('data-src');
141 const date = $(el).find('.date, .posted-on, time').first().text().trim();
142
143 if (title && href && !episodes.some((e) => e.url === href)) {
144 episodes.push({
145 title: title.replace(/\s+/g, ' ').trim(),
146 url: href.startsWith('http') ? href : `${this.baseUrl}${href}`,
147 thumbnail: thumb || null,
148 image: thumb || null,
149 date: date || null,
150 });
151 }
152 });
153
154 return {
155 success: true,
156 page,
157 total_found: episodes.length,
158 data: episodes,
159 results: episodes
160 };
161 }
162
163 async anime(animeUrl) {
164 let targetUrl = animeUrl;
165 if (!targetUrl.startsWith('http')) {
166 const sRes = await this.search(targetUrl, { limit: 1 });
167 if (!sRes.data?.length) throw new Error(`Anime "${targetUrl}" tidak ditemukan.`);
168 targetUrl = sRes.data[0].url;
169 }
170
171 const html = await this.fetchHtml(targetUrl);
172 const $ = cheerio.load(html);
173
174 const title = $('h1.entry-title, h1').first().text().trim();
175 const poster = $('.thumb img, .poster img, .entry-content img').first().attr('src') || null;
176 const synopsis = $('.desc, .entry-content p, .synopsis, .mindes').first().text().trim();
177 const score = $('.rating-value, .score, .rating').first().text().trim();
178
179 const metadata = {};
180 $('.spe span, .info-content span').each((_, el) => {
181 const txt = $(el).text().trim();
182 if (txt.includes(':')) {
183 const parts = txt.split(':');
184 const key = parts[0].trim().toLowerCase().replace(/\s+/g, '_');
185 const val = parts.slice(1).join(':').trim();
186 if (key && val) metadata[key] = val;
187 }
188 });
189
190 const episodes = [];
191 $('.lstepsiode li, .episodelist li, ul.episode-list li, a[href*="-episode-"]').each((_, el) => {
192 const epHref = $(el).is('a') ? $(el).attr('href') : $(el).find('a').first().attr('href');
193 let epTitle = $(el).find('.lchx, .title, .epsleft a').first().text().trim() || $(el).find('a').first().text().trim() || $(el).text().trim();
194
195 if (!epHref || !epHref.includes('-episode-')) return;
196 if (
197 epHref.includes('facebook.com') ||
198 epHref.includes('twitter.com') ||
199 epHref.includes('whatsapp.com') ||
200 epHref.includes('t.me') ||
201 epHref.includes('pinterest.com')
202 ) {
203 return;
204 }
205
206 epTitle = epTitle.replace(/\s+/g, ' ').trim();
207 const halfLen = Math.floor(epTitle.length / 2);
208 if (halfLen > 3 && epTitle.slice(0, halfLen) === epTitle.slice(halfLen)) {
209 epTitle = epTitle.slice(0, halfLen);
210 }
211
212 if (!epTitle) {
213 const slug = epHref.split('/').filter(Boolean).pop() || '';
214 epTitle = slug.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
215 }
216
217 const epDate = $(el).find('.date, .epsright').first().text().trim();
218
219 if (!episodes.some((e) => e.url === epHref)) {
220 episodes.push({
221 title: epTitle,
222 url: epHref.startsWith('http') ? epHref : `${this.baseUrl}${epHref}`,
223 date: epDate || null,
224 });
225 }
226 });
227
228 return {
229 success: true,
230 title: title || 'Anime Samehadaku',
231 url: targetUrl,
232 poster,
233 image: poster,
234 score: score || metadata.score || null,
235 status: metadata.status || null,
236 total_episodes: episodes.length,
237 synopsis,
238 metadata,
239 episodes,
240 };
241 }
242
243 async episode(episodeUrl) {
244 const html = await this.fetchHtml(episodeUrl);
245 const $ = cheerio.load(html);
246
247 const title = $('h1.entry-title, h1').first().text().trim();
248 const date = $('.entry-date, time, .published').first().text().trim();
249
250 const streamServers = [];
251 $('.east_player_option, #server-list li, ul.server-list li').each((_, el) => {
252 const sName = $(el).find('span').text().trim() || $(el).text().trim();
253 const sPost = $(el).attr('data-post');
254 const sNume = $(el).attr('data-nume');
255 const sType = $(el).attr('data-type');
256 if (sName && sPost) {
257 streamServers.push({
258 server: sName,
259 post_id: sPost,
260 nume: sNume,
261 type: sType,
262 });
263 }
264 });
265
266 const downloadSections = {};
267 const allDownloadUrls = [];
268 let currentFormat = 'MKV';
269
270 $('.download-eps, .box-download, .download-area, div.links_table').find('h4, b, strong, p, ul, li').each((_, el) => {
271 const text = $(el).text().replace(/\s+/g, ' ').trim();
272 const tagName = el.tagName?.toLowerCase();
273
274 if ((tagName === 'h4' || tagName === 'b' || tagName === 'strong') && (text === 'MKV' || text === 'MP4' || text.includes('x265') || text.includes('HEVC'))) {
275 if (text.includes('x265')) currentFormat = 'x265 [HEVC]';
276 else if (text === 'MP4') currentFormat = 'MP4';
277 else if (text === 'MKV') currentFormat = 'MKV';
278 if (!downloadSections[currentFormat]) downloadSections[currentFormat] = [];
279 return;
280 }
281
282 if (!downloadSections[currentFormat]) downloadSections[currentFormat] = [];
283
284 const resMatch = text.match(/(360p|480p|720p|1080p|MP4HD|FULLHD)/i);
285 if (resMatch && $(el).find('a').length > 0) {
286 const resolution = resMatch[1];
287 const servers = [];
288
289 $(el).find('a').each((_, a) => {
290 const linkHref = $(a).attr('href');
291 const linkName = $(a).text().trim();
292 if (linkHref && (linkHref.startsWith('http') || linkHref.startsWith('/')) && linkName.length > 0) {
293 servers.push({
294 server: linkName,
295 url: linkHref,
296 });
297 allDownloadUrls.push(linkHref);
298 }
299 });
300
301 if (servers.length > 0) {
302 const existing = downloadSections[currentFormat].find((r) => r.resolution.toLowerCase() === resolution.toLowerCase());
303 if (!existing) {
304 downloadSections[currentFormat].push({
305 resolution,
306 mirrors_count: servers.length,
307 mirrors: servers,
308 });
309 }
310 }
311 }
312 });
313
314 const iframeSrc = $('iframe').first().attr('src') || '';
315
316 return {
317 success: true,
318 title: title || 'Episode Samehadaku',
319 url: episodeUrl,
320 date: date || null,
321 iframe: iframeSrc,
322 streaming_servers_count: streamServers.length,
323 streaming_servers: streamServers,
324 downloads: downloadSections,
325 downloadLinks: [...new Set(allDownloadUrls)]
326 };
327 }
328
329 async schedule(day = 'all') {
330 const DAY_MAP = {
331 'senin': 'monday',
332 'monday': 'monday',
333 'selasa': 'tuesday',
334 'tuesday': 'tuesday',
335 'rabu': 'wednesday',
336 'wednesday': 'wednesday',
337 'kamis': 'thursday',
338 'thursday': 'thursday',
339 'jumat': 'friday',
340 'friday': 'friday',
341 'sabtu': 'saturday',
342 'saturday': 'saturday',
343 'minggu': 'sunday',
344 'sunday': 'sunday',
345 };
346
347 const targetDay = String(day || 'all').toLowerCase().trim();
348 if (targetDay !== 'all' && DAY_MAP[targetDay]) {
349 const enDay = DAY_MAP[targetDay];
350 const apiUrl = `${this.baseUrl}/wp-json/custom/v1/all-schedule?perpage=50&day=${enDay}`;
351 const res = await this.fetchHtml(apiUrl);
352 const rawText = cheerio.load(res)('pre').text() || cheerio.load(res)('body').text() || res;
353 const list = typeof rawText === 'object' ? rawText : JSON.parse(rawText.trim());
354
355 const formatted = (list || []).map((item) => ({
356 title: item.title,
357 url: item.url,
358 score: item.east_score || null,
359 type: item.east_type || 'TV',
360 genres: item.genre ? item.genre.split(',').map((g) => g.trim()) : [],
361 air_time: item.east_time || null,
362 day: targetDay.toUpperCase(),
363 thumbnail: item.featured_img_src || null,
364 synopsis: item.content || null,
365 }));
366
367 return {
368 success: true,
369 day: targetDay,
370 total_anime: formatted.length,
371 data: formatted,
372 };
373 }
374
375 const scheduleByDays = {};
376 for (const [idDay, enDay] of Object.entries(DAY_MAP)) {
377 if (['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'].includes(idDay)) continue;
378 try {
379 const apiUrl = `${this.baseUrl}/wp-json/custom/v1/all-schedule?perpage=50&day=${enDay}`;
380 const res = await this.fetchHtml(apiUrl);
381 const rawText = cheerio.load(res)('pre').text() || cheerio.load(res)('body').text() || res;
382 const list = typeof rawText === 'object' ? rawText : JSON.parse(rawText.trim());
383
384 scheduleByDays[idDay] = (list || []).map((item) => ({
385 title: item.title,
386 url: item.url,
387 score: item.east_score || null,
388 type: item.east_type || 'TV',
389 genres: item.genre ? item.genre.split(',').map((g) => g.trim()) : [],
390 air_time: item.east_time || null,
391 thumbnail: item.featured_img_src || null,
392 }));
393 } catch (_) {
394 scheduleByDays[idDay] = [];
395 }
396 }
397
398 return {
399 success: true,
400 schedule: scheduleByDays,
401 };
402 }
403
404 async genres() {
405 const html = await this.fetchHtml(`${this.baseUrl}/daftar-anime-2/`);
406 const $ = cheerio.load(html);
407 const genres = [];
408
409 $('a[href*="/genre/"]').each((_, el) => {
410 const href = $(el).attr('href');
411 const name = $(el).text().trim();
412 if (name && !genres.some((g) => g.name.toLowerCase() === name.toLowerCase())) {
413 const slug = href.replace(/^.*\/genre\//, '').replace(/\/$/, '');
414 genres.push({
415 name,
416 slug,
417 url: href.startsWith('http') ? href : `${this.baseUrl}${href}`,
418 });
419 }
420 });
421
422 return {
423 success: true,
424 total_genres: genres.length,
425 genres,
426 };
427 }
428
429 async batches(page = 1) {
430 const targetUrl = page > 1 ? `${this.baseUrl}/daftar-batch/page/${page}/` : `${this.baseUrl}/daftar-batch/`;
431 const html = await this.fetchHtml(targetUrl);
432 const $ = cheerio.load(html);
433 const batches = [];
434
435 $('.animpost, .animepost, article').each((_, el) => {
436 const linkEl = $(el).find('a').first();
437 const href = linkEl.attr('href');
438 let title = $(el).find('.title, h2, .entry-title').first().text().trim() || linkEl.attr('title') || '';
439 const thumb = $(el).find('img').attr('src') || $(el).find('img').attr('data-src');
440 const score = $(el).find('.score').text().trim();
441 const type = $(el).find('.type').text().trim();
442
443 title = title.replace(/\s+/g, ' ').trim();
444 const matchTitle = title.match(/([^\n]+\[BATCH\])/i) || [title];
445 const cleanTitle = matchTitle[0].replace(/^(TV|Movie)\s*\d+(\.\d+)?/i, '').trim();
446
447 if (href && href.includes('/batch/') && !batches.some((b) => b.url === href)) {
448 batches.push({
449 title: cleanTitle || title,
450 url: href.startsWith('http') ? href : `${this.baseUrl}${href}`,
451 thumbnail: thumb || null,
452 score: score || null,
453 type: type || 'Batch',
454 });
455 }
456 });
457
458 return {
459 success: true,
460 page: Number(page),
461 total_found: batches.length,
462 data: batches,
463 };
464 }
465
466 async batch(batchUrl) {
467 const html = await this.fetchHtml(batchUrl);
468 const $ = cheerio.load(html);
469
470 const title = $('h1.entry-title, h1').first().text().trim();
471 const poster = $('.thumb img, .entry-content img').first().attr('src') || null;
472 const synopsis = $('.desc, .entry-content p, .synopsis, .mindes').first().text().trim();
473
474 const downloadSections = {};
475 let currentFormat = 'MKV';
476
477 $('.download-eps, .box-download, .download-area, div.links_table').find('h4, b, strong, p, ul, li').each((_, el) => {
478 const text = $(el).text().replace(/\s+/g, ' ').trim();
479 const tagName = el.tagName?.toLowerCase();
480
481 if ((tagName === 'h4' || tagName === 'b' || tagName === 'strong') && (text === 'MKV' || text === 'MP4' || text.includes('x265') || text.includes('HEVC'))) {
482 if (text.includes('x265')) currentFormat = 'x265 [HEVC]';
483 else if (text === 'MP4') currentFormat = 'MP4';
484 else if (text === 'MKV') currentFormat = 'MKV';
485 if (!downloadSections[currentFormat]) downloadSections[currentFormat] = [];
486 return;
487 }
488
489 if (!downloadSections[currentFormat]) downloadSections[currentFormat] = [];
490
491 const resMatch = text.match(/(360p|480p|720p|1080p|MP4HD|FULLHD)/i);
492 if (resMatch && $(el).find('a').length > 0) {
493 const resolution = resMatch[1];
494 const servers = [];
495
496 $(el).find('a').each((_, a) => {
497 const linkHref = $(a).attr('href');
498 const linkName = $(a).text().trim();
499 if (linkHref && linkName.length > 0) {
500 servers.push({
501 server: linkName,
502 url: linkHref,
503 });
504 }
505 });
506
507 if (servers.length > 0) {
508 const existing = downloadSections[currentFormat].find((r) => r.resolution.toLowerCase() === resolution.toLowerCase());
509 if (!existing) {
510 downloadSections[currentFormat].push({
511 resolution,
512 mirrors_count: servers.length,
513 mirrors: servers,
514 });
515 }
516 }
517 }
518 });
519
520 return {
521 success: true,
522 title: title || 'Batch Samehadaku',
523 url: batchUrl,
524 poster,
525 synopsis: synopsis || null,
526 downloads: downloadSections,
527 };
528 }
529}
530
531const defaultClient = new SamehadakuScraper();
532
533export async function samehadakuSearch(keyword) {
534 try {
535 const res = await defaultClient.search(keyword);
536 return {
537 status: true,
538 results: (res.data || []).map((item) => ({
539 id: item.url,
540 title: item.title,
541 url: item.url,
542 image: item.thumbnail || '',
543 genre: item.type || '-',
544 type: item.type || 'TV',
545 score: item.score || '-'
546 }))
547 };
548 } catch (e) {
549 return { status: false, error: e.message };
550 }
551}
552
553export async function samehadakuLatest(page = 1) {
554 return await defaultClient.latest(page);
555}
556
557export async function samehadakuDetail(url) {
558 return await defaultClient.anime(url);
559}
560
561export async function samehadakuEpisode(url) {
562 return await defaultClient.episode(url);
563}
564
565export async function samehadakuSchedule(day = 'all') {
566 return await defaultClient.schedule(day);
567}
568
569export async function samehadakuBatches(page = 1) {
570 return await defaultClient.batches(page);
571}
572
573export async function samehadakuBatch(url) {
574 return await defaultClient.batch(url);
575}
576
577export async function samehadakuGenres() {
578 return await defaultClient.genres();
579}
580
581import { pathToFileURL } from 'node:url';
582
583// Contoh pemanggilan langsung jika file dieksekusi secara mandiri
584if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
585 try {
586 const res = await samehadakuSearch('one piece');
587 console.log(JSON.stringify(res, null, 2));
588 } catch (err) {
589 console.error('Galat Samehadaku:', err.message || err);
590 }
591}
592
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.