yh.
Note
copy ajh.
qrtz
javascript
9
4
https://www.webtoons.com
24 Jun 2026
Code
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
Gimana snippet ini menurutmu?
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>