scraper animeindo lengkap.
qrtz
javascript
25
5
https://animeindo.me
23 Jun 2026
Code
const axios = require('axios');
const cheerio = require('cheerio');
const https = require('https');
const BASE_URL = 'https://animeindo.me';
const USER_AGENTS = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7; rv:133.0) Gecko/20100101 Firefox/133.0',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0',
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1',
'Mozilla/5.0 (Linux; Android 14; SM-S921B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.104 Mobile Safari/537.36',
'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'
];
let uaIndex = 0;
function getHeaders(ref) {
const ua = USER_AGENTS[uaIndex % USER_AGENTS.length];
uaIndex++;
const isMobile = ua.includes('Mobile') || ua.includes('iPhone') || ua.includes('Android');
const platform = ua.includes('Windows') ? 'Windows' : ua.includes('Mac') ? 'macOS' : ua.includes('Linux') ? 'Linux' : 'Unknown';
return {
'User-Agent': ua,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': 'id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7',
'Accept-Encoding': 'gzip, deflate, br',
'Referer': ref || 'https://animeindo.me/',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'DNT': '1',
'Sec-Ch-Ua': `"${ua.includes('Chrome') ? 'Google Chrome' : 'Chromium'}"`,
'Sec-Ch-Ua-Mobile': isMobile ? '?1' : '?0',
'Sec-Ch-Ua-Platform': `"${platform}"`,
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-User': '?1',
'Upgrade-Insecure-Requests': '1',
'Connection': 'keep-alive'
};
}
function randomDelay(min = 500, max = 2000) {
return new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * (max - min + 1)) + min));
}
async function fetchHTML(url, retries = 5, ref = null) {
for (let i = 0; i < retries; i++) {
try {
await randomDelay(300, 800);
const res = await axios({
url,
method: 'GET',
headers: getHeaders(ref || url),
timeout: 30000,
httpsAgent: new https.Agent({ rejectUnauthorized: false, keepAlive: true }),
maxRedirects: 0,
decompress: true,
validateStatus: status => status >= 200 && status < 400
});
return res.data;
} catch (e) {
if (e.response && e.response.status >= 300 && e.response.status < 400) return e.response.data;
if (i < retries - 1) await randomDelay(1500, 4000);
else throw e;
}
}
}
async function resolveRedirect(url) {
try {
const res = await axios.get(url, {
maxRedirects: 10,
validateStatus: status => status >= 200 && status < 400,
headers: {
'User-Agent': USER_AGENTS[0],
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Referer': 'https://animeindo.me/'
},
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
timeout: 15000
});
return res.request.res.responseUrl || res.config.url;
} catch (e) {
return url;
}
}
function decodeBase64(str) {
try {
return Buffer.from(str, 'base64').toString('utf-8');
} catch (e) {
return null;
}
}
function extractSrcFromIframe(html) {
const match = html.match(/<iframe[^>]*src=["']([^"']*)["']/i);
return match ? match[1] : null;
}
class AnimeIndoScraper {
constructor() {
this.base = BASE_URL;
this.creator = 'rynaqrtz';
}
_clean(obj) {
if (obj === null || obj === undefined) return undefined;
if (Array.isArray(obj)) {
const filtered = obj.map(i => this._clean(i)).filter(i => i !== undefined);
return filtered.length ? filtered : undefined;
}
if (typeof obj === 'object') {
const result = {};
for (const key of Object.keys(obj)) {
const val = this._clean(obj[key]);
if (val !== undefined) result[key] = val;
}
return Object.keys(result).length ? result : undefined;
}
return obj;
}
_parseAnimeCard($, element) {
const $el = $(element);
const link = $el.find('a').attr('href');
const title = $el.find('.tt h2').text().trim() || $el.find('.tt').text().trim();
const poster = $el.find('img').attr('data-src') || $el.find('img').attr('src') || null;
const type = $el.find('.typez').text().trim() || null;
const status = $el.find('.status').text().trim() || null;
const episode = $el.find('.epx').text().trim() || null;
const sub = $el.find('.sb').text().trim() || null;
if (!link || !title) return null;
return {
title,
url: link.startsWith('http') ? link : this.base + link,
poster,
type,
status,
episode,
sub
};
}
_parsePagination($) {
const result = { current: 1, next: null, hasNext: false, total: null };
const pageLinks = [];
$('.hpage a, .pagination a, .pagination span, .page-numbers').each((i, el) => {
const href = $(el).attr('href');
const text = $(el).text().trim();
if (href) pageLinks.push({ text, href });
});
const numbers = pageLinks.filter(l => /^\d+$/.test(l.text)).map(l => parseInt(l.text));
if (numbers.length) result.total = Math.max(...numbers);
const current = $('.page-numbers.current, .pagination .current, .hpage .current');
if (current.length) {
const t = current.text().trim();
if (/^\d+$/.test(t)) result.current = parseInt(t);
}
if (result.total && result.current < result.total) {
result.hasNext = true;
const nextLink = pageLinks.find(l => l.text === 'Next' || l.text === '»' || l.text.toLowerCase().includes('next'));
if (nextLink && nextLink.href) {
result.next = nextLink.href.startsWith('http') ? nextLink.href : this.base + nextLink.href;
}
}
return result;
}
_parseEpisodeList($) {
const episodes = [];
$('.epcheck .eplister ul li a, #singlepisode .episodelist ul li a, .eplist .episode-item a').each((i, el) => {
const $el = $(el);
const url = $el.attr('href');
const num = $el.find('.epl-num').text().trim() || $el.find('.ep-num').text().trim() || (url ? parseInt(url.match(/episode-(\d+)/)?.[1]) : null);
const title = $el.find('.epl-title').text().trim() || $el.find('.ep-title').text().trim() || $el.find('h3').text().trim() || null;
const date = $el.find('.epl-date').text().trim() || $el.find('.ep-date').text().trim() || null;
if (url && num) {
episodes.push({
episode: parseInt(num) || num,
title: title || `Episode ${num}`,
url: url.startsWith('http') ? url : this.base + url,
releaseDate: date || null
});
}
});
return episodes.sort((a, b) => parseInt(a.episode) - parseInt(b.episode));
}
async resolveStreamUrls(episodeUrl) {
const html = await fetchHTML(episodeUrl);
const $ = cheerio.load(html);
const streams = [];
const defaultIframe = $('#pembed iframe, .player-embed iframe, #iframedc');
let defaultPlayer = null;
if (defaultIframe.length) {
let src = defaultIframe.attr('src') || defaultIframe.attr('data-src') || defaultIframe.attr('data-litespeed-src');
if (src && src !== 'about:blank') {
defaultPlayer = src;
}
}
$('select.mirror option').each((i, el) => {
const $opt = $(el);
const label = $opt.text().trim();
const value = $opt.val();
if (!value || value === '') return;
const decodedHtml = decodeBase64(value);
if (!decodedHtml) return;
const src = extractSrcFromIframe(decodedHtml);
if (src) {
streams.push({ quality: label, url: src });
}
});
for (const stream of streams) {
let finalUrl = stream.url;
if (stream.url.includes('link.desustream.com') || stream.url.includes('short.icu') || stream.url.includes('tinyurl')) {
try { finalUrl = await resolveRedirect(stream.url); } catch (e) {}
}
stream.url = finalUrl;
}
return { defaultPlayer, streams };
}
async home(page = 1) {
const url = page === 1 ? this.base + '/' : this.base + `/page/${page}/`;
const html = await fetchHTML(url);
const $ = cheerio.load(html);
const items = [];
$('.listupd .bs, .listupd .excstf .bs, .popconslide .bs').each((i, el) => {
const card = this._parseAnimeCard($, el);
if (card) items.push(card);
});
const pagination = this._parsePagination($);
return this._clean({ creator: this.creator, page: 'home', data: { url, pagination, items } });
}
async ongoing(page = 1) {
const url = page === 1 ? this.base + '/anime-ongoing/' : this.base + `/anime-ongoing/page/${page}/`;
const html = await fetchHTML(url);
const $ = cheerio.load(html);
const items = [];
$('.listupd .bs, .listupd.cp .bs').each((i, el) => {
const card = this._parseAnimeCard($, el);
if (card) items.push(card);
});
const pagination = this._parsePagination($);
return this._clean({ creator: this.creator, page: 'ongoing', data: { url, pagination, items } });
}
async schedule() {
const url = this.base + '/jadwal-anime/';
const html = await fetchHTML(url);
const $ = cheerio.load(html);
const schedule = {};
$('.schedulepage').each((i, el) => {
const $el = $(el);
const dayClass = $el.attr('class').match(/sch_(\w+)/);
const day = dayClass ? dayClass[1].charAt(0).toUpperCase() + dayClass[1].slice(1) : null;
const items = [];
$el.find('.bs').each((j, card) => {
const $card = $(card);
const title = $card.find('.tt').text().trim();
const url = $card.find('a').attr('href');
const poster = $card.find('img').attr('data-src') || $card.find('img').attr('src') || null;
const episode = $card.find('.epx').text().trim() || null;
const sub = $card.find('.sb').text().trim() || null;
const releaseTime = $card.find('.epx.cndwn').attr('data-rlsdt') || null;
if (title && url) {
items.push({
title,
url: url.startsWith('http') ? url : this.base + url,
poster,
episode,
sub,
releaseTime
});
}
});
if (day && items.length) schedule[day] = items;
});
return this._clean({ creator: this.creator, page: 'schedule', data: { url, schedule } });
}
async list(page = 1, filters = {}) {
let url = this.base + '/anime/';
const params = new URLSearchParams();
if (page > 1) params.set('page', page);
if (filters.status) params.set('status', filters.status);
if (filters.type) params.set('type', filters.type);
if (filters.order) params.set('order', filters.order);
if (filters.sub) params.set('sub', filters.sub);
if (filters.genre && filters.genre.length) params.set('genre[]', filters.genre.join(','));
if (filters.season && filters.season.length) params.set('season[]', filters.season.join(','));
const query = params.toString();
if (query) url += '?' + query;
const html = await fetchHTML(url);
const $ = cheerio.load(html);
const items = [];
$('.listupd .bs').each((i, el) => {
const card = this._parseAnimeCard($, el);
if (card) items.push(card);
});
const pagination = this._parsePagination($);
return this._clean({ creator: this.creator, page: 'list', data: { url, filters, pagination, items } });
}
async popular(page = 1) { return this.list(page, { order: 'popular' }); }
async rating(page = 1) { return this.list(page, { order: 'rating' }); }
async update(page = 1) { return this.list(page, { order: 'update' }); }
async completed(page = 1) { return this.list(page, { status: 'completed', order: 'popular' }); }
async movie(page = 1) { return this.list(page, { type: 'movie', order: 'rating' }); }
async tv(page = 1) { return this.list(page, { type: 'tv', order: 'popular' }); }
async search(query, page = 1) {
const url = page === 1 ? this.base + `/?s=${encodeURIComponent(query)}` : this.base + `/page/${page}/?s=${encodeURIComponent(query)}`;
const html = await fetchHTML(url);
const $ = cheerio.load(html);
const items = [];
$('.listupd .bs').each((i, el) => {
const card = this._parseAnimeCard($, el);
if (card) items.push(card);
});
const pagination = this._parsePagination($);
return this._clean({ creator: this.creator, page: 'search', data: { url, query, pagination, items } });
}
async detail(slug) {
const url = this.base + `/anime/${slug}/`;
const html = await fetchHTML(url);
const $ = cheerio.load(html);
const title = $('.infox .infolimit h2, .infox h1').first().text().trim();
const poster = $('.infox .thumb img, .bigcontent .thumb img').attr('data-src') || $('.infox .thumb img, .bigcontent .thumb img').attr('src') || null;
const rating = $('.numscore, .rating .numscore').text().trim() || null;
const ratingPercent = $('.rtb span').attr('style') ? parseFloat($('.rtb span').attr('style').match(/width:([\d.]+)%/)?.[1]) : null;
const alternative = $('.alter').text().trim() || null;
const synopsis = $('.desc, .entry-content p').first().text().trim() || null;
const info = {};
$('.spe span, .info-content .spe span').each((i, el) => {
const text = $(el).text().trim();
const parts = text.split(':');
if (parts.length >= 2) {
const key = parts[0].trim().replace('<b>', '').replace('</b>', '');
const val = parts.slice(1).join(':').trim();
if (key && val) info[key] = val;
}
});
const genres = [];
$('.genxed a, .info-content .genxed a').each((i, el) => {
genres.push($(el).text().trim());
});
const episodes = this._parseEpisodeList($);
const recommended = [];
$('.listupd .bs, .recommended .bs').each((i, el) => {
const card = this._parseAnimeCard($, el);
if (card) recommended.push(card);
});
return this._clean({
creator: this.creator,
page: 'detail',
slug,
data: {
url,
title,
poster,
rating,
ratingPercent,
alternative,
synopsis,
info,
genres,
episodes,
recommended: recommended.slice(0, 10)
}
});
}
async episode(slugOrUrl) {
let url, slug, episodeNum;
if (slugOrUrl.includes('http')) {
url = slugOrUrl;
const match = url.match(/\/anime\/([^\/]+)\/|\/([^\/]+)-episode-(\d+)/);
if (match) {
slug = match[1] || match[2];
episodeNum = parseInt(match[3]) || null;
}
} else {
const match = slugOrUrl.match(/^(.+?)-episode-(\d+)$/);
if (match) {
slug = match[1];
episodeNum = parseInt(match[2]);
url = this.base + `/${slug}-episode-${episodeNum}-sub-indo/`;
} else {
throw new Error('Invalid format. Use slug-episode-number');
}
}
const html = await fetchHTML(url);
const $ = cheerio.load(html);
const title = $('.entry-title, .title-section h1').first().text().trim();
const poster = $('.tb img, .thumb img').attr('data-src') || $('.tb img, .thumb img').attr('src') || null;
const info = {};
$('.spe span, .single-info .infox .spe span').each((i, el) => {
const text = $(el).text().trim();
const parts = text.split(':');
if (parts.length >= 2) {
const key = parts[0].trim().replace('<b>', '').replace('</b>', '');
const val = parts.slice(1).join(':').trim();
if (key && val) info[key] = val;
}
});
const genres = [];
$('.genxed a').each((i, el) => {
genres.push($(el).text().trim());
});
const synopsis = $('.desc, .entry-content p').first().text().trim() || null;
const episodes = this._parseEpisodeList($);
const prev = $('.naveps .nvs:first-child a, #prev a').attr('href') || null;
const next = $('.naveps .nvs:last-child a, #next a').attr('href') || null;
const allEpisodesLink = $('.naveps .nvsc a').attr('href') || null;
const streamResult = await this.resolveStreamUrls(url);
return this._clean({
creator: this.creator,
page: 'episode',
slug,
episode: episodeNum,
data: {
url,
title,
poster,
defaultPlayer: streamResult.defaultPlayer,
streams: streamResult.streams,
info,
genres,
synopsis,
episodes,
prevEpisode: prev ? (prev.startsWith('http') ? prev : this.base + prev) : null,
nextEpisode: next ? (next.startsWith('http') ? next : this.base + next) : null,
allEpisodesLink: allEpisodesLink ? (allEpisodesLink.startsWith('http') ? allEpisodesLink : this.base + allEpisodesLink) : null
}
});
}
}
if (require.main === module) {
const args = process.argv.slice(2);
const cmd = args[0];
const params = args.slice(1);
const scraper = new AnimeIndoScraper();
(async () => {
let result;
try {
switch (cmd) {
case 'home':
result = await scraper.home(parseInt(params[0]) || 1);
break;
case 'ongoing':
result = await scraper.ongoing(parseInt(params[0]) || 1);
break;
case 'schedule':
result = await scraper.schedule();
break;
case 'popular':
result = await scraper.popular(parseInt(params[0]) || 1);
break;
case 'rating':
result = await scraper.rating(parseInt(params[0]) || 1);
break;
case 'update':
result = await scraper.update(parseInt(params[0]) || 1);
break;
case 'completed':
result = await scraper.completed(parseInt(params[0]) || 1);
break;
case 'movie':
result = await scraper.movie(parseInt(params[0]) || 1);
break;
case 'tv':
result = await scraper.tv(parseInt(params[0]) || 1);
break;
case 'search':
if (!params[0]) throw new Error('Query required');
result = await scraper.search(params[0], parseInt(params[1]) || 1);
break;
case 'detail':
if (!params[0]) throw new Error('Slug required');
result = await scraper.detail(params[0]);
break;
case 'episode':
if (!params[0]) throw new Error('Slug-episode-number or URL required');
result = await scraper.episode(params[0]);
break;
case 'stream':
if (!params[0]) throw new Error('Episode URL required');
const streams = await scraper.resolveStreamUrls(params[0]);
console.log(JSON.stringify(streams, null, 2));
return;
default:
console.error(`Commands:
home [page] - Latest episodes from homepage
ongoing [page] - List ongoing anime
schedule - Weekly release schedule
popular [page] - Most popular anime
rating [page] - Highest rated anime
update [page] - Recently updated anime
completed [page] - Completed anime
movie [page] - Anime movies
tv [page] - TV series
search <query> [page]- Search anime
detail <slug> - Full anime details
episode <slug-ep-num> - Episode with stream links
stream <episode-url> - Extract video URLs episode only`);
process.exit(1);
}
console.log(JSON.stringify(result, null, 2));
} catch (err) {
console.error(JSON.stringify({ error: err.message }));
process.exit(1);
}
})();
}
module.exports = AnimeIndoScraper;Rating
Gimana snippet ini menurutmu?
Embed ke website / blog
<iframe src="https://qrtzcode.vercel.app/api/embed/anime/animeindo" style="width:100%;height:400px;border:none;border-radius:12px;"></iframe>