qrtzcode
HomeDocsAPILeaderboardChangelog
Contribute
Docs

qrtzcode

Kumpulan gist & snippet pribadi — anime, AI tools, scraper, dan randomness.

Navigate

HomeDocsAPI Reference

Links

© 2026 qrtz. All snippets welcome.

ESC

Navigate

Links

Comic

webtoon

yh.

Note

copy ajh.

Creator

qrtz

Language

javascript

Views

9

Copies

4

Base

https://www.webtoons.com

Updated

24 Jun 2026

#comic

Code

94
webtoon.js
import axios from "axios";
import * as cheerio from "cheerio";
import https from "https";

const WEBTOONS_BASE = "https://www.webtoons.com";
const VALID_DAYS = new Set([
  "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday",
  "daily", "trending", "completed",
]);
const MAX_RETRIES = 5;
const BASE_DELAY = 1000;

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 (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) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15",
  "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 (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_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1",
  "Mozilla/5.0 (iPad; CPU OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1",
  "Mozilla/5.0 (Android 14; Mobile; rv:133.0) Gecko/133.0 Firefox/133.0",
  "Mozilla/5.0 (Android 14; Mobile; wv) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36",
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0",
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0",
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0",
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Vivaldi/6.9.0.0",
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Vivaldi/6.8.0.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 Vivaldi/6.9.0.0",
  "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
  "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0",
];

function randomUA() {
  return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
}

function randomDelay(min = 100, max = 1500) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

function getHeaders() {
  const ua = randomUA();
  return {
    "User-Agent": ua,
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;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",
    "Connection": "keep-alive",
    "Upgrade-Insecure-Requests": "1",
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Sec-Fetch-User": "?1",
    "Cache-Control": "max-age=0",
    "Sec-Ch-Ua": '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
    "Sec-Ch-Ua-Mobile": "?0",
    "Sec-Ch-Ua-Platform": '"Windows"',
    "Referer": "https://www.webtoons.com/",
    "Origin": "https://www.webtoons.com",
  };
}

async function fetchHtml(url, retries = MAX_RETRIES) {
  let lastError;
  const proxy = process.env.WEBTOON_PROXY || null;
  for (let i = 0; i < retries; i++) {
    try {
      const headers = getHeaders();
      const config = {
        headers,
        timeout: 25000,
        withCredentials: true,
        httpsAgent: new https.Agent({ rejectUnauthorized: false }),
        validateStatus: () => true,
      };
      if (proxy) {
        const proxyUrl = new URL(proxy);
        config.proxy = {
          host: proxyUrl.hostname,
          port: parseInt(proxyUrl.port, 10),
          auth: proxyUrl.username && proxyUrl.password
            ? { username: proxyUrl.username, password: proxyUrl.password }
            : undefined,
        };
      }
      const res = await axios.get(url, config);
      if (res.status === 403 || res.status === 503) {
        throw new Error(`Blocked (${res.status})`);
      }
      if (res.status !== 200 || typeof res.data !== "string") {
        throw new Error(`HTTP ${res.status}`);
      }
      return res.data;
    } catch (e) {
      lastError = e;
      const delay = BASE_DELAY * Math.pow(2, i) + randomDelay(0, 500);
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error(`Failed after ${retries} retries: ${lastError?.message}`);
}

function titleNoFromUrl(url) {
  const m = String(url).match(/[?&]title_no=(\d+)/);
  return m ? Number(m[1]) : null;
}

function search($, query, limit) {
  const items = [];
  $("a[href*='/list?title_no=']").each((_, el) => {
    if (items.length >= limit) return false;
    const $a = $(el);
    const href = $a.attr("href") ?? "";
    if (!href) return;
    const absUrl = href.startsWith("http") ? href : `${WEBTOONS_BASE}${href}`;
    const titleNo = titleNoFromUrl(absUrl);
    if (!titleNo) return;
    const title = $a.find("strong.title, .info_text .title, p.subj, .subj").first().text().trim() || $a.attr("title")?.trim() || "";
    const author = $a.find(".info_text .author, div.author, .author, .by").first().text().trim();
    const thumb = $a.find("img").first().attr("src") ?? null;
    const isCanvas = absUrl.includes("/canvas/");
    const genreMatch = absUrl.match(/\/id\/([^/]+)\/[^/]+\/list/);
    const genre = !isCanvas && genreMatch ? genreMatch[1] : null;
    items.push({
      titleNo,
      title,
      author: author || null,
      thumbnail: thumb,
      genre,
      section: isCanvas ? "canvas" : "originals",
      url: absUrl.split("&webtoon-platform-redirect")[0],
    });
  });
  const seen = new Set();
  const unique = items.filter((it) => {
    const key = `${it.section}:${it.titleNo}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
  return {
    query,
    count: unique.length,
    originals: unique.filter((i) => i.section === "originals"),
    canvas: unique.filter((i) => i.section === "canvas"),
    items: unique,
  };
}

function parseDetail($, sourceUrl) {
  const title = $("h1.subj, h3.subj, .info .subj").first().text().trim() || $('meta[property="og:title"]').attr("content") || null;
  const authorRaw = $(".author_area, .ly_creator_in .author, .author").first().text().trim() || null;
  const author = authorRaw ? authorRaw.replace(/\s+/g, " ").replace(/Informasi Kreator/g, "").trim() : null;
  const synopsis = $("p.summary, .detail_body .summary").first().text().trim() || $('meta[property="og:description"]').attr("content") || null;
  const genre = $("h2.genre, p.genre").first().text().trim() || null;
  const day = $("p.day_info, .day_info").first().text().replace(/\s+/g, " ").trim() || null;
  const status = /completed|tamat|selesai/i.test(day ?? "") ? "COMPLETED" : /(every|setiap|UP)/i.test(day ?? "") ? "ONGOING" : null;
  const ratingText = $(".grade_area .grade_num, em#_starScoreAverage, .grade_num").first().text().trim() || null;
  let rating = null;
  if (ratingText) {
    const cleaned = ratingText.replace(/,/g, "").trim();
    const num = parseFloat(cleaned);
    if (!isNaN(num)) {
      rating = num > 10 ? num / 100 : num;
      if (rating > 10) rating = num;
    }
  }
  const subscribers = $(".grade_area em.cnt, .subscribe em.cnt").first().text().trim() || null;
  const thumbnail = $(".detail_header .thmb img, .detail_bg img").first().attr("src") || $('meta[property="og:image"]').attr("content") || null;
  return {
    titleNo: titleNoFromUrl(sourceUrl),
    title,
    author,
    synopsis,
    genre,
    day,
    status,
    rating,
    subscribers,
    thumbnail,
    url: sourceUrl,
  };
}

async function detail(url) {
  const html = await fetchHtml(url);
  return parseDetail(cheerio.load(html), url);
}

async function episodes(url, page = 1) {
  const sep = url.includes("?") ? "&" : "?";
  const fullUrl = `${url}${sep}page=${page}`;
  const html = await fetchHtml(fullUrl);
  const $ = cheerio.load(html);
  const list = [];
  $("#_listUl > li, ul#_listUl > li").each((_, el) => {
    const $li = $(el);
    const $a = $li.find("a").first();
    const href = $a.attr("href") ?? "";
    if (!href) return;
    const epUrl = href.startsWith("http") ? href : `${WEBTOONS_BASE}${href}`;
    const epNo = Number($li.attr("data-episode-no")) || Number(epUrl.match(/episode_no=(\d+)/)?.[1]) || null;
    const epTitle = $a.find(".subj span, .subj").first().text().trim() || $a.attr("title")?.trim() || "";
    const thumb = $a.find(".thmb img").first().attr("src") ?? null;
    const date = $a.find(".date").first().text().trim() || null;
    const likes = $li.find("em._likeitNumViewArea, .like_area em").first().text().trim() || null;
    list.push({ episodeNo: epNo, title: epTitle, thumbnail: thumb, date, likes, url: epUrl });
  });
  let totalPages = page;
  $("div.paginate a, .paginate a").each((_, el) => {
    const t = Number($(el).text().trim());
    if (Number.isFinite(t) && t > totalPages) totalPages = t;
  });
  const meta = parseDetail($, url);
  return { ...meta, page, totalPages, hasNext: page < totalPages, count: list.length, episodes: list };
}

async function trending(day) {
  const d = (day || "daily").toLowerCase();
  if (!VALID_DAYS.has(d)) throw new Error(`day '${day}' invalid. Use: monday/.../sunday/daily/trending/completed`);
  const path = d === "daily" || d === "trending" ? "/id/dailySchedule" : d === "completed" ? "/id/originals/completed" : `/id/originals/${d}`;
  const sourceUrl = `${WEBTOONS_BASE}${path}`;
  const html = await fetchHtml(sourceUrl);
  const $ = cheerio.load(html);
  const items = [];
  $("a[href*='/list?title_no=']").each((_, el) => {
    const $a = $(el);
    const href = $a.attr("href") ?? "";
    if (!href) return;
    const absUrl = href.startsWith("http") ? href : `${WEBTOONS_BASE}${href}`;
    const titleNo = titleNoFromUrl(absUrl);
    if (!titleNo) return;
    const title = $a.find(".subj, p.subj, .info .subj, .title, strong.title").first().text().trim() || $a.attr("title")?.trim() || "";
    const thumb = $a.find("img").first().attr("src") ?? null;
    const genreMatch = absUrl.match(/\/id\/([^/]+)\/[^/]+\/list/);
    const genre = genreMatch && genreMatch[1] !== "canvas" ? genreMatch[1] : null;
    const likes = $a.find(".grade_area em, .like_area em, .grade_num").first().text().trim() || null;
    items.push({ titleNo, title, thumbnail: thumb, genre, likes, url: absUrl.split("&webtoon-platform-redirect")[0] });
  });
  const seen = new Set();
  const unique = items.filter((it) => {
    if (seen.has(it.titleNo)) return false;
    seen.add(it.titleNo);
    return true;
  });
  return { day: d, sourceUrl, count: unique.length, items: unique };
}

function wrapResponse(status, code, mode, data, input = null) {
  return { creator: "rynaqrtz", status, code, mode, input, result: data };
}

async function webtoons(input) {
  try {
    const q = typeof input?.query === "string" ? input.query.trim() : "";
    const url = typeof input?.url === "string" ? input.url.trim() : "";
    const day = typeof input?.day === "string" ? input.day.trim() : "";
    const wantEpisodes = input?.episodes === true;
    const limit = Number.isFinite(input?.limit) && input.limit > 0 ? Math.min(Math.floor(input.limit), 50) : 20;
    const page = Number.isFinite(input?.page) && input.page > 0 ? Math.floor(input.page) : 1;
    if (url && !/^https?:\/\/(?:m\.|www\.)?webtoons\.com\//i.test(url)) {
      return wrapResponse(false, 400, null, null, input);
    }
    let mode, data;
    if (url && wantEpisodes) {
      mode = "episodes";
      data = await episodes(url, page);
    } else if (url) {
      mode = "detail";
      data = await detail(url);
    } else if (day) {
      mode = "trending";
      data = await trending(day);
    } else if (q) {
      mode = "search";
      const html = await fetchHtml(`${WEBTOONS_BASE}/id/search?keyword=${encodeURIComponent(q)}`);
      data = search(cheerio.load(html), q, limit);
    } else {
      return wrapResponse(false, 400, null, null, input);
    }
    return wrapResponse(true, 200, mode, data, input);
  } catch (e) {
    return wrapResponse(false, e.response?.status ?? 500, null, null, input);
  }
}

const isCLI = process.argv[1] && process.argv[1].includes("webtoon.js");
if (isCLI) {
  const args = process.argv.slice(2);
  const command = args[0];
  const target = args[1];
  const extra = args[2] ? parseInt(args[2], 10) : 1;
  if (!command || command === "--help") {
    console.log(`Usage:
  node webtoon.js search <query>
  node webtoon.js detail <url>
  node webtoon.js episodes <url> [page]
  node webtoon.js trending <day>
  node webtoon.js --help
`);
    process.exit(0);
  }
  const main = async () => {
    let input = {};
    switch (command) {
      case "search": if (!target) { console.error("Usage: node webtoon.js search <query>"); process.exit(1); } input = { query: target, limit: 20 }; break;
      case "detail": if (!target) { console.error("Usage: node webtoon.js detail <url>"); process.exit(1); } input = { url: target }; break;
      case "episodes": if (!target) { console.error("Usage: node webtoon.js episodes <url> [page]"); process.exit(1); } input = { url: target, episodes: true, page: extra || 1 }; break;
      case "trending": if (!target) { console.error("Usage: node webtoon.js trending <day>"); process.exit(1); } input = { day: target }; break;
      default: console.error("Unknown command"); process.exit(1);
    }
    const result = await webtoons(input);
    console.log(JSON.stringify(result, null, 2));
  };
  main().catch((err) => console.error("🔥", err.message));
}

export default {
  webtoons,
  search,
  detail,
  episodes,
  trending,
  fetchHtml,
  wrapResponse,
};

Rating

—(0)

Gimana snippet ini menurutmu?

</> Embed

Embed ke website / blog

<iframe src="https://qrtzcode.vercel.app/api/embed/comic/webtoon" style="width:100%;height:400px;border:none;border-radius:12px;"></iframe>