AnimeKai Scrape
JavaScriptPublicby zx-apiOct 23, 2026, 12:56 PMExpires: Never16 views
1/**
2 * Name Scrape: AnimeKai
3 * Credit By Zx
4 * Description: AnimeKai Search, Get Detail, home,...
5 * Sumber Kode: https://whatsapp.com/channel/0029VbDLqe7EquiSF4STU13o
6 * Note: Kalau Mau di sher lagi harap jangan hapus credit atau sumber saluran
7 * Larangan: Jangan Rename Scrape Kalau Gak Ada. Credit
8 *
9**/
10
11
12const axios = require("axios");
13const cheerio = require("cheerio");
14
15const BASE_URL = "https://animekai.com.ro";
16
17const HEADERS = {
18 "User-Agent":
19 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
20 Accept:
21 "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
22 "Accept-Language": "id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7",
23 Referer: BASE_URL,
24 "Upgrade-Insecure-Requests": "1",
25};
26
27const http = axios.create({
28 baseURL: BASE_URL,
29 headers: HEADERS,
30 timeout: 30000,
31});
32
33const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
34
35async function fetchHTML(path, params = {}) {
36 try {
37 const { data } = await http.get(path, { params });
38 return cheerio.load(data);
39 } catch (err) {
40 if (err.response?.status === 403) {
41 throw new Error(
42 "403 Forbidden: kemungkinan terkena proteksi Cloudflare. Coba gunakan cookie, header browser asli, atau Puppeteer/Playwright."
43 );
44 }
45 throw err;
46 }
47}
48
49const clean = (text) => (text || "").replace(/\s+/g, " ").trim();
50
51const abs = (url) => {
52 try {
53 return url ? new URL(url, BASE_URL).toString() : null;
54 } catch {
55 return null;
56 }
57};
58
59const decodeBase64 = (b64) => {
60 try {
61 return Buffer.from(b64 || "", "base64").toString("utf8");
62 } catch {
63 return "";
64 }
65};
66
67const extractIframeSrc = (html) => {
68 return html?.match(/<iframe[^>]+src=["']([^"']+)["']/i)?.[1] || null;
69};
70
71function parseAItem($, el) {
72 const $el = $(el);
73
74 const titleEl = $el.find(".title").first();
75
76 const href =
77 titleEl.attr("href") ||
78 $el.find("a.poster").attr("href") ||
79 $el.attr("href") ||
80 $el.find("a").attr("href");
81
82 return {
83 title: clean(titleEl.text()) || clean($el.attr("title")),
84 japaneseTitle: clean(
85 titleEl.attr("data-jp") || $el.find("[data-jp]").attr("data-jp")
86 ),
87 url: abs(href),
88 poster: abs(
89 $el.find("img").attr("src") || $el.find("img").attr("data-src")
90 ),
91 subEpisode: clean($el.find(".sub").text()),
92 dubEpisode: clean($el.find(".dub").text()),
93 type: clean($el.find(".info span b, .typez").first().text()),
94 };
95}
96
97function parseBsx($, el) {
98 const $el = $(el);
99
100 const a = $el.find("a").first();
101 const href = a.attr("href") || $el.attr("href");
102
103 const title = clean(
104 a.attr("title") ||
105 $el.find(".tt span a").text() ||
106 $el.find(".tt span").text() ||
107 $el.find(".inf span a").text() ||
108 $el.find(".title").text()
109 );
110
111 return {
112 title,
113 japaneseTitle: clean($el.find("[data-jp]").attr("data-jp")),
114 url: abs(href),
115 poster: abs(
116 $el.find("img").attr("src") || $el.find("img").attr("data-src")
117 ),
118 type: clean($el.find(".typez").text()),
119 subEpisode: clean($el.find(".sub").text()),
120 dubEpisode: clean($el.find(".dub").text()),
121 };
122}
123
124function parseEpisodeList($) {
125 const episodes = [];
126
127 $(".ak-cl-eps li").each((_, el) => {
128 const $el = $(el);
129 const a = $el.find("a").first();
130
131 episodes.push({
132 number: clean($el.attr("data-num") || $el.find(".gnum").text()),
133 title: clean(a.attr("title") || $el.find(".ttl").text()),
134 url: abs(a.attr("href")),
135 date: clean($el.find(".date").text()),
136 thumbnail: abs(
137 $el.find("img").attr("src") || $el.find("img").attr("data-src")
138 ),
139 isCurrent: $el.hasClass("active"),
140 });
141 });
142
143 if (episodes.length) return episodes;
144
145 $(".eplister ul li, .episodelist ul li, .episodelist li").each((_, el) => {
146 const $el = $(el);
147 const a = $el.find("a").first();
148
149 episodes.push({
150 number: clean($el.find(".epl-num, .num").text()),
151 title: clean(
152 $el.find(".epl-title, .ttl, .title").text() || a.attr("title")
153 ),
154 url: abs(a.attr("href")),
155 date: clean($el.find(".epl-date, .date").text()),
156 });
157 });
158
159 if (episodes.length) return episodes;
160
161 $('a[href*="episode-"]').each((_, el) => {
162 const $el = $(el);
163 const href = $el.attr("href");
164 const title = clean($el.attr("title") || $el.text());
165 const url = abs(href);
166
167 if (!href || !title || !url) return;
168
169 if (!episodes.some((ep) => ep.url === url)) {
170 episodes.push({
171 number: title.match(/\d+/)?.[0] || "",
172 title,
173 url,
174 });
175 }
176 });
177
178 return episodes;
179}
180
181function parseSeriesInfo($) {
182 const info = {};
183
184 $(".ak-cl-spe span, .bigcontent .infox .spe span").each((_, el) => {
185 const text = clean($(el).text());
186 const match = text.match(/^([^:]+):\s*(.+)$/);
187
188 if (match) {
189 const key = match[1].trim().toLowerCase().replace(/\s+/g, "_");
190 info[key] = match[2].trim();
191 }
192 });
193
194 const genres = $(".ak-cl-genres a, .bigcontent .infox .genxed a")
195 .map((_, el) => ({
196 name: clean($(el).text()),
197 url: abs($(el).attr("href")),
198 }))
199 .get();
200
201 return {
202 title: clean(
203 $(".ak-cl-title").text() ||
204 $(".bigcontent .infox h1").text() ||
205 $("h1.entry-title").text()
206 ),
207 japaneseTitle: clean(
208 $(".ak-cl-alt").text() || $(".bigcontent .infox .alt").text()
209 ),
210 url: abs(
211 $(".ak-cl-title a").attr("href") || $(".ak-cl-poster").attr("href")
212 ),
213 poster: abs(
214 $(".ak-cl-poster img").attr("src") ||
215 $(".bigcontent .thum img").attr("src") ||
216 $('meta[property="og:image"]').attr("content")
217 ),
218 type: info.type || clean($(".ak-cl-type").text()),
219 status: info.status,
220 released: info.released,
221 season: info.season,
222 duration: info.duration,
223 totalEpisodes: info.episodes,
224 score: clean($(".bigcontent .rt .rating").text()) || undefined,
225 genres,
226 description: clean(
227 $(".ak-cl-desc").text() ||
228 $(".entry-content p").first().text() ||
229 $('meta[name="description"]').attr("content")
230 ),
231 rawInfo: info,
232 };
233}
234
235function parseFeatured($) {
236 return $("#featuredSwiper .swiper-slide")
237 .map((_, el) => {
238 const $el = $(el);
239
240 const background =
241 $el.attr("style")?.match(/url\(['"]?(.*?)['"]?\)/)?.[1] || "";
242
243 const meta = {};
244
245 $el.find(".mics > div").each((_, item) => {
246 const label = clean($(item).find("div").first().text()).toLowerCase();
247 const value = clean($(item).find("span").text());
248
249 if (label) meta[label] = value;
250 });
251
252 return {
253 title: clean($el.find(".title").text()),
254 japaneseTitle: clean($el.find(".title").attr("data-jp")),
255 background: abs(background),
256 watchUrl: abs($el.find(".watch-btn").attr("href")),
257 detailUrl: abs($el.find(".detail-btn").attr("href")),
258 subEpisode: clean(
259 $el.find(".sub a").text() || $el.find(".sub").text()
260 ),
261 type: clean($el.find(".info span b").text()),
262 description: clean($el.find(".desc").text()),
263 meta,
264 };
265 })
266 .get();
267}
268
269function parseSchedule($) {
270 return $("#schedule-list li a")
271 .map((_, el) => {
272 const $el = $(el);
273
274 return {
275 time: clean($el.find(".time").text()),
276 title: clean($el.find(".title").text()),
277 japaneseTitle: clean($el.find(".title").attr("data-jp")),
278 episode: clean($el.find("span").last().text()),
279 url: abs($el.attr("href")),
280 status: clean($el.attr("class")),
281 };
282 })
283 .get();
284}
285
286async function getHome() {
287 const $ = await fetchHTML("/");
288
289 const featured = parseFeatured($);
290
291 const latestUpdates = $("#latest-updates .aitem-wrapper .aitem")
292 .map((_, el) => parseAItem($, el))
293 .get();
294
295 const sections = $("#alistSwiper .swiper-slide")
296 .map((_, el) => {
297 const $el = $(el);
298
299 return {
300 section: clean($el.find(".stitle").text()),
301 viewAllUrl: abs($el.find(".more-btn").attr("href")),
302 items: $el
303 .find(".aitem")
304 .map((_, item) => parseAItem($, item))
305 .get(),
306 };
307 })
308 .get();
309
310 const trending = $("#trending-anime .aitem")
311 .map((_, el) => {
312 const $el = $(el);
313
314 const background =
315 $el.attr("style")?.match(/url\(['"]?(.*?)['"]?\)/)?.[1] || "";
316
317 return {
318 rank: clean($el.find(".num").text()),
319 title: clean($el.find(".title").text()),
320 japaneseTitle: clean($el.find(".title").attr("data-jp")),
321 url: abs($el.find(".title").attr("href")),
322 poster: abs(background),
323 subEpisode: clean(
324 $el.find(".sub a").text() || $el.find(".sub").text()
325 ),
326 dubEpisode: clean(
327 $el.find(".dub a").text() || $el.find(".dub").text()
328 ),
329 type: clean($el.find(".info span b").text()),
330 };
331 })
332 .get();
333
334 const schedule = parseSchedule($);
335
336 return {
337 featured,
338 latestUpdates,
339 sections,
340 trending,
341 schedule,
342 };
343}
344
345async function searchAnime(query) {
346 const $ = await fetchHTML("/", { s: query });
347
348 const results = [];
349
350 $(".aitem").each((_, el) => {
351 results.push(parseAItem($, el));
352 });
353
354 if (results.length) {
355 return { query, total: results.length, results };
356 }
357
358 $(".bs .bsx, .bsx").each((_, el) => {
359 results.push(parseBsx($, el));
360 });
361
362 if (results.length) {
363 return { query, total: results.length, results };
364 }
365
366 $(".listupd .lexa, .lexa").each((_, el) => {
367 const $el = $(el);
368 const a = $el.find("a").first();
369
370 results.push({
371 title: clean(
372 $el.find(".dtl h2 a").text() ||
373 $el.find(".dtl h2").text() ||
374 a.attr("title")
375 ),
376 url: abs(a.attr("href")),
377 poster: abs(
378 $el.find("img").attr("src") || $el.find("img").attr("data-src")
379 ),
380 });
381 });
382
383 if (results.length) {
384 return { query, total: results.length, results };
385 }
386
387 $('main a[href*="/anime/"], main a[href*="episode-"]').each((_, el) => {
388 const $el = $(el);
389 const title = clean($el.attr("title") || $el.text());
390 const url = abs($el.attr("href"));
391
392 if (title && url && !results.some((item) => item.url === url)) {
393 results.push({ title, url });
394 }
395 });
396
397 return { query, total: results.length, results };
398}
399
400async function getAnimeDetail(animeUrl) {
401 const url = abs(animeUrl);
402
403 if (!url) {
404 throw new Error("URL anime tidak valid");
405 }
406
407 const $ = await fetchHTML(url);
408
409 const series = parseSeriesInfo($);
410 const episodes = parseEpisodeList($);
411
412 return {
413 ...series,
414 episodes,
415 };
416}
417
418async function getEpisode(episodeUrl) {
419 const url = abs(episodeUrl);
420
421 if (!url) {
422 throw new Error("URL episode tidak valid");
423 }
424
425 const $ = await fetchHTML(url);
426
427 const title = clean($(".entry-title").text());
428
429 const series = parseSeriesInfo($);
430
431 const episodeNumber = clean(
432 $('meta[itemprop="episodeNumber"]').attr("content") ||
433 $(".lm .epx").eq(1).text().replace(/episode/i, "")
434 );
435
436 const lang = clean($(".lg").text());
437
438 const released = clean($(".year .updated").text());
439
440 const mainEmbed = abs($("#pembed iframe").attr("src"));
441
442 const servers = $(".ak-srv-btn")
443 .map((_, el) => {
444 const $el = $(el);
445
446 const embedHtml = decodeBase64($el.attr("data-value"));
447 const embedSrc = extractIframeSrc(embedHtml);
448
449 return {
450 name: clean($el.text()),
451 lang: clean($el.attr("data-lang")),
452 active: $el.hasClass("active"),
453 embedUrl: abs(embedSrc),
454 embedHtml,
455 };
456 })
457 .get();
458
459 const prev = abs($('.naveps a[rel="prev"]').attr("href"));
460 const next = abs($(".naveps .nvs:last-child a").attr("href"));
461 const allEpisodes = abs($(".naveps .nvsc a").attr("href"));
462
463 const episodes = parseEpisodeList($);
464
465 return {
466 title,
467 series,
468 episodeNumber,
469 lang,
470 released,
471 player: {
472 embedUrl: mainEmbed,
473 servers,
474 },
475 navigation: {
476 prev,
477 next,
478 allEpisodes,
479 },
480 episodes,
481 warning: clean($(".warning").text()),
482 };
483}
484
485module.exports = {
486 getHome,
487 searchAnime,
488 getAnimeDetail,
489 getEpisode,
490};
491
492/**
493 * Contoh pemakaian langsung
494 * Jalankan: node animekai-scraper.js
495 */
496if (require.main === module) {
497 (async () => {
498 try {
499 console.log("=== HOME ===");
500 const home = await getHome();
501
502 console.log("Featured:", home.featured.length);
503 console.log("Latest updates:", home.latestUpdates.length);
504 console.log("Trending:", home.trending.length);
505 console.log("Schedule:", home.schedule.length);
506
507 await sleep(800);
508
509 console.log("\n=== SEARCH ===");
510 const search = await searchAnime("jojo");
511
512 console.log("Total:", search.total);
513 console.log(search.results.slice(0, 3));
514
515 if (search.results[0]?.url) {
516 await sleep(800);
517
518 console.log("\n=== DETAIL ===");
519 const detail = await getAnimeDetail(search.results[0].url);
520
521 console.log(detail.title);
522 console.log(detail.status);
523 console.log(detail.totalEpisodes);
524 console.log(detail.genres);
525 console.log("Episodes found:", detail.episodes.length);
526 }
527
528 await sleep(800);
529
530 console.log("\n=== EPISODE ===");
531 const episode = await getEpisode(
532 "https://animekai.com.ro/steel-ball-run-jojos-bizarre-adventure-2nd-stage-dub-episode-5-english-subbed/"
533 );
534
535 console.log(episode.title);
536 console.log("Episode:", episode.episodeNumber);
537 console.log("Main embed:", episode.player.embedUrl);
538 console.log("Servers:", episode.player.servers.length);
539 console.log(episode.player.servers);
540 } catch (err) {
541 console.error("ERROR:", err.message);
542 }
543 })();
544}544 lines·13,859 chars·13.5 KB
Wwrap·Ffullscreen