diff --git a/docs/en/traditional-media.md b/docs/en/traditional-media.md
index 5495a64ac..79a406e9d 100644
--- a/docs/en/traditional-media.md
+++ b/docs/en/traditional-media.md
@@ -175,6 +175,30 @@ Generates full-text feeds that the official feed doesn't provide.
+## Korean Central News Agency (KCNA)
+
+### News
+
+
+
+| Language | 조선어 | English | 中国语 | Русский | Español | 日本語 |
+|----------|------|---------|------|---------|---------|------|
+| `:lang` | `kp` | `en` | `cn` | `ru` | `es` | `jp` |
+
+| Category | `:category` |
+|------------------------------------------------------------------|------------------------------------|
+| WPK General Secretary **Kim Jong Un**'s Revolutionary Activities | `54c0ca4ca013a92cc9cf95bd4004c61a` |
+| Latest News (default) | `1ee9bdb7186944f765208f34ecfb5407` |
+| Top News | `5394b80bdae203fadef02522cfb578c0` |
+| Home News | `b2b3bcc1b0a4406ab0c36e45d5db58db` |
+| Documents | `a8754921399857ebdbb97a98a1e741f5` |
+| World | `593143484cf15d48ce85c26139582395` |
+| Society-Life | `93102e5a735d03979bc58a3a7aefb75a` |
+| External | `0f98b4623a3ef82aeea78df45c423fd0` |
+| News Commentary | `12c03a49f7dbe829bceea8ac77088c21` |
+
+
+
## Ming Pao
### Ming Pao Daily
diff --git a/docs/traditional-media.md b/docs/traditional-media.md
index fc34f0e6d..e1e4fce36 100644
--- a/docs/traditional-media.md
+++ b/docs/traditional-media.md
@@ -670,6 +670,30 @@ IT・科学 tech_science
+## 朝鲜中央通讯社
+
+### 新闻
+
+
+
+| 语言 | 조선어 | English | 中国语 | Русский | Español | 日本語 |
+| ------- | ---- | ------- | ---- | ------- | ------- | ---- |
+| `:lang` | `kp` | `en` | `cn` | `ru` | `es` | `jp` |
+
+| 分类 | `:category` |
+| ----------------------- | ---------------------------------- |
+| 朝鲜劳动党总书记**金正恩**同志革命活动新闻 | `54c0ca4ca013a92cc9cf95bd4004c61a` |
+| 最新新闻 (默认) | `1ee9bdb7186944f765208f34ecfb5407` |
+| 主要新闻 | `5394b80bdae203fadef02522cfb578c0` |
+| 国内新闻 | `b2b3bcc1b0a4406ab0c36e45d5db58db` |
+| 文件 | `a8754921399857ebdbb97a98a1e741f5` |
+| 国际新闻 | `593143484cf15d48ce85c26139582395` |
+| 社会-生活 | `93102e5a735d03979bc58a3a7aefb75a` |
+| 对外关系 | `0f98b4623a3ef82aeea78df45c423fd0` |
+| 时事解说 | `12c03a49f7dbe829bceea8ac77088c21` |
+
+
+
## 第一财经
### 直播区
diff --git a/lib/v2/kcna/maintainer.js b/lib/v2/kcna/maintainer.js
new file mode 100644
index 000000000..3c8fed33c
--- /dev/null
+++ b/lib/v2/kcna/maintainer.js
@@ -0,0 +1,3 @@
+module.exports = {
+ '/:lang/:category?': ['Rongronggg9'],
+};
diff --git a/lib/v2/kcna/news.js b/lib/v2/kcna/news.js
new file mode 100644
index 000000000..971c1cfbe
--- /dev/null
+++ b/lib/v2/kcna/news.js
@@ -0,0 +1,81 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const asyncPool = require('tiny-async-pool');
+const { art } = require('@/utils/render');
+const { parseJucheDate, fixDesc, fetchPhoto, fetchVideo } = require('./utils');
+const path = require('path');
+
+module.exports = async (ctx) => {
+ const { lang, category = '1ee9bdb7186944f765208f34ecfb5407' } = ctx.params;
+
+ const rootUrl = 'http://www.kcna.kp';
+ const pageUrl = `${rootUrl}/${lang}/category/articles/q/${category}.kcmsf`;
+
+ const response = await got(pageUrl);
+ const $ = cheerio.load(response.data);
+
+ // fix ???
+ const title = $('head > title')
+ .text()
+ .replace(/<[^>]*>/g, '');
+
+ const list = $('.article-link li a')
+ .map((_, item) => {
+ item = $(item);
+ const dateElem = item.find('.publish-time');
+ const dateString = dateElem.text();
+ dateElem.remove();
+ return {
+ title: item.text(),
+ link: rootUrl + item.attr('href'),
+ pubDate: parseJucheDate(dateString),
+ };
+ })
+ .get();
+
+ // avoid being IP-banned
+ // if being banned, 103.35.255.254 (the last hop before www.kcna.kp - 175.45.176.71) will drop the packet
+ // verify that with `mtr www.kcna.kp -Tz`
+ const items = [];
+ for await (const item of asyncPool(3, list, (item) =>
+ ctx.cache.tryGet(item.link, async () => {
+ const response = await got(item.link);
+ const $ = cheerio.load(response.data);
+ item.title = $('article-main-title').text() || item.title;
+
+ const dateElem = $('.publish-time');
+ const dateString = dateElem.text();
+ dateElem.remove();
+ item.pubDate = parseJucheDate(dateString) || item.pubDate;
+
+ const description = fixDesc($, $('.article-content-body .content-wrapper'));
+
+ // add picture and video
+ const media = $('.media-icon a')
+ .map((_, elem) => rootUrl + elem.attribs.href)
+ .get();
+ let photo, video;
+ await Promise.all(
+ media.map(async (medium) => {
+ if (medium.includes('/photo/')) {
+ photo = await fetchPhoto(ctx, medium);
+ } else if (medium.includes('/video/')) {
+ video = await fetchVideo(ctx, medium);
+ }
+ })
+ );
+
+ item.description = art(path.join(__dirname, 'templates/news.art'), { description, photo, video });
+
+ return item;
+ })
+ )) {
+ items.push(item);
+ }
+
+ ctx.state.data = {
+ title,
+ link: pageUrl,
+ item: items,
+ };
+};
diff --git a/lib/v2/kcna/radar.js b/lib/v2/kcna/radar.js
new file mode 100644
index 000000000..7e10f51c0
--- /dev/null
+++ b/lib/v2/kcna/radar.js
@@ -0,0 +1,61 @@
+module.exports = {
+ 'kcna.kp': {
+ _name: '朝鲜中央通讯社',
+ www: [
+ {
+ title: '朝鲜劳动党总书记金正恩同志革命活动新闻',
+ docs: 'https://docs.rsshub.app/traditional-media.html#chao-xian-zhong-yang-tong-xun-she',
+ source: '/:lang/category/articles/q/54c0ca4ca013a92cc9cf95bd4004c61a.kcmsf',
+ target: '/kcna/:lang/54c0ca4ca013a92cc9cf95bd4004c61a',
+ },
+ {
+ title: '最新新闻',
+ docs: 'https://docs.rsshub.app/traditional-media.html#chao-xian-zhong-yang-tong-xun-she',
+ source: ['/:lang', '/:lang/category/articles/q/1ee9bdb7186944f765208f34ecfb5407.kcmsf', '/:lang/category/articles.kcmsf'],
+ target: '/kcna/:lang',
+ },
+ {
+ title: '主要新闻',
+ docs: 'https://docs.rsshub.app/traditional-media.html#chao-xian-zhong-yang-tong-xun-she',
+ source: '/:lang/category/articles/q/5394b80bdae203fadef02522cfb578c0.kcmsf',
+ target: '/kcna/:lang/5394b80bdae203fadef02522cfb578c0',
+ },
+ {
+ title: '国内新闻',
+ docs: 'https://docs.rsshub.app/traditional-media.html#chao-xian-zhong-yang-tong-xun-she',
+ source: '/:lang/category/articles/q/b2b3bcc1b0a4406ab0c36e45d5db58db.kcmsf',
+ target: '/kcna/:lang/b2b3bcc1b0a4406ab0c36e45d5db58db',
+ },
+ {
+ title: '文件',
+ docs: 'https://docs.rsshub.app/traditional-media.html#chao-xian-zhong-yang-tong-xun-she',
+ source: '/:lang/category/articles/q/a8754921399857ebdbb97a98a1e741f5.kcmsf',
+ target: '/kcna/:lang/a8754921399857ebdbb97a98a1e741f5',
+ },
+ {
+ title: '国际新闻',
+ docs: 'https://docs.rsshub.app/traditional-media.html#chao-xian-zhong-yang-tong-xun-she',
+ source: '/:lang/category/articles/q/593143484cf15d48ce85c26139582395.kcmsf',
+ target: '/kcna/:lang/593143484cf15d48ce85c26139582395',
+ },
+ {
+ title: '社会-生活',
+ docs: 'https://docs.rsshub.app/traditional-media.html#chao-xian-zhong-yang-tong-xun-she',
+ source: '/:lang/category/articles/q/93102e5a735d03979bc58a3a7aefb75a.kcmsf',
+ target: '/kcna/:lang/93102e5a735d03979bc58a3a7aefb75a',
+ },
+ {
+ title: '对外关系',
+ docs: 'https://docs.rsshub.app/traditional-media.html#chao-xian-zhong-yang-tong-xun-she',
+ source: '/:lang/category/articles/q/0f98b4623a3ef82aeea78df45c423fd0.kcmsf',
+ target: '/kcna/:lang/0f98b4623a3ef82aeea78df45c423fd0',
+ },
+ {
+ title: '时事解说',
+ docs: 'https://docs.rsshub.app/traditional-media.html#chao-xian-zhong-yang-tong-xun-she',
+ source: '/:lang/category/articles/q/12c03a49f7dbe829bceea8ac77088c21.kcmsf',
+ target: '/kcna/:lang/12c03a49f7dbe829bceea8ac77088c21',
+ },
+ ],
+ },
+};
diff --git a/lib/v2/kcna/router.js b/lib/v2/kcna/router.js
new file mode 100644
index 000000000..70f528db7
--- /dev/null
+++ b/lib/v2/kcna/router.js
@@ -0,0 +1,3 @@
+module.exports = function (router) {
+ router.get('/:lang/:category?', require('./news'));
+};
diff --git a/lib/v2/kcna/templates/news.art b/lib/v2/kcna/templates/news.art
new file mode 100644
index 000000000..28ac97d87
--- /dev/null
+++ b/lib/v2/kcna/templates/news.art
@@ -0,0 +1,9 @@
+{{@ description }}
+{{ if photo }}
+
+{{@ photo }}
+{{ /if }}
+{{ if video }}
+
+{{@ video }}
+{{ /if }}
diff --git a/lib/v2/kcna/utils.js b/lib/v2/kcna/utils.js
new file mode 100644
index 000000000..841313d76
--- /dev/null
+++ b/lib/v2/kcna/utils.js
@@ -0,0 +1,64 @@
+const { parseDate } = require('@/utils/parse-date');
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+
+const rootUrl = 'http://www.kcna.kp';
+
+const parseJucheDate = (dateString) => {
+ if (!dateString) {
+ return null;
+ }
+
+ // https://en.wikipedia.org/wiki/Juche_calendar
+ const dateMatch = dateString.match(/(\d+)\D(\d+)\D(\d+)/);
+ const [jucheYear, month, day] = dateMatch ? dateMatch.slice(1) : [null, null, null];
+ if (jucheYear && month && day) {
+ const year = parseInt(jucheYear, 10) + 1911;
+ return parseDate(`${year}-${month}-${day}`, 'YYYY-M-D');
+ }
+ return null;
+};
+
+const fixDesc = ($, elem) => {
+ // ??? => ???
+ const $elem = $(elem);
+ $elem.find('.fSpecCs').each((_, item) => {
+ if (item.parent.name === 'nobr') {
+ $(item).unwrap();
+ }
+ item.name = 'b';
+ item.attribs = {};
+ });
+ return elem.html();
+};
+
+const fetchPhoto = (ctx, url) =>
+ ctx.cache.tryGet(url, async () => {
+ const res = await got(url);
+ const $ = cheerio.load(res.data);
+ let html = '';
+ $('.content img').each((_, item) => {
+ const src = item.attribs.src;
+ if (src) {
+ html += html ? `
` : `
`;
+ }
+ });
+ return html;
+ });
+
+const fetchVideo = (ctx, url) =>
+ ctx.cache.tryGet(url, async () => {
+ const res = await got(url);
+ const $ = cheerio.load(res.data);
+ const js = $('script[type="text/javascript"]:not([src])').html();
+ let sources = js.match(/<[^>]*source[^>]+src[^>]+>/g);
+ sources = sources && sources.map((item) => item.replace(/'/g, '"').replace(/src="([^"]+)"/g, `src="${rootUrl}$1"`));
+ return ``;
+ });
+
+module.exports = {
+ parseJucheDate,
+ fixDesc,
+ fetchPhoto,
+ fetchVideo,
+};