diff --git a/lib/routes/pixivision/index.ts b/lib/routes/pixivision/index.ts new file mode 100644 index 00000000000000..0ee57ceeb56350 --- /dev/null +++ b/lib/routes/pixivision/index.ts @@ -0,0 +1,84 @@ +import { Route, DataItem, Data } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import { processContent } from './utils'; + +export const route: Route = { + path: '/:lang/:category?', + categories: ['anime'], + example: '/pixivision/zh-tw', + parameters: { lang: 'Language', category: 'Category' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Category', + maintainers: ['SnowAgar25'], + description: `:::tip + \`https://www.pixivision.net/zh-tw/c/interview\` → \`/pixivision/zh-tw/interview\` + :::`, + radar: [ + { + source: ['www.pixivision.net/:lang'], + target: '/:lang', + }, + { + source: ['www.pixivision.net/:lang/c/:category'], + target: '/:lang/:category', + }, + ], + handler, +}; + +async function handler(ctx): Promise { + const { lang, category } = ctx.req.param(); + const baseUrl = 'https://www.pixivision.net'; + const url = category ? `${baseUrl}/${lang}/c/${category}` : `${baseUrl}/${lang}`; + + const headers = { + headers: { + Cookie: `user_lang=${lang.replace('-', '_')}`, // zh-tw → zh_tw + }, + }; + + const { data: response } = await got(url, headers); + const $ = load(response); + + const list = $('li.article-card-container a[data-gtm-action="ClickTitle"]') + .map((_, elem) => ({ + title: $(elem).text(), + link: new URL($(elem).attr('href') ?? '', baseUrl).href, + })) + .toArray(); + + const items = await Promise.all( + list.map(async (item) => { + const result = await cache.tryGet(item.link, async () => { + const { data: articleData } = await got(item.link, headers); + const $article = load(articleData); + + const processedDescription = processContent($article, lang); + + return { + title: item.title, + description: processedDescription, + link: item.link, + pubDate: parseDate($article('time').attr('datetime') ?? ''), + } as DataItem; + }); + return result; + }) + ); + + return { + title: `${$('.ssc__header').length ? $('.ssc__header').text() : 'New'} - pixivision`, + link: url, + item: items.filter((item): item is DataItem => !!item), + }; +} diff --git a/lib/routes/pixivision/namespace.ts b/lib/routes/pixivision/namespace.ts new file mode 100644 index 00000000000000..3461490ae96d9b --- /dev/null +++ b/lib/routes/pixivision/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'pixivision', + url: 'www.pixivision.net', +}; diff --git a/lib/routes/pixivision/utils.ts b/lib/routes/pixivision/utils.ts new file mode 100644 index 00000000000000..4d8c8e08896b87 --- /dev/null +++ b/lib/routes/pixivision/utils.ts @@ -0,0 +1,86 @@ +import { CheerioAPI } from 'cheerio'; +import { config } from '@/config'; + +const multiImagePrompt = { + en: (count) => `${count} images in total`, + zh: (count) => `共${count}张图`, + 'zh-tw': (count) => `共${count}張圖`, + ko: (count) => `총 ${count}개의 이미지`, + ja: (count) => `計${count}枚の画像`, +}; + +export function processContent($: CheerioAPI, lang: string): string { + // 移除作者頭像 + $('.am__work__user-icon-container').remove(); + + // 插畫標題&作者 + $('.am__work__title').attr('style', 'display: inline;'); + $('.am__work__user-name').attr('style', 'display: inline; margin-left: 10px;'); + + // 處理多張圖片的提示 + $('.mic__label').each((_, elem) => { + const $label = $(elem); + const count = $label.text(); + const $workContainer = $label.parentsUntil('.am__work').last().parent(); + const $titleContainer = $workContainer.find('.am__work__title-container'); + + $titleContainer.append(`

${multiImagePrompt[lang](count)}

`); + $label.remove(); + }); + + // 插畫間隔 + $('.article-item, ._feature-article-body__pixiv_illust').after('
'); + + // Remove Label & Tags + $('.arc__thumbnail-label').remove(); + $('.arc__footer-container').remove(); + + // pixivision card + $('article._article-card').each((_, article) => { + const $article = $(article); + + const $thumbnail = $article.find('._thumbnail'); + const thumbnailStyle = $thumbnail.attr('style'); + const bgImageMatch = thumbnailStyle?.match(/url\((.*?)\)/); + const imageUrl = bgImageMatch ? bgImageMatch[1] : ''; + + $thumbnail.remove(); + + if (imageUrl) { + $article.prepend(`Article thumbnail`); + } + }); + + // 處理 tweet + $('.fab__script').each((_, elem) => { + const $elem = $(elem); + const $link = $elem.find('blockquote > a'); + const href = $link.attr('href'); + + if (href) { + const match = href.match(/\/status\/(\d+)/); + if (match) { + const tweetId = match[1]; + $elem.html(` + + `); + $elem.find('blockquote').remove(); + } + } + }); + + return ( + $('.am__body') + .html() + ?.replace(/https:\/\/i\.pximg\.net/g, config.pixiv.imgProxy || '') || '' + ); +}