AnimeKai Scrape

JavaScriptPublic
by zx-apiOct 23, 2026, 12:56 PMExpires: Never16 views
AnimeKai Scrape
Raw
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