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, + }; +};