diff --git a/assets/radar-rules.js b/assets/radar-rules.js
index 679f5dba00..aaadae7bfc 100644
--- a/assets/radar-rules.js
+++ b/assets/radar-rules.js
@@ -3042,6 +3042,34 @@
},
],
},
+ 'e-hentai.org/': {
+ _name: 'E-Hentai',
+ '.': [
+ {
+ title: '收藏',
+ docs: 'https://docs.rsshub.app/picture.html#ehentai',
+ source: '/favorites.php',
+ target: '/ehentai/favorites',
+ },
+ {
+ title: '标签',
+ docs: 'https://docs.rsshub.app/picture.html#ehentai',
+ source: '/tag/:tag',
+ target: '/ehentai/tag/:tag',
+ },
+ {
+ title: '搜索',
+ docs: 'https://docs.rsshub.app/picture.html#ehentai',
+ source: '/',
+ target: (params, url) => {
+ const keyword = new URL(url).searchParams.toString();
+ if (keyword) {
+ return `/ehentai/search/${keyword}`;
+ }
+ },
+ },
+ ],
+ },
'iyingdi.com': {
_name: '旅法师营地',
www: [
diff --git a/docs/install/README.md b/docs/install/README.md
index efdecd5c6c..9a7f9b71fc 100644
--- a/docs/install/README.md
+++ b/docs/install/README.md
@@ -696,3 +696,8 @@ RSSHub 支持使用访问密钥 / 码,白名单和黑名单三种方式进行
| | cloudflare |
| | cloudflare |
| | digitalocean |
+
+- E-Hentai
+ - `EH_IPB_MEMBER_ID`: E-Hentai 账户登录后 cookie 的 `ipb_member_id` 值
+ - `EH_IPB_PASS_HASH`: E-Hentai 账户登录后 cookie 的 `ipb_pass_hash` 值
+ - `EH_SK`: E-Hentai 账户登录后 cookie 中的`sk`值
diff --git a/docs/picture.md b/docs/picture.md
index 6a23adbc18..b7b6eddf8e 100644
--- a/docs/picture.md
+++ b/docs/picture.md
@@ -479,3 +479,17 @@ R18 显示
### 最新主题
+
+## E-Hentai
+
+### 收藏
+
+
+
+### 标签
+
+
+
+### 搜索
+
+
diff --git a/lib/config.js b/lib/config.js
index 387acc21d5..6474d0fd52 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -193,6 +193,11 @@ const calculateValue = () => {
wordpress: {
cdnUrl: envs.WORDPRESS_CDN,
},
+ ehentai: {
+ ipb_member_id: envs.EH_IPB_MEMBER_ID,
+ ipb_pass_hash: envs.EH_IPB_PASS_HASH,
+ sk: envs.EH_SK,
+ },
};
};
calculateValue();
diff --git a/lib/router.js b/lib/router.js
index caebe2ee0c..b58dbd5e70 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -4140,6 +4140,11 @@ router.get('/tongli/news/:type', lazyloadRouteHandler('./routes/tongli/news'));
// OR
router.get('/or/:id?', lazyloadRouteHandler('./routes/or'));
+// e-hentai
+router.get('/ehentai/favorites/:favcat?/:order?/:page?/:bittorrent?', lazyloadRouteHandler('./routes/ehentai/favorites'));
+router.get('/ehentai/search/:params?/:page?/:bittorrent?', lazyloadRouteHandler('./routes/ehentai/search'));
+router.get('/ehentai/tag/:tag/:page?/:bittorrent?', lazyloadRouteHandler('./routes/ehentai/tag'));
+
// 字型故事
router.get('/fontstory', require('./routes/fontstory/tw'));
diff --git a/lib/routes/ehentai/ehapi.js b/lib/routes/ehentai/ehapi.js
new file mode 100644
index 0000000000..7be6c4efe5
--- /dev/null
+++ b/lib/routes/ehentai/ehapi.js
@@ -0,0 +1,134 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const config = require('@/config').value.ehentai;
+
+const headers = {};
+const has_cookie = config.ipb_member_id && config.ipb_pass_hash && config.sk;
+if (has_cookie) {
+ const { ipb_member_id, ipb_pass_hash, sk } = config;
+ headers.cookie = `ipb_member_id=${ipb_member_id};ipb_pass_hash=${ipb_pass_hash};sk=${sk}`;
+}
+
+function ehgot(url) {
+ return got({ method: 'get', url: `https://e-hentai.org/${url}`, headers });
+}
+
+async function parsePage(cache, data, get_bittorrent = false) {
+ const $ = cheerio.load(data);
+ const table = $('table[class="itg gltc"] tbody');
+ if (!table) {
+ return [];
+ }
+
+ async function parseTableRow(cache, element) {
+ const el = $(element);
+ const el_a = el.find('td[class="gl3c glname"] a');
+ const el_img = el.find('td.gl2c div.glthumb div img');
+ const title = el_a.find('div.glink').html();
+ const thumbnail = el_img.attr('data-src') ? el_img.attr('data-src') : el_img.attr('src');
+ const description = `
`;
+ const pubDate = el.find('div[id^="posted_"]').html();
+ const link = el_a.attr('href');
+ if (title && link) {
+ const item = { title, description, pubDate, link };
+ if (get_bittorrent) {
+ const el_down = el.find('td.gl2c div div.gldown');
+ const bittorrent_page_url = el_down.find('a').attr('href');
+ if (bittorrent_page_url) {
+ const bittorrent_url = await getBittorrent(cache, bittorrent_page_url);
+ if (bittorrent_url) {
+ item.enclosure_url = bittorrent_url;
+ item.enclosure_type = 'application/x-bittorrent';
+ item.bittorrent_page_url = bittorrent_page_url;
+ }
+ }
+ }
+ return item;
+ }
+ }
+
+ const item_Promises = [];
+ table.children('tr').each((index, element) => {
+ item_Promises.push(parseTableRow(cache, element));
+ });
+ const items_with_null = await Promise.all(item_Promises);
+
+ const items = [];
+ for (const item of items_with_null) {
+ if (item) {
+ items.push(item);
+ }
+ }
+ return items;
+}
+
+let p = '';
+
+function getBittorrent(cache, bittorrent_page_url) {
+ return cache.tryGet(bittorrent_page_url, async () => {
+ try {
+ const response = await got({ method: 'get', url: bittorrent_page_url, headers });
+ const $ = cheerio.load(response.data);
+ const el_forms = $('form').get();
+ let bittorrent_url = undefined;
+ for (const el_form of el_forms) {
+ const el_a = $(el_form).find('a');
+ const onclick = el_a.attr('onclick');
+ if (onclick) {
+ const match = onclick.match(/'(.*?)'/);
+ if (match) {
+ bittorrent_url = match[1];
+ const match_p = bittorrent_url.match(/torrent\?p=(.*?)$/);
+ if (match_p) {
+ p = match_p[1];
+ }
+ break;
+ }
+ }
+ }
+ return bittorrent_url;
+ } catch {
+ return undefined;
+ }
+ });
+}
+
+function updateBittorrent_url(cache, items) {
+ // 下种子文件需要动态密码,密码每几次请求就更新一次
+ for (const item of items) {
+ if (item.enclosure_url) {
+ item.enclosure_url = item.enclosure_url.replace(/torrent\?p=.*$/, `torrent?p=${p}`);
+ cache.set(item.bittorrent_page_url, item.enclosure_url);
+ }
+ }
+ return items;
+}
+
+async function gatherItemsByPage(cache, url, get_bittorrent = false) {
+ const response = await ehgot(url);
+ const items = await parsePage(cache, response.data, get_bittorrent);
+ return updateBittorrent_url(cache, items);
+}
+
+async function getFavoritesItems(cache, page, favcat, inline_set, get_bittorrent = false) {
+ const response = await ehgot(`favorites.php?favcat=${favcat}&inline_set=${inline_set}`);
+ if (parseInt(page) === 0) {
+ const items = await parsePage(cache, response.data, get_bittorrent);
+ return updateBittorrent_url(cache, items);
+ }
+ return gatherItemsByPage(cache, `favorites.php?page=${page}&favcat=${favcat}`, get_bittorrent);
+}
+
+function getSearchItems(cache, params, page = undefined, get_bittorrent = false) {
+ if (page) {
+ return gatherItemsByPage(cache, `?page=${page}&${params}`, get_bittorrent);
+ } else {
+ return gatherItemsByPage(cache, `?${params}`, get_bittorrent);
+ }
+}
+
+function getTagItems(cache, tag, page, get_bittorrent = false) {
+ return gatherItemsByPage(cache, `tag/${tag}/${page}`, get_bittorrent);
+}
+
+module.exports = { getFavoritesItems, getSearchItems, getTagItems };
diff --git a/lib/routes/ehentai/favorites.js b/lib/routes/ehentai/favorites.js
new file mode 100644
index 0000000000..83a61129bf
--- /dev/null
+++ b/lib/routes/ehentai/favorites.js
@@ -0,0 +1,19 @@
+const EhAPI = require('./ehapi');
+const config = require('@/config').value;
+
+module.exports = async (ctx) => {
+ if (!config.ehentai || !config.ehentai.ipb_member_id || !config.ehentai.ipb_pass_hash || !config.ehentai.sk) {
+ throw 'Ehentai favorites RSS is disabled due to the lack of relevant config';
+ }
+ const favcat = ctx.params.favcat ? parseInt(ctx.params.favcat) : 0;
+ const page = ctx.params.page ? parseInt(ctx.params.page) : 0;
+ const bittorrent = ctx.params.bittorrent || false;
+ const inline_set = ctx.params.order === 'posted' ? 'fs_p' : 'fs_f';
+ const items = await EhAPI.getFavoritesItems(ctx.cache, page, favcat, inline_set, bittorrent);
+
+ ctx.state.data = {
+ title: 'E-Hentai Favorites',
+ link: `https://e-hentai.org/favorites.php?favcat=${favcat}&inline_set=${inline_set}`,
+ item: items,
+ };
+};
diff --git a/lib/routes/ehentai/search.js b/lib/routes/ehentai/search.js
new file mode 100644
index 0000000000..5429f6428e
--- /dev/null
+++ b/lib/routes/ehentai/search.js
@@ -0,0 +1,20 @@
+const EhAPI = require('./ehapi');
+
+module.exports = async (ctx) => {
+ const page = ctx.params.page;
+ let params = ctx.params.params;
+ const bittorrent = ctx.params.bittorrent || false;
+ let items;
+ if (page) {
+ // 如果定义了page,就要覆盖params
+ params = params.replace(/&*page=[^&]$/, '').replace(/page=[^&]&/, '');
+ items = await EhAPI.getSearchItems(ctx.cache, params, parseInt(page), bittorrent);
+ } else {
+ items = await EhAPI.getSearchItems(ctx.cache, params, undefined, bittorrent);
+ }
+ ctx.state.data = {
+ title: 'E-Hentai Search',
+ link: `https://e-hentai.org/?${params}`,
+ item: items,
+ };
+};
diff --git a/lib/routes/ehentai/tag.js b/lib/routes/ehentai/tag.js
new file mode 100644
index 0000000000..94eec2694d
--- /dev/null
+++ b/lib/routes/ehentai/tag.js
@@ -0,0 +1,13 @@
+const EhAPI = require('./ehapi');
+
+module.exports = async (ctx) => {
+ const page = ctx.params.page ? parseInt(ctx.params.page) : 0;
+ const tag = ctx.params.tag;
+ const bittorrent = ctx.params.bittorrent || false;
+ const items = await EhAPI.getTagItems(ctx.cache, tag, page, bittorrent);
+ ctx.state.data = {
+ title: 'E-Hentai Search',
+ link: `https://e-hentai.org/tag/${tag}`,
+ item: items,
+ };
+};