Edit: animek
Nama Worker
Kode Sumber
--5de61e71f6439a7aaee3d8f9de3e2847d5436bfd3b171c0489239ac1dd22 Content-Disposition: form-data; name="worker.js" export default { async fetch(request) { const url = new URL(request.url); const path = url.pathname; // === PAGES (match docs) === if (path === "/" || path === "/home") return pageHome(url); if (path === "/anime") return pageHome(url, { tab: "anime" }); if (path === "/donghua") return pageHome(url, { tab: "donghua" }); if (path === "/latest") return pageGridFromUpstream(url, { upstreamPath: "/latest", title: "Episode Terbaru", active: "home" }); if (path === "/explore") return pageGridFromUpstream(url, { upstreamPath: "/explore", title: "Explorasi", active: "explore" }); if (path === "/genres") return pageGenres(url); if (path.startsWith("/genre/")) return pageGenreDetail(url, path.split("/")[2]); if (path.startsWith("/catalog/")) return pageCatalog(url, path.split("/")[2]); if (path === "/schedule") return pageEmbedUpstream(url, { upstreamPath: "/schedule", title: "Jadwal", active: "schedule" }); if (path === "/search") return pageSearch(url); if (path.startsWith("/series/")) return pageSeries(url, path.split("/")[2]); if (path.startsWith("/watch/")) return pageWatch(url, path.split("/")[2]); return new Response("Not Found", { status: 404 }); } }; /* ================= CONFIG ================= */ const UPSTREAM = "https://nonton.cahyokntl.site"; /* ================= FETCH ================= */ async function fetchUpstream(pathAndQuery) { const res = await fetch(UPSTREAM + pathAndQuery, { headers: { "user-agent": "Mozilla/5.0", "accept": "text/html,application/json;q=0.9,*/*;q=0.8" } }); const ct = res.headers.get("content-type") || ""; const text = await res.text(); return { ok: res.ok, status: res.status, ct, text }; } /* ================= UTIL ================= */ const esc = s => String(s ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); const decode = s => String(s ?? "") .replaceAll("&", "&") .replaceAll(""", '"') .replaceAll("'", "'") .replaceAll("<", "<") .replaceAll(">", ">"); function q(url, key, def = "") { return url.searchParams.get(key) ?? def; } /* ================= PARSERS ================= */ // cards like /latest function parseCards(html) { const out = []; const re = /onclick="window\.location\.href='\/series\/([^']+)'[\s\S]*?<img src="([^"]+)"[^>]*alt="([^"]+)"[\s\S]*?<div class="badge-ep">Ep\s*([^<]+)<\/div>[\s\S]*?(?:<div class="badge-rating">★\s*([^<]+)<\/div>)?/g; let m; while ((m = re.exec(html))) { out.push({ id: m[1], poster: m[2], title: decode(m[3]), ep: (m[4] || "").trim(), rating: (m[5] || "").trim() }); } return out; } function parseSeriesHTML(html) { const title = decode(html.match(/<h1 class="title">([^<]+)/)?.[1] || ""); const synopsis = html.match(/<div class="synopsis">([\s\S]*?)<\/div>/)?.[1] || ""; const backdrop = html.match(/backdrop-img" src="([^"]+)/)?.[1] || ""; const episodes = []; const re = /<a href="\/watch\/([^"]+)"[^>]*class="ep-item[\s\S]*?<div class="ep-num">([^<]+)<\/div>[\s\S]*?(?:<div class="ep-date">([^<]+)<\/div>)?/g; let m; while ((m = re.exec(html))) { episodes.push({ id: m[1], label: decode(m[2] || ""), date: decode(m[3] || "") }); } const firstEp = html.match(/href="\/watch\/([^"]+)"[^>]*class="btn-start"/)?.[1] || (episodes[0]?.id ?? ""); return { title, synopsis, backdrop, episodes, firstEp }; } function parseWatchSources(html) { const sources = []; const re = /changeQuality\('([^']+)'/g; let m; while ((m = re.exec(html))) { const url = m[1].replace(/&/g, "&"); const q = url.match(/-(\d+p)-/)?.[1] || "auto"; sources.push({ q, url }); } // fallback <source src=""> if (!sources.length) { const re2 = /<source[^>]+src="([^"]+)"/g; while ((m = re2.exec(html))) { const url = m[1].replace(/&/g, "&"); sources.push({ q: url.match(/-(\d+p)-/)?.[1] || "auto", url }); } } const seen = new Set(); return sources.filter(s => (seen.has(s.url) ? false : (seen.add(s.url), true))); } function parseGenresFromHTML(html) { const out = []; const re = /href="\/genre\/([^"?/]+)(?:\?[^"]*)?"[^>]*>([^<]+)<\/a>/g; let m; while ((m = re.exec(html))) { const id = decode(m[1]).trim(); const name = decode(m[2]).trim(); if (id && name) out.push({ id, name }); } const map = new Map(); for (const g of out) if (!map.has(g.id)) map.set(g.id, g); return [...map.values()]; } /* ================= UI ================= */ function shell({ title, active, tab, body, searchValue = "" }) { const tabs = ["all", "anime", "donghua"]; const tabSafe = tabs.includes(tab) ? tab : "all"; return new Response(`<!doctype html> <html lang="id"> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width,initial-scale=1"/> <title>${esc(title)}</title> <style> :root{ --bg:#0b0f14; --line:rgba(255,255,255,.10); --txt:#e5e7eb; --mut:#9ca3af; --red:#ff2d2d; } *{box-sizing:border-box} body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;background:linear-gradient(180deg,#05070a,var(--bg) 30%);color:var(--txt)} a{color:inherit;text-decoration:none} .wrap{max-width:1100px;margin:0 auto;padding:16px 16px 92px} .topbar{display:flex;align-items:center;justify-content:space-between;margin:10px 0 8px} .brand{font-size:40px;font-weight:900}.brand span{color:var(--red)} .iconBtn{width:44px;height:44px;border-radius:999px;border:1px solid var(--line);background:rgba(255,255,255,.04);display:flex;align-items:center;justify-content:center} .tabs{display:flex;gap:22px;margin:8px 0 14px;font-size:22px;font-weight:900} .tab{opacity:.55;position:relative;padding-bottom:10px} .tab.active{opacity:1}.tab.active:after{content:"";position:absolute;left:0;bottom:0;width:30px;height:4px;background:var(--red);border-radius:999px} .chips{display:flex;gap:12px;flex-wrap:wrap;margin:10px 0 18px} .chip{padding:14px 18px;border-radius:999px;border:1px solid var(--line);background:rgba(255,255,255,.04);display:flex;gap:10px;align-items:center;font-weight:800} .section{margin-top:18px} .secHead{display:flex;align-items:center;justify-content:space-between;margin:0 0 12px} .secTitle{display:flex;align-items:center;gap:12px;font-size:28px;font-weight:900} .bar{width:4px;height:26px;background:var(--red);border-radius:8px} .seeAll{color:var(--mut);font-size:18px;font-weight:800} .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px} @media(min-width:860px){.grid{grid-template-columns:repeat(5,1fr)}} .card{border:1px solid var(--line);border-radius:22px;overflow:hidden;background:rgba(255,255,255,.04)} .poster{aspect-ratio:2/3;background:#0b1220;position:relative} .poster img{width:100%;height:100%;object-fit:cover;display:block} .badgeTop{position:absolute;left:10px;top:10px;background:#ffbf00;color:#111;font-weight:900;padding:8px 12px;border-radius:12px;font-size:14px} .badgeEp{position:absolute;left:10px;bottom:10px;background:var(--red);color:#fff;font-weight:900;padding:8px 12px;border-radius:12px;font-size:14px} .badgeRate{position:absolute;right:10px;top:10px;background:rgba(0,0,0,.55);border:1px solid var(--line);padding:8px 10px;border-radius:12px;font-weight:900;font-size:13px} .cardTitle{padding:10px 10px 12px;font-weight:900;font-size:16px;line-height:1.2} .bottomNav{position:fixed;left:0;right:0;bottom:0;background:rgba(0,0,0,.82);backdrop-filter:blur(10px);border-top:1px solid rgba(255,255,255,.10);display:flex;justify-content:space-around;padding:10px 8px} .bnItem{display:flex;flex-direction:column;align-items:center;gap:6px;color:var(--mut);font-weight:800} .bnItem.active{color:var(--red)} .bnDot{width:10px;height:10px;border-radius:999px;background:currentColor;opacity:.9} .backLink{display:inline-flex;gap:10px;align-items:center;color:var(--mut);margin:8px 0 12px;font-weight:900} .hero{border:1px solid var(--line);border-radius:18px;overflow:hidden;background:#0b1220} .heroImg{width:100%;height:220px;object-fit:cover;display:block;filter:saturate(1.1)} .heroBody{padding:14px} .h1{font-size:22px;font-weight:900;margin:0 0 8px} .syn{color:var(--mut);line-height:1.6} .eps{display:flex;flex-wrap:wrap;gap:10px;margin-top:12px} .ep{padding:10px 14px;border-radius:999px;border:1px solid var(--line);background:rgba(255,255,255,.04);font-weight:900} .player{border-radius:18px;overflow:hidden;border:1px solid var(--line);background:#000} video{width:100%;display:block} .qRow{display:flex;gap:14px;justify-content:center;flex-wrap:wrap;margin-top:16px} .qBtn{min-width:110px;text-align:center;padding:16px 18px;border-radius:10px;border:1px solid var(--line);background:rgba(255,255,255,.08);font-weight:900;font-size:18px;color:var(--txt)} .qBtn.active{background:var(--red);border-color:transparent} .searchModal{position:fixed;inset:0;background:rgba(0,0,0,.65);display:none;align-items:flex-start;justify-content:center;padding:24px} .searchModal.active{display:flex} .searchBox{width:min(720px,100%);background:#0f141c;border:1px solid var(--line);border-radius:18px;padding:14px} .searchBox input{width:100%;padding:14px;border-radius:12px;border:1px solid var(--line);background:rgba(255,255,255,.06);color:var(--txt);outline:none;font-size:16px} .small{color:var(--mut);font-size:12px;margin-top:10px} .iframeWrap{border:1px solid var(--line);border-radius:18px;overflow:hidden} .iframeWrap iframe{width:100%;height:75vh;border:0;background:#000} .pre{white-space:pre-wrap;background:rgba(255,255,255,.04);border:1px solid var(--line);padding:14px;border-radius:14px;color:var(--txt)} </style> </head> <body> <div class="wrap"> <div class="topbar"> <div class="brand"><span>Cahyo</span>Kntl</div> <a class="iconBtn" href="/search" aria-label="Search">🔍</a> </div> <div class="tabs"> <a class="tab ${tabSafe==="all"?"active":""}" href="/">Semua</a> <a class="tab ${tabSafe==="anime"?"active":""}" href="/anime">Anime</a> <a class="tab ${tabSafe==="donghua"?"active":""}" href="/donghua">Donghua</a> </div> <div class="chips"> <a class="chip" href="https://t.me/" target="_blank" rel="noreferrer">✈️ Channel 1</a> <a class="chip" href="https://t.me/" target="_blank" rel="noreferrer">✈️ Channel 2</a> <a class="chip" href="#" onclick="alert('Ganti link Admin di kode worker.js');return false;">👥 Admin</a> </div> ${body} </div> <div class="searchModal ${active === "search" ? "active" : ""}" id="searchModal"> <div class="searchBox"> <form action="/search" method="GET"> <input name="search" value="${esc(searchValue)}" placeholder="Cari judul..." /> <div class="small">Tekan Enter untuk mencari</div> </form> </div> </div> <div class="bottomNav"> <a class="bnItem ${active==="home"?"active":""}" href="/"><div class="bnDot"></div><div>Home</div></a> <a class="bnItem ${active==="genres"?"active":""}" href="/genres"><div class="bnDot"></div><div>Genre</div></a> <a class="bnItem ${active==="explore"?"active":""}" href="/explore"><div class="bnDot"></div><div>Explorasi</div></a> <a class="bnItem ${active==="schedule"?"active":""}" href="/schedule"><div class="bnDot"></div><div>Jadwal</div></a> </div> <script> if (location.pathname === "/search") { const m = document.getElementById("searchModal"); if (m) m.classList.add("active"); } document.addEventListener("click", (e)=>{ const m = document.getElementById("searchModal"); if(!m) return; if(e.target === m) location.href = "/"; }); </script> </body> </html>`, { headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } }); } function cardHTML(x) { return `<a class="card" href="/series/${esc(x.id)}"> <div class="poster"> ${x.poster ? `<img src="${esc(x.poster)}" alt="${esc(x.title)}" loading="lazy">` : ""} <div class="badgeTop">Baru</div> ${x.ep ? `<div class="badgeEp">Ep ${esc(x.ep)}</div>` : ""} ${x.rating ? `<div class="badgeRate">★ ${esc(x.rating)}</div>` : ""} </div> <div class="cardTitle">${esc(x.title)}</div> </a>`; } /* ================= PAGES ================= */ async function pageHome(url, opts = {}) { const tab = opts.tab || "all"; // Use the exact endpoints from docs const upstreamPath = tab === "anime" ? "/anime" : tab === "donghua" ? "/donghua" : "/"; // Homepage often HTML; try parse cards; fallback embed const home = await fetchUpstream(upstreamPath); let latestCards = parseCards(home.text); // If homepage doesn't contain cards, use /latest as source if (!latestCards.length) { const latest = await fetchUpstream("/latest"); latestCards = parseCards(latest.text); } // Popular from /explore, fallback to latest slice const explore = await fetchUpstream("/explore"); let exploreCards = parseCards(explore.text); if (!exploreCards.length) exploreCards = latestCards.slice(0, 10); const body = ` <div class="section"> <div class="secHead"> <div class="secTitle"><span class="bar"></span>Episode Terbaru</div> <a class="seeAll" href="/latest">Lihat Semua ›</a> </div> <div class="grid">${latestCards.slice(0, 10).map(cardHTML).join("")}</div> </div> <div class="section"> <div class="secHead"> <div class="secTitle"><span class="bar"></span>Sedang Populer</div> <a class="seeAll" href="/explore">Lihat Semua ›</a> </div> <div class="grid">${exploreCards.slice(0, 10).map(cardHTML).join("")}</div> </div> `; return shell({ title: "CahyoKntl Clone", active: "home", tab, body }); } async function pageGridFromUpstream(url, { upstreamPath, title, active }) { const resp = await fetchUpstream(upstreamPath + (url.search || "")); const cards = parseCards(resp.text); // if parse fails: embed upstream so tetap berfungsi if (!cards.length) { return pageEmbedUpstream(url, { upstreamPath: upstreamPath + (url.search || ""), title, active }); } const body = ` <a class="backLink" href="/">← Kembali</a> <div class="section"> <div class="secHead"> <div class="secTitle"><span class="bar"></span>${esc(title)}</div> <span class="seeAll"></span> </div> <div class="grid">${cards.map(cardHTML).join("")}</div> </div> `; return shell({ title, active, tab: "all", body }); } async function pageSeries(url, seriesId) { const resp = await fetchUpstream(`/series/${encodeURIComponent(seriesId)}`); const s = parseSeriesHTML(resp.text); // If parse fails: embed upstream if (!s.title && !s.episodes.length) { return pageEmbedUpstream(url, { upstreamPath: `/series/${encodeURIComponent(seriesId)}`, title: "Series", active: "home" }); } const body = ` <a class="backLink" href="/">← Kembali</a> <div class="hero"> ${s.backdrop ? `<img class="heroImg" src="${esc(s.backdrop)}" alt="">` : ""} <div class="heroBody"> <div class="h1">${esc(s.title || seriesId)}</div> <div class="syn">${s.synopsis || ""}</div> ${s.firstEp ? `<div class="eps" style="margin-top:14px"> <a class="ep" href="/watch/${esc(s.firstEp)}" style="background:var(--red);border-color:transparent">Mulai Nonton</a> </div>` : ""} </div> </div> <div class="section"> <div class="secHead"> <div class="secTitle"><span class="bar"></span>Episode</div> <span class="seeAll"></span> </div> <div class="eps"> ${s.episodes.map(e => `<a class="ep" href="/watch/${esc(e.id)}">${esc(e.label)}${e.date ? ` • <span style="color:var(--mut)">${esc(e.date)}</span>` : ""}</a>`).join("")} </div> </div> `; return shell({ title: s.title || "Series", active: "home", tab: "all", body }); } async function pageWatch(url, episodeId) { const resp = await fetchUpstream(`/watch/${encodeURIComponent(episodeId)}`); const sources = parseWatchSources(resp.text); if (!sources.length) { return pageEmbedUpstream(url, { upstreamPath: `/watch/${encodeURIComponent(episodeId)}`, title: "Watch", active: "home" }); } const first = sources[0]?.url || ""; const body = ` <a class="backLink" href="/">← Kembali</a> <div class="player"> <video id="v" controls autoplay playsinline></video> </div> <div class="qRow"> ${sources.map((s,i)=>`<button class="qBtn ${i===0?"active":""}" onclick="play('${esc(s.url)}',this)">${esc(s.q)}</button>`).join("")} </div> <script> const v = document.getElementById("v"); function play(url, btn){ document.querySelectorAll(".qBtn").forEach(b=>b.classList.remove("active")); if(btn) btn.classList.add("active"); v.src = url; v.play().catch(()=>{}); } play(${JSON.stringify(first)}, document.querySelector(".qBtn")); </script> `; return shell({ title: "Watch", active: "home", tab: "all", body }); } async function pageSearch(url) { const term = q(url, "search", ""); if (!term) { return shell({ title: "Search", active: "search", tab: "all", body: `<div class="pre">Masukkan kata kunci pencarian.</div>`, searchValue: "" }); } const resp = await fetchUpstream(`/search?search=${encodeURIComponent(term)}`); const cards = parseCards(resp.text); if (!cards.length) { // fallback embed upstream search (tetap fungsi) return pageEmbedUpstream(url, { upstreamPath: `/search?search=${encodeURIComponent(term)}`, title: "Search", active: "search" }); } const body = ` <div class="section"> <div class="secHead"> <div class="secTitle"><span class="bar"></span>Hasil Pencarian</div> <span class="seeAll"></span> </div> <div class="grid">${cards.map(cardHTML).join("")}</div> </div> `; return shell({ title: "Search", active: "search", tab: "all", body, searchValue: term }); } async function pageGenres(url) { const resp = await fetchUpstream(`/genres`); let genres = parseGenresFromHTML(resp.text); // If failed, embed upstream genres page (tetap fungsi) if (!genres.length) { return pageEmbedUpstream(url, { upstreamPath: "/genres", title: "Genre", active: "genres" }); } const body = ` <div class="section"> <div class="secHead"> <div class="secTitle"><span class="bar"></span>Genre</div> <span class="seeAll"></span> </div> <div class="eps"> ${genres.map(g => `<a class="ep" href="/genre/${encodeURIComponent(g.id)}">${esc(g.name)}</a>`).join("")} </div> </div> `; return shell({ title: "Genre", active: "genres", tab: "all", body }); } async function pageGenreDetail(url, genreId) { const resp = await fetchUpstream(`/genre/${encodeURIComponent(genreId)}?title=Genre&page=1`); const cards = parseCards(resp.text); if (!cards.length) { return pageEmbedUpstream(url, { upstreamPath: `/genre/${encodeURIComponent(genreId)}?title=Genre&page=1`, title: "Genre", active: "genres" }); } const body = ` <a class="backLink" href="/genres">← Kembali</a> <div class="section"> <div class="secHead"> <div class="secTitle"><span class="bar"></span>Genre: ${esc(genreId)}</div> <span class="seeAll"></span> </div> <div class="grid">${cards.map(cardHTML).join("")}</div> </div> `; return shell({ title: "Genre", active: "genres", tab: "all", body }); } async function pageCatalog(url, sectionType) { const resp = await fetchUpstream(`/catalog/${encodeURIComponent(sectionType)}` + (url.search || "")); const cards = parseCards(resp.text); if (!cards.length) { return pageEmbedUpstream(url, { upstreamPath: `/catalog/${encodeURIComponent(sectionType)}` + (url.search || ""), title: "Catalog", active: "explore" }); } const body = ` <a class="backLink" href="/explore">← Kembali</a> <div class="section"> <div class="secHead"> <div class="secTitle"><span class="bar"></span>Catalog: ${esc(sectionType)}</div> <span class="seeAll"></span> </div> <div class="grid">${cards.map(cardHTML).join("")}</div> </div> `; return shell({ title: "Catalog", active: "explore", tab: "all", body }); } async function pageEmbedUpstream(url, { upstreamPath, title, active }) { // embed upstream for 100% functionality when parsing unknown format const body = ` <a class="backLink" href="/">← Kembali</a> <div class="section"> <div class="secHead"> <div class="secTitle"><span class="bar"></span>${esc(title)}</div> <span class="seeAll"></span> </div> <div class="iframeWrap"> <iframe src="${esc(UPSTREAM + upstreamPath)}" referrerpolicy="no-referrer"></iframe> </div> <div class="small" style="margin-top:10px;color:var(--mut)"> Mode fallback aktif karena format halaman upstream berbeda dari parser. </div> </div> `; return shell({ title, active, tab: "all", body }); } --5de61e71f6439a7aaee3d8f9de3e2847d5436bfd3b171c0489239ac1dd22--
Bindings
Tambah KV
Tambah R2
Tambah D1
← Kembali
Simpan & Deploy
Hapus Script Worker Ini