diff --git a/lib/routes/jimmyspa/books.ts b/lib/routes/jimmyspa/books.ts new file mode 100644 index 00000000000000..d925acd216816f --- /dev/null +++ b/lib/routes/jimmyspa/books.ts @@ -0,0 +1,112 @@ +import { Route, ViewType } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { art } from '@/utils/render'; +import { getCurrentPath } from '@/utils/helpers'; +import cache from '@/utils/cache'; +import path from 'node:path'; + +const __dirname = getCurrentPath(import.meta.url); +export const route: Route = { + path: '/books/:language', + categories: ['design'], + view: ViewType.Articles, + example: '/jimmyspa/books/tw', + parameters: { + language: { + description: '语言', + options: [ + { value: 'tw', label: '臺灣正體' }, + { value: 'en', label: 'English' }, + { value: 'jp', label: '日本語' }, + ], + }, + }, + radar: [ + { + source: ['www.jimmyspa.com/:language/Books'], + }, + ], + name: 'Books', + description: ` +| language | Description | +| --- | --- | +| tw | 臺灣正體 | +| en | English | +| jp | 日本語 | + `, + maintainers: ['FYLSen'], + handler, +}; + +async function handler(ctx) { + const language = ctx.req.param('language'); + const baseUrl = 'https://www.jimmyspa.com'; + const booksListUrl = new URL(`/${language}/Books/Ajax/changeList?year=&keyword=&categoryId=0&page=1`, baseUrl).href; + + const listResponse = await got(booksListUrl); + const listPage = load(listResponse.data.view); + + const bookItems = listPage('ul#appendWork li.work_block') + .toArray() + .map(async (bookElement) => { + const bookContent = load(bookElement); + const bookTitle = bookContent('p.tit').text(); + const bookImageRelative = bookContent('div.work_img img').prop('src') || ''; + const bookImageUrl = bookImageRelative ? baseUrl + bookImageRelative : ''; + const bookDetailUrl = bookContent('li.work_block').prop('data-route'); + + const { renderedDescription, publishDate } = (await cache.tryGet(bookDetailUrl, async () => { + const detailResponse = await got(bookDetailUrl); + const detailPage = load(detailResponse.data); + const bookDescription = detailPage('article.intro_cont').html() || ''; + const bookInfoWrap = detailPage('div.info_wrap').html() || ''; + + const processedDescription = bookDescription.replaceAll(/]*>/g, (imgTag) => + imgTag.replaceAll(/\b(src|data-src)="(?!http|https|\/\/)([^"]*)"/g, (_, attrName, relativePath) => { + const absoluteImageUrl = new URL(relativePath, baseUrl).href; + return `${attrName}="${absoluteImageUrl}"`; + }) + ); + + const publishDateMatch = bookInfoWrap.match(/(首次出版|First Published|初版)<\/span>\s*([^<]+)<\/span>/); + const publishDate = publishDateMatch ? parseDate(publishDateMatch[2] + '-02') : ''; + + const renderedDescription = art(path.join(__dirname, 'templates/description.art'), { + images: bookImageUrl + ? [ + { + src: bookImageUrl, + alt: bookTitle, + }, + ] + : undefined, + description: processedDescription, + }); + + return { + renderedDescription, + publishDate, + }; + })) as { renderedDescription: string; publishDate: string }; + + return { + title: bookTitle, + link: bookDetailUrl, + description: renderedDescription, + pubDate: publishDate, + content: { + html: renderedDescription, + text: bookTitle, + }, + }; + }); + + return { + title: `幾米 - 幾米創作(${language})`, + link: `${baseUrl}/${language}/Books`, + allowEmpty: true, + item: await Promise.all(bookItems), + }; +} diff --git a/lib/routes/jimmyspa/namespace.ts b/lib/routes/jimmyspa/namespace.ts new file mode 100644 index 00000000000000..2b92c8958c87ea --- /dev/null +++ b/lib/routes/jimmyspa/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '幾米 JIMMY S.P.A. Official Website', + url: 'www.jimmyspa.com', + lang: 'zh-TW', +}; diff --git a/lib/routes/jimmyspa/news.ts b/lib/routes/jimmyspa/news.ts new file mode 100644 index 00000000000000..5d7788640f4dc0 --- /dev/null +++ b/lib/routes/jimmyspa/news.ts @@ -0,0 +1,124 @@ +import { Route, ViewType } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { art } from '@/utils/render'; +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); +import path from 'node:path'; + +export const route: Route = { + path: '/news/:language', + categories: ['design'], + view: ViewType.Pictures, + example: '/jimmyspa/news/tw', + parameters: { + language: { + description: '语言', + options: [ + { value: 'tw', label: '臺灣正體' }, + { value: 'en', label: 'English' }, + { value: 'jp', label: '日本語' }, + ], + }, + }, + radar: [ + { + source: ['www.jimmyspa.com/:language/News'], + }, + ], + name: 'News', + description: ` +| language | Description | +| --- | --- | +| tw | 臺灣正體 | +| en | English | +| jp | 日本語 | + `, + maintainers: ['FYLSen'], + handler, +}; + +async function handler(ctx) { + const language = ctx.req.param('language'); + const rootUrl = 'https://www.jimmyspa.com'; + + const currentUrl = new URL(`/${language}/News/Ajax/changeList?year=&keyword=&categoryId=0&page=1`, rootUrl).href; + + const responseData = await got(currentUrl); + + const $ = load(responseData.data.view); + + const items = $('ul#appendNews li.card_block') + .toArray() + .map((item) => { + const $$ = load(item); + const title = $$('a.news_card .info_wrap h3').text(); + const image = $$('a.news_card .card_img img').prop('src') || ''; + const link = $$('a.news_card').prop('data-route'); + const itemdate = $$('a.news_card div.date').html() || ''; + const pubDate = convertHtmlDateToStandardFormat(itemdate.toString()); + + const description = art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + alt: title, + }, + ] + : undefined, + description: $$('a.news_card .info_wrap p').text(), + }); + + return { + title, + link, + description, + pubDate, + content: { + html: description, + text: title, + }, + }; + }); + + return { + title: `幾米 - 最新消息(${language})`, + link: `${rootUrl}/${language}/News`, + allowEmpty: true, + item: items, + }; +} + +function convertHtmlDateToStandardFormat(htmlContent: string): Date | undefined { + const dateRegex = /

(\d{1,2})<\/p>\s*

(\d{1,2})\s*\.\s*([A-Za-z]{3})<\/p>/; + const match = htmlContent.match(dateRegex); + + if (match) { + const day = Number.parseInt(match[1]) + 1; + const year = match[2]; + const monthAbbreviation = match[3]; + + const monthMapping: { [key: string]: string } = { + Jan: '01', + Feb: '02', + Mar: '03', + Apr: '04', + May: '05', + Jun: '06', + Jul: '07', + Aug: '08', + Sep: '09', + Oct: '10', + Nov: '11', + Dec: '12', + }; + + const month = monthMapping[monthAbbreviation] || ''; + + return parseDate(`20${year}-${month}-${day}`); + } + + return undefined; +} diff --git a/lib/routes/jimmyspa/templates/description.art b/lib/routes/jimmyspa/templates/description.art new file mode 100644 index 00000000000000..dfab19230c1108 --- /dev/null +++ b/lib/routes/jimmyspa/templates/description.art @@ -0,0 +1,17 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} +

+ {{ image.alt }} +
+ {{ /if }} + {{ /each }} +{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file