commit 4cd5c7ac222333c2c159cc1a9c1fd1c9083956d2 Author: xiongxiao Date: Thu Mar 26 00:00:19 2026 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..771b053 --- /dev/null +++ b/AGENTS.md @@ -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 展示解耦,方便后续维护和扩展。 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a47234 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# 通用当前热点信息汇集 + + + +### 参考 + +``` +git clone https://github.com/ourongxing/newsnow.git +git clone https://github.com/baiwumm/next-daily-hot +``` diff --git a/package.json b/package.json new file mode 100644 index 0000000..d26ef5d --- /dev/null +++ b/package.json @@ -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 (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" + } +} diff --git a/src/hotnow/36kr/README.md b/src/hotnow/36kr/README.md new file mode 100644 index 0000000..be38dd7 --- /dev/null +++ b/src/hotnow/36kr/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/36kr/api.test.ts b/src/hotnow/36kr/api.test.ts new file mode 100644 index 0000000..aad6839 --- /dev/null +++ b/src/hotnow/36kr/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/36kr/api.ts b/src/hotnow/36kr/api.ts new file mode 100644 index 0000000..b566882 --- /dev/null +++ b/src/hotnow/36kr/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/baidu/README.md b/src/hotnow/baidu/README.md new file mode 100644 index 0000000..8e74aa1 --- /dev/null +++ b/src/hotnow/baidu/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/baidu/api.test.ts b/src/hotnow/baidu/api.test.ts new file mode 100644 index 0000000..9b3602f --- /dev/null +++ b/src/hotnow/baidu/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/baidu/api.ts b/src/hotnow/baidu/api.ts new file mode 100644 index 0000000..b4ff15d --- /dev/null +++ b/src/hotnow/baidu/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/baidutieba/README.md b/src/hotnow/baidutieba/README.md new file mode 100644 index 0000000..8c8f51a --- /dev/null +++ b/src/hotnow/baidutieba/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/baidutieba/api.test.ts b/src/hotnow/baidutieba/api.test.ts new file mode 100644 index 0000000..ef6d5a7 --- /dev/null +++ b/src/hotnow/baidutieba/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/baidutieba/api.ts b/src/hotnow/baidutieba/api.ts new file mode 100644 index 0000000..d2c317e --- /dev/null +++ b/src/hotnow/baidutieba/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/bilibili/README.md b/src/hotnow/bilibili/README.md new file mode 100644 index 0000000..c613d7c --- /dev/null +++ b/src/hotnow/bilibili/README.md @@ -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 + } + } + ] + } +} +``` \ No newline at end of file diff --git a/src/hotnow/bilibili/api.test.ts b/src/hotnow/bilibili/api.test.ts new file mode 100644 index 0000000..5ce13ac --- /dev/null +++ b/src/hotnow/bilibili/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/bilibili/api.ts b/src/hotnow/bilibili/api.ts new file mode 100644 index 0000000..5b15c90 --- /dev/null +++ b/src/hotnow/bilibili/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/csdn/README.md b/src/hotnow/csdn/README.md new file mode 100644 index 0000000..b4f679c --- /dev/null +++ b/src/hotnow/csdn/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/csdn/api.test.ts b/src/hotnow/csdn/api.test.ts new file mode 100644 index 0000000..c665fbc --- /dev/null +++ b/src/hotnow/csdn/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/csdn/api.ts b/src/hotnow/csdn/api.ts new file mode 100644 index 0000000..86606b8 --- /dev/null +++ b/src/hotnow/csdn/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/dongchedi/README.md b/src/hotnow/dongchedi/README.md new file mode 100644 index 0000000..c316dcc --- /dev/null +++ b/src/hotnow/dongchedi/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/dongchedi/api.test.ts b/src/hotnow/dongchedi/api.test.ts new file mode 100644 index 0000000..afbd928 --- /dev/null +++ b/src/hotnow/dongchedi/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/dongchedi/api.ts b/src/hotnow/dongchedi/api.ts new file mode 100644 index 0000000..9e849a2 --- /dev/null +++ b/src/hotnow/dongchedi/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/douban-movic/README.md b/src/hotnow/douban-movic/README.md new file mode 100644 index 0000000..95fe137 --- /dev/null +++ b/src/hotnow/douban-movic/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/douban-movic/api.test.ts b/src/hotnow/douban-movic/api.test.ts new file mode 100644 index 0000000..985415e --- /dev/null +++ b/src/hotnow/douban-movic/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/douban-movic/api.ts b/src/hotnow/douban-movic/api.ts new file mode 100644 index 0000000..7914004 --- /dev/null +++ b/src/hotnow/douban-movic/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/douyin/README.md b/src/hotnow/douyin/README.md new file mode 100644 index 0000000..44033d8 --- /dev/null +++ b/src/hotnow/douyin/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/douyin/api.test.ts b/src/hotnow/douyin/api.test.ts new file mode 100644 index 0000000..f1665ee --- /dev/null +++ b/src/hotnow/douyin/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/douyin/api.ts b/src/hotnow/douyin/api.ts new file mode 100644 index 0000000..cf4079e --- /dev/null +++ b/src/hotnow/douyin/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/github-trending/README.md b/src/hotnow/github-trending/README.md new file mode 100644 index 0000000..130605a --- /dev/null +++ b/src/hotnow/github-trending/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/github-trending/api.test.ts b/src/hotnow/github-trending/api.test.ts new file mode 100644 index 0000000..426888e --- /dev/null +++ b/src/hotnow/github-trending/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/github-trending/api.ts b/src/hotnow/github-trending/api.ts new file mode 100644 index 0000000..38dc352 --- /dev/null +++ b/src/hotnow/github-trending/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/hello-github/README.md b/src/hotnow/hello-github/README.md new file mode 100644 index 0000000..4a70f48 --- /dev/null +++ b/src/hotnow/hello-github/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/hello-github/api.test.ts b/src/hotnow/hello-github/api.test.ts new file mode 100644 index 0000000..424bc5b --- /dev/null +++ b/src/hotnow/hello-github/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/hello-github/api.ts b/src/hotnow/hello-github/api.ts new file mode 100644 index 0000000..cfc7039 --- /dev/null +++ b/src/hotnow/hello-github/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/history-today/README.md b/src/hotnow/history-today/README.md new file mode 100644 index 0000000..389cd25 --- /dev/null +++ b/src/hotnow/history-today/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/history-today/api.test.ts b/src/hotnow/history-today/api.test.ts new file mode 100644 index 0000000..c6575ed --- /dev/null +++ b/src/hotnow/history-today/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/history-today/api.ts b/src/hotnow/history-today/api.ts new file mode 100644 index 0000000..67981da --- /dev/null +++ b/src/hotnow/history-today/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/hupu/README.md b/src/hotnow/hupu/README.md new file mode 100644 index 0000000..0391d53 --- /dev/null +++ b/src/hotnow/hupu/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/hupu/api.test.ts b/src/hotnow/hupu/api.test.ts new file mode 100644 index 0000000..4bae024 --- /dev/null +++ b/src/hotnow/hupu/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/hupu/api.ts b/src/hotnow/hupu/api.ts new file mode 100644 index 0000000..a64166d --- /dev/null +++ b/src/hotnow/hupu/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/huxiu/README.md b/src/hotnow/huxiu/README.md new file mode 100644 index 0000000..801cda7 --- /dev/null +++ b/src/hotnow/huxiu/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/huxiu/api.test.ts b/src/hotnow/huxiu/api.test.ts new file mode 100644 index 0000000..5612e50 --- /dev/null +++ b/src/hotnow/huxiu/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/huxiu/api.ts b/src/hotnow/huxiu/api.ts new file mode 100644 index 0000000..49804d4 --- /dev/null +++ b/src/hotnow/huxiu/api.ts @@ -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 { + 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(//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); \ No newline at end of file diff --git a/src/hotnow/ifanr/README.md b/src/hotnow/ifanr/README.md new file mode 100644 index 0000000..c3066b5 --- /dev/null +++ b/src/hotnow/ifanr/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/ifanr/api.test.ts b/src/hotnow/ifanr/api.test.ts new file mode 100644 index 0000000..061ad78 --- /dev/null +++ b/src/hotnow/ifanr/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/ifanr/api.ts b/src/hotnow/ifanr/api.ts new file mode 100644 index 0000000..967c435 --- /dev/null +++ b/src/hotnow/ifanr/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/ithome/README.md b/src/hotnow/ithome/README.md new file mode 100644 index 0000000..1c47065 --- /dev/null +++ b/src/hotnow/ithome/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/ithome/api.test.ts b/src/hotnow/ithome/api.test.ts new file mode 100644 index 0000000..6e7a61e --- /dev/null +++ b/src/hotnow/ithome/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/ithome/api.ts b/src/hotnow/ithome/api.ts new file mode 100644 index 0000000..ef5b4d1 --- /dev/null +++ b/src/hotnow/ithome/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/juejin/README.md b/src/hotnow/juejin/README.md new file mode 100644 index 0000000..a2b2f38 --- /dev/null +++ b/src/hotnow/juejin/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/juejin/api.test.ts b/src/hotnow/juejin/api.test.ts new file mode 100644 index 0000000..39e9dc5 --- /dev/null +++ b/src/hotnow/juejin/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/juejin/api.ts b/src/hotnow/juejin/api.ts new file mode 100644 index 0000000..c99fb62 --- /dev/null +++ b/src/hotnow/juejin/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/kuaishou/README.md b/src/hotnow/kuaishou/README.md new file mode 100644 index 0000000..43ec4d7 --- /dev/null +++ b/src/hotnow/kuaishou/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/kuaishou/api.test.ts b/src/hotnow/kuaishou/api.test.ts new file mode 100644 index 0000000..f3c63dc --- /dev/null +++ b/src/hotnow/kuaishou/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/kuaishou/api.ts b/src/hotnow/kuaishou/api.ts new file mode 100644 index 0000000..b92a095 --- /dev/null +++ b/src/hotnow/kuaishou/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/lol/README.md b/src/hotnow/lol/README.md new file mode 100644 index 0000000..431e394 --- /dev/null +++ b/src/hotnow/lol/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/lol/api.test.ts b/src/hotnow/lol/api.test.ts new file mode 100644 index 0000000..229c81d --- /dev/null +++ b/src/hotnow/lol/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/lol/api.ts b/src/hotnow/lol/api.ts new file mode 100644 index 0000000..7aa5eb7 --- /dev/null +++ b/src/hotnow/lol/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/netease-music/README.md b/src/hotnow/netease-music/README.md new file mode 100644 index 0000000..d6ab734 --- /dev/null +++ b/src/hotnow/netease-music/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/netease-music/api.test.ts b/src/hotnow/netease-music/api.test.ts new file mode 100644 index 0000000..858433c --- /dev/null +++ b/src/hotnow/netease-music/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/netease-music/api.ts b/src/hotnow/netease-music/api.ts new file mode 100644 index 0000000..f5a5e0f --- /dev/null +++ b/src/hotnow/netease-music/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/netease/README.md b/src/hotnow/netease/README.md new file mode 100644 index 0000000..5ca123f --- /dev/null +++ b/src/hotnow/netease/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/netease/api.test.ts b/src/hotnow/netease/api.test.ts new file mode 100644 index 0000000..7337832 --- /dev/null +++ b/src/hotnow/netease/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/netease/api.ts b/src/hotnow/netease/api.ts new file mode 100644 index 0000000..53caf46 --- /dev/null +++ b/src/hotnow/netease/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/qq/README.md b/src/hotnow/qq/README.md new file mode 100644 index 0000000..20fa8f7 --- /dev/null +++ b/src/hotnow/qq/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/qq/api.test.ts b/src/hotnow/qq/api.test.ts new file mode 100644 index 0000000..9a951cc --- /dev/null +++ b/src/hotnow/qq/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/qq/api.ts b/src/hotnow/qq/api.ts new file mode 100644 index 0000000..a590c38 --- /dev/null +++ b/src/hotnow/qq/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/quark/README.md b/src/hotnow/quark/README.md new file mode 100644 index 0000000..fa4d734 --- /dev/null +++ b/src/hotnow/quark/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/quark/api.test.ts b/src/hotnow/quark/api.test.ts new file mode 100644 index 0000000..b70fa07 --- /dev/null +++ b/src/hotnow/quark/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/quark/api.ts b/src/hotnow/quark/api.ts new file mode 100644 index 0000000..f007ef7 --- /dev/null +++ b/src/hotnow/quark/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/thepaper/README.md b/src/hotnow/thepaper/README.md new file mode 100644 index 0000000..bc63966 --- /dev/null +++ b/src/hotnow/thepaper/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/thepaper/api.test.ts b/src/hotnow/thepaper/api.test.ts new file mode 100644 index 0000000..fdbdd48 --- /dev/null +++ b/src/hotnow/thepaper/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/thepaper/api.ts b/src/hotnow/thepaper/api.ts new file mode 100644 index 0000000..4752639 --- /dev/null +++ b/src/hotnow/thepaper/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/toutiao/README.md b/src/hotnow/toutiao/README.md new file mode 100644 index 0000000..9580ac2 --- /dev/null +++ b/src/hotnow/toutiao/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/toutiao/api.test.ts b/src/hotnow/toutiao/api.test.ts new file mode 100644 index 0000000..c907dda --- /dev/null +++ b/src/hotnow/toutiao/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/toutiao/api.ts b/src/hotnow/toutiao/api.ts new file mode 100644 index 0000000..0596184 --- /dev/null +++ b/src/hotnow/toutiao/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/type.ts b/src/hotnow/type.ts new file mode 100644 index 0000000..d4a5f5a --- /dev/null +++ b/src/hotnow/type.ts @@ -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; // 原始数据 + }; \ No newline at end of file diff --git a/src/hotnow/utils.ts b/src/hotnow/utils.ts new file mode 100644 index 0000000..0e5c805 --- /dev/null +++ b/src/hotnow/utils.ts @@ -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', + }, +]; \ No newline at end of file diff --git a/src/hotnow/weibo/README.md b/src/hotnow/weibo/README.md new file mode 100644 index 0000000..7e8de69 --- /dev/null +++ b/src/hotnow/weibo/README.md @@ -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": "热" + } + ] + } +} +``` \ No newline at end of file diff --git a/src/hotnow/weibo/api.test.ts b/src/hotnow/weibo/api.test.ts new file mode 100644 index 0000000..e435485 --- /dev/null +++ b/src/hotnow/weibo/api.test.ts @@ -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(); + }); +}); \ No newline at end of file diff --git a/src/hotnow/weibo/api.ts b/src/hotnow/weibo/api.ts new file mode 100644 index 0000000..26957db --- /dev/null +++ b/src/hotnow/weibo/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/weread/README.md b/src/hotnow/weread/README.md new file mode 100644 index 0000000..859a99d --- /dev/null +++ b/src/hotnow/weread/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/weread/api.test.ts b/src/hotnow/weread/api.test.ts new file mode 100644 index 0000000..36d8993 --- /dev/null +++ b/src/hotnow/weread/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/weread/api.ts b/src/hotnow/weread/api.ts new file mode 100644 index 0000000..b692490 --- /dev/null +++ b/src/hotnow/weread/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/woshipm/README.md b/src/hotnow/woshipm/README.md new file mode 100644 index 0000000..8a26194 --- /dev/null +++ b/src/hotnow/woshipm/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/woshipm/api.test.ts b/src/hotnow/woshipm/api.test.ts new file mode 100644 index 0000000..d39ea93 --- /dev/null +++ b/src/hotnow/woshipm/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/woshipm/api.ts b/src/hotnow/woshipm/api.ts new file mode 100644 index 0000000..9420bb1 --- /dev/null +++ b/src/hotnow/woshipm/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/xiaohongshu/README.md b/src/hotnow/xiaohongshu/README.md new file mode 100644 index 0000000..ffdef0e --- /dev/null +++ b/src/hotnow/xiaohongshu/README.md @@ -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": "热" + } + ] + } +} +``` \ No newline at end of file diff --git a/src/hotnow/xiaohongshu/api.test.ts b/src/hotnow/xiaohongshu/api.test.ts new file mode 100644 index 0000000..8283517 --- /dev/null +++ b/src/hotnow/xiaohongshu/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/xiaohongshu/api.ts b/src/hotnow/xiaohongshu/api.ts new file mode 100644 index 0000000..3aea40e --- /dev/null +++ b/src/hotnow/xiaohongshu/api.ts @@ -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 { + // 官方 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); \ No newline at end of file diff --git a/src/hotnow/zhihu-daily/README.md b/src/hotnow/zhihu-daily/README.md new file mode 100644 index 0000000..faa0e14 --- /dev/null +++ b/src/hotnow/zhihu-daily/README.md @@ -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; // 移动端链接 +} +``` \ No newline at end of file diff --git a/src/hotnow/zhihu-daily/api.test.ts b/src/hotnow/zhihu-daily/api.test.ts new file mode 100644 index 0000000..95fc4c9 --- /dev/null +++ b/src/hotnow/zhihu-daily/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/zhihu-daily/api.ts b/src/hotnow/zhihu-daily/api.ts new file mode 100644 index 0000000..c75959b --- /dev/null +++ b/src/hotnow/zhihu-daily/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/src/hotnow/zhihu/README.md b/src/hotnow/zhihu/README.md new file mode 100644 index 0000000..c95827f --- /dev/null +++ b/src/hotnow/zhihu/README.md @@ -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万" + } + ] +} +``` \ No newline at end of file diff --git a/src/hotnow/zhihu/api.test.ts b/src/hotnow/zhihu/api.test.ts new file mode 100644 index 0000000..0a97b0f --- /dev/null +++ b/src/hotnow/zhihu/api.test.ts @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/src/hotnow/zhihu/api.ts b/src/hotnow/zhihu/api.ts new file mode 100644 index 0000000..4f6c3fa --- /dev/null +++ b/src/hotnow/zhihu/api.ts @@ -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 { + 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); \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..ef935b6 --- /dev/null +++ b/vitest.config.ts @@ -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"), + }, + }, +}); \ No newline at end of file