Edit: animasu
Nama Worker
Kode Sumber
--8dccb4a09eada806e91b931fc6b20e0281b9ce26e79d81cfc23eab3ccf28 Content-Disposition: form-data; name="worker.js" export default { async fetch(request) { const url = new URL(request.url); const path = url.pathname; try { // === PAGES === 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 pageLatest(url); if (path === "/explore") return pageExplore(url); if (path === "/genres") return pageGenres(url); if (path.startsWith("/genre/")) return pageGenreDetail(url); if (path.startsWith("/catalog/")) return pageCatalog(url); if (path === "/schedule") return pageSchedule(url); if (path === "/search") return pageSearch(url); if (path.startsWith("/series/")) return pageSeries(url); if (path.startsWith("/watch/")) return pageWatch(url); // assets passthrough optional if ( path.startsWith("/assets/") || path.endsWith(".js") || path.endsWith(".css") || path.endsWith(".png") || path.endsWith(".jpg") || path.endsWith(".jpeg") || path.endsWith(".svg") || path.endsWith(".webp") ) { return passThrough(request); } return new Response("Not Found", { status: 404 }); } catch (err) { return new Response(`Worker error: ${err?.message || err}`, { status: 500 }); } } }; /* ================= 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 }; } async function passThrough(request) { const url = new URL(request.url); const up = await fetch(UPSTREAM + url.pathname + url.search, { headers: { "user-agent": request.headers.get("user-agent") || "Mozilla/5.0" } }); return new Response(up.body, { status: up.status, headers: stripBadHeaders(up.headers) }); } function stripBadHeaders(h) { const out = new Headers(h); out.delete("content-security-policy"); out.delete("content-security-policy-report-only"); out.delete("x-frame-options"); out.delete("content-encoding"); return out; } /* ================= UTIL ================= */ const esc = (s) => String(s ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); const decodeHtml = (s) => String(s ?? "") .replaceAll("&", "&") .replaceAll(""", '"') .replaceAll("'", "'") .replaceAll("<", "<") .replaceAll(">", ">"); function q(url, key, def = "") { return url.searchParams.get(key) ?? def; } // normalize weird upstream: ...?title=Action?title=Genre&page=2 function normalizeHref(href) { if (!href) return href; try { // absolute -> local path+query if (href.startsWith("http")) { const u = new URL(href); href = u.pathname + u.search; } } catch {} const idx = href.indexOf("?"); if (idx === -1) return href; const base = href.slice(0, idx); const rest = href.slice(idx + 1).replaceAll("?", "&"); // fix double '?' return `${base}?${rest}`; } function dedupById(items, key = "id") { const m = new Map(); for (const it of items || []) { const k = it?.[key]; if (k && !m.has(k)) m.set(k, it); } return [...m.values()]; } /* ================= PARSERS ================= */ /** * Universal cards parser for: * 1) /latest: onclick window.location.href='/series/ID' * 2) /search: <a href="/series/ID" class="card"> ... <div class="card-title"> ... <div class="card-meta">★ 8.8 • TV * 3) /explore,/catalog,/genre: <div class="card" data-href="/series/ID"> ... <span>Eps ..</span> ... ★ .. */ function parseCardsUniversal(html) { const out = []; // (1) onclick style { const re = /onclick="window\.location\.href='\/series\/([^']+)'[\s\S]*?<img[^>]+src="([^"]+)"[\s\S]*?<div class="card-title">([\s\S]*?)<\/div>[\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], href: `/series/${m[1]}`, poster: (m[2] || "").trim(), title: decodeHtml((m[3] || "").trim()), ep: (m[4] || "").trim(), rating: (m[5] || "").trim(), type: "" }); } } // (2) anchor card style (search) { const re = /<a[^>]+href="\/series\/([^"]+)"[^>]*class="card"[^>]*>[\s\S]*?<img[^>]+src="([^"]+)"[\s\S]*?<div class="card-title">([\s\S]*?)<\/div>[\s\S]*?<div class="card-meta">([\s\S]*?)<\/div>/g; let m; while ((m = re.exec(html))) { const meta = decodeHtml(m[4] || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim(); const rating = meta.match(/★\s*([\d.]+)/)?.[1] || ""; const type = meta.match(/\b(TV|ONA|OVA|Movie|Special)\b/i)?.[1] || ""; out.push({ id: m[1], href: `/series/${m[1]}`, poster: (m[2] || "").trim(), title: decodeHtml((m[3] || "").trim()), ep: "", rating, type }); } } // (3) div card data-href style (explore/catalog/genre/schedule) { const re = /<div[^>]*class="card[^"]*"[^>]*data-href="\/series\/([^"]+)"[\s\S]*?<img[^>]+src="([^"]+)"[\s\S]*?<div class="card-title">([\s\S]*?)<\/div>[\s\S]*?<div class="card-meta">([\s\S]*?)<\/div>/g; let m; while ((m = re.exec(html))) { const meta = decodeHtml(m[4] || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim(); const rating = meta.match(/★\s*([\d.]+)/)?.[1] || ""; const ep = meta.match(/Eps\s*(\d+)/i)?.[1] || ""; out.push({ id: m[1], href: `/series/${m[1]}`, poster: (m[2] || "").trim(), title: decodeHtml((m[3] || "").trim()), ep, rating, type: "" }); } } return dedupById(out, "id"); } function parseExploreSections(html) { const sections = []; const re = /<div class="section">[\s\S]*?<span class="section-title">([\s\S]*?)<\/span>[\s\S]*?<a href="([^"]+)" class="see-all[^"]*">[\s\S]*?<\/a>[\s\S]*?<div class="scroll-row">([\s\S]*?)<\/div>[\s\S]*?<\/div>/g; let m; while ((m = re.exec(html))) { sections.push({ title: decodeHtml((m[1] || "").trim()), seeAll: normalizeHref(m[2] || ""), cards: parseCardsUniversal(m[3] || "") }); } return sections; } function parseGenres(html) { const out = []; const re = /href="(\/genre\/[^"]+)"\s+class="genre-chip[^"]*">([\s\S]*?)<\/a>/g; let m; while ((m = re.exec(html))) { const href = normalizeHref(m[1]); const name = decodeHtml(m[2]).replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim(); out.push({ name, href }); } return out; } function parsePagination(html) { // search uses: <a href="/search?...&page=2" class="disabled"> // catalog/genre uses: <a href="..." class="page-btn ... disabled"> const links = [...html.matchAll(/<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g)].map(x => ({ href: normalizeHref(decodeHtml(x[1] || "")), text: decodeHtml((x[2] || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()) })); const prev = links.find(l => /Sebelumnya/i.test(l.text))?.href || ""; const next = links.find(l => /Selanjutnya/i.test(l.text))?.href || ""; return { prev, next }; } function parseScheduleDays(html) { // schedule has containers: <div id="content-Senin" class="schedule-container active"> ...cards... const out = []; const re = /<div id="content-([^"]+)" class="schedule-container[^"]*">([\s\S]*?)<\/div>\s*(?=<div id="content-|<\/body>)/g; let m; while ((m = re.exec(html))) { out.push({ day: decodeHtml(m[1] || "").trim(), cards: parseCardsUniversal(m[2] || "") }); } // fallback: if upstream changed, still try single-page parse if (!out.length) { out.push({ day: "Jadwal", cards: parseCardsUniversal(html) }); } return out; } function parseSeriesHTML(html) { const title = decodeHtml(html.match(/<h1 class="title">([^<]+)/)?.[1] || "") || decodeHtml(html.match(/<title>([^<]+)<\/title>/i)?.[1] || ""); const synopsisRaw = html.match(/<div class="synopsis">([\s\S]*?)<\/div>/)?.[1] || ""; const synopsis = decodeHtml(synopsisRaw).replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim(); const backdrop = html.match(/class="backdrop-img"[^>]+src="([^"]+)"/i)?.[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: decodeHtml(m[2] || ""), date: decodeHtml(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 = decodeHtml(m[1] || ""); const q = url.match(/-(\d+p)-/)?.[1] || "auto"; sources.push({ q, url }); } if (!sources.length) { const re2 = /<source[^>]+src="([^"]+)"/g; while ((m = re2.exec(html))) { const url = decodeHtml(m[1] || ""); const q = url.match(/-(\d+p)-/)?.[1] || "auto"; sources.push({ q, url }); } } const seen = new Set(); return sources.filter(s => (seen.has(s.url) ? false : (seen.add(s.url), true))); } /* ================= 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;-webkit-tap-highlight-color:transparent;outline:none} 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} /* schedule tabs */ .dayTabs{display:flex;gap:10px;overflow:auto;padding:8px 2px 0} .dayBtn{background:rgba(255,255,255,.06);border:1px solid var(--line);color:var(--mut); padding:10px 16px;border-radius:999px;font-weight:900;white-space:nowrap} .dayBtn.active{background:var(--red);border-color:transparent;color:#fff} .dayPane{display:none} .dayPane.active{display:block} </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="${esc(x.href)}"> <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"; const upstreamPath = tab === "anime" ? "/anime" : tab === "donghua" ? "/donghua" : "/"; // Latest from /latest const latest = await fetchUpstream("/latest"); const latestCards = parseCardsUniversal(latest.text); // Explore sections -> take first section cards as “populer” const explore = await fetchUpstream("/explore"); const sections = parseExploreSections(explore.text); const popular = sections[0]?.cards?.length ? sections[0].cards : parseCardsUniversal(explore.text); 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">${(popular || []).slice(0, 10).map(cardHTML).join("")}</div> </div> `; return shell({ title: "CahyoKntl Clone", active: "home", tab, body }); } async function pageLatest(url) { const resp = await fetchUpstream("/latest" + (url.search || "")); const cards = parseCardsUniversal(resp.text); const body = ` <a class="backLink" href="/">← Kembali</a> <div class="section"> <div class="secHead"> <div class="secTitle"><span class="bar"></span>Episode Terbaru</div> <span class="seeAll"></span> </div> <div class="grid">${cards.map(cardHTML).join("")}</div> </div> `; return shell({ title: "Episode Terbaru", active: "home", tab: "all", body }); } async function pageExplore(url) { const resp = await fetchUpstream("/explore" + (url.search || "")); const sections = parseExploreSections(resp.text); // render sections like upstream (see-all -> /catalog/...) const body = ` <a class="backLink" href="/">← Kembali</a> ${sections.map(sec => ` <div class="section"> <div class="secHead"> <div class="secTitle"><span class="bar"></span>${esc(sec.title)}</div> ${sec.seeAll ? `<a class="seeAll" href="${esc(sec.seeAll)}">Lihat Semua ›</a>` : `<span class="seeAll"></span>`} </div> <div class="grid">${sec.cards.slice(0, 10).map(cardHTML).join("")}</div> </div> `).join("")} `; return shell({ title: "Explorasi", active: "explore", tab: "all", body }); } async function pageGenres(url) { const resp = await fetchUpstream("/genres"); const genres = parseGenres(resp.text); const body = ` <a class="backLink" href="/">← Kembali</a> <div class="section"> <div class="secHead"> <div class="secTitle"><span class="bar"></span>Pilih Genre</div> <span class="seeAll"></span> </div> <div class="eps"> ${genres.map(g => `<a class="ep" href="${esc(g.href)}">${esc(g.name)}</a>`).join("")} </div> </div> `; return shell({ title: "Genre", active: "genres", tab: "all", body }); } async function pageGenreDetail(url) { // keep query from URL (title/page) because upstream depends on it const genreId = url.pathname.split("/")[2] || ""; const title = q(url, "title", "Genre"); const page = q(url, "page", "1"); const upstreamHref = `/genre/${encodeURIComponent(genreId)}?title=${encodeURIComponent(title)}&page=${encodeURIComponent(page)}`; const resp = await fetchUpstream(upstreamHref); const cards = parseCardsUniversal(resp.text); const pag = parsePagination(resp.text); const prevHref = pag.prev ? normalizeHref(pag.prev) : ""; const nextHref = pag.next ? normalizeHref(pag.next) : ""; const body = ` <a class="backLink" href="/genres">← 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 style="display:flex;justify-content:center;gap:12px;margin-top:16px"> <a class="ep" style="padding:12px 18px" href="${esc(prevHref || "#")}" ${prevHref ? "" : 'onclick="return false"'}>⬅ Sebelumnya</a> <a class="ep" style="padding:12px 18px" href="${esc(nextHref || "#")}" ${nextHref ? "" : 'onclick="return false"'}>Selanjutnya ➡</a> </div> </div> `; return shell({ title: `Genre: ${title}`, active: "genres", tab: "all", body }); } async function pageCatalog(url) { const sectionType = url.pathname.split("/")[2] || ""; const upstreamHref = `/catalog/${encodeURIComponent(sectionType)}${url.search || ""}`; const resp = await fetchUpstream(upstreamHref); const cards = parseCardsUniversal(resp.text); const pag = parsePagination(resp.text); const prevHref = pag.prev ? normalizeHref(pag.prev) : ""; const nextHref = pag.next ? normalizeHref(pag.next) : ""; 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 style="display:flex;justify-content:center;gap:12px;margin-top:16px"> <a class="ep" style="padding:12px 18px" href="${esc(prevHref || "#")}" ${prevHref ? "" : 'onclick="return false"'}>⬅ Sebelumnya</a> <a class="ep" style="padding:12px 18px" href="${esc(nextHref || "#")}" ${nextHref ? "" : 'onclick="return false"'}>Selanjutnya ➡</a> </div> </div> `; return shell({ title: "Catalog", active: "explore", tab: "all", body }); } async function pageSchedule(url) { const resp = await fetchUpstream("/schedule"); const days = parseScheduleDays(resp.text); const firstDay = days[0]?.day || "Senin"; const tabs = days.map(d => `<button class="dayBtn" data-day="${esc(d.day)}">${esc(d.day)}</button>`).join(""); const panes = days.map(d => ` <div class="dayPane" data-pane="${esc(d.day)}"> <div class="section"> <div class="secHead"> <div class="secTitle"><span class="bar"></span>${esc(d.day)}</div> <span class="seeAll"></span> </div> <div class="grid">${(d.cards || []).map(cardHTML).join("")}</div> </div> </div> `).join(""); const body = ` <a class="backLink" href="/">← Kembali</a> <div class="section"> <div class="secHead"> <div class="secTitle"><span class="bar"></span>Jadwal</div> <span class="seeAll"></span> </div> <div class="dayTabs">${tabs}</div> </div> ${panes} <script> const first = ${JSON.stringify(firstDay)}; const btns = [...document.querySelectorAll(".dayBtn")]; const panes = [...document.querySelectorAll(".dayPane")]; function show(day){ btns.forEach(b=>b.classList.toggle("active", b.dataset.day===day)); panes.forEach(p=>p.classList.toggle("active", p.dataset.pane===day)); } btns.forEach(b=>b.addEventListener("click",()=>show(b.dataset.day))); show(first); </script> `; return shell({ title: "Jadwal", active: "schedule", tab: "all", body }); } async function pageSearch(url) { const term = q(url, "search", ""); const page = q(url, "page", "1"); if (!term) { return shell({ title: "Search", active: "search", tab: "all", body: `<div class="section"><div class="secHead"><div class="secTitle"><span class="bar"></span>Pencarian</div></div><div class="small">Klik ikon 🔍 lalu cari judul.</div></div>`, searchValue: "" }); } const resp = await fetchUpstream(`/search?search=${encodeURIComponent(term)}&page=${encodeURIComponent(page)}`); const cards = parseCardsUniversal(resp.text); const pag = parsePagination(resp.text); const prevHref = pag.prev ? normalizeHref(pag.prev) : ""; const nextHref = pag.next ? normalizeHref(pag.next) : ""; const body = ` <a class="backLink" href="/">← Kembali</a> <div class="section"> <div class="secHead"> <div class="secTitle"><span class="bar"></span>Hasil: ${esc(term)}</div> <span class="seeAll"></span> </div> <div class="grid">${cards.map(cardHTML).join("")}</div> <div style="display:flex;justify-content:center;gap:12px;margin-top:16px"> <a class="ep" style="padding:12px 18px" href="${esc(prevHref || "#")}" ${prevHref ? "" : 'onclick="return false"'}>⬅ Sebelumnya</a> <a class="ep" style="padding:12px 18px" href="${esc(nextHref || "#")}" ${nextHref ? "" : 'onclick="return false"'}>Selanjutnya ➡</a> </div> </div> `; return shell({ title: "Search", active: "search", tab: "all", body, searchValue: term }); } async function pageSeries(url) { const seriesId = url.pathname.split("/")[2] || ""; const resp = await fetchUpstream(`/series/${encodeURIComponent(seriesId)}`); const s = parseSeriesHTML(resp.text); 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">${esc(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) { const episodeId = url.pathname.split("/")[2] || ""; const resp = await fetchUpstream(`/watch/${encodeURIComponent(episodeId)}`); const sources = parseWatchSources(resp.text); const first = sources[0]?.url || ""; const body = ` <a class="backLink" href="javascript:history.back()">← Kembali</a> <div class="player"> <video id="v" controls autoplay playsinline controlsList="nodownload"></video> </div> <div class="qRow"> ${sources.map((s,i)=>`<button class="qBtn ${i===0?"active":""}" onclick="play(${JSON.stringify(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(()=>{}); } ${first ? `play(${JSON.stringify(first)}, document.querySelector(".qBtn"));` : ""} </script> `; return shell({ title: "Watch", active: "home", tab: "all", body }); } --8dccb4a09eada806e91b931fc6b20e0281b9ce26e79d81cfc23eab3ccf28--
Bindings
Tambah KV
Tambah R2
Tambah D1
← Kembali
Simpan & Deploy
Hapus Script Worker Ini