init
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
27
AGENTS.md
Normal file
27
AGENTS.md
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
# AGENTS.md
|
||||
|
||||
使用 fetch 获取热榜数据,定义了一个 `HotListItem` 类型来描述热榜子项的结构。每个热榜子项包含一个唯一的 `id`、标题、可选的描述、封面图、热度信息、提示信息、URL 地址、移动端 URL 地址、标签以及原始数据。
|
||||
|
||||
```ts
|
||||
/**
|
||||
* @description: 热榜子项
|
||||
*/
|
||||
export type HotListItem = {
|
||||
id: string | number; // 唯一 key
|
||||
title: string; // 标题
|
||||
description?: string; // 描述
|
||||
cover?: string; // 封面图
|
||||
hot?: number | string; // 热度
|
||||
tip?: string; // 如果不显示热度,显示其他信息
|
||||
url: string; // 地址
|
||||
mobileUrl?: string; // 移动端地址
|
||||
label?: string; // 标签(微博)
|
||||
originData?: any; // 原始数据
|
||||
};
|
||||
```
|
||||
每一个模块有一个 label,代表这个模块的功能。同时每一个模块会导出一个 main 函数,main 函数会返回一个 Promise,Promise resolve 的值是一个 HotListItem 数组,代表这个模块获取到的热榜数据。
|
||||
|
||||
## hotnow 模块
|
||||
|
||||
单独的 api 的模块,纯粹负责获取数据,不涉及任何 UI 相关的东西。这样做的好处是可以让数据获取和 UI 展示解耦,方便后续维护和扩展。
|
||||
10
README.md
Normal file
10
README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# 通用当前热点信息汇集
|
||||
|
||||
|
||||
|
||||
### 参考
|
||||
|
||||
```
|
||||
git clone https://github.com/ourongxing/newsnow.git
|
||||
git clone https://github.com/baiwumm/next-daily-hot
|
||||
```
|
||||
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "hotnow",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.32.1",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"cheerio": "^1.2.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.20",
|
||||
"vitest": "^4.1.1"
|
||||
}
|
||||
}
|
||||
22
src/hotnow/36kr/README.md
Normal file
22
src/hotnow/36kr/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 36kr-24小时热榜
|
||||
|
||||
调用 36kr 官方 API 获取 24 小时热榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://gateway.36kr.com/api/mis/nav/home/nav/rank/hot`
|
||||
- **Method**: POST
|
||||
- **Headers**: Content-Type: application/json
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // 文章ID
|
||||
title: string; // 文章标题
|
||||
cover?: string; // 封面图
|
||||
hot: number | string; // 阅读数
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/36kr/api.test.ts
Normal file
19
src/hotnow/36kr/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("36kr", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
51
src/hotnow/36kr/api.ts
Normal file
51
src/hotnow/36kr/api.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '36kr-24小时热榜',
|
||||
icon: 'https://36kr.com/favicon.ico',
|
||||
color: '#3296CC',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://gateway.36kr.com/api/mis/nav/home/nav/rank/hot';
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
partner_id: "wap",
|
||||
param: {
|
||||
siteId: 1,
|
||||
platformId: 2,
|
||||
},
|
||||
timestamp: new Date().getTime(),
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.code === 0) {
|
||||
const result: HotListItem[] = responseBody.data?.hotRankList.map((v) => {
|
||||
return {
|
||||
id: v.itemId,
|
||||
title: v?.templateMaterial?.widgetTitle,
|
||||
cover: v?.templateMaterial.widgetImage,
|
||||
hot: v?.templateMaterial.statRead,
|
||||
url: `https://www.36kr.com/p/${v.itemId}`,
|
||||
mobileUrl: `https://m.36kr.com/p/${v.itemId}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
20
src/hotnow/baidu/README.md
Normal file
20
src/hotnow/baidu/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 百度-热搜榜
|
||||
|
||||
调用百度热搜榜官方 API 获取实时热搜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://top.baidu.com/api/board?platform=wise&tab=realtime`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // 排名索引
|
||||
title: string; // 热搜词
|
||||
label?: string; // 新增热词标签
|
||||
url: string; // PC 端搜索链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/baidu/api.test.ts
Normal file
19
src/hotnow/baidu/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("baidu", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
36
src/hotnow/baidu/api.ts
Normal file
36
src/hotnow/baidu/api.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '百度-热搜榜',
|
||||
icon: 'https://www.baidu.com/favicon.ico',
|
||||
color: '#2932E1',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://top.baidu.com/api/board?platform=wise&tab=realtime';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.success) {
|
||||
const result: HotListItem[] = responseBody.data.cards[0]?.content[0]?.content.map((v) => {
|
||||
return {
|
||||
id: v.index,
|
||||
title: v.word,
|
||||
label: v.newHotName,
|
||||
url: `https://www.baidu.com/s?wd=${encodeURIComponent(v.word)}`,
|
||||
mobileUrl: v.url,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
22
src/hotnow/baidutieba/README.md
Normal file
22
src/hotnow/baidutieba/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 百度贴吧-热议榜
|
||||
|
||||
调用百度贴吧官方 API 获取热议榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://tieba.baidu.com/hottopic/browse/topicList`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // topic_id
|
||||
title: string; // 话题名称
|
||||
description?: string; // 话题描述
|
||||
cover?: string; // 封面图
|
||||
hot: number | string; // 讨论数
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/baidutieba/api.test.ts
Normal file
19
src/hotnow/baidutieba/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("baidutieba", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
38
src/hotnow/baidutieba/api.ts
Normal file
38
src/hotnow/baidutieba/api.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '百度贴吧-热议榜',
|
||||
icon: 'https://tieba.baidu.com/favicon.ico',
|
||||
color: '#4676D8',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://tieba.baidu.com/hottopic/browse/topicList';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.errmsg === 'success') {
|
||||
const result: HotListItem[] = responseBody.data.bang_topic.topic_list.map((v) => {
|
||||
return {
|
||||
id: v.topic_id.toString(),
|
||||
title: v.topic_name,
|
||||
description: v.topic_desc,
|
||||
cover: v.topic_pic,
|
||||
hot: v.discuss_num,
|
||||
url: v.topic_url,
|
||||
mobileUrl: v.topic_url,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
43
src/hotnow/bilibili/README.md
Normal file
43
src/hotnow/bilibili/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 哔哩哔哩-热门榜
|
||||
|
||||
调用哔哩哔哩官方 API 获取热门视频排行榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://api.bilibili.com/x/web-interface/ranking/v2`
|
||||
- **Method**: GET
|
||||
- **Headers**: 需要 Referer 头信息
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // 唯一标识 (bvid)
|
||||
title: string; // 视频标题
|
||||
description?: string; // 视频描述
|
||||
cover?: string; // 封面图
|
||||
hot: number | string; // 播放量
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
|
||||
## 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"bvid": "BV1234567890",
|
||||
"title": "视频标题",
|
||||
"desc": "视频描述",
|
||||
"pic": "封面URL",
|
||||
"stat": {
|
||||
"view": 1000000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
19
src/hotnow/bilibili/api.test.ts
Normal file
19
src/hotnow/bilibili/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("bilibili", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
45
src/hotnow/bilibili/api.ts
Normal file
45
src/hotnow/bilibili/api.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '哔哩哔哩-热门榜',
|
||||
icon: 'https://www.bilibili.com/favicon.ico',
|
||||
color: '#FB7299',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://api.bilibili.com/x/web-interface/ranking/v2';
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Referer: `https://www.bilibili.com/ranking/all`,
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
const data = responseBody?.data?.realtime || responseBody?.data?.list;
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
const result: HotListItem[] = data.map((v) => {
|
||||
return {
|
||||
id: v.bvid,
|
||||
title: v.title,
|
||||
description: v.desc,
|
||||
cover: v.pic.replace(/http:/, 'https:'),
|
||||
hot: v.stat.view,
|
||||
url: v.short_link_v2 || `https://b23.tv/${v.bvid}`,
|
||||
mobileUrl: `https://m.bilibili.com/video/${v.bvid}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
20
src/hotnow/csdn/README.md
Normal file
20
src/hotnow/csdn/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# CSDN-热榜
|
||||
|
||||
调用 CSDN 官方 API 获取热榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://blog.csdn.net/phoenix/web/blog/hot-rank?page=0&pageSize=100`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // articleDetailUrl
|
||||
title: string; // 文章标题
|
||||
tip?: string; // 热度分数
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/csdn/api.test.ts
Normal file
19
src/hotnow/csdn/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("csdn", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
42
src/hotnow/csdn/api.ts
Normal file
42
src/hotnow/csdn/api.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: 'CSDN-热榜',
|
||||
icon: 'https://blog.csdn.net/favicon.ico',
|
||||
color: '#FA7040',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://blog.csdn.net/phoenix/web/blog/hot-rank?page=0&pageSize=100';
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
||||
},
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.code === 200) {
|
||||
const result: HotListItem[] = responseBody.data.map((v) => {
|
||||
return {
|
||||
id: v.articleDetailUrl,
|
||||
title: v.articleTitle,
|
||||
tip: v.pcHotRankScore,
|
||||
url: v.articleDetailUrl,
|
||||
mobileUrl: v.articleDetailUrl,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
20
src/hotnow/dongchedi/README.md
Normal file
20
src/hotnow/dongchedi/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 懂车帝-热搜榜
|
||||
|
||||
通过解析懂车帝网页获取热搜榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://www.dongchedi.com/news`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // 索引
|
||||
title: string; // 标题
|
||||
hot: number | string; // 热度分数
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/dongchedi/api.test.ts
Normal file
19
src/hotnow/dongchedi/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("dongchedi", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
37
src/hotnow/dongchedi/api.ts
Normal file
37
src/hotnow/dongchedi/api.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '懂车帝-热搜榜',
|
||||
icon: 'https://www.dongchedi.com/favicon.ico',
|
||||
color: '#2B5D8A',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://www.dongchedi.com/news';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.text();
|
||||
const $ = cheerio.load(responseBody);
|
||||
const json = $('script#__NEXT_DATA__', responseBody).contents().text();
|
||||
const data = JSON.parse(json);
|
||||
const result: HotListItem[] = (data?.props?.pageProps?.hotSearchList || []).map((v, idx) => {
|
||||
return {
|
||||
id: idx + 1,
|
||||
title: v.title,
|
||||
hot: v.score,
|
||||
url: `https://www.dongchedi.com/search?keyword=${encodeURIComponent(v.title)}`,
|
||||
mobileUrl: `https://www.dongchedi.com/search?keyword=${encodeURIComponent(v.title)}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
21
src/hotnow/douban-movic/README.md
Normal file
21
src/hotnow/douban-movic/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 豆瓣电影-新片榜
|
||||
|
||||
通过解析豆瓣电影网页获取新片榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://movie.douban.com/chart/`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // 电影ID
|
||||
title: string; // 电影标题
|
||||
description?: string; // 电影描述/演员
|
||||
hot: number | string; // 评价人数
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/douban-movic/api.test.ts
Normal file
19
src/hotnow/douban-movic/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("douban-movic", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
50
src/hotnow/douban-movic/api.ts
Normal file
50
src/hotnow/douban-movic/api.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '豆瓣电影-新片榜',
|
||||
icon: 'https://movie.douban.com/favicon.ico',
|
||||
color: '#23B000',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://movie.douban.com/chart/';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.text();
|
||||
const getNumbers = (text: string | undefined) => {
|
||||
if (!text) return 10000000;
|
||||
const regex = /\d+/;
|
||||
const match = text.match(regex);
|
||||
if (match) {
|
||||
return Number(match[0]);
|
||||
} else {
|
||||
return 10000000;
|
||||
}
|
||||
};
|
||||
const $ = cheerio.load(responseBody);
|
||||
const listDom = $('.article tr.item');
|
||||
const result: HotListItem[] = listDom.toArray().map((item) => {
|
||||
const dom = $(item);
|
||||
const url = dom.find('a').attr('href') || '';
|
||||
const score = dom.find('.rating_nums').text() ?? '0.0';
|
||||
return {
|
||||
id: String(getNumbers(url)),
|
||||
title: `${dom.find('.pl2 a').text().replace(/\s+/g, ' ').trim().replace(/\n/g, '')}`,
|
||||
description: dom.find('p.pl').text(),
|
||||
hot: getNumbers(dom.find('span.pl').text()),
|
||||
url,
|
||||
mobileUrl: `https://m.douban.com/movie/subject/${getNumbers(url)}/`,
|
||||
originData: dom.html(),
|
||||
};
|
||||
});
|
||||
return result;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
21
src/hotnow/douyin/README.md
Normal file
21
src/hotnow/douyin/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 抖音-热点榜
|
||||
|
||||
调用抖音官方 API 获取热点榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://aweme.snssdk.com/aweme/v1/hot/search/list/`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // group_id
|
||||
title: string; // 热点词
|
||||
cover?: string; // 封面图
|
||||
hot: number | string; // 热度值
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/douyin/api.test.ts
Normal file
19
src/hotnow/douyin/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("douyin", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
37
src/hotnow/douyin/api.ts
Normal file
37
src/hotnow/douyin/api.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '抖音-热点榜',
|
||||
icon: 'https://www.douyin.com/favicon.ico',
|
||||
color: '#00F2EA',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://aweme.snssdk.com/aweme/v1/hot/search/list/';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.status_code === 0) {
|
||||
const result: HotListItem[] = responseBody.data.word_list.map((v) => {
|
||||
return {
|
||||
id: v.group_id,
|
||||
title: v.word,
|
||||
cover: `${v.word_cover.url_list[0]}`,
|
||||
hot: Number(v.hot_value),
|
||||
url: `https://www.douyin.com/hot/${encodeURIComponent(v.sentence_id)}`,
|
||||
mobileUrl: `https://www.douyin.com/hot/${encodeURIComponent(v.sentence_id)}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
21
src/hotnow/github-trending/README.md
Normal file
21
src/hotnow/github-trending/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Github-热门仓库
|
||||
|
||||
通过解析 Github Trending 页面获取热门仓库数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://github.com/trending`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // 仓库路径
|
||||
title: string; // 仓库名称 (owner/repo)
|
||||
description?: string; // 仓库描述
|
||||
tip?: string; // star 数量
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/github-trending/api.test.ts
Normal file
19
src/hotnow/github-trending/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("github-trending", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
62
src/hotnow/github-trending/api.ts
Normal file
62
src/hotnow/github-trending/api.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: 'Github-热门仓库',
|
||||
icon: 'https://github.com/favicon.ico',
|
||||
color: '#24292E',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const baseUrl = 'https://github.com';
|
||||
const url = `${baseUrl}/trending`;
|
||||
try {
|
||||
const response = await fetch(`${url}`, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
||||
},
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const formatStars = (count: number): string => {
|
||||
if (count < 1000) return count.toString();
|
||||
if (count < 1_000_000) {
|
||||
return `${(count / 1000).toFixed(1).replace(/\.0$/, '')}K`;
|
||||
}
|
||||
return `${(count / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`;
|
||||
};
|
||||
const responseBody = await response.text();
|
||||
const $ = cheerio.load(responseBody);
|
||||
const listDom = $('.Box article.Box-row');
|
||||
const result: HotListItem[] = listDom.get().map((repo, index) => {
|
||||
const $repo = $(repo);
|
||||
const relativeUrl = $repo.find('.h3').find('a').attr('href');
|
||||
return {
|
||||
id: relativeUrl || String(index),
|
||||
title: (relativeUrl || '').replace(/^\//, ''),
|
||||
description: $repo.find('p.my-1').text().trim() || '',
|
||||
tip: formatStars(parseInt(
|
||||
$repo
|
||||
.find(".mr-3 svg[aria-label='star']")
|
||||
.first()
|
||||
.parent()
|
||||
.text()
|
||||
.trim()
|
||||
.replace(',', '') || '0',
|
||||
10
|
||||
)),
|
||||
url: `${baseUrl}${relativeUrl}`,
|
||||
mobileUrl: `${baseUrl}${relativeUrl}`,
|
||||
originData: $repo.html(),
|
||||
};
|
||||
});
|
||||
return result;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
21
src/hotnow/hello-github/README.md
Normal file
21
src/hotnow/hello-github/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# HelloGithub-精选
|
||||
|
||||
调用 HelloGithub 官方 API 获取精选仓库数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://api.hellogithub.com/v1/?sort_by=featured&page=1&rank_by=newest&tid=all`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // item_id
|
||||
title: string; // 仓库名称-标题
|
||||
description?: string; // 仓库描述
|
||||
hot: number | string; // 总点击数
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/hello-github/api.test.ts
Normal file
19
src/hotnow/hello-github/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("hello-github", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
43
src/hotnow/hello-github/api.ts
Normal file
43
src/hotnow/hello-github/api.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: 'HelloGithub-精选',
|
||||
icon: 'https://hellogithub.com/favicon.ico',
|
||||
color: '#FF6A00',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://api.hellogithub.com/v1/?sort_by=featured&page=1&rank_by=newest&tid=all';
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
||||
},
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.success) {
|
||||
const result: HotListItem[] = responseBody.data.map((v) => {
|
||||
return {
|
||||
id: v.item_id,
|
||||
title: `${v.name}-${v.title}`,
|
||||
description: v.summary,
|
||||
hot: v.clicks_total,
|
||||
url: `https://hellogithub.com/repository/${v.full_name}`,
|
||||
mobileUrl: `https://hellogithub.com/repository/${v.full_name}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
21
src/hotnow/history-today/README.md
Normal file
21
src/hotnow/history-today/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 百度百科-历史上的今天
|
||||
|
||||
调用百度百科官方 API 获取历史上的今天数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://baike.baidu.com/cms/home/eventsOnHistory/{month}.json`
|
||||
- **Method**: GET
|
||||
- **Note**: month 格式为 MM,如 01, 02 等
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // 索引
|
||||
title: string; // 事件标题
|
||||
tip?: string; // 年份
|
||||
url: string; // 详情链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/history-today/api.test.ts
Normal file
19
src/hotnow/history-today/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("history-today", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
35
src/hotnow/history-today/api.ts
Normal file
35
src/hotnow/history-today/api.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '百度百科-历史上的今天',
|
||||
icon: 'https://baike.baidu.com/favicon.ico',
|
||||
color: '#2932E1',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const month = (new Date().getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = new Date().getDate().toString().padStart(2, '0');
|
||||
const url = `https://baike.baidu.com/cms/home/eventsOnHistory/${month}.json`;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
const result: HotListItem[] = responseBody[month][month + day].map((v, index: number) => {
|
||||
return {
|
||||
id: index,
|
||||
title: v.title.replace(/<[^>]+>/g, ''),
|
||||
tip: v.year,
|
||||
url: v.link,
|
||||
mobileUrl: v.link,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
22
src/hotnow/hupu/README.md
Normal file
22
src/hotnow/hupu/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 虎扑-步行街热帖
|
||||
|
||||
通过解析虎扑网页获取步行街热帖数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://bbs.hupu.com/all-gambia`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // tid
|
||||
title: string; // 帖子标题
|
||||
description?: string; // 帖子描述
|
||||
cover?: string; // 封面图
|
||||
tip?: string; // 亮帖数
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/hupu/api.test.ts
Normal file
19
src/hotnow/hupu/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("hupu", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
41
src/hotnow/hupu/api.ts
Normal file
41
src/hotnow/hupu/api.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '虎扑-步行街热帖',
|
||||
icon: 'https://bbs.hupu.com/favicon.ico',
|
||||
color: '#1E8E3E',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://bbs.hupu.com/all-gambia';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.text();
|
||||
const $ = cheerio.load(responseBody);
|
||||
const json = $("script").first();
|
||||
const data = JSON.parse(json.text().split("window.$$data=")[1])
|
||||
.pageData
|
||||
.threads;
|
||||
const result: HotListItem[] = data.map((v) => {
|
||||
return {
|
||||
id: v.tid,
|
||||
title: v.title,
|
||||
description: v.desc,
|
||||
cover: v.cover,
|
||||
tip: v.lights,
|
||||
url: `https://bbs.hupu.com${v.url}`,
|
||||
mobileUrl: `https://bbs.hupu.com${v.url}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
22
src/hotnow/huxiu/README.md
Normal file
22
src/hotnow/huxiu/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 虎嗅-最新资讯
|
||||
|
||||
调用虎嗅官方 API 获取最新资讯数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://moment-api.huxiu.com/web-v3/moment/feed?platform=www`
|
||||
- **Method**: GET
|
||||
- **Headers**: 需要 User-Agent 和 Referer 头信息
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // object_id
|
||||
title: string; // 标题
|
||||
description?: string; // 内容简介
|
||||
tip?: string; // 发布时间
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/huxiu/api.test.ts
Normal file
19
src/hotnow/huxiu/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("huxiu", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
50
src/hotnow/huxiu/api.ts
Normal file
50
src/hotnow/huxiu/api.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '虎嗅-最新资讯',
|
||||
icon: 'https://www.huxiu.com/favicon.ico',
|
||||
color: '#FF6600',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://moment-api.huxiu.com/web-v3/moment/feed?platform=www';
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
Referer: "https://www.huxiu.com/moment/",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.success) {
|
||||
const result: HotListItem[] = responseBody?.data?.moment_list?.datalist.map((v) => {
|
||||
const content = (v.content || "").replace(/<br\s*\/?>/gi, "\n");
|
||||
const [titleLine, ...rest] = content
|
||||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const title = titleLine?.replace(/。$/, "") || "";
|
||||
const intro = rest.join("\n");
|
||||
const id = v.object_id;
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
description: intro,
|
||||
tip: v.format_time,
|
||||
url: `https://www.huxiu.com/moment/${id}.html`,
|
||||
mobileUrl: `https://m.huxiu.com/moment/${id}.html`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
19
src/hotnow/ifanr/README.md
Normal file
19
src/hotnow/ifanr/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 爱范儿-快讯
|
||||
|
||||
调用爱范儿官方 API 获取快讯数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://sso.ifanr.com/api/v5/wp/buzz/?limit=50&offset=0`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // post_id
|
||||
title: string; // 快讯标题
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/ifanr/api.test.ts
Normal file
19
src/hotnow/ifanr/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("ifanr", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
36
src/hotnow/ifanr/api.ts
Normal file
36
src/hotnow/ifanr/api.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '爱范儿-快讯',
|
||||
icon: 'https://www.ifanr.com/favicon.ico',
|
||||
color: '#007AFF',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://sso.ifanr.com/api/v5/wp/buzz/?limit=50&offset=0';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
const data = responseBody?.objects;
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
const result: HotListItem[] = data.map((v) => {
|
||||
return {
|
||||
id: v.post_id,
|
||||
title: v.post_title,
|
||||
url: v.buzz_original_url || `https://www.ifanr.com/${v.post_id}`,
|
||||
mobileUrl: v.buzz_original_url || `https://www.ifanr.com/digest/${v.post_id}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
21
src/hotnow/ithome/README.md
Normal file
21
src/hotnow/ithome/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# IT之家-热榜
|
||||
|
||||
通过解析 IT之家移动版网页获取热榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://m.ithome.com/rankm`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // 索引
|
||||
title: string; // 标题
|
||||
cover?: string; // 封面图
|
||||
hot: number | string; // 评论数
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/ithome/api.test.ts
Normal file
19
src/hotnow/ithome/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("ithome", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
48
src/hotnow/ithome/api.ts
Normal file
48
src/hotnow/ithome/api.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: 'IT之家-热榜',
|
||||
icon: 'https://www.ithome.com/favicon.ico',
|
||||
color: '#EE4344',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://m.ithome.com/rankm';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.text();
|
||||
const replaceLink = (url: string, getId: boolean = false) => {
|
||||
const match = url.match(/[html|live]\/(\d+)\.htm/);
|
||||
if (match && match[1]) {
|
||||
return getId
|
||||
? match[1]
|
||||
: `https://www.ithome.com/0/${match[1].slice(0, 3)}/${match[1].slice(3)}.htm`;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
const $ = cheerio.load(responseBody);
|
||||
const listDom = $(".rank-box .placeholder");
|
||||
const result: HotListItem[] = listDom.toArray().map((item, index) => {
|
||||
const dom = $(item);
|
||||
const href = dom.find("a").attr("href");
|
||||
return {
|
||||
id: index,
|
||||
title: dom.find(".plc-title").text().trim(),
|
||||
cover: dom.find("img").attr("data-original"),
|
||||
hot: Number(dom.find(".review-num").text().replace(/\D/g, "")),
|
||||
url: href ? replaceLink(href) : "",
|
||||
mobileUrl: href ? replaceLink(href) : "",
|
||||
originData: dom.html(),
|
||||
};
|
||||
});
|
||||
return result;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
20
src/hotnow/juejin/README.md
Normal file
20
src/hotnow/juejin/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 稀土掘金-热榜
|
||||
|
||||
调用掘金官方 API 获取热榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://api.juejin.cn/content_api/v1/content/article_rank?category_id=1&type=hot`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // content_id
|
||||
title: string; // 文章标题
|
||||
hot: number | string; // 热度排名
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/juejin/api.test.ts
Normal file
19
src/hotnow/juejin/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("juejin", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
36
src/hotnow/juejin/api.ts
Normal file
36
src/hotnow/juejin/api.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '稀土掘金-热榜',
|
||||
icon: 'https://juejin.cn/favicon.ico',
|
||||
color: '#007AFF',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://api.juejin.cn/content_api/v1/content/article_rank?category_id=1&type=hot';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.err_msg === 'success') {
|
||||
const result: HotListItem[] = responseBody.data.map((v) => {
|
||||
return {
|
||||
id: v.content.content_id,
|
||||
title: v.content.title,
|
||||
hot: v.content_counter.hot_rank,
|
||||
url: `https://juejin.cn/post/${v.content.content_id}`,
|
||||
mobileUrl: `https://juejin.cn/post/${v.content.content_id}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
20
src/hotnow/kuaishou/README.md
Normal file
20
src/hotnow/kuaishou/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 快手-热榜
|
||||
|
||||
通过解析快手网页获取热榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://www.kuaishou.com/?isHome=1`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // 视频ID
|
||||
title: string; // 视频名称
|
||||
hot: number | string; // 热度值
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/kuaishou/api.test.ts
Normal file
19
src/hotnow/kuaishou/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("kuaishou", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
42
src/hotnow/kuaishou/api.ts
Normal file
42
src/hotnow/kuaishou/api.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '快手-热榜',
|
||||
icon: 'https://www.kuaishou.com/favicon.ico',
|
||||
color: '#FF0000',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://www.kuaishou.com/?isHome=1';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.text();
|
||||
const result: HotListItem[] = [];
|
||||
const pattern = /window.__APOLLO_STATE__=(.*);\(function\(\)/s;
|
||||
const idPattern = /clientCacheKey=([A-Za-z0-9]+)/s;
|
||||
const matchResult = responseBody.match(pattern);
|
||||
const jsonObject = matchResult ? JSON.parse(matchResult[1])['defaultClient'] : [];
|
||||
|
||||
const allItems = jsonObject['$ROOT_QUERY.visionHotRank({"page":"home"})']['items'];
|
||||
allItems.forEach((v) => {
|
||||
const image = jsonObject[v.id]['poster'];
|
||||
const id = image.match(idPattern)[1];
|
||||
result.push({
|
||||
id,
|
||||
title: jsonObject[v.id]['name'],
|
||||
hot: jsonObject[v.id]['hotValue']?.replace('万', '') * 10000,
|
||||
url: `https://www.kuaishou.com/short-video/${id}`,
|
||||
mobileUrl: `https://www.kuaishou.com/short-video/${id}`,
|
||||
originData: jsonObject[v.id],
|
||||
});
|
||||
});
|
||||
return result;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
22
src/hotnow/lol/README.md
Normal file
22
src/hotnow/lol/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 英雄联盟-更新公告
|
||||
|
||||
调用英雄联盟官方 API 获取更新公告数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://apps.game.qq.com/cmc/zmMcnTargetContentList?page=1&num=50&target=24&source=web_pc`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // iDocID
|
||||
title: string; // 公告标题
|
||||
description?: string; // 作者
|
||||
cover?: string; // 封面图
|
||||
hot: number | string; // 播放量
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/lol/api.test.ts
Normal file
19
src/hotnow/lol/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("lol", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
38
src/hotnow/lol/api.ts
Normal file
38
src/hotnow/lol/api.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '英雄联盟-更新公告',
|
||||
icon: 'https://lol.qq.com/favicon.ico',
|
||||
color: '#C89B3C',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://apps.game.qq.com/cmc/zmMcnTargetContentList?page=1&num=50&target=24&source=web_pc';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.status === 1) {
|
||||
const result: HotListItem[] = responseBody.data.result.map((v) => {
|
||||
return {
|
||||
id: v.iDocID,
|
||||
title: v.sTitle,
|
||||
description: v.sAuthor,
|
||||
cover: v.sIMG,
|
||||
hot: Number(v.iTotalPlay),
|
||||
url: `https://lol.qq.com/news/detail.shtml?docid=${encodeURIComponent(v.iDocID)}`,
|
||||
mobileUrl: `https://lol.qq.com/news/detail.shtml?docid=${encodeURIComponent(v.iDocID)}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
22
src/hotnow/netease-music/README.md
Normal file
22
src/hotnow/netease-music/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 网易云音乐-新歌榜
|
||||
|
||||
调用网易云音乐官方 API 获取新歌榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://music.163.com/api/playlist/detail?id=3778678`
|
||||
- **Method**: GET
|
||||
- **Headers**: 需要 authority 和 referer 头信息
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // 歌曲ID
|
||||
title: string; // 歌曲名称
|
||||
cover?: string; // 封面图
|
||||
tip?: string; // 时长 (mm:ss)
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/netease-music/api.test.ts
Normal file
19
src/hotnow/netease-music/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("netease-music", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
50
src/hotnow/netease-music/api.ts
Normal file
50
src/hotnow/netease-music/api.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '网易云音乐-新歌榜',
|
||||
icon: 'https://music.163.com/favicon.ico',
|
||||
color: '#C20C0C',
|
||||
};
|
||||
|
||||
const convertMillisecondsToTime = (milliseconds: number): string => {
|
||||
const seconds = Math.floor((milliseconds / 1000) % 60);
|
||||
const minutes = Math.floor(milliseconds / (1000 * 60));
|
||||
const formattedSeconds = seconds < 10 ? '0' + seconds : seconds.toString();
|
||||
const formattedMinutes = minutes < 10 ? '0' + minutes : minutes.toString();
|
||||
return `${formattedMinutes}:${formattedSeconds}`;
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://music.163.com/api/playlist/detail?id=3778678';
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
authority: 'music.163.com',
|
||||
referer: 'https://music.163.com/',
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.code === 200) {
|
||||
const result: HotListItem[] = responseBody.result.tracks.map((v) => {
|
||||
return {
|
||||
id: v.id,
|
||||
title: v.name,
|
||||
tip: convertMillisecondsToTime(v.duration),
|
||||
cover: v.album.picUrl,
|
||||
url: `https://music.163.com/#/song?id=${v.id}`,
|
||||
mobileUrl: `https://music.163.com/m/song?id=${v.id}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
21
src/hotnow/netease/README.md
Normal file
21
src/hotnow/netease/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 网易新闻-热榜
|
||||
|
||||
调用网易新闻官方 API 获取热榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://m.163.com/fe/api/hot/news/flow`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // skipID
|
||||
title: string; // 标题
|
||||
description?: string; // 描述
|
||||
cover?: string; // 封面图
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/netease/api.test.ts
Normal file
19
src/hotnow/netease/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("netease", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
37
src/hotnow/netease/api.ts
Normal file
37
src/hotnow/netease/api.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '网易新闻-热榜',
|
||||
icon: 'https://news.163.com/favicon.ico',
|
||||
color: '#C20C0C',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://m.163.com/fe/api/hot/news/flow';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.msg === 'success') {
|
||||
const result: HotListItem[] = responseBody.data.list.map((v) => {
|
||||
return {
|
||||
id: v.skipID,
|
||||
title: v.title,
|
||||
description: v._keyword,
|
||||
cover: v.imgsrc,
|
||||
url: `https://www.163.com/dy/article/${v.skipID}.html`,
|
||||
mobileUrl: v.url,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
22
src/hotnow/qq/README.md
Normal file
22
src/hotnow/qq/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 腾讯新闻-热点榜
|
||||
|
||||
调用腾讯新闻官方 API 获取热点榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://r.inews.qq.com/gw/event/hot_ranking_list`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // 新闻ID
|
||||
title: string; // 新闻标题
|
||||
description?: string; // 新闻摘要
|
||||
cover?: string; // 封面图
|
||||
hot: number | string; // 阅读数
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/qq/api.test.ts
Normal file
19
src/hotnow/qq/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("qq", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
38
src/hotnow/qq/api.ts
Normal file
38
src/hotnow/qq/api.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '腾讯新闻-热点榜',
|
||||
icon: 'https://www.qq.com/favicon.ico',
|
||||
color: '#FF6600',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://r.inews.qq.com/gw/event/hot_ranking_list';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.ret === 0) {
|
||||
const result: HotListItem[] = responseBody.idlist[0].newslist.slice(1).map((v) => {
|
||||
return {
|
||||
id: v.id,
|
||||
title: v.title,
|
||||
description: v.abstract,
|
||||
cover: v.miniProShareImage,
|
||||
hot: v.readCount,
|
||||
url: `https://new.qq.com/rain/a/${v.id}`,
|
||||
mobileUrl: `https://view.inews.qq.com/a/${v.id}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
20
src/hotnow/quark/README.md
Normal file
20
src/hotnow/quark/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 夸克-今日热点
|
||||
|
||||
调用夸克官方 API 获取今日热点数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://iflow.quark.cn/iflow/api/v1/article/aggregation?aggregation_id=16665090098771297825&count=50&bottom_pos=0`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // 文章ID
|
||||
title: string; // 标题
|
||||
tip?: string; // 发布时间 (HH:mm)
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/quark/api.test.ts
Normal file
19
src/hotnow/quark/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("quark", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
37
src/hotnow/quark/api.ts
Normal file
37
src/hotnow/quark/api.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '夸克-今日热点',
|
||||
icon: 'https://quark.com/favicon.ico',
|
||||
color: '#4B9EFF',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://iflow.quark.cn/iflow/api/v1/article/aggregation?aggregation_id=16665090098771297825&count=50&bottom_pos=0';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.status === 0) {
|
||||
const result: HotListItem[] = responseBody.data.articles.map((v) => {
|
||||
return {
|
||||
id: v.id,
|
||||
title: v.title,
|
||||
tip: dayjs(v.publish_time).format('HH:mm'),
|
||||
url: `https://123.quark.cn/detail?item_id=${v.id}`,
|
||||
mobileUrl: `https://123.quark.cn/detail?item_id=${v.id}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
21
src/hotnow/thepaper/README.md
Normal file
21
src/hotnow/thepaper/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 澎湃新闻-热榜
|
||||
|
||||
调用澎湃新闻官方 API 获取热榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://cache.thepaper.cn/contentapi/wwwIndex/rightSidebar`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // contId
|
||||
title: string; // 新闻标题
|
||||
cover?: string; // 封面图
|
||||
hot: number | string; // 点赞数
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/thepaper/api.test.ts
Normal file
19
src/hotnow/thepaper/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("thepaper", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
37
src/hotnow/thepaper/api.ts
Normal file
37
src/hotnow/thepaper/api.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '澎湃新闻-热榜',
|
||||
icon: 'https://www.thepaper.cn/favicon.ico',
|
||||
color: '#C20C0C',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://cache.thepaper.cn/contentapi/wwwIndex/rightSidebar';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.resultCode === 1) {
|
||||
const result: HotListItem[] = responseBody.data.hotNews.map((v) => {
|
||||
return {
|
||||
id: v.contId,
|
||||
title: v.name,
|
||||
cover: v.pic,
|
||||
hot: v.praiseTimes,
|
||||
url: `https://www.thepaper.cn/newsDetail_forward_${v.contId}`,
|
||||
mobileUrl: `https://m.thepaper.cn/newsDetail_forward_${v.contId}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
21
src/hotnow/toutiao/README.md
Normal file
21
src/hotnow/toutiao/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 今日头条-热榜
|
||||
|
||||
调用今日头条官方 API 获取热榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // ClusterId
|
||||
title: string; // 标题
|
||||
cover?: string; // 封面图
|
||||
hot: number | string; // 热度值
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/toutiao/api.test.ts
Normal file
19
src/hotnow/toutiao/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("toutiao", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
37
src/hotnow/toutiao/api.ts
Normal file
37
src/hotnow/toutiao/api.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '今日头条-热榜',
|
||||
icon: 'https://www.toutiao.com/favicon.ico',
|
||||
color: '#F85959',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.status === 'success') {
|
||||
const result: HotListItem[] = responseBody.data.map((v) => {
|
||||
return {
|
||||
id: v.ClusterId,
|
||||
title: v.Title,
|
||||
cover: v.Image.url,
|
||||
hot: v.HotValue,
|
||||
url: `https://www.toutiao.com/trending/${v.ClusterIdStr}/`,
|
||||
mobileUrl: `https://api.toutiaoapi.com/feoffline/amos_land/new/html/main/index.html?topic_id=${v.ClusterIdStr}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
15
src/hotnow/type.ts
Normal file
15
src/hotnow/type.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @description: 热榜子项
|
||||
*/
|
||||
export type HotListItem = {
|
||||
id: string | number; // 唯一 key
|
||||
title: string; // 标题
|
||||
description?: string; // 描述
|
||||
cover?: string; // 封面图
|
||||
hot?: number | string; // 热度
|
||||
tip?: string; // 如果不显示热度,显示其他信息
|
||||
url: string; // 地址
|
||||
mobileUrl?: string; // 移动端地址
|
||||
label?: string; // 标签(微博)
|
||||
originData?: any; // 原始数据
|
||||
};
|
||||
122
src/hotnow/utils.ts
Normal file
122
src/hotnow/utils.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
export const hotnows = [
|
||||
{
|
||||
title: '小红书',
|
||||
path: './xiaohongshu/api.ts',
|
||||
},
|
||||
{
|
||||
title: '微博',
|
||||
path: './weibo/api.ts',
|
||||
},
|
||||
{
|
||||
title: '知乎',
|
||||
path: './zhihu/api.ts',
|
||||
},
|
||||
{
|
||||
title: '哔哩哔哩',
|
||||
path: './bilibili/api.ts',
|
||||
},
|
||||
{
|
||||
title: '36kr',
|
||||
path: './36kr/api.ts',
|
||||
},
|
||||
{
|
||||
title: '百度',
|
||||
path: './baidu/api.ts',
|
||||
},
|
||||
{
|
||||
title: '抖音',
|
||||
path: './douyin/api.ts',
|
||||
},
|
||||
{
|
||||
title: '今日头条',
|
||||
path: './toutiao/api.ts',
|
||||
},
|
||||
{
|
||||
title: '网易新闻',
|
||||
path: './netease/api.ts',
|
||||
},
|
||||
{
|
||||
title: 'IT之家',
|
||||
path: './ithome/api.ts',
|
||||
},
|
||||
{
|
||||
title: '虎嗅',
|
||||
path: './huxiu/api.ts',
|
||||
},
|
||||
{
|
||||
title: '稀土掘金',
|
||||
path: './juejin/api.ts',
|
||||
},
|
||||
{
|
||||
title: 'CSDN',
|
||||
path: './csdn/api.ts',
|
||||
},
|
||||
{
|
||||
title: '百度贴吧',
|
||||
path: './baidutieba/api.ts',
|
||||
},
|
||||
{
|
||||
title: '懂车帝',
|
||||
path: './dongchedi/api.ts',
|
||||
},
|
||||
{
|
||||
title: '快手',
|
||||
path: './kuaishou/api.ts',
|
||||
},
|
||||
{
|
||||
title: '英雄联盟',
|
||||
path: './lol/api.ts',
|
||||
},
|
||||
{
|
||||
title: '豆瓣电影',
|
||||
path: './douban-movic/api.ts',
|
||||
},
|
||||
{
|
||||
title: 'Github',
|
||||
path: './github-trending/api.ts',
|
||||
},
|
||||
{
|
||||
title: '虎扑',
|
||||
path: './hupu/api.ts',
|
||||
},
|
||||
{
|
||||
title: '爱范儿',
|
||||
path: './ifanr/api.ts',
|
||||
},
|
||||
{
|
||||
title: '微信读书',
|
||||
path: './weread/api.ts',
|
||||
},
|
||||
{
|
||||
title: '人人都是产品经理',
|
||||
path: './woshipm/api.ts',
|
||||
},
|
||||
{
|
||||
title: '网易云音乐',
|
||||
path: './netease-music/api.ts',
|
||||
},
|
||||
{
|
||||
title: '夸克',
|
||||
path: './quark/api.ts',
|
||||
},
|
||||
{
|
||||
title: '腾讯新闻',
|
||||
path: './qq/api.ts',
|
||||
},
|
||||
{
|
||||
title: '澎湃新闻',
|
||||
path: './thepaper/api.ts',
|
||||
},
|
||||
{
|
||||
title: '历史上的今天',
|
||||
path: './history-today/api.ts',
|
||||
},
|
||||
{
|
||||
title: 'HelloGithub',
|
||||
path: './hello-github/api.ts',
|
||||
},
|
||||
{
|
||||
title: '知乎日报',
|
||||
path: './zhihu-daily/api.ts',
|
||||
},
|
||||
];
|
||||
41
src/hotnow/weibo/README.md
Normal file
41
src/hotnow/weibo/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 微博-热搜榜
|
||||
|
||||
调用微博官方 API 获取实时热搜榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://weibo.com/ajax/side/hotSearch`
|
||||
- **Method**: GET
|
||||
- **Headers**: 需要 User-Agent, Referer, Accept 头信息
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // 唯一标识
|
||||
title: string; // 热搜词
|
||||
description?: string; // 描述
|
||||
hot: number | string; // 热度值
|
||||
label?: string; // 标签(热、沸、新、暖、爆)
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
|
||||
## 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": 1,
|
||||
"data": {
|
||||
"realtime": [
|
||||
{
|
||||
"mid": "123456",
|
||||
"word": "热搜词",
|
||||
"num": 1000000,
|
||||
"label_name": "热"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
24
src/hotnow/weibo/api.test.ts
Normal file
24
src/hotnow/weibo/api.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("weibo", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle errors gracefully", async () => {
|
||||
const result = await main();
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
46
src/hotnow/weibo/api.ts
Normal file
46
src/hotnow/weibo/api.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '微博-热搜榜',
|
||||
icon: 'https://weibo.com/favicon.ico',
|
||||
color: '#FF8200',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://weibo.com/ajax/side/hotSearch';
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
Referer: 'https://weibo.com/',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.ok === 1) {
|
||||
const result: HotListItem[] = responseBody.data.realtime.map((v) => {
|
||||
const key = v.word_scheme ? v.word_scheme : `#${v.word}`;
|
||||
return {
|
||||
id: v.mid,
|
||||
title: v.word,
|
||||
description: key,
|
||||
hot: v.num,
|
||||
label: v.label_name,
|
||||
url: `https://s.weibo.com/weibo?q=${encodeURIComponent(key)}&t=31&band_rank=1&Refer=top`,
|
||||
mobileUrl: `https://s.weibo.com/weibo?q=${encodeURIComponent(key)}&t=31&band_rank=1&Refer=top`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
21
src/hotnow/weread/README.md
Normal file
21
src/hotnow/weread/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 微信读书-飙升榜
|
||||
|
||||
调用微信读书官方 API 获取飙升榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://weread.qq.com/web/bookListInCategory/rising?rank=1`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // bookId
|
||||
title: string; // 书籍标题
|
||||
cover?: string; // 封面图
|
||||
hot: number | string; // 阅读数
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/weread/api.test.ts
Normal file
19
src/hotnow/weread/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("weread", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
81
src/hotnow/weread/api.ts
Normal file
81
src/hotnow/weread/api.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '微信读书-飙升榜',
|
||||
icon: 'https://weread.qq.com/favicon.ico',
|
||||
color: '#E9432B',
|
||||
};
|
||||
|
||||
const getWereadID = (bookId: string): string | null => {
|
||||
try {
|
||||
const str = CryptoJS.MD5(bookId).toString();
|
||||
let strSub = str.substring(0, 3);
|
||||
let fa;
|
||||
if (/^\d*$/.test(bookId)) {
|
||||
const chunks = [];
|
||||
for (let i = 0; i < bookId.length; i += 9) {
|
||||
const chunk = bookId.substring(i, i + 9);
|
||||
chunks.push(parseInt(chunk).toString(16));
|
||||
}
|
||||
fa = ['3', chunks];
|
||||
} else {
|
||||
let hexStr = '';
|
||||
for (let i = 0; i < bookId.length; i++) {
|
||||
hexStr += bookId.charCodeAt(i).toString(16);
|
||||
}
|
||||
fa = ['4', [hexStr]];
|
||||
}
|
||||
strSub += fa[0];
|
||||
strSub += '2' + str.substring(str.length - 2);
|
||||
for (let i = 0; i < fa[1].length; i++) {
|
||||
const sub = fa[1][i];
|
||||
const subLength = sub.length.toString(16);
|
||||
const subLengthPadded = subLength.length === 1 ? '0' + subLength : subLength;
|
||||
strSub += subLengthPadded + sub;
|
||||
if (i < fa[1].length - 1) {
|
||||
strSub += 'g';
|
||||
}
|
||||
}
|
||||
if (strSub.length < 20) {
|
||||
strSub += str.substring(0, 20 - strSub.length);
|
||||
}
|
||||
const finalStr = CryptoJS.MD5(strSub).toString();
|
||||
strSub += finalStr.substring(0, 3);
|
||||
return strSub;
|
||||
} catch (error) {
|
||||
console.error('处理微信读书 ID 时出现错误:' + error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://weread.qq.com/web/bookListInCategory/rising?rank=1';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.books) {
|
||||
const result: HotListItem[] = responseBody.books.map((v) => {
|
||||
const info = v.bookInfo;
|
||||
return {
|
||||
id: info.bookId,
|
||||
title: info.title,
|
||||
hot: v.readingCount,
|
||||
cover: info.cover.replace('s_', 't9_'),
|
||||
url: `https://weread.qq.com/web/bookDetail/${getWereadID(info.bookId)}`,
|
||||
mobileUrl: `https://weread.qq.com/web/bookDetail/${getWereadID(info.bookId)}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
22
src/hotnow/woshipm/README.md
Normal file
22
src/hotnow/woshipm/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 人人都是产品经理-热榜
|
||||
|
||||
调用人人都是产品经理官方 API 获取热榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://www.woshipm.com/api2/app/article/popular/daily`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // 文章ID
|
||||
title: string; // 文章标题
|
||||
description?: string; // 文章摘要
|
||||
cover?: string; // 封面图
|
||||
hot: number | string; // 热度分数
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/woshipm/api.test.ts
Normal file
19
src/hotnow/woshipm/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("woshipm", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
45
src/hotnow/woshipm/api.ts
Normal file
45
src/hotnow/woshipm/api.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '人人都是产品经理-热榜',
|
||||
icon: 'https://www.woshipm.com/favicon.ico',
|
||||
color: '#F7B500',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://www.woshipm.com/api2/app/article/popular/daily';
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
||||
},
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.CODE === 200) {
|
||||
const result: HotListItem[] = responseBody.RESULT.map((v) => {
|
||||
const articleUrl = `https://www.woshipm.com/${v.data.type}/${v.data.id}.html`;
|
||||
return {
|
||||
id: v.data.id,
|
||||
title: v.data.articleTitle,
|
||||
description: v.data.articleSummary,
|
||||
hot: v.scores,
|
||||
cover: v.data.imageUrl,
|
||||
url: articleUrl,
|
||||
mobileUrl: articleUrl,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
40
src/hotnow/xiaohongshu/README.md
Normal file
40
src/hotnow/xiaohongshu/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 小红书-实时热榜
|
||||
|
||||
调用小红书官方 API 获取实时热榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://edith.xiaohongshu.com/api/sns/v1/search/hot_list`
|
||||
- **Method**: GET
|
||||
- **Headers**: 需要特定的 User-Agent 和 Referer 头信息
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string; // 唯一标识
|
||||
title: string; // 热搜词
|
||||
hot: number; // 热度值
|
||||
label?: string; // 标签(热、沸、新等)
|
||||
url: string; // PC 端链接
|
||||
mobileUrl: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
|
||||
## 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "dora_2178732",
|
||||
"title": "用万能旅行拍照姿势美美出片",
|
||||
"score": "934.9w",
|
||||
"word_type": "热"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
19
src/hotnow/xiaohongshu/api.test.ts
Normal file
19
src/hotnow/xiaohongshu/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("xiaohongshu", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
55
src/hotnow/xiaohongshu/api.ts
Normal file
55
src/hotnow/xiaohongshu/api.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { HotListItem } from "../type";
|
||||
export const label = {
|
||||
name: '小红书-实时热榜',
|
||||
icon: 'https://www.xiaohongshu.com/favicon.ico',
|
||||
color: '#FF0000',
|
||||
}
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
// 官方 url
|
||||
const url = 'https://edith.xiaohongshu.com/api/sns/v1/search/hot_list';
|
||||
const xhsHeaders = {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.7(0x18000733) NetType/WIFI Language/zh_CN',
|
||||
referer: 'https://app.xhs.cn/',
|
||||
'xy-direction': '22',
|
||||
shield:
|
||||
'XYAAAAAQAAAAEAAABTAAAAUzUWEe4xG1IYD9/c+qCLOlKGmTtFa+lG434Oe+FTRagxxoaz6rUWSZ3+juJYz8RZqct+oNMyZQxLEBaBEL+H3i0RhOBVGrauzVSARchIWFYwbwkV',
|
||||
'xy-platform-info':
|
||||
'platform=iOS&version=8.7&build=8070515&deviceId=C323D3A5-6A27-4CE6-AA0E-51C9D4C26A24&bundle=com.xingin.discover',
|
||||
'xy-common-params':
|
||||
'app_id=ECFAAF02&build=8070515&channel=AppStore&deviceId=C323D3A5-6A27-4CE6-AA0E-51C9D4C26A24&device_fingerprint=20230920120211bd7b71a80778509cf4211099ea911000010d2f20f6050264&device_fingerprint1=20230920120211bd7b71a80778509cf4211099ea911000010d2f20f6050264&device_model=phone&fid=1695182528-0-0-63b29d709954a1bb8c8733eb2fb58f29&gid=7dc4f3d168c355f1a886c54a898c6ef21fe7b9a847359afc77fc24ad&identifier_flag=0&lang=zh-Hans&launch_id=716882697&platform=iOS&project_id=ECFAAF&sid=session.1695189743787849952190&t=1695190591&teenager=0&tz=Asia/Shanghai&uis=light&version=8.7',
|
||||
}
|
||||
try {
|
||||
// 请求数据
|
||||
const response = await fetch(url, {
|
||||
headers: xhsHeaders,
|
||||
});
|
||||
if (!response.ok) {
|
||||
// 如果请求失败,抛出错误,不进行缓存
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
// 得到请求体
|
||||
const responseBody = await response.json();
|
||||
// 处理数据
|
||||
if (responseBody.success) {
|
||||
const result: HotListItem[] = responseBody.data?.items.map((v: any): HotListItem => {
|
||||
return {
|
||||
id: v.id,
|
||||
title: v.title,
|
||||
hot: v.score,
|
||||
label: (!v.word_type || v.word_type === '无') ? undefined : v.word_type,
|
||||
url: `https://www.xiaohongshu.com/search_result?keyword=${encodeURIComponent(v.title)}`,
|
||||
mobileUrl: `https://www.xiaohongshu.com/search_result?keyword=${encodeURIComponent(v.title)}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
// 请求失败,返回空数据
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
20
src/hotnow/zhihu-daily/README.md
Normal file
20
src/hotnow/zhihu-daily/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 知乎日报-推荐榜
|
||||
|
||||
调用知乎日报官方 API 获取最新推荐数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://daily.zhihu.com/api/4/news/latest`
|
||||
- **Method**: GET
|
||||
- **Headers**: 需要 Referer 和 Host 头信息
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // 新闻ID
|
||||
title: string; // 新闻标题
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
19
src/hotnow/zhihu-daily/api.test.ts
Normal file
19
src/hotnow/zhihu-daily/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("zhihu-daily", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
43
src/hotnow/zhihu-daily/api.ts
Normal file
43
src/hotnow/zhihu-daily/api.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '知乎日报-推荐榜',
|
||||
icon: 'https://daily.zhihu.com/favicon.ico',
|
||||
color: '#0084FF',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://daily.zhihu.com/api/4/news/latest';
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Referer: "https://daily.zhihu.com/api/4/news/latest",
|
||||
Host: "daily.zhihu.com",
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
const data = responseBody?.stories;
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
const result: HotListItem[] = data.map((v) => {
|
||||
return {
|
||||
id: v.id,
|
||||
title: v.title,
|
||||
url: v.url,
|
||||
mobileUrl: v.url,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
37
src/hotnow/zhihu/README.md
Normal file
37
src/hotnow/zhihu/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# 知乎-热榜
|
||||
|
||||
调用知乎官方 API 获取热榜数据。
|
||||
|
||||
## API
|
||||
|
||||
- **URL**: `https://api.zhihu.com/topstory/hot-list`
|
||||
- **Method**: GET
|
||||
|
||||
## 数据结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string | number; // 唯一标识
|
||||
title: string; // 标题
|
||||
cover?: string; // 封面图
|
||||
hot: number | string; // 热度值
|
||||
url: string; // PC 端链接
|
||||
mobileUrl?: string; // 移动端链接
|
||||
}
|
||||
```
|
||||
|
||||
## 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "123456",
|
||||
"target": {
|
||||
"title": "问题标题"
|
||||
},
|
||||
"detail_text": "1000万"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
19
src/hotnow/zhihu/api.test.ts
Normal file
19
src/hotnow/zhihu/api.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { main } from "./api";
|
||||
|
||||
describe("zhihu", () => {
|
||||
it("should return an array", async () => {
|
||||
const result = await main();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return items with required fields", async () => {
|
||||
const result = await main();
|
||||
if (result.length > 0) {
|
||||
const item = result[0];
|
||||
expect(item).toHaveProperty("id");
|
||||
expect(item).toHaveProperty("title");
|
||||
expect(item).toHaveProperty("url");
|
||||
}
|
||||
});
|
||||
});
|
||||
37
src/hotnow/zhihu/api.ts
Normal file
37
src/hotnow/zhihu/api.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { HotListItem } from "../type";
|
||||
|
||||
export const label = {
|
||||
name: '知乎-热榜',
|
||||
icon: 'https://static.zhihu.com/heifetz/favicon.ico',
|
||||
color: '#0084FF',
|
||||
};
|
||||
|
||||
export const main = async function(): Promise<HotListItem[]> {
|
||||
const url = 'https://api.zhihu.com/topstory/hot-list';
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求错误 ${label.name}`);
|
||||
}
|
||||
const responseBody = await response.json();
|
||||
if (responseBody.data) {
|
||||
const result: HotListItem[] = responseBody.data.map((v) => {
|
||||
return {
|
||||
id: v.id,
|
||||
title: v.target.title,
|
||||
cover: v.children[0].thumbnail,
|
||||
hot: parseInt(v.detail_text.replace(/[^\d]/g, '')) * 10000,
|
||||
url: `https://www.zhihu.com/question/${v.card_id.replace('Q_', '')}`,
|
||||
mobileUrl: `https://www.zhihu.com/question/${v.card_id.replace('Q_', '')}`,
|
||||
originData: v,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
main().then(console.log).catch(console.error);
|
||||
18
vitest.config.ts
Normal file
18
vitest.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import { fileURLToPath } from "url";
|
||||
import { resolve } from "path";
|
||||
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
include: ["src/**/*.test.ts"],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user