-
Notifications
You must be signed in to change notification settings - Fork 7.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(route): add route for chikubi.jp (#16656)
* feat(route): add route for chikubi.jp * refactor: Adjust metadata and selector usage
- Loading branch information
Showing
3 changed files
with
163 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { Route, Data } from '@/types'; | ||
import got from '@/utils/got'; | ||
import { load } from 'cheerio'; | ||
import { processItems } from './utils'; | ||
|
||
export const route: Route = { | ||
path: '/:category?', | ||
categories: ['multimedia'], | ||
example: '/chikubi', | ||
parameters: { category: '分類,見下表,默認爲最新' }, | ||
features: { | ||
requireConfig: false, | ||
requirePuppeteer: false, | ||
antiCrawler: false, | ||
supportBT: false, | ||
supportPodcast: false, | ||
supportScihub: false, | ||
}, | ||
name: 'Category', | ||
maintainers: ['snowagar25'], | ||
handler, | ||
description: `| 最新 | 殿堂 | 動畫 | VR | 漫畫 | 音聲 | CG | | ||
| ------ | ---- | ----- | -- | ----- | ----- | -- | | ||
| (empty) | best | video | vr | comic | voice | cg |`, | ||
radar: [ | ||
{ | ||
source: ['chikubi.jp/:category', 'chikubi.jp/'], | ||
target: '/:category', | ||
}, | ||
], | ||
}; | ||
|
||
const categoryMap = { | ||
'': { url: '/page/1', title: '最新', selector: '.article_list_area > article > a' }, | ||
best: { url: '/best-nipple-article', title: '殿堂', selector: '.article-list:first .title > a' }, | ||
video: { url: '/nipple-video', title: '動畫', selector: 'ul.video_list > li > a' }, | ||
vr: { url: '/nipple-video-category/cat-nipple-video-vr', title: 'VR', selector: 'ul.video_list > li > a' }, | ||
comic: { url: '/comic', title: '漫畫', selector: '.section:nth-of-type(2) .list-doujin .photo a' }, | ||
voice: { url: '/voice', title: '音聲', selector: 'ul.list-doujin > li > .photo > a' }, | ||
cg: { url: '/cg', title: 'CG', selector: 'ul.list-doujin > li > .photo > a' }, | ||
}; | ||
|
||
async function handler(ctx): Promise<Data> { | ||
const category = ctx.req.param('category') ?? ''; | ||
const baseUrl = 'https://chikubi.jp'; | ||
|
||
const { url, title, selector } = categoryMap[category]; | ||
|
||
const response = await got(`${baseUrl}${url}`); | ||
const $ = load(response.data); | ||
|
||
let list = $(selector) | ||
.toArray() | ||
.map((item) => { | ||
const $item = $(item); | ||
return { | ||
title: $item.text().trim(), | ||
link: new URL($item.attr('href') ?? '', baseUrl).href, | ||
}; | ||
}); | ||
|
||
// 限制殿堂最多獲取30個 | ||
if (category === 'best') { | ||
list = list.slice(0, 30); | ||
} | ||
|
||
// 獲取內文 | ||
const items = await processItems(list); | ||
|
||
return { | ||
title: `${title} - chikubi.jp`, | ||
link: `${baseUrl}${url}`, | ||
item: items, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import type { Namespace } from '@/types'; | ||
|
||
export const namespace: Namespace = { | ||
name: '乳首ふぇち', | ||
url: 'chikubi.jp', | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { DataItem } from '@/types'; | ||
import cache from '@/utils/cache'; | ||
import got from '@/utils/got'; | ||
import { load } from 'cheerio'; | ||
import { parseDate } from '@/utils/parse-date'; | ||
|
||
interface ListItem { | ||
title: string; | ||
link: string; | ||
} | ||
|
||
interface ContentSelectors { | ||
title: string; | ||
description: string[]; | ||
} | ||
|
||
const contentTypes: Record<string, ContentSelectors> = { | ||
doujin: { | ||
title: '.doujin-title', | ||
description: ['.doujin-detail', '.section', '.area-buy > a.btn'], | ||
}, | ||
video: { | ||
title: '.video-title', | ||
description: ['.video-data', '.section', '.lp-samplearea a.btn'], | ||
}, | ||
article: { | ||
title: '.article_title', | ||
description: ['.article_icatch', '.article_contents'], | ||
}, | ||
}; | ||
|
||
function getContentType(link: string): keyof typeof contentTypes { | ||
const typePatterns = { | ||
doujin: ['/cg/', '/comic/', '/voice/'], | ||
video: ['/nipple-video/'], | ||
article: ['/post-'], | ||
}; | ||
|
||
for (const [type, patterns] of Object.entries(typePatterns)) { | ||
if (patterns.some((pattern) => link.includes(pattern))) { | ||
return type as keyof typeof contentTypes; | ||
} | ||
} | ||
|
||
throw new Error(`Unknown content type for link: ${link}`); | ||
} | ||
|
||
export async function processItems(list: ListItem[]): Promise<DataItem[]> { | ||
const items = await Promise.all( | ||
list.map((item) => | ||
cache.tryGet(item.link, async () => { | ||
const detailResponse = await got(item.link); | ||
const $ = load(detailResponse.data); | ||
|
||
const contentType = getContentType(item.link); | ||
const selectors = contentTypes[contentType]; | ||
|
||
const title = $(selectors.title).text().trim() || item.title; | ||
const description = selectors.description | ||
.map((selector) => | ||
$(selector) | ||
.map((_, el) => $(el).clone().wrap('<div>').parent().html()) | ||
.toArray() | ||
.join('') | ||
) | ||
.join(''); | ||
|
||
const pubDateStr = $('meta[property="article:published_time"]').attr('content'); | ||
const pubDate = pubDateStr ? parseDate(pubDateStr) : undefined; | ||
|
||
return { | ||
title, | ||
description, | ||
link: item.link, | ||
pubDate, | ||
} as DataItem; | ||
}) | ||
) | ||
); | ||
|
||
return items.filter((item): item is DataItem => item !== null); | ||
} |