diff --git a/lib/routes/ehentai/ehapi.ts b/lib/routes/ehentai/ehapi.ts index 7753a10bb12fb6..842a1b6c0854c0 100644 --- a/lib/routes/ehentai/ehapi.ts +++ b/lib/routes/ehentai/ehapi.ts @@ -5,7 +5,7 @@ import { load } from 'cheerio'; 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) { @@ -40,28 +40,19 @@ function ehgot_thumb(cache, thumb_url) { }); } -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'; @@ -69,72 +60,155 @@ async function parsePage(cache, data, get_bittorrent = false, embed_thumb = fals 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 = `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 ? `thumbnail` : ''; + if (my_tags && tags_selector) { + const highlighted_tags = el.find(`${tags_selector}[style]`); + if (highlighted_tags.length > 0) { + let highlighted_tags_html = '

'; + highlighted_tags.each((_, tag) => { + highlighted_tags_html += `${$(tag).text()}  `; + }); + highlighted_tags_html += '

'; + description += highlighted_tags_html; + } + } + return description; + } + + // --- 辅助函数:获取BT种子信息 --- + async function getEnclosureInfo(el, cache) { + 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); @@ -184,28 +258,49 @@ function updateBittorrent_url(cache, items) { 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 }; diff --git a/lib/routes/ehentai/favorites.ts b/lib/routes/ehentai/favorites.ts index 14abc9d46dd1ca..f133f467e571d0 100644 --- a/lib/routes/ehentai/favorites.ts +++ b/lib/routes/ehentai/favorites.ts @@ -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', parameters: { favcat: 'Favorites folder number', order: '`posted`(Sort by gallery release time) , `favorited`(Sort by time added to favorites)', @@ -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'; 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 ? { diff --git a/lib/routes/ehentai/namespace.ts b/lib/routes/ehentai/namespace.ts index 65b118d97b5756..0add71124a8123 100644 --- a/lib/routes/ehentai/namespace.ts +++ b/lib/routes/ehentai/namespace.ts @@ -7,6 +7,7 @@ export const namespace: Namespace = { | 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 | +| my\_tags | Whether to include highlighted tags from My Tags in the description | 0/1/true/false | false |`, lang: 'en', }; diff --git a/lib/routes/ehentai/popular.ts b/lib/routes/ehentai/popular.ts new file mode 100644 index 00000000000000..667440f6f0f5cb --- /dev/null +++ b/lib/routes/ehentai/popular.ts @@ -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'; + 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, + }; +} diff --git a/lib/routes/ehentai/search.ts b/lib/routes/ehentai/search.ts index f52936925b5910..e114134b2040da 100644 --- a/lib/routes/ehentai/search.ts +++ b/lib/routes/ehentai/search.ts @@ -5,7 +5,7 @@ import EhAPI from './ehapi'; export const route: Route = { path: '/search/:params?/:page?/:routeParams?', categories: ['picture'], - example: '/ehentai/search/f_cats=1021/0/bittorrent=true&embed_thumb=false', + example: '/ehentai/search/f_cats=1021/0/bittorrent=true&embed_thumb=false&my_tags=true', parameters: { params: 'Search parameters. You can copy the content after `https://e-hentai.org/?`', page: 'Page number, set 0 to get latest', routeParams: 'Additional parameters, see the table above' }, features: { requireConfig: false, @@ -25,15 +25,16 @@ async function handler(ctx) { const page = ctx.req.param('page'); let params = ctx.req.param('params'); 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'; let items; if (page) { // 如果定义了page,就要覆盖params params = params.replace(/&*next=[^&]$/, '').replace(/next=[^&]&/, ''); - items = await EhAPI.getSearchItems(cache, params, page, bittorrent, embed_thumb); + items = await EhAPI.getSearchItems(cache, params, page, bittorrent, embed_thumb, my_tags); } else { - items = await EhAPI.getSearchItems(cache, params, undefined, bittorrent, embed_thumb); + items = await EhAPI.getSearchItems(cache, params, undefined, bittorrent, embed_thumb, my_tags); } let title = params; const match = /f_search=([^&]+)/.exec(title); diff --git a/lib/routes/ehentai/tag.ts b/lib/routes/ehentai/tag.ts index b5d37229d81534..7d2575d1af9534 100644 --- a/lib/routes/ehentai/tag.ts +++ b/lib/routes/ehentai/tag.ts @@ -5,7 +5,7 @@ import EhAPI from './ehapi'; export const route: Route = { path: '/tag/:tag/:page?/:routeParams?', categories: ['picture'], - example: '/ehentai/tag/language:chinese/0/bittorrent=true&embed_thumb=false', + example: '/ehentai/tag/language:chinese/0/bittorrent=true&embed_thumb=false&my_tags=true', parameters: { tag: 'Tag', page: 'Page number, set 0 to get latest', routeParams: 'Additional parameters, see the table above' }, features: { requireConfig: false, @@ -25,9 +25,10 @@ async function handler(ctx) { const page = ctx.req.param('page'); const tag = ctx.req.param('tag'); const routeParams = new URLSearchParams(ctx.req.param('routeParams')); - const bittorrent = routeParams.get('bittorrent') || false; - const embed_thumb = routeParams.get('embed_thumb') || false; - const items = await EhAPI.getTagItems(cache, tag, page, bittorrent, embed_thumb); + const bittorrent = routeParams.get('bittorrent') === 'true'; + const embed_thumb = routeParams.get('embed_thumb') === 'true'; + const my_tags = routeParams.get('my_tags') === 'true'; + const items = await EhAPI.getTagItems(cache, tag, page, bittorrent, embed_thumb, my_tags); return EhAPI.from_ex ? { diff --git a/lib/routes/ehentai/toplist.ts b/lib/routes/ehentai/toplist.ts new file mode 100644 index 00000000000000..88ddf28ec0a1f7 --- /dev/null +++ b/lib/routes/ehentai/toplist.ts @@ -0,0 +1,83 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import EhAPI from './ehapi'; + +const categoryMap = { + yesterday: 15, + pastmonth: 13, + pastyear: 12, + alltime: 11, +}; + +export const route: Route = { + path: '/toplist/:category?/:page?/:routeParams?', + categories: ['picture'], + example: '/ehentai/toplist/yesterday/0/bittorrent=true&embed_thumb=false&my_tags=true', + parameters: { + category: `Category, see table below. Defaults to 'yesterday'`, + page: 'Page number', + 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: 'Toplist', + maintainers: ['yindaheng98', 'syrinka', 'rosystain'], + handler, + description: ` +| Yesterday | Past Month | Past Year | All Time | +| :-------: | :--------: | :-------: | :-------: | +| yesterday | pastmonth | pastyear | alltime | +`, +}; + +async function handler(ctx) { + let category = ctx.req.param('category'); + let page = ctx.req.param('page'); + let routeParams = ctx.req.param('routeParams'); + + // Case 3: /toplist/0/bittorrent=true -> category='0', page='bittorrent=true', routeParams=undefined + if (page && !routeParams && (page.includes('bittorrent=') || page.includes('embed_thumb=') || page.includes('my_tags='))) { + routeParams = page; + page = category; + category = 'yesterday'; + } + + // Case 1: /toplist/0 -> category='0', page=undefined + // Case 2: /toplist/bittorrent=true -> category='bittorrent=true', page=undefined + if (category && !page && !routeParams) { + if (/^\d+$/.test(category)) { + // Case 1 + page = category; + category = 'yesterday'; + } else if (category.includes('bittorrent=') || category.includes('embed_thumb=') || category.includes('my_tags=')) { + // Case 2 + routeParams = category; + category = 'yesterday'; + } + } + + category = category ?? 'yesterday'; + + const tl = categoryMap[category] || 15; + 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'; + + const items = await EhAPI.getToplistItems(cache, tl, page, bittorrent, embed_thumb, my_tags); + + const title = Object.keys(categoryMap).find((key) => categoryMap[key] === tl) || 'yesterday'; + + return { + title: `E-Hentai Toplist - ${title}`, + link: `https://e-hentai.org/toplist.php?tl=${tl}`, + item: items, + }; +} diff --git a/lib/routes/ehentai/watched.ts b/lib/routes/ehentai/watched.ts new file mode 100644 index 00000000000000..bcfe3b936b44a3 --- /dev/null +++ b/lib/routes/ehentai/watched.ts @@ -0,0 +1,64 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import EhAPI from './ehapi'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; +import { URLSearchParams } from 'node:url'; + +export const route: Route = { + path: '/watched/:params?/:routeParams?', + categories: ['picture'], + example: '/ehentai/watched/f_cats=1021/bittorrent=true&embed_thumb=false&my_tags=true', + parameters: { + params: 'Search parameters. You can copy the content after `https://e-hentai.org/watched?`', + routeParams: 'Additional parameters, see the table above', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: true, + supportBT: true, + supportPodcast: false, + supportScihub: false, + nsfw: true, + }, + name: 'Watched', + maintainers: ['yindaheng98', 'syrinka', 'rosystain'], + handler, +}; + +async function handler(ctx) { + if (!EhAPI.has_cookie) { + throw new ConfigNotFoundError('Ehentai watched RSS is disabled due to the lack of cookie config'); + } + + 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'; + + const items = await EhAPI.getWatchedItems(cache, params, bittorrent, embed_thumb, my_tags); + + let title = params; + const match = /f_search=([^&]+)/.exec(title); + title = match?.[1] ? decodeURIComponent(match[1]) : 'Watched'; + + return EhAPI.from_ex + ? { + title: `${title} - ExHentai Watched`, + link: `https://exhentai.org/watched?${params || ''}`, + item: items, + } + : { + title: `${title} - E-Hentai Watched`, + link: `https://e-hentai.org/watched?${params || ''}`, + item: items, + }; +}