Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 166 additions & 71 deletions lib/routes/ehentai/ehapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import path from 'node:path';
import { config } from '@/config';

const headers = {};
const headers: any = {};
const has_cookie = config.ehentai.ipb_member_id && config.ehentai.ipb_pass_hash && config.ehentai.sk;
const from_ex = has_cookie && config.ehentai.igneous;
if (has_cookie) {
Expand Down Expand Up @@ -40,101 +40,175 @@
});
}

async function parsePage(cache, data, get_bittorrent = false, embed_thumb = false) {
async function parsePage(cache, data, get_bittorrent = false, embed_thumb = false, my_tags = false) {
const $ = load(data);
// "m" for Minimal
// "p" for Minimal+
// "l" for Compact
// "e" for Extended
// "t" for Thumbnail
let layout = 't';

// "itg gld" for Thumbnail
let galleries = $('div[class^="itg gld"]');
// "itg gltm" for Minimal or Minimal+
if (galleries.length <= 0) {
galleries = $('table[class^="itg gltm"] tbody');
layout = 'm';
}
// "itg gltc" for Compact
if (galleries.length <= 0) {
galleries = $('table[class^="itg gltc"] tbody');
layout = 'l';
}
// "itg glte" for Extended
if (galleries.length <= 0) {
galleries = $('table[class^="itg glte"] tbody');
layout = 'e';
}
if (galleries.length <= 0) {
return [];
}

async function parseElement(cache, element) {
const el = $(element);
const title = el.find('.glink').html();
const rawDate = el.find('div[id^="posted_"]').text();
const pubDate = rawDate ? timezone(rawDate, 0) : rawDate;
let el_a;
let el_img;
// match layout
if ('mpl'.includes(layout)) {
// Minimal, Minimal+, Compact
el_a = el.find('td[class^="gl3"] a');
el_img = el.find('td[class^=gl2] div.glthumb div img');
} else if (layout === 'e') {
// Extended
el_a = el.find('td[class^="gl1"] a');
el_img = el_a.find('img');
} else if (layout === 't') {
const layoutConfigs = {
t: {
// Thumbnail
el_a = el.find('div[class^="gl3t"] a');
el_img = el_a.find('img');
}
const link = el_a.attr('href');
link_selector: 'div[class^="gl3t"] a',
thumb_selector: 'div[class^="gl3t"] a img',
category_selector: 'div.gl5t .cs',
tags_selector: 'div.gl6t .gt',
has_author: false,
},
m: {
// Minimal, Minimal+
link_selector: 'td[class^="gl3"] a',
thumb_selector: 'td[class^=gl2] div.glthumb div img',
category_selector: 'td.gl1c.glcat .cn',
tags_selector: 'td.gl3c.glname div.gt',
has_author: false,
},
l: {
// Compact
link_selector: 'td[class^="gl3"] a',
thumb_selector: 'td[class^=gl2] div.glthumb div img',
category_selector: 'td.gl1c.glcat .cn',
tags_selector: 'td.gl3c.glname div.gt',
has_author: true,
},
e: {
// Extended
link_selector: 'td[class^="gl1"] a',
thumb_selector: 'td[class^="gl1"] a img',
category_selector: 'div.gl3e .cn',
tags_selector: 'table div[title]',
has_author: true,
},
};

// --- 辅助函数:处理缩略图 ---
async function processThumbnail(el_img, cache) {
let thumbnail = el_img.data('src') ?? el_img.attr('src');
if (config.ehentai.img_proxy && thumbnail) {
if (!thumbnail) {
return '';
}

if (config.ehentai.img_proxy) {
const url = new URL(thumbnail);
thumbnail = config.ehentai.img_proxy + url.pathname + url.search;
}
if (embed_thumb && thumbnail) {
thumbnail = await ehgot_thumb(cache, thumbnail);

if (embed_thumb) {
return await ehgot_thumb(cache, thumbnail);
}
const description = `<img src='${thumbnail}' alt='thumbnail'>`;
if (title && link) {
const item = { title, description, pubDate, link };
if (get_bittorrent) {
const el_down = el.find('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 thumbnail;
}

// --- 辅助函数:构建描述 ---
function buildDescription(thumbnail, el, tags_selector) {
let description = thumbnail ? `<img src='${thumbnail}' alt='thumbnail'>` : '';
if (my_tags && tags_selector) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not add tags to description. Having them in category is already enough.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not add tags to description. Having them in category is already enough.

Thank you for the suggestion. As an optional feature that relies on cookies and is disabled by default, I believe its presence in the description is valuable.

my_tags and the tags in category are not the same. my_tags refers to highlight tags set by the E-Hentai user themselves, while the tags in category are all the tags for that item.

The my_tags in the description can inform users that an item contains elements of interest to them, and in this respect, it is not a redundant implementation of category.

Concurrently, including appropriate data in the description also allows RSSHub's filter_description to be utilized for more granular filtering rules.

Additionally, in some E-Hentai layouts (such as the most common thumbnail view), tags information is lost, while my_tags associated highlighted tags are unaffected. Treating them as two different types of data facilitates more reliable conditional judgments in some automated workflows (e.g., automated downloads, message forwarding).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So why not have the my_tags in category in a form of my_tags:some_tag_i_like similar to

category: [`category:${category.toLowerCase()}`, ...(tags || [])],

Since you mentioned filter_description why can't one simply apply filtering with filter_category? Especially if you have a closer look at what's inside description. The description will have a thumbnail from

const thumbnail = await processThumbnail(el.find(config.thumb_selector), cache);

In processThumbnail the thumbnail can be created from
return await ehgot_thumb(cache, thumbnail);
which return a base64 encoded image string
function ehgot_thumb(cache, thumb_url) {
return cache.tryGet(thumb_url, async () => {
try {
const buffer = await got({ method: 'get', responseType: 'buffer', url: thumb_url, headers });
const data = buffer.body.toString('base64');
const ext = path.extname(thumb_url).slice(1);
return `data:image/${ext};base64,${data}`;

Performing string searching filter_description on a long string description will always be slower than searching in short strings filter_category.

const highlighted_tags = el.find(`${tags_selector}[style]`);
if (highlighted_tags.length > 0) {
let highlighted_tags_html = '<p>';
highlighted_tags.each((_, tag) => {
highlighted_tags_html += `<code>${$(tag).text()}</code>&nbsp;&nbsp;`;
});
highlighted_tags_html += '</p>';
description += highlighted_tags_html;
}
}
return description;
}

// --- 辅助函数:获取BT种子信息 ---
async function getEnclosureInfo(el, cache) {

Check warning

Code scanning / ESLint

Move function definitions to the highest possible scope. Warning

Move async function 'getEnclosureInfo' to the outer scope.
const el_down = el.find('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) {
return {
enclosure_url: bittorrent_url,
enclosure_type: 'application/x-bittorrent',
bittorrent_page_url,
};
}
if ('le'.includes(layout)) {
// artist tags will only show in Compact or Extended layout
// get artist names as author
item.author = $(el)
.find('div.gt[title^="artist:"]')
.toArray()
.map((tag) => $(tag).text())
.join(' / ');
}
return null;
}

// --- 重构后的核心解析函数 ---
async function parseElement(cache, element) {
const el = $(element);
const config = layoutConfigs[layout];

// 1. 基本信息提取
const title = el.find('.glink').html();
const rawDate = el.find('div[id^="posted_"]').text();
const el_a = el.find(config.link_selector);
const link = el_a.attr('href');
if (!title || !rawDate || !link) {
return null; // 如果没有标题、日期或链接,则为无效条目
}

const pubDate = timezone(rawDate, 0);
const category = el.find(config.category_selector).text();

const tags = el
.find(config.tags_selector)
.toArray()
.map((tag) => $(tag).attr('title'));

// 2. 调用辅助函数处理复杂逻辑
const thumbnail = await processThumbnail(el.find(config.thumb_selector), cache);
const description = buildDescription(thumbnail, el, config.tags_selector);

// 3. 组装核心 item
const item: any = {
title,
description,
pubDate,
link,
category: [`category:${category.toLowerCase()}`, ...(tags || [])],
};

// 4. 按需附加额外信息
if (get_bittorrent) {
const enclosure = await getEnclosureInfo(el, cache);
if (enclosure) {
Object.assign(item, enclosure);
}
return item;
}

if (config.has_author) {
item.author = el
.find('div.gt[title^="artist:"]')
.toArray()
.map((tag) => $(tag).text())
.join(' / ');
}

return item;
}

const item_Promises = [];
// --- 后续逻辑保持不变 ---
const item_Promises: any[] = [];
galleries.children().each((index, element) => {
item_Promises.push(parseElement(cache, element));
});
const items_with_null = await Promise.all(item_Promises);

const items = [];
const items: any[] = [];
for (const item of items_with_null) {
if (item) {
items.push(item);
Expand Down Expand Up @@ -184,28 +258,49 @@
return items;
}

async function gatherItemsByPage(cache, url, get_bittorrent = false, embed_thumb = false) {
async function gatherItemsByPage(cache, url, get_bittorrent = false, embed_thumb = false, my_tags = false) {
const response = await ehgot(url);
const items = await parsePage(cache, response.data, get_bittorrent, embed_thumb);
const items = await parsePage(cache, response.data, get_bittorrent, embed_thumb, my_tags);
return updateBittorrent_url(cache, items);
}

async function getFavoritesItems(cache, favcat, inline_set, page, get_bittorrent = false, embed_thumb = false) {
async function getFavoritesItems(cache, favcat, inline_set, page, get_bittorrent = false, embed_thumb = false, my_tags = false) {
const response = await ehgot(`favorites.php?favcat=${favcat}&inline_set=${inline_set}`);
if (page) {
return gatherItemsByPage(cache, `favorites.php?favcat=${favcat}&next=${page}`, get_bittorrent, embed_thumb);
return gatherItemsByPage(cache, `favorites.php?favcat=${favcat}&next=${page}`, get_bittorrent, embed_thumb, my_tags);
} else {
const items = await parsePage(cache, response.data, get_bittorrent, embed_thumb);
const items = await parsePage(cache, response.data, get_bittorrent, embed_thumb, my_tags);
return updateBittorrent_url(cache, items);
}
}

function getSearchItems(cache, params, page, get_bittorrent = false, embed_thumb = false) {
return page ? gatherItemsByPage(cache, `?${params}&next=${page}`, get_bittorrent, embed_thumb) : gatherItemsByPage(cache, `?${params}`, get_bittorrent, embed_thumb);
function getSearchItems(cache, params, page, get_bittorrent = false, embed_thumb = false, my_tags = false) {
return page ? gatherItemsByPage(cache, `?${params}&next=${page}`, get_bittorrent, embed_thumb, my_tags) : gatherItemsByPage(cache, `?${params}`, get_bittorrent, embed_thumb, my_tags);
}

function getTagItems(cache, tag, page, get_bittorrent = false, embed_thumb = false, my_tags = false) {
return page ? gatherItemsByPage(cache, `tag/${tag}?next=${page}`, get_bittorrent, embed_thumb, my_tags) : gatherItemsByPage(cache, `tag/${tag}`, get_bittorrent, embed_thumb, my_tags);
}

function getWatchedItems(cache, params, get_bittorrent = false, embed_thumb = false, my_tags = false) {
const url = `watched?${params || ''}`;
return gatherItemsByPage(cache, url, get_bittorrent, embed_thumb, my_tags);
}

function getPopularItems(cache, params, get_bittorrent = false, embed_thumb = false, my_tags = false) {
const url = `popular?${params || ''}`;
return gatherItemsByPage(cache, url, get_bittorrent, embed_thumb, my_tags);
}

function getTagItems(cache, tag, page, get_bittorrent = false, embed_thumb = false) {
return page ? gatherItemsByPage(cache, `tag/${tag}?next=${page}`, get_bittorrent, embed_thumb) : gatherItemsByPage(cache, `tag/${tag}`, get_bittorrent, embed_thumb);
async function getToplistItems(cache, tl, page, get_bittorrent = false, embed_thumb = false, my_tags = false) {
let url = `toplist.php?tl=${tl}`;
if (page) {
url = `${url}&p=${page}`;
}
// toplist is e-hentai only
const response = await got({ method: 'get', url: `https://e-hentai.org/${url}`, headers });
const items = await parsePage(cache, response.data, get_bittorrent, embed_thumb, my_tags);
return updateBittorrent_url(cache, items);
}

export default { getFavoritesItems, getSearchItems, getTagItems, has_cookie, from_ex };
export default { getFavoritesItems, getSearchItems, getTagItems, getWatchedItems, getPopularItems, getToplistItems, has_cookie, from_ex };
9 changes: 5 additions & 4 deletions lib/routes/ehentai/favorites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ConfigNotFoundError from '@/errors/types/config-not-found';
export const route: Route = {
path: '/favorites/:favcat?/:order?/:page?/:routeParams?',
categories: ['picture'],
example: '/ehentai/favorites/0/posted/0/bittorrent=true&embed_thumb=false',
example: '/ehentai/favorites/0/posted/0/bittorrent=true&embed_thumb=false&my_tags=true',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not change the example unless it doesn't work anymore.

parameters: {
favcat: 'Favorites folder number',
order: '`posted`(Sort by gallery release time) , `favorited`(Sort by time added to favorites)',
Expand Down Expand Up @@ -34,10 +34,11 @@ async function handler(ctx) {
const favcat = ctx.req.param('favcat') ? Number.parseInt(ctx.req.param('favcat')) : 0;
const page = ctx.req.param('page');
const routeParams = new URLSearchParams(ctx.req.param('routeParams'));
const bittorrent = routeParams.get('bittorrent') || false;
const embed_thumb = routeParams.get('embed_thumb') || false;
const bittorrent = routeParams.get('bittorrent') === 'true';
const embed_thumb = routeParams.get('embed_thumb') === 'true';
const my_tags = routeParams.get('my_tags') === 'true';
Comment on lines +37 to +39
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not change the default value.

| bittorrent | Whether include a link to the latest torrent | 0/1/true/false | false |
| embed\_thumb | Whether the cover image is embedded in the RSS feed rather than given as a link | 0/1/true/false | false |
| my\_tags | Whether to include highlighted tags from My Tags in the description | 0/1/true/false | false |`,

const inline_set = ctx.req.param('order') === 'posted' ? 'fs_p' : 'fs_f';
const items = await EhAPI.getFavoritesItems(cache, favcat, inline_set, page, bittorrent, embed_thumb);
const items = await EhAPI.getFavoritesItems(cache, favcat, inline_set, page, bittorrent, embed_thumb, my_tags);

return EhAPI.from_ex
? {
Expand Down
3 changes: 2 additions & 1 deletion lib/routes/ehentai/namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
| Key | Meaning | Accepted keys | Default value |
| ------------ | ------------------------------------------------------------------------------- | -------------- | ------------- |
| bittorrent | Whether include a link to the latest torrent | 0/1/true/false | false |
| embed\_thumb | Whether the cover image is embedded in the RSS feed rather than given as a link | 0/1/true/false | false |`,
| embed\_thumb | Whether the cover image is embedded in the RSS feed rather than given as a link | 0/1/true/false | false |

Check warning

Code scanning / ESLint

Disallow unnecessary escape characters Warning

Unnecessary escape character: \_.
| my\_tags | Whether to include highlighted tags from My Tags in the description | 0/1/true/false | false |`,

Check warning

Code scanning / ESLint

Disallow unnecessary escape characters Warning

Unnecessary escape character: \_.
lang: 'en',
};
53 changes: 53 additions & 0 deletions lib/routes/ehentai/popular.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Route } from '@/types';
import cache from '@/utils/cache';
import EhAPI from './ehapi';

export const route: Route = {
path: '/popular/:params?/:routeParams?',
categories: ['picture'],
example: '/ehentai/popular/f_sft=on&f_sfu=on&f_sfl=on/bittorrent=true&embed_thumb=false&my_tags=true',
parameters: {
params: 'Filter parameters. You can copy the content after `https://e-hentai.org/popular?`',
routeParams: 'Additional parameters, see the table above. E.g. `bittorrent=true&embed_thumb=false`',
},
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: true,
supportBT: true,
supportPodcast: false,
supportScihub: false,
nsfw: true,
},
name: 'Popular',
maintainers: ['yindaheng98', 'syrinka', 'rosystain'],
handler,
};

async function handler(ctx) {
let params = ctx.req.param('params') ?? '';
let routeParams = ctx.req.param('routeParams');

if (params && !routeParams && (params.includes('bittorrent=') || params.includes('embed_thumb=') || params.includes('my_tags='))) {
routeParams = params;
params = '';
}

const routeParamsParsed = new URLSearchParams(routeParams);
const bittorrent = routeParamsParsed.get('bittorrent') === 'true';
const embed_thumb = routeParamsParsed.get('embed_thumb') === 'true';
const my_tags = routeParamsParsed.get('my_tags') === 'true';
Comment on lines +37 to +39
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not change the default value.

| bittorrent | Whether include a link to the latest torrent | 0/1/true/false | false |
| embed\_thumb | Whether the cover image is embedded in the RSS feed rather than given as a link | 0/1/true/false | false |
| my\_tags | Whether to include highlighted tags from My Tags in the description | 0/1/true/false | false |`,

const items = await EhAPI.getPopularItems(cache, params, bittorrent, embed_thumb, my_tags);

return EhAPI.from_ex
? {
title: `ExHentai Popular`,
link: `https://exhentai.org/popular${params || ''}`,
item: items,
}
: {
title: `E-Hentai Popular`,
link: `https://e-hentai.org/popular${params || ''}`,
item: items,
};
}
Loading
Loading