/** * Name Scrape: AnimeKai * Credit By Zx * Description: AnimeKai Search, Get Detail, home,... * Sumber Kode: https://whatsapp.com/channel/0029VbDLqe7EquiSF4STU13o * Note: Kalau Mau di sher lagi harap jangan hapus credit atau sumber saluran * Larangan: Jangan Rename Scrape Kalau Gak Ada. Credit * **/ const axios = require("axios"); const cheerio = require("cheerio"); const BASE_URL = "https://animekai.com.ro"; const HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7", Referer: BASE_URL, "Upgrade-Insecure-Requests": "1", }; const http = axios.create({ baseURL: BASE_URL, headers: HEADERS, timeout: 30000, }); const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function fetchHTML(path, params = {}) { try { const { data } = await http.get(path, { params }); return cheerio.load(data); } catch (err) { if (err.response?.status === 403) { throw new Error( "403 Forbidden: kemungkinan terkena proteksi Cloudflare. Coba gunakan cookie, header browser asli, atau Puppeteer/Playwright." ); } throw err; } } const clean = (text) => (text || "").replace(/\s+/g, " ").trim(); const abs = (url) => { try { return url ? new URL(url, BASE_URL).toString() : null; } catch { return null; } }; const decodeBase64 = (b64) => { try { return Buffer.from(b64 || "", "base64").toString("utf8"); } catch { return ""; } }; const extractIframeSrc = (html) => { return html?.match(/]+src=["']([^"']+)["']/i)?.[1] || null; }; function parseAItem($, el) { const $el = $(el); const titleEl = $el.find(".title").first(); const href = titleEl.attr("href") || $el.find("a.poster").attr("href") || $el.attr("href") || $el.find("a").attr("href"); return { title: clean(titleEl.text()) || clean($el.attr("title")), japaneseTitle: clean( titleEl.attr("data-jp") || $el.find("[data-jp]").attr("data-jp") ), url: abs(href), poster: abs( $el.find("img").attr("src") || $el.find("img").attr("data-src") ), subEpisode: clean($el.find(".sub").text()), dubEpisode: clean($el.find(".dub").text()), type: clean($el.find(".info span b, .typez").first().text()), }; } function parseBsx($, el) { const $el = $(el); const a = $el.find("a").first(); const href = a.attr("href") || $el.attr("href"); const title = clean( a.attr("title") || $el.find(".tt span a").text() || $el.find(".tt span").text() || $el.find(".inf span a").text() || $el.find(".title").text() ); return { title, japaneseTitle: clean($el.find("[data-jp]").attr("data-jp")), url: abs(href), poster: abs( $el.find("img").attr("src") || $el.find("img").attr("data-src") ), type: clean($el.find(".typez").text()), subEpisode: clean($el.find(".sub").text()), dubEpisode: clean($el.find(".dub").text()), }; } function parseEpisodeList($) { const episodes = []; $(".ak-cl-eps li").each((_, el) => { const $el = $(el); const a = $el.find("a").first(); episodes.push({ number: clean($el.attr("data-num") || $el.find(".gnum").text()), title: clean(a.attr("title") || $el.find(".ttl").text()), url: abs(a.attr("href")), date: clean($el.find(".date").text()), thumbnail: abs( $el.find("img").attr("src") || $el.find("img").attr("data-src") ), isCurrent: $el.hasClass("active"), }); }); if (episodes.length) return episodes; $(".eplister ul li, .episodelist ul li, .episodelist li").each((_, el) => { const $el = $(el); const a = $el.find("a").first(); episodes.push({ number: clean($el.find(".epl-num, .num").text()), title: clean( $el.find(".epl-title, .ttl, .title").text() || a.attr("title") ), url: abs(a.attr("href")), date: clean($el.find(".epl-date, .date").text()), }); }); if (episodes.length) return episodes; $('a[href*="episode-"]').each((_, el) => { const $el = $(el); const href = $el.attr("href"); const title = clean($el.attr("title") || $el.text()); const url = abs(href); if (!href || !title || !url) return; if (!episodes.some((ep) => ep.url === url)) { episodes.push({ number: title.match(/\d+/)?.[0] || "", title, url, }); } }); return episodes; } function parseSeriesInfo($) { const info = {}; $(".ak-cl-spe span, .bigcontent .infox .spe span").each((_, el) => { const text = clean($(el).text()); const match = text.match(/^([^:]+):\s*(.+)$/); if (match) { const key = match[1].trim().toLowerCase().replace(/\s+/g, "_"); info[key] = match[2].trim(); } }); const genres = $(".ak-cl-genres a, .bigcontent .infox .genxed a") .map((_, el) => ({ name: clean($(el).text()), url: abs($(el).attr("href")), })) .get(); return { title: clean( $(".ak-cl-title").text() || $(".bigcontent .infox h1").text() || $("h1.entry-title").text() ), japaneseTitle: clean( $(".ak-cl-alt").text() || $(".bigcontent .infox .alt").text() ), url: abs( $(".ak-cl-title a").attr("href") || $(".ak-cl-poster").attr("href") ), poster: abs( $(".ak-cl-poster img").attr("src") || $(".bigcontent .thum img").attr("src") || $('meta[property="og:image"]').attr("content") ), type: info.type || clean($(".ak-cl-type").text()), status: info.status, released: info.released, season: info.season, duration: info.duration, totalEpisodes: info.episodes, score: clean($(".bigcontent .rt .rating").text()) || undefined, genres, description: clean( $(".ak-cl-desc").text() || $(".entry-content p").first().text() || $('meta[name="description"]').attr("content") ), rawInfo: info, }; } function parseFeatured($) { return $("#featuredSwiper .swiper-slide") .map((_, el) => { const $el = $(el); const background = $el.attr("style")?.match(/url\(['"]?(.*?)['"]?\)/)?.[1] || ""; const meta = {}; $el.find(".mics > div").each((_, item) => { const label = clean($(item).find("div").first().text()).toLowerCase(); const value = clean($(item).find("span").text()); if (label) meta[label] = value; }); return { title: clean($el.find(".title").text()), japaneseTitle: clean($el.find(".title").attr("data-jp")), background: abs(background), watchUrl: abs($el.find(".watch-btn").attr("href")), detailUrl: abs($el.find(".detail-btn").attr("href")), subEpisode: clean( $el.find(".sub a").text() || $el.find(".sub").text() ), type: clean($el.find(".info span b").text()), description: clean($el.find(".desc").text()), meta, }; }) .get(); } function parseSchedule($) { return $("#schedule-list li a") .map((_, el) => { const $el = $(el); return { time: clean($el.find(".time").text()), title: clean($el.find(".title").text()), japaneseTitle: clean($el.find(".title").attr("data-jp")), episode: clean($el.find("span").last().text()), url: abs($el.attr("href")), status: clean($el.attr("class")), }; }) .get(); } async function getHome() { const $ = await fetchHTML("/"); const featured = parseFeatured($); const latestUpdates = $("#latest-updates .aitem-wrapper .aitem") .map((_, el) => parseAItem($, el)) .get(); const sections = $("#alistSwiper .swiper-slide") .map((_, el) => { const $el = $(el); return { section: clean($el.find(".stitle").text()), viewAllUrl: abs($el.find(".more-btn").attr("href")), items: $el .find(".aitem") .map((_, item) => parseAItem($, item)) .get(), }; }) .get(); const trending = $("#trending-anime .aitem") .map((_, el) => { const $el = $(el); const background = $el.attr("style")?.match(/url\(['"]?(.*?)['"]?\)/)?.[1] || ""; return { rank: clean($el.find(".num").text()), title: clean($el.find(".title").text()), japaneseTitle: clean($el.find(".title").attr("data-jp")), url: abs($el.find(".title").attr("href")), poster: abs(background), subEpisode: clean( $el.find(".sub a").text() || $el.find(".sub").text() ), dubEpisode: clean( $el.find(".dub a").text() || $el.find(".dub").text() ), type: clean($el.find(".info span b").text()), }; }) .get(); const schedule = parseSchedule($); return { featured, latestUpdates, sections, trending, schedule, }; } async function searchAnime(query) { const $ = await fetchHTML("/", { s: query }); const results = []; $(".aitem").each((_, el) => { results.push(parseAItem($, el)); }); if (results.length) { return { query, total: results.length, results }; } $(".bs .bsx, .bsx").each((_, el) => { results.push(parseBsx($, el)); }); if (results.length) { return { query, total: results.length, results }; } $(".listupd .lexa, .lexa").each((_, el) => { const $el = $(el); const a = $el.find("a").first(); results.push({ title: clean( $el.find(".dtl h2 a").text() || $el.find(".dtl h2").text() || a.attr("title") ), url: abs(a.attr("href")), poster: abs( $el.find("img").attr("src") || $el.find("img").attr("data-src") ), }); }); if (results.length) { return { query, total: results.length, results }; } $('main a[href*="/anime/"], main a[href*="episode-"]').each((_, el) => { const $el = $(el); const title = clean($el.attr("title") || $el.text()); const url = abs($el.attr("href")); if (title && url && !results.some((item) => item.url === url)) { results.push({ title, url }); } }); return { query, total: results.length, results }; } async function getAnimeDetail(animeUrl) { const url = abs(animeUrl); if (!url) { throw new Error("URL anime tidak valid"); } const $ = await fetchHTML(url); const series = parseSeriesInfo($); const episodes = parseEpisodeList($); return { ...series, episodes, }; } async function getEpisode(episodeUrl) { const url = abs(episodeUrl); if (!url) { throw new Error("URL episode tidak valid"); } const $ = await fetchHTML(url); const title = clean($(".entry-title").text()); const series = parseSeriesInfo($); const episodeNumber = clean( $('meta[itemprop="episodeNumber"]').attr("content") || $(".lm .epx").eq(1).text().replace(/episode/i, "") ); const lang = clean($(".lg").text()); const released = clean($(".year .updated").text()); const mainEmbed = abs($("#pembed iframe").attr("src")); const servers = $(".ak-srv-btn") .map((_, el) => { const $el = $(el); const embedHtml = decodeBase64($el.attr("data-value")); const embedSrc = extractIframeSrc(embedHtml); return { name: clean($el.text()), lang: clean($el.attr("data-lang")), active: $el.hasClass("active"), embedUrl: abs(embedSrc), embedHtml, }; }) .get(); const prev = abs($('.naveps a[rel="prev"]').attr("href")); const next = abs($(".naveps .nvs:last-child a").attr("href")); const allEpisodes = abs($(".naveps .nvsc a").attr("href")); const episodes = parseEpisodeList($); return { title, series, episodeNumber, lang, released, player: { embedUrl: mainEmbed, servers, }, navigation: { prev, next, allEpisodes, }, episodes, warning: clean($(".warning").text()), }; } module.exports = { getHome, searchAnime, getAnimeDetail, getEpisode, }; /** * Contoh pemakaian langsung * Jalankan: node animekai-scraper.js */ if (require.main === module) { (async () => { try { console.log("=== HOME ==="); const home = await getHome(); console.log("Featured:", home.featured.length); console.log("Latest updates:", home.latestUpdates.length); console.log("Trending:", home.trending.length); console.log("Schedule:", home.schedule.length); await sleep(800); console.log("\n=== SEARCH ==="); const search = await searchAnime("jojo"); console.log("Total:", search.total); console.log(search.results.slice(0, 3)); if (search.results[0]?.url) { await sleep(800); console.log("\n=== DETAIL ==="); const detail = await getAnimeDetail(search.results[0].url); console.log(detail.title); console.log(detail.status); console.log(detail.totalEpisodes); console.log(detail.genres); console.log("Episodes found:", detail.episodes.length); } await sleep(800); console.log("\n=== EPISODE ==="); const episode = await getEpisode( "https://animekai.com.ro/steel-ball-run-jojos-bizarre-adventure-2nd-stage-dub-episode-5-english-subbed/" ); console.log(episode.title); console.log("Episode:", episode.episodeNumber); console.log("Main embed:", episode.player.embedUrl); console.log("Servers:", episode.player.servers.length); console.log(episode.player.servers); } catch (err) { console.error("ERROR:", err.message); } })(); }