Skip to content
This repository was archived by the owner on Mar 23, 2025. It is now read-only.

Commit 8119dec

Browse files
authored
Merge pull request #11 from browningluke/master
Add Webtoons plugin
2 parents 39b7590 + 1f4db53 commit 8119dec

File tree

3 files changed

+198
-0
lines changed

3 files changed

+198
-0
lines changed

plugins/webtoons/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Webtoons Plugin
2+
3+
This is the Mango plugin for [Webtoons](https://www.webtoons.com/en/).
4+
5+
Note that
6+
7+
- All downloaded chapters will be placed in their respective series folder in your library.
8+
- Cloudflare rate limits extensive downloads. Adjust `wait_seconds` in `info.json` if you plan to download many chapters in one go (or chapters with many pages).
9+
- For webtoons with a large number of chapters (200+), getting the chapter list takes marginally longer amount of time.
10+
11+
Maintained by [@browningluke](https://github.com/browningluke).

plugins/webtoons/index.js

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
var currentPage = 0;
2+
var digits = 0;
3+
var imgURLs;
4+
5+
const BASE_URL = "https://www.webtoons.com";
6+
const MOBILE_URL = "https://m.webtoons.com";
7+
const SEARCH_URL = "https://ac.webtoons.com/ac?q=en%5E";
8+
const SEARCH_PARAMS = "&q_enc=UTF-8&st=1&r_format=json&r_enc=UTF-8";
9+
10+
const LIST_ENDPOINT = "/episodeList?titleNo=";
11+
const PAGE_QUERY = "&page=";
12+
13+
function listChapters(query) {
14+
15+
try {
16+
var searchResp = mango.get(SEARCH_URL + encodeURI(query) + SEARCH_PARAMS).body;
17+
var search = JSON.parse(searchResp);
18+
} catch (error) {
19+
mango.raise("An error occured while searching.");
20+
}
21+
22+
if (search["items"].length == 0) mango.raise("Could not find a webtoon with that title.");
23+
24+
var mangaID;
25+
for (var i = 0; i < search["items"][0].length; i++) {
26+
var item = search["items"][0][i];
27+
28+
// Get first webtoon, ignore authors
29+
if (item[1][0] == "TITLE") {
30+
mangaID = item[3][0];
31+
break;
32+
}
33+
}
34+
35+
if (!mangaID) mango.raise("Could not find a webtoon with that title.");
36+
37+
try {
38+
var resp = mango.get(BASE_URL + LIST_ENDPOINT + mangaID);
39+
var urlLocation = resp.headers.Location;
40+
} catch (error) {
41+
mango.raise("Could not get webtoon page.");
42+
}
43+
44+
if (!urlLocation) mango.raise("Could not get webtoon page.");
45+
46+
chapters = [];
47+
var html = mango.get(MOBILE_URL + urlLocation, {
48+
'referer': MOBILE_URL
49+
}).body;
50+
51+
if (!html) mango.raise("Failed to get chapter list.");
52+
53+
var liChapters = mango.css(html, "ul#_episodeList li[id*=episode]")
54+
55+
if (!liChapters) mango.raise("Failed to find chapters.");
56+
57+
liChapters.forEach(function(chapter) {
58+
var url = mango.attribute(mango.css(chapter, "a")[0], 'href');
59+
60+
var chapterIDRegex = /webtoons\.com\/\w{2}\/.+\/(\w-?)+\/(.+)\//;
61+
var chapterIDMatch = chapterIDRegex.exec(url);
62+
63+
var chapterID;
64+
try {
65+
chapterID = chapterIDMatch[2];
66+
} catch (error) {
67+
mango.raise("Failed to get a chapter ID.");
68+
}
69+
70+
var subjectNode = mango.css(chapter, ".ellipsis")[0]
71+
var subject = mango.text(subjectNode);
72+
73+
if (!subject) mango.raise("Failed to get a chapter name.")
74+
75+
var numNode = mango.css(chapter, ".col.num");
76+
var num = mango.text(numNode[0]).substring(1);
77+
78+
var dateNode = mango.css(chapter, ".date");
79+
var date = mango.text(dateNode[0]);
80+
date = date.replace("UP", ""); // Remove webtoons "UP" tag on latest chapter
81+
82+
// Encode chapter in following format: idMANGAIDchCHAPTERIDnumNUM_NUM
83+
var chapterFullID = "id" + mangaID + "ch" + chapterID + "num" + num;
84+
chapterFullID = chapterFullID.replace(/\-/g, "_");
85+
86+
if (!chapterFullID) mango.raise("Failed to generate chapter full ID.");
87+
88+
slimObj = {}
89+
slimObj['id'] = chapterFullID;
90+
slimObj['title'] = subject;
91+
slimObj['#'] = num;
92+
slimObj['Date'] = date;
93+
94+
chapters.push(slimObj);
95+
});
96+
97+
try {
98+
var chapterTitleNode = mango.css(html, 'meta[property="og:title"]');
99+
var chapterTitle = mango.attribute(chapterTitleNode[0], "content");
100+
} catch (error) {
101+
mango.raise("Could not get title.");
102+
}
103+
104+
return JSON.stringify({
105+
chapters: chapters,
106+
title: chapterTitle
107+
});
108+
}
109+
110+
function selectChapter(id) {
111+
var mangaIDMatch = /id(\d+)ch(.+)num(\d+)/.exec(id);
112+
var mangaID = mangaIDMatch[1];
113+
var mangaChapterSlug = mangaIDMatch[2].replace(/\_/g, "-");
114+
var mangaChapterNum = mangaIDMatch[3].replace(/\_/g, ".");
115+
116+
try {
117+
var resp = mango.get(BASE_URL + LIST_ENDPOINT + mangaID);
118+
var urlLocation = resp.headers.Location;
119+
} catch (error) {
120+
mango.raise("Could not get webtoon chapter list.");
121+
}
122+
123+
var viewerURL = BASE_URL + urlLocation.replace(/list/, mangaChapterSlug + "/viewer")
124+
+ "&episode_no=" + mangaChapterNum;
125+
126+
var html = mango.get(viewerURL).body;
127+
128+
if(!html) mango.raise("Failed to load chapter images.");
129+
130+
var titleNode = mango.css(html, ".subj_info .subj_episode");
131+
132+
// Chapters get saved as NUM - CHAPTERNAME.cbz
133+
// This is done since some webtoons have names like:
134+
// `Episode 10` and `Season 2 Episode 10`, which
135+
// throws off the sorting.
136+
var chapterTitle = mangaChapterNum + " - " + mango.text(titleNode[0]);
137+
138+
var imgList = mango.css(html, "#_imageList img");
139+
140+
imgURLs = [];
141+
imgList.forEach(function(element) {
142+
imgURLs.push(
143+
mango.attribute(element, "data-url")
144+
);
145+
})
146+
147+
currentPage = 0;
148+
digits = Math.floor(Math.log10(imgURLs.length)) + 1;
149+
150+
return JSON.stringify({
151+
title: chapterTitle,
152+
pages: imgURLs.length
153+
});
154+
}
155+
156+
function nextPage() {
157+
if (currentPage >= imgURLs.length) {
158+
return JSON.stringify({});
159+
}
160+
161+
var url = imgURLs[currentPage];
162+
var filename = pad(currentPage, digits) + '.' + /\.(\w{3})($|\?\w+)/.exec(url)[1];
163+
164+
currentPage += 1;
165+
return JSON.stringify({
166+
url: url,
167+
filename: filename,
168+
headers: {
169+
'referer': BASE_URL + "/"
170+
}
171+
});
172+
}
173+
174+
// https://stackoverflow.com/a/10073788
175+
function pad(n, width, z) {
176+
z = z || '0';
177+
n = n + '';
178+
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
179+
}

plugins/webtoons/info.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "Webtoons",
3+
"title": "Webtoons",
4+
"author": "TheBritishAccent - [email protected]",
5+
"version": "v1.0",
6+
"placeholder": "Search Webtoons",
7+
"wait_seconds": 1
8+
}

0 commit comments

Comments
 (0)