/** * 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 "); return detail(slug); }, watch: ([slug, ep]) => { if (!slug || !ep) throw new Error("Usage: watch "); return watch(slug, ep); }, search: (args) => { const q = args.join(" "); if (!q) throw new Error("Usage: search "); 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();