Skip to content

Commit

Permalink
feat(route): add route for chikubi.jp (#16656)
Browse files Browse the repository at this point in the history
* feat(route): add route for chikubi.jp

* refactor: Adjust metadata and selector usage
  • Loading branch information
Tsuyumi25 authored Sep 9, 2024
1 parent 5cf7400 commit 60aa1b7
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 0 deletions.
75 changes: 75 additions & 0 deletions lib/routes/chikubi/index.ts
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,
};
}
6 changes: 6 additions & 0 deletions lib/routes/chikubi/namespace.ts
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',
};
82 changes: 82 additions & 0 deletions lib/routes/chikubi/utils.ts
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);
}

0 comments on commit 60aa1b7

Please sign in to comment.