diff --git a/docs/anime.md b/docs/anime.md
index 8305e0f381..38db46aa6e 100644
--- a/docs/anime.md
+++ b/docs/anime.md
@@ -133,6 +133,14 @@ pageClass: routes
+## Webtoons
+
+### 漫画更新
+
+
+
+比如漫画公主彻夜未眠的网址为https://www.webtoons.com/zh-hant/drama/gongzhuweimian/list?title_no=894, 则`lang=zh-hant`,`category=drama`,`name=gongzhucheyeweimian`,`id=894`.
+
## 嘀哩嘀哩 - dilidili
### 嘀哩嘀哩番剧更新
@@ -159,7 +167,7 @@ pageClass: routes
### 漫画更新
-
+
## 動漫狂
diff --git a/lib/router.js b/lib/router.js
index 3c55a7bf80..eaee47d123 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -883,7 +883,9 @@ router.get('/cartoonmad/comic/:id', require('./routes/cartoonmad/comic'));
// Vol
router.get('/vol/:mode?', require('./routes/vol/lastupdate'));
// 咚漫
-router.get('/dongmanmanhua/comic/:category/:name/:id', require('./routes/dongmanmanhua/comic'));
+router.get('/dongmanmanhua/:category/:name/:id', require('./routes/dongmanmanhua/comic'));
+// webtoons
+router.get('/webtoons/:lang/:category/:name/:id', require('./routes/webtoons/comic'));
// Tits Guru
router.get('/tits-guru/home', require('./routes/titsguru/home'));
diff --git a/lib/routes/dongmanmanhua/comic.js b/lib/routes/dongmanmanhua/comic.js
index 5368df7640..5ca4b9dc65 100644
--- a/lib/routes/dongmanmanhua/comic.js
+++ b/lib/routes/dongmanmanhua/comic.js
@@ -1,45 +1,45 @@
-const cheerio = require('cheerio');
+const parser = require('@/utils/rss-parser');
const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const dateParser = require('@/utils/dateParser');
const domain = 'https://www.dongmanmanhua.cn';
module.exports = async (ctx) => {
- const category = ctx.params.category;
- const name = ctx.params.name;
- const id = ctx.params.id;
-
+ const { category, name, id } = ctx.params;
const comicLink = `${domain}/${category}/${name}/list?title_no=${id}`;
- const { data } = await got.get(comicLink);
- const $ = cheerio.load(data);
+ const rssLink = `${domain}/${category}/${name}/rss?title_no=${id}`;
- const bookName = $('.detail_header .info .subj').text();
- const title = $('#_listUl span.subj')
- .map(function() {
- return $(this).text();
- })
- .get();
- const date = $('#_listUl span.date')
- .map(function() {
- return $(this)
- .text()
- .replace(/\n|\r|\t/g, '');
- })
- .get();
- const link = $('#_listUl > li > a')
- .map(function() {
- return 'https:' + $(this).attr('href');
- })
- .get();
- const resultItem = title.map((t, i) => ({
- title: t,
- pubDate: new Date(date[i]).toUTCString(),
- link: link[i],
- description: `${t}`,
- }));
-
- ctx.state.data = {
- title: `咚漫 ${bookName}`,
- link: comicLink,
- description: `咚漫 ${bookName}`,
- item: resultItem,
- };
+ let rss;
+ try {
+ const body = await parser.parseURL(rssLink);
+ rss = {
+ title: `咚漫 - ${body.title}`,
+ link: comicLink,
+ description: body.description,
+ item: body.items.map((x) => ({
+ title: x.title,
+ pubDate: dateParser(x.pubDate, 'DD MMMM YYYY HH:mm:ss', 'zh-cn'),
+ link: x.link,
+ description: `${x.title}`,
+ })),
+ };
+ } catch (error) {
+ const { body } = await got.get(comicLink);
+ const $ = cheerio.load(body);
+ rss = {
+ title: `咚漫 - ${$('.detail_header .info .subj').text()}`,
+ link: comicLink,
+ description: $('p.summary').text(),
+ item: $('#_listUl > li > a')
+ .toArray()
+ .map((ep) => ({
+ title: $('.subj > span', ep).text(),
+ pubDate: new Date($('.date', ep).text()).toUTCString(),
+ link: $(ep).attr('href'),
+ description: `${$('.subj > span', ep).text()}`,
+ })),
+ };
+ }
+ rss.item = rss.item.sort((a, b) => (new Date(a.pubDate) > new Date(b.pubDate) ? -1 : 1));
+ ctx.state.data = rss;
};
diff --git a/lib/routes/webtoons/comic.js b/lib/routes/webtoons/comic.js
new file mode 100644
index 0000000000..d7b16983b2
--- /dev/null
+++ b/lib/routes/webtoons/comic.js
@@ -0,0 +1,54 @@
+const parser = require('@/utils/rss-parser');
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const dateParser = require('@/utils/dateParser');
+const domain = 'https://www.webtoons.com';
+
+module.exports = async (ctx) => {
+ const { lang, category, name, id } = ctx.params;
+ const comicLink = `${domain}/${lang}/${category}/${name}/list?title_no=${id}`;
+ const rssLink = `${domain}/${lang}/${category}/${name}/rss?title_no=${id}`;
+ const dP = (html, lang) => {
+ if (lang === 'zh-cn' || lang === 'zh-hant') {
+ return dateParser(html, 'DD MMMM YYYY HH:mm:ss', lang);
+ } else if (lang === 'en') {
+ return dateParser(html, 'DD MMM YYYY HH:mm:ss');
+ } else {
+ return html;
+ }
+ };
+
+ let rss;
+ try {
+ const body = await parser.parseURL(rssLink);
+ rss = {
+ title: `Webtoons - ${body.title}`,
+ link: comicLink,
+ description: body.description,
+ item: body.items.map((x) => ({
+ title: x.title,
+ pubDate: dP(x.pubDate, lang),
+ link: x.link,
+ description: `${x.title}`,
+ })),
+ };
+ } catch (error) {
+ const { body } = await got.get(comicLink);
+ const $ = cheerio.load(body);
+ rss = {
+ title: `Webtoons - ${$('.detail_header .info .subj').text()}`,
+ link: comicLink,
+ description: $('p.summary').text(),
+ item: $('#_listUl > li > a')
+ .toArray()
+ .map((ep) => ({
+ title: $('.subj > span', ep).text(),
+ pubDate: new Date($('.date', ep).text()).toUTCString(),
+ link: $(ep).attr('href'),
+ description: `${$('.subj > span', ep).text()}`,
+ })),
+ };
+ }
+ rss.item = rss.item.sort((a, b) => (new Date(a.pubDate) > new Date(b.pubDate) ? -1 : 1));
+ ctx.state.data = rss;
+};
diff --git a/lib/utils/dateParser.js b/lib/utils/dateParser.js
new file mode 100644
index 0000000000..887dd296cf
--- /dev/null
+++ b/lib/utils/dateParser.js
@@ -0,0 +1,92 @@
+const date = require('./date');
+const dayjs = require('dayjs');
+const customParseFormat = require('dayjs/plugin/customParseFormat');
+const logger = require('./logger');
+const utc = require('dayjs/plugin/utc');
+dayjs.extend(utc);
+dayjs.extend(customParseFormat);
+
+/**
+ * Convert unconventional i8n to the one supported by dayjs https://bit.ly/2psVwIJ
+ * @param {String} x i8n string
+ */
+const i8nconv = (x) => {
+ const c = {
+ 'zh-hans': 'zh-cn',
+ 'zh-chs': 'zh-cn',
+ 'zh-sg': 'zh-cn',
+ 'zh-hant': 'zh-hk',
+ 'zh-cht': 'zh-hk',
+ 'zh-mo': 'zh-hk',
+ };
+ for (const prop in c) {
+ if (RegExp(`^${prop}$`, 'i').test(x)) {
+ x = c[prop];
+ break;
+ }
+ }
+ return x;
+};
+
+/**
+ * A function to convert a string of time based on specified format
+ * @param {string} [html] A string of time to convert.
+ * @param {string} [customFormat=undefined] Format to parse html by dayjs.
+ * @param {string} [lang=en] Language (must be supported by dayjs).
+ * @param {int} [htmlOffset=0] UTC offset of html. It will be neglected if html contains timezone indicated by strings like "+0800".
+ */
+const tStringParser = (html, customFormat = undefined, lang = 'en', htmlOffset = 0) => {
+ lang = i8nconv(lang);
+
+ // Remove weekdays and comma from the string
+ // dayjs v1.8.16 is not able to parse weekdays
+ // https://github.com/iamkun/dayjs/blob/dev/docs/en/Plugin.md#list-of-all-available-format-tokens
+ // We don't remove weekdayMini since the month may contains weekdayMini, like "六" in "六月"
+ let removeStr = [];
+ if (lang !== 'en') {
+ try {
+ require(`dayjs/locale/${lang}`);
+ if (/^zh/.test(lang)) {
+ removeStr = removeStr.concat([',']);
+ }
+ // Add locale
+ dayjs.locale(lang);
+ } catch (error) {
+ logger.error(`Locale "${lang}" passed to dateParser is not supported by dayjs`);
+ return date(html);
+ }
+ }
+ Object.values(dayjs.Ls).forEach((k) => {
+ ['weekdays', 'weekdaysShort'].forEach((x) => {
+ if (k.hasOwnProperty(x)) {
+ const a = k[x].map((z) => `${z}`);
+ removeStr = removeStr.concat(...a);
+ }
+ });
+ });
+ removeStr = removeStr.concat([',', 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']);
+ let htmlP = html;
+ removeStr.forEach((x) => {
+ // Order matters
+ htmlP = htmlP.replace(RegExp(x, 'gi'), '');
+ });
+
+ const d = dayjs.utc(htmlP, customFormat);
+ // console.log(htmlP,d)
+ if (d.isValid()) {
+ if (/[+-](\d{2}:?\d{2})/.test(html)) {
+ return d.toDate().toUTCString();
+ } else {
+ return d
+ .add(htmlOffset, 'h')
+ .toDate()
+ .toUTCString();
+ }
+ } else {
+ return date(html);
+ }
+};
+
+tStringParser.i8nconv = i8nconv;
+
+module.exports = tStringParser;
diff --git a/test/utils/dateParser.js b/test/utils/dateParser.js
new file mode 100644
index 0000000000..3b11881cb3
--- /dev/null
+++ b/test/utils/dateParser.js
@@ -0,0 +1,103 @@
+const dateParser = require('../../lib/utils/dateParser');
+const dayjs = require('dayjs');
+const utc = require('dayjs/plugin/utc');
+const MockDate = require('mockdate');
+dayjs.extend(utc);
+
+describe('dateParser', () => {
+ MockDate.set('2019-01-01');
+ const now = new Date();
+ const serverOffset = now.getTimezoneOffset() / 60;
+ require('dayjs/locale/zh-cn');
+ require('dayjs/locale/zh-hk');
+
+ // ['en', 'zh-cn', 'zh-hant'].forEach((lang0) => {
+ // const lang = dateParser.i8nconv(lang0);
+ // dayjs.locale(lang);
+
+ // Test of input as a string of UTC Time
+ test(`UTCString`, () => {
+ expect(
+ dateParser(
+ dayjs
+ .utc(now.toUTCString())
+ .locale('en')
+ .format('YYYY-MM-DD HH:mm:ss')
+ )
+ ).toBe(now.toUTCString());
+ });
+
+ // Test of input as a string of local time with timezone in ISO 8601
+ test(`ISO 8601`, () => {
+ expect(
+ dateParser(
+ dayjs(now.toUTCString())
+ .locale('en')
+ .format('YYYY-MM-DDTHH:mm:ssZ')
+ )
+ ).toBe(now.toUTCString());
+ });
+
+ // Test of input as a string of local time with timezone set by htmlOffset
+ test(`htmlOffset`, () => {
+ expect(
+ dateParser(
+ dayjs(now.toUTCString())
+ .locale('en')
+ .format('YYYY-MM-DDTHH:mm:ss'),
+ null,
+ 'en',
+ serverOffset
+ )
+ ).toBe(now.toUTCString());
+ });
+
+ // Test of input as a string of UTC Time with week
+ test(`en UTCString with week`, () => {
+ expect(
+ dateParser(
+ dayjs
+ .utc(now.toUTCString())
+ .locale('en')
+ .format('dddd, DD MMMM YYYY HH:mm:ss'),
+ 'DD MMMM YYYY HH:mm:ss'
+ )
+ ).toBe(now.toUTCString());
+ });
+
+ test(`zh-cn UTCString with week`, () => {
+ expect(
+ dateParser(
+ dayjs
+ .utc(now.toUTCString())
+ .locale('zh-cn')
+ .format('dddd, DD MMMM YYYY HH:mm:ss'),
+ 'DD MMMM YYYY HH:mm:ss',
+ 'zh-cn'
+ )
+ ).toBe(now.toUTCString());
+ });
+
+ test(`zh-hant UTCString with week`, () => {
+ expect(
+ dateParser(
+ dayjs
+ .utc(now.toUTCString())
+ .locale(dateParser.i8nconv('zh-hant'))
+ .format('dddd, DD MMMM YYYY HH:mm:ss'),
+ 'DD MMMM YYYY HH:mm:ss',
+ 'zh-hant'
+ )
+ ).toBe(now.toUTCString());
+ });
+
+ // fallback
+ test('fallback', () => {
+ expect(+new Date(dateParser('10分钟前'))).toBe(+now - 10 * 60 * 1000);
+ });
+
+ // error handling
+ test('error handling', () => {
+ expect(+new Date(dateParser('10分钟前', null, 'Klingon'))).toBe(+now - 10 * 60 * 1000);
+ });
+});