Featured image of post API | DailyHotAPI 自定义 RSS 订阅接口

API | DailyHotAPI 自定义 RSS 订阅接口

原项目已经提供了一些订阅接口,但是无法涵盖每个人的实际使用情况,所以简单介绍一下如何自定义自己需要的订阅接口。

今日热榜 API,一个聚合热门数据的 API 接口,支持 RSS 模式 及 Vercel 部署,并含有配套的前端项目

拉取项目

git clone https://github.com/imsyy/DailyHotApi.git
cd DailyHotApi

安装依赖

npm install

修改配置

复制 /.env.example 文件并重命名为 /.env 并修改配置,包括Redis端口密码等

...
# 允许的域名
ALLOWED_DOMAIN = "*"

# 允许的主域名,填写格式为 imsyy.top
## 若填写该项,将忽略 ALLOWED_DOMAIN
ALLOWED_HOST="jianbing.tk"

# ROBOT
DISALLOW_ROBOT = true

# Redis
REDIS_HOST="127.0.0.1"
REDIS_PORT=6379
REDIS_PASSWORD="Your Password"
...

开发

npm run dev

成功启动后程序会在控制台输出可访问的地址

编译运行

npm run build
npm run start

成功启动后程序会在控制台输出可访问的地址

自定义RSS订阅

自定义RSS订阅通常只要新增两个部分的内容:

1.src\routes路径下的xxx.ts文件,比如nodeseek.ts,用来处理与 NodeSeek相关的路由和数据获取逻辑。

2.src\router.type.d.ts文件中的特定数据结构的类型接口。例如,"36kr" 类型接口,在36kr.ts中使用:

return {
    ...result,
    data: list.map((v: RouterType["36kr"]) => { //此处的RouterType
      const item = v.templateMaterial;
      return {
        id: v.itemId,
        title: item.widgetTitle,
        cover: item.widgetImage,
        author: item.authorName,
        timestamp: getTime(v.publishTime),
        hot: item.statCollect || undefined,
        url: `https://www.36kr.com/p/${v.itemId}`,
        mobileUrl: `https://m.36kr.com/p/${v.itemId}`,
      };
    }),
};

当然也可以不定义新的类型接口,只添加xxx.ts文件,借助parseRSS处理:

/* parseRSS.ts */
import RSSParser from "rss-parser";
import logger from "./logger.js";

/**
 * 提取 RSS 内容
 * @param content HTML 内容
 * @returns RSS 内容
 */
export const extractRss = (content: string): string | null => {
  // 匹配 <rss> 标签及内容
  const rssRegex = /(<rss[\s\S]*?<\/rss>)/i;
  const matches = content.match(rssRegex);
  return matches ? matches[0] : null;
};

/**
 * 解析 RSS 内容
 * @param rssContent RSS 内容
 * @returns 解析后的 RSS 内容
 */
export const parseRSS = async (rssContent: string) => {
  const parser = new RSSParser();
  // 是否为网址
  const isUrl = (url: string) => {
    try {
      new URL(url);
      return true;
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
    } catch (error) {
      return false;
    }
  };
  try {
    const feed = isUrl(rssContent)
      ? await parser.parseURL(rssContent)
      : await parser.parseString(rssContent);
    const items = feed.items.map((item) => ({
      title: item.title, // 文章标题
      link: item.link, // 文章链接
      pubDate: item.pubDate, // 发布日期
      author: item.creator ?? item.author, // 作者
      content: item.content, // 内容
      contentSnippet: item.contentSnippet, // 内容摘要
      guid: item.guid, // 全局唯一标识符
      categories: item.categories, // 分类
    }));
    // 返回解析数据
    return items;
  } catch (error) {
    logger.error("❌ [RSS] An error occurred while parsing RSS content");
    throw error;
  }
};

一般网站的RSS订阅都能借助src\utils\parseRSS.ts文件正确解析:

const getList = async (noCache: boolean) => {
  const url = `https://rss.nodeseek.com/`;
  const result = await get({ url, noCache });
  const list = await parseRSS(result.data);
  return {
    ...result,
    data: list.map((v, i) => ({
      id: v.guid || i,
      title: v.title || "",
      desc: v.content?.trim() || "",
      author: v.author,
      timestamp: getTime(v.pubDate || 0),
      hot: undefined,
      url: v.link || "",
      mobileUrl: v.link || "",
    })),
  };
};

部分订阅需要自己处理请求返回的数据,同时新增RouterType,且不借助parseRSS处理result.data

const getList = async (options: Options, noCache: boolean) => {
  const { type } = options;
  const url = `https://linux.do/${type}.json`; 
  const result = await get({ url, noCache });
  const list = result.data?.topic_list?.topics;
  const filteredIds = [5, 293017, 298988]; 
  return {
    fromCache: result.fromCache,
    updateTime: result.updateTime,
    data: list
    .filter((v: RouterType["linuxdo"]) => !filteredIds.includes(v.id)) // 过滤掉 id 在 filteredIds 列表中的条目
    .map((v: RouterType["linuxdo"]) => ({
      id: v.id,
      title: v.title,
      timestamp: v.last_comment_at || v.created_at, 
      hot: v.highest_post_number,
      url: `https://linux.do/t/topic/${v.id}`,
      mobileUrl: `https://linux.do/t/topic/${v.id}`,
    })),
  };
};

注意:除了自定义RSS订阅以外,还可以自行修改DailyHotApi项目的icon、页脚等信息,其中Home.tsx中的<img>标签中的图片为base64格式,需自行转码。

Docker部署

将本地开发好的项目部署至自己的服务器

# 列出所有正在运行的容器
docker ps

# 列出所有镜像,并筛选出名称包含 dailyhot-api 的镜像
docker images | grep dailyhot-api

# 停止名为 dailyhot-api 的容器(可以使用容器名称或 ID)
docker stop dailyhot-api

# 删除名为 dailyhot-api 的容器(可以使用容器名称或 ID)
docker rm dailyhot-api

# 构建镜像,使用当前目录的 Dockerfile,并将镜像命名为 dailyhot-api
docker build -t dailyhot-api .

# 运行容器,设置自动重启,映射主机的 6688 端口到容器的 6688 端口,并在后台运行
docker run --restart always -p 6688:6688 -d --name dailyhot-api dailyhot-api

# 或使用 Docker Compose 启动服务,后台运行
docker-compose up -d

注意:原项目中包含Redis缓存配置,需与服务器中端口和密码保持一致。

此外,Redis 缓存时长默认为 3600 秒(即一小时),你可以在 config.ts 文件中通过修改环境变量 CACHE_TTL 的值来调整该时长。

成品展示

Demo

预览:神马值得看

参考

使用 Hugo 构建
主题 StackJimmy 设计