Loading snippets...
scraper nonton anime, get home, search, detail, watch, download, dll cek sendiri
/**
* oploverz watch anime
* base: https://vip.oploverz.ltd
* Creator: ShanMolvyr
* Jangan Hapus Kreator woi ^_^
*
*
* Note: cek https://snippet.vyr.my.id/shanmolvyr/oploverz/README.md
* Sumber: https://whatsapp.com/channel/0029VbB4Kw8EFeXfeExaXc3Q
*/
const axios = require("axios");
const cheerio = require("cheerio");
const BASE = "https://vip.oploverz.ltd";
const BACKAPI = "https://backapi.oploverz.ac/uploads/";
const HEADERS_HTML = {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"accept-language": "id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7",
"cache-control": "no-cache",
"pragma": "no-cache",
"user-agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36",
};
const HEADERS_DATA = {
"accept": "*/*",
"accept-language": "id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7",
"cache-control": "no-cache",
"pragma": "no-cache",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"user-agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36",
};
const http = axios.create({ baseURL: BASE, timeout: 20000 });
const AD_DOMAINS = [
"blogger.com", "blogspot.com", "paladindrama",
"glamour", "mayhap", "histats",
"slot", "casino", "mpo", "judol",
"placehold.co", "comentario", "cloudflareinsights",
];
function isClean(url = "") {
if (!url) return false;
const lower = url.toLowerCase();
return !AD_DOMAINS.some(d => lower.includes(d));
}
// ─── SVELTEKIT __data.json DECODER ───────────────────────────────────────────
//
// Format: { type:"data", nodes:[ {type:"skip"}, ..., {type:"data", data:[...]} ] }
// data[] adalah flat array:
// arr[0] = root object { key: index, key: index, ... }
// arr[N] = value literal ATAU object {key:index} ATAU array [index, index, ...]
// Semua angka adalah reference ke index lain dalam array (deduplication).
function decodeSvelteFlat(raw) {
if (!raw || !Array.isArray(raw.nodes)) return null;
const dataNode = raw.nodes.find(n => n?.type === "data" && Array.isArray(n.data));
if (!dataNode) return null;
const arr = dataNode.data;
function resolve(idx) {
if (idx === null || idx === undefined) return null;
const val = arr[idx];
if (val === null || val === undefined) return val;
if (typeof val !== "object") return val; // string, number, boolean
if (Array.isArray(val)) return val.map(i => resolve(i));
// Plain object: { key: index, ... }
const result = {};
for (const [k, v] of Object.entries(val)) result[k] = resolve(v);
return result;
}
return resolve(0);
}
// ─── HTTP HELPERS ─────────────────────────────────────────────────────────────
async function fetchDataJson(path, referer) {
// Root path "/" → endpoint harus "/__data.json" bukan "//__data.json"
const endpoint = (path === "/" ? "" : path) + "/__data.json?x-sveltekit-invalidated=001";
const res = await http.get(
endpoint,
{ headers: { ...HEADERS_DATA, Referer: referer || BASE + "/" } }
);
return decodeSvelteFlat(res.data);
}
// Fallback HTML fetch (untuk series list & search yang belum tentu punya __data.json bersih)
async function fetchHTML(path) {
const res = await http.get(path, { headers: HEADERS_HTML });
return res.data;
}
// ─── URL HELPER ──────────────────────────────────────────────────────────────
// Poster bisa berupa full URL atau relative path (posters/xxx.jpg)
function fullUrl(path) {
if (!path) return null;
if (path.startsWith("http")) return path;
return BACKAPI + path;
}
// ─── AD FILTER ───────────────────────────────────────────────────────────────
function fmtStreamUrls(streamUrl = []) {
return (streamUrl || [])
.filter(s => s?.url && isClean(s.url))
.map(s => ({ label: s.source, url: s.url }));
}
function fmtDownloads(downloadUrl = []) {
return (downloadUrl || []).flatMap(fmt =>
(fmt?.resolutions || []).flatMap(res =>
(res?.download_links || [])
.filter(l => l?.url && isClean(l.url))
.map(l => ({
format: fmt.format || null,
quality: res.quality || null,
host: l.host || null,
url: l.url,
}))
)
);
}
// ─── FORMATTERS ──────────────────────────────────────────────────────────────
function fmtSeries(s) {
if (!s?.slug) return null;
return {
id: s.id || null,
title: s.title || null,
japaneseTitle: s.japaneseTitle || null,
slug: s.slug,
status: s.status || null,
poster: fullUrl(s.poster),
score: s.score || null,
genres: (s.genres || []).map(g => g?.name || g).filter(Boolean),
studio: s.studio?.name || null,
season: s.season?.name || null,
totalEpisodes: s.totalEpisodes || null,
releaseDate: s.releaseDate || null,
releaseType: s.releaseType || null,
url: `${BASE}/series/${s.slug}`,
};
}
function fmtEpisodeCard(ep) {
if (!ep) return null;
return {
id: ep.id || null,
seriesTitle: ep.series?.title || null,
seriesSlug: ep.series?.slug || null,
episodeNumber: ep.episodeNumber || null,
subbed: ep.subbed || null,
poster: fullUrl(ep.series?.poster) || null,
releasedAt: ep.releasedAt || null,
streamUrls: fmtStreamUrls(ep.streamUrl),
downloadUrls: fmtDownloads(ep.downloadUrl),
url: ep.series?.slug
? `${BASE}/series/${ep.series.slug}/episode/${ep.episodeNumber}`
: null,
};
}
// ─── PAGES ────────────────────────────────────────────────────────────────────
async function home() {
// Home punya /__data.json dengan semua data (trending, recently, latestEpisodes)
const decoded = await fetchDataJson("/", BASE + "/");
if (!decoded) throw new Error("Gagal decode home __data.json");
const trending = decoded.trending || {};
const recently = decoded.recently || {};
const latestEpisodes = decoded.latestEpisodes || {};
// Banner tetap dari HTML (carousel DOM, tidak ada di __data.json)
const html = await fetchHTML("/");
const $ = cheerio.load(html);
const banners = [];
$("[data-embla-slide]").each((_, el) => {
const img = $(el).find("img[src*='banners']").attr("src") || null;
const slug = $(el).find("a[href^='/series/']").first().attr("href")
?.replace("/series/", "").replace(/\/.*/, "");
const title = $(el).find("h1 span, h2 span").first().text().trim();
const desc = $(el).find("p").filter((_, p) => $(p).text().trim().length > 5).first().text().trim();
if (img && slug && !banners.find(b => b.slug === slug)) {
banners.push({ image: img, title, slug, description: desc, url: `${BASE}/series/${slug}` });
}
});
return {
page: "home",
banners,
trending: (trending.data || []).map(fmtSeries).filter(Boolean),
recently: (recently.data || []).map(fmtSeries).filter(Boolean),
latestEpisodes: (latestEpisodes.data || []).map(fmtEpisodeCard).filter(Boolean),
meta: {
trending: trending.meta || {},
recently: recently.meta || {},
latestEpisodes: latestEpisodes.meta || {},
},
};
}
async function seriesList(page = 1, sort_by = "recently", genre = "") {
// Series list: __data.json dengan query params sebagai suffix (bukan di path)
const query = new URLSearchParams({ page, sort_by, ...(genre && { genre }) });
const res = await http.get(
`/series/__data.json?x-sveltekit-invalidated=001&${query}`,
{ headers: { ...HEADERS_DATA, Referer: BASE + "/" } }
).catch(() => null);
const decoded = res ? decodeSvelteFlat(res.data) : null;
// Key series list ada di decoded.allSeries.data
let items = (decoded?.allSeries?.data || []).map(s => fmtSeries(s)).filter(Boolean);
const meta = decoded?.allSeries?.meta || {};
// Fallback cheerio kalau __data.json gagal
if (!items.length) {
const html = await fetchHTML(`/series?${query}`);
const $ = cheerio.load(html);
$("a[href^='/series/']").each((_, el) => {
const slug = $(el).attr("href")?.replace("/series/", "").replace(/\/.*/, "");
const title = $(el).find("img").attr("alt") || "";
const img = $(el).find("img").attr("src") || null;
if (slug && title && !items.find(x => x.slug === slug))
items.push({ slug, title, poster: fullUrl(img), url: `${BASE}/series/${slug}` });
});
}
return {
page: "series",
total: meta.total || items.length,
pagination: {
currentPage: meta.currentPage || +page,
lastPage: meta.lastPage || 1,
perPage: meta.perPage || items.length,
},
items,
};
}
async function detail(slug) {
const decoded = await fetchDataJson(`/series/${slug}`, BASE + "/");
if (!decoded) throw new Error(`Gagal decode __data.json untuk: ${slug}`);
const s = decoded.series || {};
const eps = decoded.episodes || {};
const epList = (eps.data || eps || []);
return {
page: "detail",
id: s.id || null,
title: s.title || null,
japaneseTitle: s.japaneseTitle || null,
slug,
description: s.description || null,
status: s.status || null,
poster: fullUrl(s.poster),
score: s.score || null,
genres: (s.genres || []).map(g => g?.name || g).filter(Boolean),
studio: s.studio?.name || null,
season: s.season?.name || null,
totalEpisodes: s.totalEpisodes || epList.length,
releaseDate: s.releaseDate || null,
releaseType: s.releaseType || null,
batchDownload: s.batchDownloadUrl || null,
episodesMeta: {
currentPage: eps.meta?.currentPage || 1,
lastPage: eps.meta?.lastPage || 1,
total: eps.meta?.total || epList.length,
},
episodes: Array.isArray(epList)
? epList
.map(ep => ({
episodeNumber: ep.episodeNumber || null,
title: ep.title || null,
releasedAt: ep.releasedAt || null,
url: `${BASE}/series/${slug}/episode/${ep.episodeNumber}`,
}))
.filter(ep => ep.episodeNumber)
.sort((a, b) => +a.episodeNumber - +b.episodeNumber)
: [],
url: `${BASE}/series/${slug}`,
};
}
async function watch(slug, epNumber) {
const decoded = await fetchDataJson(
`/series/${slug}/episode/${epNumber}`,
`${BASE}/series/${slug}`
);
if (!decoded) throw new Error(`Gagal decode __data.json untuk: ${slug} ep ${epNumber}`);
const ep = decoded.episode || {};
const all = decoded.allEpisodes || decoded.episodes || {};
const allList = all.data || all || [];
return {
page: "watch",
id: ep.id || null,
seriesTitle: ep.series?.title || decoded.series?.title || null,
seriesSlug: ep.series?.slug || slug,
episodeNumber: ep.episodeNumber || epNumber,
subbed: ep.subbed || null,
poster: fullUrl(ep.series?.poster) || fullUrl(decoded.series?.poster) || null,
releasedAt: ep.releasedAt || null,
streamUrls: fmtStreamUrls(ep.streamUrl),
downloadUrls: fmtDownloads(ep.downloadUrl),
allEpisodes: Array.isArray(allList)
? allList
.map(e => ({
episodeNumber: e.episodeNumber || null,
releasedAt: e.releasedAt || null,
url: `${BASE}/series/${slug}/episode/${e.episodeNumber}`,
}))
.filter(e => e.episodeNumber)
.sort((a, b) => +a.episodeNumber - +b.episodeNumber)
: [],
url: `${BASE}/series/${slug}/episode/${epNumber}`,
};
}
async function search(query) {
// Search via __data.json dengan query param terpisah
const res = await http.get(
`/series/__data.json?x-sveltekit-invalidated=001&q=${encodeURIComponent(query)}`,
{ headers: { ...HEADERS_DATA, Referer: BASE + "/" } }
).catch(() => null);
const decoded = res ? decodeSvelteFlat(res.data) : null;
// Key search hasil ada di decoded.allSeries.data (filter dilakukan server-side via q param)
let items = (decoded?.allSeries?.data || []).map(s => fmtSeries(s)).filter(Boolean);
const meta = decoded?.allSeries?.meta || {};
if (!items.length) {
const html = await fetchHTML(`/series?q=${encodeURIComponent(query)}`);
const $ = cheerio.load(html);
$("a[href^='/series/']").each((_, el) => {
const slug = $(el).attr("href")?.replace("/series/", "").replace(/\/.*/, "");
const title = $(el).find("img").attr("alt") || "";
const img = $(el).find("img").attr("src") || null;
if (slug && title && !items.find(x => x.slug === slug))
items.push({ slug, title, poster: fullUrl(img), url: `${BASE}/series/${slug}` });
});
}
return {
page: "search",
query,
total: meta.total || items.length,
pagination: { currentPage: meta.currentPage || 1, lastPage: meta.lastPage || 1 },
items,
};
}
// ─── MAIN ─────────────────────────────────────────────────────────────────────
const COMMANDS = {
home: () => home(),
series: ([p, s, g]=[]) => seriesList(p, s, g),
detail: ([slug]) => { if (!slug) throw new Error("Usage: detail <slug>"); return detail(slug); },
watch: ([slug, ep]) => { if (!slug || !ep) throw new Error("Usage: watch <slug> <episode>"); return watch(slug, ep); },
search: (args) => { const q = args.join(" "); if (!q) throw new Error("Usage: search <query>"); return search(q); },
};
async function main() {
const [,, cmd, ...args] = process.argv;
if (!cmd || !COMMANDS[cmd]) {
console.error(`Usage: node oploverz.js [${Object.keys(COMMANDS).join("|")}] [...args]`);
console.error(" node oploverz.js home");
console.error(" node oploverz.js series 1 recently action");
console.error(" node oploverz.js detail one-piece");
console.error(" node oploverz.js watch tsue-to-tsurugi-no-wistoria-s2 1");
console.error(" node oploverz.js search naruto");
process.exit(1);
}
try {
const result = await COMMANDS[cmd](args);
console.log(JSON.stringify(result, null, 2));
} catch (e) {
console.error("[error]", e.message);
if (e.response) console.error(`HTTP ${e.response.status} → ${e.config?.url}`);
process.exit(1);
}
}
main();
bashnpm install axios cheerio
bash* node oploverz.js home * node oploverz.js series [page] [sort_by] [genre] * node oploverz.js detail <slug> * node oploverz.js watch <slug> <episode> * node oploverz.js search <query>
bashby ShanMolvyr