diff --git a/.github/workflows/docker-test-cont.yml b/.github/workflows/docker-test-cont.yml index 4ca787ee20d407..bb13948ab4b75a 100644 --- a/.github/workflows/docker-test-cont.yml +++ b/.github/workflows/docker-test-cont.yml @@ -58,7 +58,7 @@ jobs: if: (env.TEST_CONTINUE) run: | set -ex - gzip -cvd docker-image/rsshub.tar.gz | docker load + zstd -d --stdout docker-image/rsshub.tar.zst | docker load docker run -d \ --name rsshub \ -e NODE_ENV=dev \ diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml index 727cf3d4afc17d..2f3031e84921a5 100644 --- a/.github/workflows/docker-test.yml +++ b/.github/workflows/docker-test.yml @@ -66,11 +66,11 @@ jobs: run: bash scripts/docker/test-docker.sh - name: Export Docker image - run: docker save rsshub:latest | gzip -1cf > rsshub.tar.gz + run: docker save rsshub:latest | zstdmt -o rsshub.tar.zst - name: Upload Docker image uses: actions/upload-artifact@v4 with: name: docker-image - path: rsshub.tar.gz + path: rsshub.tar.zst retention-days: 1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e480b0dab19b64..d3d9d120ec810f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [ 20, 21 ] + node-version: [ 20, 22 ] name: Vitest on Node ${{ matrix.node-version }} steps: - uses: actions/checkout@v4 @@ -60,7 +60,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [ 20, 21 ] + node-version: [ 20, 22 ] chromium: - name: bundled Chromium dependency: '' @@ -117,7 +117,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [ 20, 21 ] + node-version: [ 20, 22 ] name: Build radar and maintainer on Node ${{ matrix.node-version }} steps: - uses: actions/checkout@v4 diff --git a/lib/router.js b/lib/router.js index ff1fd44059a7a0..36c0aa9fbe5954 100644 --- a/lib/router.js +++ b/lib/router.js @@ -25,9 +25,6 @@ router.get('/benedictevans', lazyloadRouteHandler('./routes/benedictevans/recent // router.get('/jianshu/collection/:id', lazyloadRouteHandler('./routes/jianshu/collection')); // router.get('/jianshu/user/:id', lazyloadRouteHandler('./routes/jianshu/user')); -// pixiv-fanbox -router.get('/fanbox/:user?', lazyloadRouteHandler('./routes/fanbox/main')); - // Disqus router.get('/disqus/posts/:forum', lazyloadRouteHandler('./routes/disqus/posts')); diff --git a/lib/routes-deprecated/fanbox/conv.js b/lib/routes-deprecated/fanbox/conv.js deleted file mode 100644 index 40dfff634b74f6..00000000000000 --- a/lib/routes-deprecated/fanbox/conv.js +++ /dev/null @@ -1,227 +0,0 @@ -const got = require('@/utils/got'); - -const get_header = require('./header'); - -async function get_twitter(t) { - try { - const resp = await got(`https://publish.twitter.com/oembed?url=${t}`); - return resp.data.html; - } catch { - return `
This tweet may not exist
`; - } -} - -async function get_fanbox(p) { - try { - const m = p.match(/creator\/(\d+)\/post\/(\d+)/); - const post_id = m[2]; - const api_url = `https://api.fanbox.cc/post.info?postId=${post_id}`; - const resp = await got(api_url, { headers: get_header() }); - const post = resp.data.body; - - const home_url = `https://${post.creatorId}.fanbox.cc`; - const web_url = `${home_url}/posts/${post.id}`; - const datetime = new Date(post.updatedDatetime).toLocaleString('ja'); - - const box_html = ` -
- -
- ${post.title} -
-
- ${post.user.name} - Modify: ${datetime} - ${post.feeRequired} JPY -
- `; - return { url: web_url, html: box_html }; - } catch { - return { url: null, html: `
fanbox post (${p}) may not exist
` }; - } -} - -// embedded items -async function embed_map(e) { - const id = e.contentId || e.videoId; - const sp = e.serviceProvider; - - let ret = `Unknown host: ${sp}, with ID: ${id}`; - let url = null; - - try { - switch (sp) { - case 'youtube': - url = `https://www.youtube.com/embed/${id}`; - ret = ``; - break; - case 'vimeo': - url = `https://player.vimeo.com/video/${id}`; - ret = ``; - break; - case 'soundcloud': - url = `https://soundcloud.com/${id}`; - ret = ``; - break; - case 'twitter': - url = `https://twitter.com/i/status/${id}`; - ret = await get_twitter(url); - break; - case 'google_forms': - url = `https://docs.google.com/forms/d/e/${id}/viewform?embedded=true`; - ret = ``; - break; - case 'fanbox': { - const info = await get_fanbox(id); - url = info.url; - ret = info.html; - break; - } - case 'gist': - url = `https://gist.github.com/${id}`; - ret = ``; - break; - } - if (url) { - ret += `
Click here if embedded content is not loaded.`; - } - } catch (error) { - error; - } - - return ret; -} - -// render

blocks -function passage_conv(p) { - const seg = [...p.text]; - // seg.push(''); - if (p.styles) { - p.styles.map((s) => { - switch (s.type) { - case 'bold': - seg[s.offset] = `` + seg[s.offset]; - seg[s.offset + s.length - 1] += ``; - break; - } - return s; - }); - } - if (p.links) { - p.links.map((l) => { - seg[l.offset] = `` + seg[l.offset]; - seg[l.offset + l.length - 1] += ``; - return l; - }); - } - const ret = seg.join(''); - // console.log(ret) - return ret; -} - -// article types -function text_t(body) { - return body.text || ''; -} - -function image_t(body) { - let ret = body.text || ''; - body.images.map((i) => (ret += `


`)); - return ret; -} - -function file_t(body) { - let ret = body.text || ''; - body.files.map((f) => (ret += `
${f.name}.${f.extension}`)); - return ret; -} - -async function video_t(body) { - let ret = body.text || ''; - ret += (await embed_map(body.video)) || ''; - return ret; -} - -async function blog_t(body) { - let ret = []; - for (let x = 0; x < body.blocks.length; ++x) { - const b = body.blocks[x]; - ret.push('

'); - - switch (b.type) { - case 'p': - ret.push(passage_conv(b)); - break; - case 'header': - ret.push(`

${b.text}

`); - break; - case 'image': { - const i = body.imageMap[b.imageId]; - ret.push(``); - break; - } - case 'file': { - const f = body.fileMap[b.fileId]; - ret.push(`${f.name}.${f.extension}`); - break; - } - case 'embed': - ret.push(embed_map(body.embedMap[b.embedId])); // Promise object - break; - } - } - ret = await Promise.all(ret); // get real data - return ret.join(''); -} - -// parse by type -async function conv_article(i) { - let ret = ''; - if (i.title) { - ret += `[${i.type}] ${i.title}
`; - } - if (i.feeRequired !== 0) { - ret += `Fee Required: ${i.feeRequired} JPY/month
`; - } - if (i.coverImageUrl) { - ret += `
`; - } - - if (!i.body) { - ret += i.excerpt; - return ret; - } - - // console.log(i); - // skip paywall - - switch (i.type) { - case 'text': - ret += text_t(i.body); - break; - case 'file': - ret += file_t(i.body); - break; - case 'image': - ret += image_t(i.body); - break; - case 'video': - ret += await video_t(i.body); - break; - case 'article': - ret += await blog_t(i.body); - break; - default: - ret += 'Unsupported content (RSSHub)'; - } - return ret; -} - -// render wrapper -module.exports = async (i) => ({ - title: i.title || `No title`, - description: await conv_article(i), - pubDate: new Date(i.publishedDatetime).toUTCString(), - link: `https://${i.creatorId}.fanbox.cc/posts/${i.id}`, - category: i.tags, -}); diff --git a/lib/routes-deprecated/fanbox/header.js b/lib/routes-deprecated/fanbox/header.js deleted file mode 100644 index 55eb6b99bb0e8f..00000000000000 --- a/lib/routes-deprecated/fanbox/header.js +++ /dev/null @@ -1,13 +0,0 @@ -const config = require('@/config').value; - -// unlock contents paid by user -module.exports = () => { - const sessid = config.fanbox.session; - let cookie = ''; - if (sessid) { - cookie += `FANBOXSESSID=${sessid}`; - } - const headers = { origin: 'https://fanbox.cc', cookie }; - - return headers; -}; diff --git a/lib/routes-deprecated/fanbox/main.js b/lib/routes-deprecated/fanbox/main.js deleted file mode 100644 index dd4219e8be188d..00000000000000 --- a/lib/routes-deprecated/fanbox/main.js +++ /dev/null @@ -1,45 +0,0 @@ -// pixiv fanbox, maybe blocked by upstream - -// params: -// user?: fanbox domain name - -const got = require('@/utils/got'); -const { isValidHost } = require('@/utils/valid-host'); -const conv_item = require('./conv'); -const get_header = require('./header'); - -module.exports = async (ctx) => { - const user = ctx.params.user || 'official'; // if no user specified, just go to official page - if (!isValidHost(user)) { - throw new Error('Invalid user'); - } - const box_url = `https://${user}.fanbox.cc`; - - // get user info - let title = `${user}'s fanbox`; - let descr = title; - - try { - const user_api = `https://api.fanbox.cc/creator.get?creatorId=${user}`; - const resp_u = await got(user_api, { headers: get_header() }); - title = `${resp_u.data.body.user.name}'s fanbox`; - descr = resp_u.data.description; - } catch (error) { - error; - } - - // get user posts - const posts_api = `https://api.fanbox.cc/post.listCreator?creatorId=${user}&limit=20`; - const response = await got(posts_api, { headers: get_header() }); - - // render posts - const items = await Promise.all(response.data.body.items.map((i) => conv_item(i))); - - // return rss feed - ctx.state.data = { - title, - link: box_url, - description: descr, - item: items, - }; -}; diff --git a/lib/routes/copernicium/index.ts b/lib/routes/copernicium/index.ts index 5edf43c3f35660..aceeeaff813281 100644 --- a/lib/routes/copernicium/index.ts +++ b/lib/routes/copernicium/index.ts @@ -21,6 +21,7 @@ async function handler(ctx) { ['环球视角', '4_1'], ['人文叙述', '4_3'], ['观点评论', '4_5'], + ['专题报道', '4_7'], ]); if (!CATEGORY_TO_ARG_MAP.get(ctx.req.param().category)) { throw new Error('The requested category does not exist or is not supported.'); diff --git a/lib/routes/fanbox/index.ts b/lib/routes/fanbox/index.ts new file mode 100644 index 00000000000000..b8e84e31901a92 --- /dev/null +++ b/lib/routes/fanbox/index.ts @@ -0,0 +1,62 @@ +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import type { Data, Route } from '@/types'; +import { isValidHost } from '@/utils/valid-host'; +import type { Context } from 'hono'; +import { getHeaders, parseItem } from './utils'; +import type { PostListResponse, UserInfoResponse } from './types'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/:creator', + categories: ['social-media'], + example: '/fanbox/official', + parameters: { creator: 'fanbox user name' }, + maintainers: ['KarasuShin'], + name: 'Creator', + handler, + features: { + requireConfig: [ + { + name: 'FANBOX_SESSION_ID', + description: 'Required for private posts. Can be found in browser DevTools -> Application -> Cookies -> https://www.fanbox.cc -> FANBOXSESSID', + optional: true, + }, + ], + }, +}; + +async function handler(ctx: Context): Promise { + const creator = ctx.req.param('creator'); + if (!isValidHost(creator)) { + throw new InvalidParameterError('Invalid user name'); + } + + let title = `Fanbox - ${creator}`; + + let description: string | undefined; + + let image: string | undefined; + + try { + const userApi = `https://api.fanbox.cc/creator.get?creatorId=${creator}`; + const userInfoResponse = (await ofetch(userApi, { + headers: getHeaders(), + })) as UserInfoResponse; + title = `Fanbox - ${userInfoResponse.body.user.name}`; + description = userInfoResponse.body.description; + image = userInfoResponse.body.user.iconUrl; + } catch { + // ignore + } + + const postListResponse = (await ofetch(`https://api.fanbox.cc/post.listCreator?creatorId=${creator}&limit=20`, { headers: getHeaders() })) as PostListResponse; + const items = await Promise.all(postListResponse.body.items.map((i) => parseItem(i))); + + return { + title, + link: `https://${creator}.fanbox.cc`, + description, + image, + item: items, + }; +} diff --git a/lib/routes/thecatcity/namespace.ts b/lib/routes/fanbox/namespace.ts similarity index 60% rename from lib/routes/thecatcity/namespace.ts rename to lib/routes/fanbox/namespace.ts index 297bbae5492f9c..0244a7afd5fd78 100644 --- a/lib/routes/thecatcity/namespace.ts +++ b/lib/routes/fanbox/namespace.ts @@ -1,6 +1,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: '貓奴日常', - url: 'thecatcity.com', + name: 'fanbox', + url: 'https://www.fanbox.cc', }; diff --git a/lib/routes/fanbox/templates/fanbox-post.art b/lib/routes/fanbox/templates/fanbox-post.art new file mode 100644 index 00000000000000..ebf43dd4f1aaa0 --- /dev/null +++ b/lib/routes/fanbox/templates/fanbox-post.art @@ -0,0 +1,7 @@ + +

{{title}}

+ {{user.name}} +
+
+ {{excerpt}} +
diff --git a/lib/routes/fanbox/types.ts b/lib/routes/fanbox/types.ts new file mode 100644 index 00000000000000..a3198e9a6276cc --- /dev/null +++ b/lib/routes/fanbox/types.ts @@ -0,0 +1,239 @@ +export interface UserInfoResponse { + body: { + user: { + userId: string; + name: string; + iconUrl: string; + }; + creatorId: string; + description: string; + hasAdultContent: boolean; + coverImageUrl: string; + profileLinks: string[]; + profileItems: { + id: string; + type: string; + serviceProvider: string; + videoId: string; + }[]; + isFollowed: boolean; + isSupported: boolean; + isStopped: boolean; + isAcceptingRequest: boolean; + hasBoothShop: boolean; + }; +} + +export interface PostListResponse { + body: { + items: PostItem[]; + nextUrl: string | null; + }; +} + +export interface PostDetailResponse { + body: PostDetail; +} + +export interface PostItem { + commentCount: number; + cover: { + type: string; + url: string; + }; + creatorId: string; + excerpt: string; + feeRequired: number; + hasAdultContent: boolean; + id: string; + isLiked: boolean; + isRestricted: boolean; + likeCount: number; + publishedDatetime: string; + tags: string[]; + title: string; + updatedDatetime: string; + user: { + iconUrl: string; + name: string; + userId: string; + }; +} + +interface BasicPost { + commentCount: number; + commentList: { + items: { + body: string; + createdDatetime: string; + id: string; + isLiked: boolean; + isOwn: boolean; + likeCount: number; + parentCommentId: string; + replies: { + body: string; + createdDatetime: string; + id: string; + isLiked: boolean; + isOwn: boolean; + likeCount: number; + parentCommentId: string; + rootCommentId: string; + }[]; + rootCommentId: string; + user: { + iconUrl: string; + name: string; + userId: string; + }; + }[]; + nextUrl: string | null; + }; + coverImageUrl: string | null; + creatorId: string; + excerpt: string; + feeRequired: number; + hasAdultContent: boolean; + id: string; + imageForShare: string; + isLiked: boolean; + isRestricted: boolean; + likeCount: number; + nextPost: { + id: string; + title: string; + publishedDatetime: string; + }; + publishedDatetime: string; + tags: string[]; + title: string; + updatedDatetime: string; +} + +export interface ArticlePost extends BasicPost { + type: 'article'; + body: { + blocks: Block[]; + embedMap: { + [key: string]: unknown; + }; + fileMap: { + [key: string]: { + id: string; + extension: string; + name: string; + size: number; + url: string; + }; + }; + imageMap: { + [key: string]: { + id: string; + originalUrl: string; + thumbnailUrl: string; + width: number; + height: number; + extension: string; + }; + }; + urlEmbedMap: { + [key: string]: + | { + type: 'html'; + html: string; + id: string; + } + | { + type: 'fanbox.post'; + id: string; + postInfo: PostItem; + }; + }; + }; +} + +export interface FilePost extends BasicPost { + type: 'file'; + body: { + files: { + extension: string; + id: string; + name: string; + size: number; + url: string; + }[]; + text: string; + }; +} + +export interface VideoPost extends BasicPost { + type: 'video'; + body: { + text: string; + video: { + serviceProvider: 'youtube' | 'vimeo' | 'soundcloud'; + videoId: 'string'; + }; + }; +} + +export interface ImagePost extends BasicPost { + type: 'image'; + body: { + images: { + id: string; + originalUrl: string; + thumbnailUrl: string; + width: number; + height: number; + extension: string; + }[]; + text: string; + }; +} + +export interface TextPost extends BasicPost { + type: 'text'; + body: { + text: string; + }; +} + +export interface PostDetailResponse { + body: PostDetail; +} + +interface TextBlock { + type: 'p'; + text: string; + styles?: { + length: number; + offset: number; + type: 'bold'; + }[]; +} + +interface HeaderBlock { + type: 'header'; + text: string; +} + +interface ImageBlock { + type: 'image'; + imageId: string; +} + +interface FileBlock { + type: 'file'; + fileId: string; +} + +interface EmbedBlock { + type: 'url_embed'; + urlEmbedId: string; +} + +type PostDetail = ArticlePost | FilePost | ImagePost | VideoPost | TextPost; + +type Block = TextBlock | HeaderBlock | ImageBlock | FileBlock | EmbedBlock; diff --git a/lib/routes/fanbox/utils.ts b/lib/routes/fanbox/utils.ts new file mode 100644 index 00000000000000..c6fbf83613c00c --- /dev/null +++ b/lib/routes/fanbox/utils.ts @@ -0,0 +1,188 @@ +import { config } from '@/config'; +import type { DataItem } from '@/types'; +import ofetch from '@/utils/ofetch'; +import type { ArticlePost, FilePost, ImagePost, PostDetailResponse, PostItem, TextPost, VideoPost } from './types'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { getCurrentPath } from '@/utils/helpers'; + +const __dirname = getCurrentPath(import.meta.url); + +export function getHeaders() { + const sessionid = config.fanbox.session; + const cookie = sessionid ? `FANBOXSESSID=${sessionid}` : ''; + return { + origin: 'https://fanbox.cc', + cookie, + }; +} + +function embedUrlMap(urlEmbed: ArticlePost['body']['urlEmbedMap'][string]) { + switch (urlEmbed.type) { + case 'html': + return urlEmbed.html; + case 'fanbox.post': + return art(path.join(__dirname, 'templates/fanbox-post.art'), { + postUrl: `https://${urlEmbed.postInfo.creatorId}.fanbox.cc/posts/${urlEmbed.postInfo.id}`, + title: urlEmbed.postInfo.title, + user: urlEmbed.postInfo.user, + excerpt: urlEmbed.postInfo.excerpt, + }); + default: + return ''; + } +} + +function passageConv(p) { + const seg = [...p.text]; + if (p.styles) { + p.styles.map((s) => { + switch (s.type) { + case 'bold': + seg[s.offset] = `` + seg[s.offset]; + seg[s.offset + s.length - 1] += ``; + break; + default: + } + return s; + }); + } + if (p.links) { + p.links.map((l) => { + seg[l.offset] = `` + seg[l.offset]; + seg[l.offset + l.length - 1] += ``; + return l; + }); + } + const ret = seg.join(''); + return ret; +} + +function parseText(body: TextPost['body']) { + return body.text || ''; +} + +function parseImage(body: ImagePost['body']) { + let ret = body.text || ''; + for (const i of body.images) { + ret += `
`; + } + return ret; +} + +function parseFile(body: FilePost['body']) { + let ret = body.text || ''; + for (const f of body.files) { + ret += `
${f.name}.${f.extension}`; + } + return ret; +} + +async function parseVideo(body: VideoPost['body']) { + let ret = ''; + switch (body.video.serviceProvider) { + case 'soundcloud': + ret += await getSoundCloudEmbedUrl(body.video.videoId); + break; + case 'youtube': + ret += ``; + break; + case 'vimeo': + ret += ``; + break; + default: + } + ret += `
${body.text}`; + return ret; +} + +async function parseArtile(body: ArticlePost['body']) { + let ret: Array = []; + for (let x = 0; x < body.blocks.length; ++x) { + const b = body.blocks[x]; + ret.push('

'); + + switch (b.type) { + case 'p': + ret.push(passageConv(b)); + break; + case 'header': + ret.push(`

${b.text}

`); + break; + case 'image': { + const i = body.imageMap[b.imageId]; + ret.push(``); + break; + } + case 'file': { + const file = body.fileMap[b.fileId]; + ret.push(`${file.name}.${file.extension}`); + break; + } + case 'url_embed': + ret.push(embedUrlMap(body.urlEmbedMap[b.urlEmbedId])); + break; + default: + } + } + ret = await Promise.all(ret); + return ret.join(''); +} + +async function parseDetail(i: PostDetailResponse['body']) { + let ret = ''; + if (i.feeRequired !== 0) { + ret += `Fee Required: ${i.feeRequired} JPY/month
`; + } + if (i.coverImageUrl) { + ret += `
`; + } + + if (!i.body) { + ret += i.excerpt; + return ret; + } + + switch (i.type) { + case 'text': + ret += parseText(i.body); + break; + case 'file': + ret += parseFile(i.body); + break; + case 'image': + ret += parseImage(i.body); + break; + case 'video': + ret += await parseVideo(i.body); + break; + case 'article': + ret += await parseArtile(i.body); + break; + default: + ret += 'Unsupported content (RSSHub)'; + } + return ret; +} + +export function parseItem(item: PostItem) { + return cache.tryGet(`fanbox-${item.id}-${item.updatedDatetime}`, async () => { + const postDetail = (await ofetch(`https://api.fanbox.cc/post.info?postId=${item.id}`, { headers: getHeaders() })) as PostDetailResponse; + return { + title: item.title || `No title`, + description: await parseDetail(postDetail.body), + pubDate: parseDate(item.updatedDatetime), + link: `https://${item.creatorId}.fanbox.cc/posts/${item.id}`, + category: item.tags, + }; + }) as Promise; +} + +async function getSoundCloudEmbedUrl(videoId: string) { + const videoUrl = `https://soundcloud.com/${videoId}`; + const apiUrl = `https://soundcloud.com/oembed?url=${encodeURIComponent(videoUrl)}&format=json&maxheight=400&format=json`; + const resp = await ofetch(apiUrl); + return resp.html; +} diff --git a/lib/routes/hackernews/index.ts b/lib/routes/hackernews/index.ts index 21b8e8b52a6c44..259a3ff22c1c52 100644 --- a/lib/routes/hackernews/index.ts +++ b/lib/routes/hackernews/index.ts @@ -19,7 +19,7 @@ export const route: Route = { }, radar: [ { - source: ['ycombinator.com/:section', 'ycombinator.com/'], + source: ['news.ycombinator.com/:section', 'news.ycombinator.com/'], }, ], name: '用户', diff --git a/lib/routes/linkedin/jobs.ts b/lib/routes/linkedin/jobs.ts index 9cf1fb9676d437..9b87514c37686c 100644 --- a/lib/routes/linkedin/jobs.ts +++ b/lib/routes/linkedin/jobs.ts @@ -1,16 +1,21 @@ import { Route } from '@/types'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; -import { parseJobSearch, KEYWORDS_QUERY_KEY, JOB_TYPES, JOB_TYPES_QUERY_KEY, EXP_LEVELS_QUERY_KEY, parseParamsToSearchParams, EXP_LEVELS, parseParamsToString } from './utils'; +import { EXP_LEVELS, EXP_LEVELS_QUERY_KEY, JOB_TYPES, JOB_TYPES_QUERY_KEY, KEYWORDS_QUERY_KEY, parseJobSearch, parseParamsToSearchParams, parseParamsToString, parseRouteParam } from './utils'; const BASE_URL = 'https://www.linkedin.com/'; const JOB_SEARCH_PATH = '/jobs-guest/jobs/api/seeMoreJobPostings/search'; export const route: Route = { - path: '/jobs/:job_types/:exp_levels/:keywords?', - categories: ['other'], + path: '/jobs/:job_types/:exp_levels/:keywords?/:routeParams?', + categories: ['social-media'], example: '/linkedin/jobs/C-P/1/software engineer', - parameters: { job_types: "See the following table for details, use '-' as delimiter", exp_levels: "See the following table for details, use '-' as delimiter", keywords: 'keywords' }, + parameters: { + job_types: "See the following table for details, use '-' as delimiter", + exp_levels: "See the following table for details, use '-' as delimiter", + keywords: 'keywords', + routeParams: 'additional query parameters, see the table below', + }, features: { requireConfig: false, requirePuppeteer: false, @@ -19,8 +24,29 @@ export const route: Route = { supportPodcast: false, supportScihub: false, }, + radar: [ + { + source: ['www.linkedin.com/jobs/search'], + // Migrate from https://github.com/DIYgod/RSSHub-Radar/blob/096589db99f993c262ec8edb51a5676325439bc5/src/lib/radar-rules.ts#L15501 + target: (params, url) => { + const searchParams = new URLSearchParams(new URL(url).search); + const fJT = parseRouteParam(searchParams.get('f_JT')); + const fE = parseRouteParam(searchParams.get('f_E')); + const keywords = encodeURIComponent(searchParams.get('keywords') || ''); + + const newSearchParams = new URLSearchParams(); + // Copy non-existent key-value pairs from searchParams to newSearchParams + for (const [key, value] of searchParams.entries()) { + if (!['f_JT', 'f_E', 'keywords'].includes(key)) { + newSearchParams.append(key, value); + } + } + return `/linkedin/jobs/${fJT}/${fE}/${keywords}/?${newSearchParams.toString()}`; + }, + }, + ], name: 'Jobs', - maintainers: [], + maintainers: ['BrandNewLifeJackie26', 'zhoukuncheng'], handler, description: `#### \`job_types\` list @@ -34,10 +60,34 @@ export const route: Route = { | --------- | ----------- | --------- | ---------------- | -------- | --- | | 1 | 2 | 3 | 4 | 5 | all | + #### \`routeParams\` additional query parameters + + ##### \`f_WT\` list + + | Onsite | Remote | Hybrid | + | ------ | ------- | ------ | + | 1 | 2 | 3 | + + ##### \`geoId\` + + Geographic location ID. You can find this ID in the URL of a LinkedIn job search page that is filtered by location. + + For example: + 91000012 is the ID of East Asia. + + ##### \`f_TPR\` + + Time posted range. Here are some possible values: + + * \`r86400\`: Past 24 hours + * \`r604800\`: Past week + * \`r2592000\`: Past month + For example: 1. If we want to search software engineer jobs of all levels and all job types, use \`/linkedin/jobs/all/all/software engineer\` 2. If we want to search all entry level contractor/part time software engineer jobs, use \`/linkedin/jobs/P-C/2/software engineer\` + 3. If we want to search remote mid-senior level software engineer jobs in APAC posted within the last month, use \`/linkedin/jobs/F/4/software%20engineer/f_WT=2&geoId=91000003&f_TPR=r2592000\` **To make it easier, the recommended way is to start a search on [LinkedIn](https://www.linkedin.com/jobs/search) and use [RSSHub Radar](https://github.com/DIYgod/RSSHub-Radar) to load the specific feed.**`, }; @@ -45,23 +95,31 @@ export const route: Route = { async function handler(ctx) { const jobTypesParam = parseParamsToSearchParams(ctx.req.param('job_types'), JOB_TYPES); const expLevelsParam = parseParamsToSearchParams(ctx.req.param('exp_levels'), EXP_LEVELS); + const routeParams = new URLSearchParams(ctx.req.param('routeParams')); let url = new URL(JOB_SEARCH_PATH, BASE_URL); + + // keep for backward compatibility url.searchParams.append(KEYWORDS_QUERY_KEY, ctx.req.param('keywords') || ''); url.searchParams.append(JOB_TYPES_QUERY_KEY, jobTypesParam); // see JOB_TYPES in utils.js url.searchParams.append(EXP_LEVELS_QUERY_KEY, expLevelsParam); // see EXPERIENCE_LEVELS in utils.js + + // Add route params to URL + for (const [key, value] of routeParams) { + if (!url.searchParams.has(key)) { + url.searchParams.append(key, value); + } + } url = url.toString(); // Parse job search page - const response = await got({ - method: 'get', - url, - }); - const jobs = parseJobSearch(response.data); + const response = await ofetch(url); + const jobs = parseJobSearch(response); const jobTypes = parseParamsToString(ctx.req.param('job_types'), JOB_TYPES); const expLevels = parseParamsToString(ctx.req.param('exp_levels'), EXP_LEVELS); const feedTitle = 'LinkedIn Job Listing' + (jobTypes ? ` | Job Types: ${jobTypes}` : '') + (expLevels ? ` | Experience Levels: ${expLevels}` : '') + (ctx.req.param('keywords') ? ` | Keywords: ${ctx.req.param('keywords')}` : ''); + return { title: feedTitle, link: url, diff --git a/lib/routes/linkedin/namespace.ts b/lib/routes/linkedin/namespace.ts index fb87f9995359d3..4069ccd7cd50a9 100644 --- a/lib/routes/linkedin/namespace.ts +++ b/lib/routes/linkedin/namespace.ts @@ -1,6 +1,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'LinkedIn 领英中国', + name: 'LinkedIn 领英', url: 'linkedin.com', }; diff --git a/lib/routes/linkedin/utils.ts b/lib/routes/linkedin/utils.ts index a4a67e449ee492..d069ddb2280ae7 100644 --- a/lib/routes/linkedin/utils.ts +++ b/lib/routes/linkedin/utils.ts @@ -40,6 +40,10 @@ const EXP_LEVELS = { * as search param in url */ function parseParamsToSearchParams(params, map) { + if (!params) { + return ''; + } // Handle undefined params + const validParamValues = params.split('-').filter((v) => v in map); return validParamValues.join(','); } @@ -54,6 +58,10 @@ function parseParamsToSearchParams(params, map) { * @returns param value strings separated by ',' */ function parseParamsToString(params, map) { + if (!params) { + return ''; + } // Handle undefined params + const validParamValues = params .split('-') .filter((v) => v in map) @@ -108,4 +116,11 @@ function parseJobDetail(data) { return job; } -export { parseParamsToSearchParams, parseParamsToString, parseJobDetail, parseJobSearch, JOB_TYPES, JOB_TYPES_QUERY_KEY, EXP_LEVELS, EXP_LEVELS_QUERY_KEY, KEYWORDS_QUERY_KEY }; +const parseRouteParam = (searchParam: string | null): string => { + if (!searchParam || typeof searchParam !== 'string') { + return 'all'; + } + return encodeURIComponent(searchParam.split(',').join('-')); +}; + +export { parseParamsToSearchParams, parseParamsToString, parseJobDetail, parseJobSearch, parseRouteParam, JOB_TYPES, JOB_TYPES_QUERY_KEY, EXP_LEVELS, EXP_LEVELS_QUERY_KEY, KEYWORDS_QUERY_KEY }; diff --git a/lib/routes/thecatcity/terms-map.ts b/lib/routes/thecatcity/terms-map.ts deleted file mode 100644 index 016cdc597ff4ae..00000000000000 --- a/lib/routes/thecatcity/terms-map.ts +++ /dev/null @@ -1,28 +0,0 @@ -const termsMap = { - '': { - title: 'CatCity 貓奴日常 | 貓咪日常照顧、新手準備、貓用品、貓咪醫療', - slug: '/', - }, - 1: { - title: '貓物分享|流行小物、貓咪用品', - slug: '/category/cute-item', - }, - 2: { - title: '貓咪新聞|貓界人氣熱話、貓電影', - slug: '/category/funny-news', - }, - 3: { - title: '養貓大全|貓咪飲食與醫療、行為心理、貓測驗與冷知識', - slug: '/category/knowledge', - }, - 4: { - title: '貓奴景點|貓咪咖啡廳與餐廳、貓奴旅行景點推薦', - slug: '/category/hot-spot', - }, - 5: { - title: '新手養貓教學|養貓準備與花費、日常照顧', - slug: '/category/raise-cats', - }, -}; - -export { termsMap }; diff --git a/lib/routes/thecatcity/index.ts b/lib/routes/thepetcity/index.ts similarity index 51% rename from lib/routes/thecatcity/index.ts rename to lib/routes/thepetcity/index.ts index 9a849cbb333887..fb43599da5a5ae 100644 --- a/lib/routes/thecatcity/index.ts +++ b/lib/routes/thepetcity/index.ts @@ -1,47 +1,38 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; import { termsMap } from './terms-map'; -const baseUrl = 'https://thecatcity.com'; +const baseUrl = 'https://thepetcity.co'; export const route: Route = { path: '/:term?', categories: ['new-media'], - example: '/thecatcity', + example: '/thepetcity', parameters: { term: '見下表,留空為全部文章' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['thecatcity.com/'], - target: '', - }, - ], + radar: Object.entries(termsMap).map(([key, value]) => ({ + title: value.title, + source: [...new Set([`thepetcity.co${value.slug}`, 'thepetcity.co/'])], + target: key ? `/${key}` : '', + })), name: '分類', - maintainers: ['TonyRL'], + maintainers: ['TonyRL', 'bigfei'], handler, - url: 'thecatcity.com/', - description: `| 貓物分享 | 貓咪新聞 | 養貓大全 | 貓奴景點 | 新手養貓教學 | - | -------- | -------- | -------- | -------- | ------------ | - | 1 | 2 | 3 | 4 | 5 |`, + url: 'thepetcity.co/', + description: `| Column Name | TermID | + | -------------------- | ------ | + | Knowledge飼養大全 | 3 | + | Funny News毛孩趣聞 | 2 | + | Raise Pets 養寵物新手 | 5 | + | Hot Spot 毛孩打卡點 | 4 | + | Pet Staff 毛孩好物 | 1 |`, }; async function handler(ctx) { const term = ctx.req.param('term'); - const { data } = await got(`${baseUrl}/node_api/v1/articles/posts`, { - searchParams: { - pageId: 977_080_509_047_743, - term, - }, - }); + const searchParams = term ? { pageId: 977_080_509_047_743, term } : { pageId: 977_080_509_047_743 }; + const data = await ofetch(`${baseUrl}/node_api/v1/articles/posts`, { query: { ...searchParams } }); const list = data.data.posts.map((post) => ({ title: post.title, @@ -55,8 +46,8 @@ async function handler(ctx) { const items = await Promise.all( list.map((item) => cache.tryGet(item.guid, async () => { - const { data } = await got(item.api, { - searchParams: { + const data = await ofetch(item.api, { + query: { pageId: 977_080_509_047_743, }, }); @@ -70,9 +61,9 @@ async function handler(ctx) { return { title: termsMap[term] ? termsMap[term].title : termsMap[''].title, - description: '提供貓咪日常照顧、新手準備、貓用品、貓咪醫療、貓飲食與行為等相關知識,以及療癒貓影片、貓趣聞、貓小物流行資訊,不論你是貓奴、還是貓控,一切所需都在貓奴日常找到', + description: '專屬毛孩愛好者的資訊平台,不論你是貓奴、狗奴,還是其他動物控,一起發掘最新的萌寵趣聞、有趣的寵物飼養知識、訓練動物、竉物用品推介、豐富多樣的寵物可愛影片。', link: baseUrl, - image: 'https://assets.presslogic.com/presslogic-hk-tc/static/favicon.ico', + image: 'https://assets.presslogic.com/presslogic-hk-pc/static/favicon.ico', item: items, }; } diff --git a/lib/routes/thepetcity/namespace.ts b/lib/routes/thepetcity/namespace.ts new file mode 100644 index 00000000000000..83114d6e092cf6 --- /dev/null +++ b/lib/routes/thepetcity/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'PetCity 毛孩日常', + url: 'thepetcity.com', +}; diff --git a/lib/routes/thepetcity/terms-map.ts b/lib/routes/thepetcity/terms-map.ts new file mode 100644 index 00000000000000..824feb75d160a8 --- /dev/null +++ b/lib/routes/thepetcity/terms-map.ts @@ -0,0 +1,28 @@ +const termsMap = { + '': { + title: 'PetCity 毛孩日常 | 飼養竉物、竉物用品、萌寵趣聞', + slug: '/', + }, + 1: { + title: 'Pet Staff 毛孩好物', + slug: '/category/cute-item', + }, + 2: { + title: 'Funny News毛孩趣聞', + slug: '/category/funny-news', + }, + 3: { + title: 'Knowledge飼養大全', + slug: '/category/knowledge', + }, + 4: { + title: 'Hot Spot 毛孩打卡點', + slug: '/category/hot-spot', + }, + 5: { + title: 'Raise Pets 養寵物新手', + slug: '/category/raise-cats', + }, +}; + +export { termsMap }; diff --git a/lib/routes/ttv/index.ts b/lib/routes/ttv/index.ts new file mode 100644 index 00000000000000..042c693a9363e6 --- /dev/null +++ b/lib/routes/ttv/index.ts @@ -0,0 +1,78 @@ +import { Route } from '@/types'; + +import got from '@/utils/got'; +import timezone from '@/utils/timezone'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import cache from '@/utils/cache'; + +export const route: Route = { + path: '/:category?', + categories: ['traditional-media'], + example: '/ttv', + parameters: { category: '分类' }, + name: '分类', + maintainers: ['dzx-dzx'], + radar: [ + { + source: ['news.ttv.com.tw/:category'], + }, + ], + handler, +}; + +async function handler(ctx) { + const rootUrl = 'https://news.ttv.com.tw'; + const category = ctx.req.param('category') ?? 'realtime'; + const currentUrl = `${rootUrl}/${['realtime', 'focus'].includes(category) ? category : `category/${category}`}`; + + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = load(response.data); + + let items = $('div.news-list li') + .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.query.limit) : 30) + .toArray() + .map((item) => { + item = $(item); + + return { + link: $(item).find('a').attr('href'), + }; + }); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link, async () => { + const detailResponse = await got({ + method: 'get', + url: item.link, + }); + + const content = load(detailResponse.data); + + item.title = content('title').text(); + item.pubDate = timezone(parseDate(content('meta[property="article:published_time"]').attr('content')), +8); + item.category = content('div.article-body ul.tag') + .find('a') + .toArray() + .map((t) => content(t).text()); + const section = content("meta[property='article:section']").attr('content'); + if (!item.category.includes(section)) { + item.category.push(section); + } + item.description = content('#newscontent').html(); + return item; + }) + ) + ); + + return { + title: $('title').text(), + link: currentUrl, + item: items, + }; +} diff --git a/lib/routes/ttv/namespace.ts b/lib/routes/ttv/namespace.ts new file mode 100644 index 00000000000000..95ba16c62df026 --- /dev/null +++ b/lib/routes/ttv/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '台視新聞網', + url: 'news.ttv.com.tw', +}; diff --git a/lib/routes/weekendhk/posts.ts b/lib/routes/weekendhk/posts.ts index 6b84ae2fdf30dd..d642a4c6c05192 100644 --- a/lib/routes/weekendhk/posts.ts +++ b/lib/routes/weekendhk/posts.ts @@ -4,13 +4,13 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/', + example: '/weekendhk', radar: [ { source: ['weekendhk.com/'], - target: '', }, ], - name: 'Unknown', + name: '最新文章', maintainers: ['TonyRL'], handler, url: 'weekendhk.com/', diff --git a/lib/routes/wellcee/namespace.ts b/lib/routes/wellcee/namespace.ts new file mode 100644 index 00000000000000..8641c71079161b --- /dev/null +++ b/lib/routes/wellcee/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Wellcee 唯心所寓', + url: 'wellcee.com', + categories: ['other'], +}; diff --git a/lib/routes/wellcee/rent.ts b/lib/routes/wellcee/rent.ts new file mode 100644 index 00000000000000..dc67f30c91bfef --- /dev/null +++ b/lib/routes/wellcee/rent.ts @@ -0,0 +1,81 @@ +import { Route } from '@/types'; +import type { Context } from 'hono'; +import { District, House } from './types'; + +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { baseUrl, getCitys, getDistricts } from './utils'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import { art } from '@/utils/render'; +import path from 'path'; +import { getCurrentPath } from '@/utils/helpers'; + +const __dirname = getCurrentPath(import.meta.url); +const render = (data) => art(path.join(__dirname, 'templates', 'house.art'), data); + +export const route: Route = { + path: '/rent/:city/:district?', + example: '/wellcee/rent/北京', + parameters: { + city: '城市', + district: '地区', + }, + name: '租房信息', + maintainers: ['TonyRL'], + handler, + url: 'www.wellcee.com', + description: '支持的城市可以通过 [/wellcee/support-city](https://rsshub.app/wellcee/support-city) 获取', +}; + +async function handler(ctx: Context) { + const { city, district = '' } = ctx.req.param(); + const citys = await getCitys(); + const cityInfo = citys.find((item) => item.chCityName === city); + if (!cityInfo) { + throw new InvalidParameterError('Invalid city'); + } + + let districtInfo: District | undefined; + if (district) { + const districts = await getDistricts(cityInfo.id); + const d = districts.find((item) => item.name === district); + if (!d) { + throw new InvalidParameterError('Invalid district'); + } + districtInfo = d; + } + + const response = await ofetch(`${baseUrl}/api/house/filter`, { + method: 'POST', + body: { + districtIds: districtInfo?.id ? [districtInfo.id] : [], + subways: [], + rentTypeIds: [], + timeTypeIds: [], + price: [], + tagTypeIds: [], + cityId: cityInfo.id, + lang: 1, + pn: 1, + }, + }); + + const items = (response.data.list as House[]).map((item) => ({ + title: item.address, + link: `${baseUrl}/rent-apartment/${item.id}`, + description: render({ item }), + pubDate: parseDate(item.loginTime, 'X'), + author: item.userInfo.name, + category: [...item.tags, ...item.typeTags], + })); + + return { + title: `${city}${districtInfo?.name ?? ''}租房信息 - Wellcee`, + description: `${cityInfo.statics.online_text} ${cityInfo.statics.total_text}`, + image: cityInfo.icon, + icon: cityInfo.icon, + logo: cityInfo.icon, + link: `${baseUrl}/rent-apartment/${cityInfo.cityName}/list?cityId=${cityInfo.id}&lang=zh${district ? `#districtIds=${districtInfo?.id}` : ''}`, + item: items, + }; +} diff --git a/lib/routes/wellcee/support-city.ts b/lib/routes/wellcee/support-city.ts new file mode 100644 index 00000000000000..2b0b88d9b729f2 --- /dev/null +++ b/lib/routes/wellcee/support-city.ts @@ -0,0 +1,46 @@ +import { Route } from '@/types'; +import type { Context } from 'hono'; + +import { baseUrl, getCitys, getDistricts } from './utils'; + +export const route: Route = { + path: '/support-city', + example: '/wellcee/support-city', + name: '支持的城市', + maintainers: ['TonyRL'], + radar: [ + { + source: ['www.wellcee.com'], + }, + ], + handler, + url: 'www.wellcee.com', +}; + +async function handler(ctx: Context) { + const citys = await getCitys(); + + const list = await Promise.all( + citys.map(async (city) => ({ + ...city, + district: await getDistricts(city.id), + })) + ); + const requestHost = new URL(ctx.req.url).host; + + const items = list.flatMap((city) => + city.district.map((district) => ({ + title: `${city.chCityName} - ${district.name}`, + description: `${city.chCityName} - ${district.name}`, + link: `https://${requestHost}/wellcee/rent/${city.chCityName}/${district.name}`, + })) + ); + + return { + title: '支持的城市 - Wellcee', + description: + '上海国际化租房平台|北京合租&找室友|香港留学生租房|深圳无中介租房|广州外国人租房 |杭州高品质租房|成都房东直租;同志友好&宠物友好;Wellcee 的生活方式:社交|活动|交友|美食|宠物领养|音乐&艺术;Wellcee 的二手市集:家居|电子|奢侈品|时尚。', + link: baseUrl, + item: items, + }; +} diff --git a/lib/routes/wellcee/templates/house.art b/lib/routes/wellcee/templates/house.art new file mode 100644 index 00000000000000..2fb3b737931dc7 --- /dev/null +++ b/lib/routes/wellcee/templates/house.art @@ -0,0 +1,18 @@ +租金: {{ item.rent }}
+{{ if item.dailyRent }} +日租: {{ item.dailyRent }}
+{{ /if }} +
+ +{{ if item.video }} + +
+{{ /if }} + +{{ if item.imgs }} + {{ each item.imgs img }} +
+ {{ /each }} +{{ /if }} diff --git a/lib/routes/wellcee/types.ts b/lib/routes/wellcee/types.ts new file mode 100644 index 00000000000000..9e718a0087066c --- /dev/null +++ b/lib/routes/wellcee/types.ts @@ -0,0 +1,58 @@ +export interface City { + id: string; + icon: string; + city: string; + enCityName: string; + cityName: string; + chCityName: string; + twCityName: string; + statics: { + total_text: string; + online_text: string; + total_count: number; + online_count: number; + }; + district: District[]; +} + +export interface District { + id: string; + longitude: number; + latitude: number; + name: string; + business: { + id: string; + name: string; + }[]; +} + +export interface House { + id: string; + imgs: string[]; + ev: number; + city: string; + district: string; + tagsText: string; + tags: string[]; + typeTags: string[]; + rent: string; + video: string; + personalDesc: string; + address: string; + longitude: number; + latitude: number; + collectNum: number; + isCollect: boolean; + userInfo: { + id: string; + name: string; + gender: number; + avatar: string; + language: string[]; + loginTime: string; + tenant_status: number; + landlord_status: number; + }; + loginTime: number; + dailyRent: string; +} diff --git a/lib/routes/wellcee/utils.ts b/lib/routes/wellcee/utils.ts new file mode 100644 index 00000000000000..f10baf402c8abd --- /dev/null +++ b/lib/routes/wellcee/utils.ts @@ -0,0 +1,45 @@ +import { City, District } from './types'; + +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; +import { config } from '@/config'; + +export const baseUrl = 'https://www.wellcee.com'; +export const getCitys = () => + cache.tryGet( + 'wellcee:citys', + async () => { + const response = await ofetch(`${baseUrl}/api/home/index`, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + lang: '1', + userId: '', + type: '1', + }).toString(), + }); + + return response.data.citys; + }, + config.cache.routeExpire, + false + ) as Promise; + +export const getDistricts = (cityId: string) => + cache.tryGet( + `wellcee:city:${cityId}`, + async () => { + const response = await ofetch(`${baseUrl}/api/house/filterType`, { + query: { + cityId, + lang: '1', + }, + }); + + return response.data.district; + }, + config.cache.routeExpire, + false + ) as Promise; diff --git a/lib/routes/yenpress/namespace.ts b/lib/routes/yenpress/namespace.ts new file mode 100644 index 00000000000000..2749ef6d48edf1 --- /dev/null +++ b/lib/routes/yenpress/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Yen Press', + url: 'yenpress.com', + categories: ['reading'], +}; diff --git a/lib/routes/yenpress/series.ts b/lib/routes/yenpress/series.ts new file mode 100644 index 00000000000000..e953ac205aea7c --- /dev/null +++ b/lib/routes/yenpress/series.ts @@ -0,0 +1,115 @@ +import { DataItem, Route } from '@/types'; +import type { Context } from 'hono'; + +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; +import * as cheerio from 'cheerio'; +import timezone from '@/utils/timezone'; +import { parseDate } from '@/utils/parse-date'; +import { art } from '@/utils/render'; +import path from 'node:path'; +import { getCurrentPath } from '@/utils/helpers'; + +const __dirname = getCurrentPath(import.meta.url); + +const render = (data) => art(path.join(__dirname, 'templates', 'series.art'), data); + +export const route: Route = { + path: '/series/:name', + example: '/yenpress/series/alya-sometimes-hides-her-feelings-in-russian', + parameters: { name: 'Series name' }, + name: 'Series', + maintainers: ['TonyRL'], + handler, + radar: [ + { + source: ['yenpress.com/series/:name'], + target: '/series/:name', + }, + ], +}; + +async function handler(ctx: Context) { + const { name: series } = ctx.req.param(); + const baseUrl = 'https://yenpress.com'; + const link = `https://yenpress.com/series/${series}`; + + const response = await ofetch(link); + const $ = cheerio.load(response); + + const list = $('.show-more-container .inline_block') + .toArray() + .map((item) => { + const $item = $(item); + return { + title: $item.find('span').text().trim(), + link: new URL($item.find('a').attr('href')!, baseUrl).href, + }; + }) as DataItem[]; + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link!, async () => { + const response = await ofetch(item.link!); + const $ = cheerio.load(response); + + item.category = $('.detail-labels.mobile-only') + .eq(0) + .find('a') + .toArray() + .map((a) => $(a).text().trim()); + + $('svg, .social-share, .desktop-only, .detail-labels').remove(); + + const cover = $('.book-info').find('.book-cover-img').html(); + + const bookInfo = $('.buy-info .deliver') + .toArray() + .map((item, i) => ({ + deliver: $(item).text().trim(), + price: $('.book-price').eq(i).text().trim(), + from: $('.services') + .eq(i) + .find('.service') + .toArray() + .map((service) => { + const a = $(service).find('a'); + return { + name: a.text().trim(), + link: a.attr('href'), + }; + }), + detail: $('.detail-info') + .eq(i) + .find('div span') + .toArray() + .map((span) => { + const $span = $(span); + return { + key: $span.text().trim(), + value: $span.next().text().trim(), + }; + }), + })); + + const info = $('.book-info'); + info.find('.buy-info, .series-cover').remove(); + + item.description = render({ + cover: cover! + info.html(), + bookInfo, + }); + item.pubDate = timezone(parseDate(bookInfo[0].detail.find((d) => d.key === 'Release Date')!.value), 0); + + return item; + }) + ) + ); + + return { + title: $('head title').text().trim(), + description: $('.social-share p').text().trim(), + link, + item: items, + }; +} diff --git a/lib/routes/yenpress/templates/series.art b/lib/routes/yenpress/templates/series.art new file mode 100644 index 00000000000000..5e108752caddad --- /dev/null +++ b/lib/routes/yenpress/templates/series.art @@ -0,0 +1,25 @@ +{{ if cover }} + {{@ cover }}
+{{ /if }} + +{{ if bookInfo }} + {{ each bookInfo info }} +

{{ info.deliver }}: {{ info.price }}

+ Buy from: + + FULL DETAILS + + {{ each info.detail d }} + + + + + {{ /each }} +
{{ d.key }}{{ d.value }}
+
+ {{ /each }} +{{ /if }} diff --git a/package.json b/package.json index 47385bec3276e1..40be609a372f50 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ }, "dependencies": { "@hono/node-server": "1.11.1", - "@hono/swagger-ui": "0.2.1", + "@hono/swagger-ui": "0.2.2", "@hono/zod-openapi": "0.11.0", "@notionhq/client": "2.2.15", "@postlight/parser": "2.2.3", @@ -75,7 +75,7 @@ "fanfou-sdk": "5.0.0", "form-data": "4.0.0", "googleapis": "135.0.0", - "hono": "4.2.8", + "hono": "4.2.9", "html-to-text": "9.0.5", "https-proxy-agent": "7.0.4", "iconv-lite": "0.6.3", @@ -128,7 +128,7 @@ "@babel/preset-env": "7.24.5", "@babel/preset-typescript": "7.24.1", "@microsoft/eslint-formatter-sarif": "3.1.0", - "@stylistic/eslint-plugin": "1.7.2", + "@stylistic/eslint-plugin": "1.8.0", "@types/aes-js": "3.1.4", "@types/babel__preset-env": "7.9.6", "@types/crypto-js": "4.2.2", @@ -156,11 +156,11 @@ "@typescript-eslint/eslint-plugin": "7.8.0", "@typescript-eslint/parser": "7.8.0", "@vercel/nft": "0.26.4", - "@vitest/coverage-v8": "1.5.2", + "@vitest/coverage-v8": "1.5.3", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-nibble": "8.1.0", - "eslint-plugin-n": "17.3.1", + "eslint-plugin-n": "17.4.0", "eslint-plugin-prettier": "5.1.3", "eslint-plugin-unicorn": "52.0.0", "eslint-plugin-yml": "1.14.0", @@ -177,8 +177,9 @@ "typescript": "5.4.5", "unified": "11.0.4", "vite-tsconfig-paths": "4.3.2", - "vitest": "1.5.2" + "vitest": "1.5.3" }, + "packageManager": "pnpm@8.15.7+sha256.50783dd0fa303852de2dd1557cd4b9f07cb5b018154a6e76d0f40635d6cee019", "engines": { "node": ">=20" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 966d783b1df741..36119b38a5d2a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,11 @@ dependencies: specifier: 1.11.1 version: 1.11.1 '@hono/swagger-ui': - specifier: 0.2.1 - version: 0.2.1(hono@4.2.8) + specifier: 0.2.2 + version: 0.2.2(hono@4.2.9) '@hono/zod-openapi': specifier: 0.11.0 - version: 0.11.0(hono@4.2.8)(zod@3.23.5) + version: 0.11.0(hono@4.2.9)(zod@3.23.5) '@notionhq/client': specifier: 2.2.15 version: 2.2.15 @@ -81,8 +81,8 @@ dependencies: specifier: 135.0.0 version: 135.0.0 hono: - specifier: 4.2.8 - version: 4.2.8 + specifier: 4.2.9 + version: 4.2.9 html-to-text: specifier: 9.0.5 version: 9.0.5 @@ -236,8 +236,8 @@ devDependencies: specifier: 3.1.0 version: 3.1.0 '@stylistic/eslint-plugin': - specifier: 1.7.2 - version: 1.7.2(eslint@8.57.0)(typescript@5.4.5) + specifier: 1.8.0 + version: 1.8.0(eslint@8.57.0)(typescript@5.4.5) '@types/aes-js': specifier: 3.1.4 version: 3.1.4 @@ -320,8 +320,8 @@ devDependencies: specifier: 0.26.4 version: 0.26.4 '@vitest/coverage-v8': - specifier: 1.5.2 - version: 1.5.2(vitest@1.5.2) + specifier: 1.5.3 + version: 1.5.3(vitest@1.5.3) eslint: specifier: 8.57.0 version: 8.57.0 @@ -332,8 +332,8 @@ devDependencies: specifier: 8.1.0 version: 8.1.0(eslint@8.57.0) eslint-plugin-n: - specifier: 17.3.1 - version: 17.3.1(eslint@8.57.0) + specifier: 17.4.0 + version: 17.4.0(eslint@8.57.0) eslint-plugin-prettier: specifier: 5.1.3 version: 5.1.3(@types/eslint@8.56.10)(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5) @@ -383,8 +383,8 @@ devDependencies: specifier: 4.3.2 version: 4.3.2(typescript@5.4.5) vitest: - specifier: 1.5.2 - version: 1.5.2(@types/node@20.12.7)(jsdom@24.0.0) + specifier: 1.5.3 + version: 1.5.3(@types/node@20.12.7)(jsdom@24.0.0) packages: @@ -465,7 +465,7 @@ packages: resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@babel/helper-builder-binary-assignment-operator-visitor@7.22.15: @@ -601,7 +601,7 @@ packages: '@babel/helper-module-imports': 7.24.3 '@babel/helper-simple-access': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-validator-identifier': 7.24.5 dev: true /@babel/helper-optimise-call-expression@7.22.5: @@ -649,7 +649,7 @@ packages: resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@babel/helper-skip-transparent-expression-wrappers@7.22.5: @@ -673,11 +673,6 @@ packages: '@babel/types': 7.24.5 dev: true - /@babel/helper-string-parser@7.23.4: - resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} - engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-string-parser@7.24.1: resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} engines: {node: '>=6.9.0'} @@ -731,7 +726,7 @@ packages: engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.5(@babel/core@7.24.4): @@ -1683,15 +1678,6 @@ packages: - supports-color dev: true - /@babel/types@7.24.0: - resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-string-parser': 7.23.4 - '@babel/helper-validator-identifier': 7.22.20 - to-fast-properties: 2.0.0 - dev: true - /@babel/types@7.24.5: resolution: {integrity: sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==} engines: {node: '>=6.9.0'} @@ -2190,15 +2176,15 @@ packages: engines: {node: '>=18.14.1'} dev: false - /@hono/swagger-ui@0.2.1(hono@4.2.8): - resolution: {integrity: sha512-wBxVMRe3/v8xH4o6icmwztiIq0DG0s7+jHVMHVUAoFFCWEQNL2iskMmQtrhSDtsFmBZUeUFQUaaJ6Ir6DOmHLA==} + /@hono/swagger-ui@0.2.2(hono@4.2.9): + resolution: {integrity: sha512-Bco6XdKkTP7yIVWXpquLQayvjwzDmsmsESjF7VcrU/ZPTYafGvvpHf7Z6PHTWyp+JpdYORZXlyZ75T3At2KfAA==} peerDependencies: hono: '*' dependencies: - hono: 4.2.8 + hono: 4.2.9 dev: false - /@hono/zod-openapi@0.11.0(hono@4.2.8)(zod@3.23.5): + /@hono/zod-openapi@0.11.0(hono@4.2.9)(zod@3.23.5): resolution: {integrity: sha512-thbxV4lWJoDo1NjF8ZGnd0muD3UHUpRqpKvS3RI+kWCXU05nyuViymUbPvVpp+O6i5SjovITTF91NRMTraZm3Q==} engines: {node: '>=16.0.0'} peerDependencies: @@ -2206,18 +2192,18 @@ packages: zod: 3.* dependencies: '@asteasolutions/zod-to-openapi': 7.0.0(zod@3.23.5) - '@hono/zod-validator': 0.2.1(hono@4.2.8)(zod@3.23.5) - hono: 4.2.8 + '@hono/zod-validator': 0.2.1(hono@4.2.9)(zod@3.23.5) + hono: 4.2.9 zod: 3.23.5 dev: false - /@hono/zod-validator@0.2.1(hono@4.2.8)(zod@3.23.5): + /@hono/zod-validator@0.2.1(hono@4.2.9)(zod@3.23.5): resolution: {integrity: sha512-HFoxln7Q6JsE64qz2WBS28SD33UB2alp3aRKmcWnNLDzEL1BLsWfbdX6e1HIiUprHYTIXf5y7ax8eYidKUwyaA==} peerDependencies: hono: '>=3.9.0' zod: ^3.19.1 dependencies: - hono: 4.2.8 + hono: 4.2.9 zod: 3.23.5 dev: false @@ -2771,8 +2757,8 @@ packages: engines: {node: '>=16'} dev: true - /@stylistic/eslint-plugin-js@1.7.2(eslint@8.57.0): - resolution: {integrity: sha512-ZYX7C5p7zlHbACwFLU+lISVh6tdcRP/++PWegh2Sy0UgMT5kU0XkPa2tKWEtJYzZmPhJxu9LxbnWcnE/tTwSDQ==} + /@stylistic/eslint-plugin-js@1.8.0(eslint@8.57.0): + resolution: {integrity: sha512-jdvnzt+pZPg8TfclZlTZPiUbbima93ylvQ+wNgHLNmup3obY6heQvgewSu9i2CfS61BnRByv+F9fxQLPoNeHag==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: '>=8.40.0' @@ -2785,21 +2771,21 @@ packages: espree: 9.6.1 dev: true - /@stylistic/eslint-plugin-jsx@1.7.2(eslint@8.57.0): - resolution: {integrity: sha512-lNZR5PR0HLJPs+kY0y8fy6KroKlYqA5PwsYWpVYWzqZWiL5jgAeUo4s9yLFYjJjzildJ5MsTVMy/xP81Qz6GXg==} + /@stylistic/eslint-plugin-jsx@1.8.0(eslint@8.57.0): + resolution: {integrity: sha512-PC7tYXipF03TTilGJva1amAham7qOAFXT5r5jLTY6iIxkFqyb6H7Ljx5pv8d7n98VyIVidOEKY/AP8vNzAFNKg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: '>=8.40.0' dependencies: - '@stylistic/eslint-plugin-js': 1.7.2(eslint@8.57.0) + '@stylistic/eslint-plugin-js': 1.8.0(eslint@8.57.0) '@types/eslint': 8.56.10 eslint: 8.57.0 estraverse: 5.3.0 picomatch: 4.0.2 dev: true - /@stylistic/eslint-plugin-plus@1.7.2(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-luUfRVbBVtt0+/FNt8/76BANJEzb/nHWasHD7UUjyMrch2U9xUKpObrkTCzqBuisKek+uFupwGjqXqDP07+fQw==} + /@stylistic/eslint-plugin-plus@1.8.0(eslint@8.57.0)(typescript@5.4.5): + resolution: {integrity: sha512-TkrjzzYmTuAaLvFwtxomsgMUD8g8PREOQOQzTfKmiJ6oc4XOyFW4q/L9ES1J3UFSLybNCwbhu36lhXJut1w2Sg==} peerDependencies: eslint: '*' dependencies: @@ -2811,13 +2797,13 @@ packages: - typescript dev: true - /@stylistic/eslint-plugin-ts@1.7.2(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-szX89YPocwCe4T0eT3alj7MwEzDHt5+B+kb/vQfSSLIjI9CGgoWrgj50zU8PtaDctTh4ZieFBzU/lRmkSUo0RQ==} + /@stylistic/eslint-plugin-ts@1.8.0(eslint@8.57.0)(typescript@5.4.5): + resolution: {integrity: sha512-WuCIhz4JEHxzhAWjrBASMGj6Or1wAjDqTsRIck3DRRrw/FJ8C/8AAuHPk8ECHNSDI5PZ0OT72nF2uSUn0aQq1w==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: '>=8.40.0' dependencies: - '@stylistic/eslint-plugin-js': 1.7.2(eslint@8.57.0) + '@stylistic/eslint-plugin-js': 1.8.0(eslint@8.57.0) '@types/eslint': 8.56.10 '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 @@ -2826,16 +2812,16 @@ packages: - typescript dev: true - /@stylistic/eslint-plugin@1.7.2(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-TesaPR4AOCeD4unwu9gZCdTe8SsUpykriICuwXV8GFBgESuVbfVp+S8g6xTWe9ntVR803bNMtnr2UhxHW0iFqg==} + /@stylistic/eslint-plugin@1.8.0(eslint@8.57.0)(typescript@5.4.5): + resolution: {integrity: sha512-JRR0lCDU97AiE0X6qTc/uf8Hv0yETUdyJgoNzTLUIWdhVJVe/KGPnFmEsO1iXfNUIS6vhv3JJ5vaZ2qtXhZe1g==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: '>=8.40.0' dependencies: - '@stylistic/eslint-plugin-js': 1.7.2(eslint@8.57.0) - '@stylistic/eslint-plugin-jsx': 1.7.2(eslint@8.57.0) - '@stylistic/eslint-plugin-plus': 1.7.2(eslint@8.57.0)(typescript@5.4.5) - '@stylistic/eslint-plugin-ts': 1.7.2(eslint@8.57.0)(typescript@5.4.5) + '@stylistic/eslint-plugin-js': 1.8.0(eslint@8.57.0) + '@stylistic/eslint-plugin-jsx': 1.8.0(eslint@8.57.0) + '@stylistic/eslint-plugin-plus': 1.8.0(eslint@8.57.0)(typescript@5.4.5) + '@stylistic/eslint-plugin-ts': 1.8.0(eslint@8.57.0)(typescript@5.4.5) '@types/eslint': 8.56.10 eslint: 8.57.0 transitivePeerDependencies: @@ -3331,10 +3317,10 @@ packages: - supports-color dev: true - /@vitest/coverage-v8@1.5.2(vitest@1.5.2): - resolution: {integrity: sha512-QJqxRnbCwNtbbegK9E93rBmhN3dbfG1bC/o52Bqr0zGCYhQzwgwvrJBG7Q8vw3zilX6Ryy6oa/mkZku2lLJx1Q==} + /@vitest/coverage-v8@1.5.3(vitest@1.5.3): + resolution: {integrity: sha512-DPyGSu/fPHOJuPxzFSQoT4N/Fu/2aJfZRtEpEp8GI7NHsXBGE94CQ+pbEGBUMFjatsHPDJw/+TAF9r4ens2CNw==} peerDependencies: - vitest: 1.5.2 + vitest: 1.5.3 dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -3349,43 +3335,43 @@ packages: std-env: 3.7.0 strip-literal: 2.0.0 test-exclude: 6.0.0 - vitest: 1.5.2(@types/node@20.12.7)(jsdom@24.0.0) + vitest: 1.5.3(@types/node@20.12.7)(jsdom@24.0.0) transitivePeerDependencies: - supports-color dev: true - /@vitest/expect@1.5.2: - resolution: {integrity: sha512-rf7MTD1WCoDlN3FfYJ9Llfp0PbdtOMZ3FIF0AVkDnKbp3oiMW1c8AmvRZBcqbAhDUAvF52e9zx4WQM1r3oraVA==} + /@vitest/expect@1.5.3: + resolution: {integrity: sha512-y+waPz31pOFr3rD7vWTbwiLe5+MgsMm40jTZbQE8p8/qXyBX3CQsIXRx9XK12IbY7q/t5a5aM/ckt33b4PxK2g==} dependencies: - '@vitest/spy': 1.5.2 - '@vitest/utils': 1.5.2 + '@vitest/spy': 1.5.3 + '@vitest/utils': 1.5.3 chai: 4.4.1 dev: true - /@vitest/runner@1.5.2: - resolution: {integrity: sha512-7IJ7sJhMZrqx7HIEpv3WrMYcq8ZNz9L6alo81Y6f8hV5mIE6yVZsFoivLZmr0D777klm1ReqonE9LyChdcmw6g==} + /@vitest/runner@1.5.3: + resolution: {integrity: sha512-7PlfuReN8692IKQIdCxwir1AOaP5THfNkp0Uc4BKr2na+9lALNit7ub9l3/R7MP8aV61+mHKRGiqEKRIwu6iiQ==} dependencies: - '@vitest/utils': 1.5.2 + '@vitest/utils': 1.5.3 p-limit: 5.0.0 pathe: 1.1.2 dev: true - /@vitest/snapshot@1.5.2: - resolution: {integrity: sha512-CTEp/lTYos8fuCc9+Z55Ga5NVPKUgExritjF5VY7heRFUfheoAqBneUlvXSUJHUZPjnPmyZA96yLRJDP1QATFQ==} + /@vitest/snapshot@1.5.3: + resolution: {integrity: sha512-K3mvIsjyKYBhNIDujMD2gfQEzddLe51nNOAf45yKRt/QFJcUIeTQd2trRvv6M6oCBHNVnZwFWbQ4yj96ibiDsA==} dependencies: magic-string: 0.30.8 pathe: 1.1.2 pretty-format: 29.7.0 dev: true - /@vitest/spy@1.5.2: - resolution: {integrity: sha512-xCcPvI8JpCtgikT9nLpHPL1/81AYqZy1GCy4+MCHBE7xi8jgsYkULpW5hrx5PGLgOQjUpb6fd15lqcriJ40tfQ==} + /@vitest/spy@1.5.3: + resolution: {integrity: sha512-Llj7Jgs6lbnL55WoshJUUacdJfjU2honvGcAJBxhra5TPEzTJH8ZuhI3p/JwqqfnTr4PmP7nDmOXP53MS7GJlg==} dependencies: tinyspy: 2.2.1 dev: true - /@vitest/utils@1.5.2: - resolution: {integrity: sha512-sWOmyofuXLJ85VvXNsroZur7mOJGiQeM0JN3/0D1uU8U9bGFM69X1iqHaRXl6R8BwaLY6yPCogP257zxTzkUdA==} + /@vitest/utils@1.5.3: + resolution: {integrity: sha512-rE9DTN1BRhzkzqNQO+kw8ZgfeEBCLXiHJwetk668shmNBpSagQxneT5eSqEBLP+cqSiAeecvQmbpFfdMyLcIQA==} dependencies: diff-sequences: 29.6.3 estree-walker: 3.0.3 @@ -4957,8 +4943,8 @@ packages: eslint-compat-utils: 0.1.2(eslint@8.57.0) dev: true - /eslint-plugin-n@17.3.1(eslint@8.57.0): - resolution: {integrity: sha512-25+HTtKe1F8U/M4ERmdzbz/xkm/gaY0OYC8Fcv1z/WvpLJ8Xfh9LzJ13JV5uj4QyCUD8kOPJrNjn/3y+tc57Vw==} + /eslint-plugin-n@17.4.0(eslint@8.57.0): + resolution: {integrity: sha512-RtgGgNpYxECwE9dFr+D66RtbN0B8r/fY6ZF8EVsmK2YnZxE8/n9LNQhgnkL9z37UFZjYVmvMuC32qu7fQBsLVQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: '>=8.23.0' @@ -5868,8 +5854,8 @@ packages: resolution: {integrity: sha512-4FP6J0oI8jqb6gLLl9tSwVdosWJ/AKSGJ+HwYf6Ixe4MUcEkst4uWzpVQrNOCin0fzTRQbXV8ePheU8WiiDYBw==} dev: false - /hono@4.2.8: - resolution: {integrity: sha512-re/zNrOWb7Sp9KhojlMEgcgvqsE8Rgk9IcmumqsbKa9ruPT5XuOcx1U+xuNaI4SUnwrPsiTQ72MiodtpJEVfjg==} + /hono@4.2.9: + resolution: {integrity: sha512-59FAv52UxDWUt/NlC0NzrRCjeVCThUnVlqlrKYm+k80XujBu6uJwBIa5gACKKZWobjA0MJ6Vds0I3URKf383Cw==} engines: {node: '>=16.0.0'} dev: false @@ -6856,7 +6842,7 @@ packages: resolution: {integrity: sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==} dependencies: '@babel/parser': 7.24.4 - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 source-map-js: 1.2.0 dev: true @@ -9580,8 +9566,8 @@ packages: vfile-message: 4.0.2 dev: true - /vite-node@1.5.2(@types/node@20.12.7): - resolution: {integrity: sha512-Y8p91kz9zU+bWtF7HGt6DVw2JbhyuB2RlZix3FPYAYmUyZ3n7iTp8eSyLyY6sxtPegvxQtmlTMhfPhUfCUF93A==} + /vite-node@1.5.3(@types/node@20.12.7): + resolution: {integrity: sha512-axFo00qiCpU/JLd8N1gu9iEYL3xTbMbMrbe5nDp9GL0nb6gurIdZLkkFogZXWnE8Oyy5kfSLwNVIcVsnhE7lgQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true dependencies: @@ -9653,15 +9639,15 @@ packages: fsevents: 2.3.3 dev: true - /vitest@1.5.2(@types/node@20.12.7)(jsdom@24.0.0): - resolution: {integrity: sha512-l9gwIkq16ug3xY7BxHwcBQovLZG75zZL0PlsiYQbf76Rz6QGs54416UWMtC0jXeihvHvcHrf2ROEjkQRVpoZYw==} + /vitest@1.5.3(@types/node@20.12.7)(jsdom@24.0.0): + resolution: {integrity: sha512-2oM7nLXylw3mQlW6GXnRriw+7YvZFk/YNV8AxIC3Z3MfFbuziLGWP9GPxxu/7nRlXhqyxBikpamr+lEEj1sUEw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.5.2 - '@vitest/ui': 1.5.2 + '@vitest/browser': 1.5.3 + '@vitest/ui': 1.5.3 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -9679,11 +9665,11 @@ packages: optional: true dependencies: '@types/node': 20.12.7 - '@vitest/expect': 1.5.2 - '@vitest/runner': 1.5.2 - '@vitest/snapshot': 1.5.2 - '@vitest/spy': 1.5.2 - '@vitest/utils': 1.5.2 + '@vitest/expect': 1.5.3 + '@vitest/runner': 1.5.3 + '@vitest/snapshot': 1.5.3 + '@vitest/spy': 1.5.3 + '@vitest/utils': 1.5.3 acorn-walk: 8.3.2 chai: 4.4.1 debug: 4.3.4 @@ -9698,7 +9684,7 @@ packages: tinybench: 2.6.0 tinypool: 0.8.3 vite: 5.2.9(@types/node@20.12.7) - vite-node: 1.5.2(@types/node@20.12.7) + vite-node: 1.5.3(@types/node@20.12.7) why-is-node-running: 2.2.2 transitivePeerDependencies: - less