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 = `
-
- `;
- 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 }}
+
+ {{ d.key }} |
+ {{ d.value }} |
+
+ {{ /each }}
+
+
+ {{ /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