Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 26 additions & 14 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,20 @@ bun run start
# 代码检查(oxlint)
bun run lint

# 单元测试
bun test

# Docker 构建与运行
docker build -t meting-api .
docker run -p 80:80 -e METING_URL=https://example.com -e METING_TOKEN=secret meting-api
```

项目没有测试套件。
单元测试位于 `test/` 目录(auth/lyric/router),使用 Bun 内置测试运行器。

## 技术栈

- **运行时**: Bun (ES Module)
- **HTTP 服务**: 原生 Bun.serve API(非框架)
- **运行时**: Bun (ES Module);Vercel 部署时为 Node.js Serverless Runtime(非 Edge)
- **HTTP 服务**: 原生 Bun.serve API(非框架),共享 handler 基于 Web 标准 `Request`/`Response`
- **核心库**: @meting/core ^1.6.0(音乐 API 封装)
- **缓存**: lru-cache ^11.x
- **日志**: pino(JSON 格式)+ pino-pretty(开发环境)
Expand All @@ -39,25 +42,31 @@ docker run -p 80:80 -e METING_URL=https://example.com -e METING_TOKEN=secret met

### 请求处理链

入口 `src/index.js` 使用 Bun.serve 启动 HTTP/HTTPS 服务器。中间件按函数组合模式串联:
入口 `src/index.js` 使用 Bun.serve 启动 HTTP/HTTPS 服务器(Vercel 部署时由 `api/` 下的函数作为入口)。中间件按函数组合模式串联:

```
Bun.serve → CORS 处理 → logger 中间件 → error 中间件 → router → service
Bun.serve / Vercel 函数 → CORS 处理 → logger 中间件 → error 中间件 → router → service
```

路由分派是手动 if-else(非框架路由器):
- `GET {prefix}/api` → `src/service/api.js`(核心 API)
路由解析在 `src/router.js`(纯函数 parseRoute + route handler,非框架路由器),支持两种形式:
- 传统 `GET {prefix}/api?server=&type=&id=` → `src/service/api.js`(核心 API)
- RESTful `GET {prefix}/api/:server/search` / `GET {prefix}/api/:server/:type/:id` → 同上
- `GET {prefix}/demo` → `src/service/demo.js`(演示播放器页面)
- 其他 → 404

### 文件职责

| 文件 | 职责 |
|------|------|
| `src/index.js` | 应用入口,Bun.serve 启动,CORS 处理,路由分派,中间件组合 |
| `src/index.js` | Bun 适配器:Bun.serve 启动 HTTP/HTTPS 服务器,复用共享 handler |
| `src/app.js` | 共享 handler:`createApp()` 组合 CORS + logger + error + router,兼容 Bun 与 Vercel |
| `src/router.js` | 路由解析:纯函数 `parseRoute`(路径→路由描述)与 `route` handler,支持传统与 RESTful 两种形式 |
| `src/config.js` | 环境变量解析为结构化配置对象 |
| `src/service/api.js` | 核心业务:参数校验→鉴权→缓存→调用上游API→URL转换→响应组装 |
| `src/service/api.js` | 核心业务:参数校验→鉴权→缓存→调用上游API→URL转换→响应组装(导出 `resolve`) |
| `src/service/auth.js` | 鉴权:生成敏感接口(lrc/url/pic)的 HMAC-SHA1 token |
| `src/service/demo.js` | 返回嵌入 APlayer + Meting.js 的 HTML 演示页 |
| `api/index.js` | Vercel 入口(根路由) |
| `api/[...path].js` | Vercel 入口(catch-all 路由,兜底 RESTful 路径) |
| `src/middleware/logger.js` | 请求日志:生成 requestId,记录响应时间和状态码 |
| `src/middleware/errors.js` | 统一异常捕获,通过 `x-error-message` 响应头传递错误信息 |
| `src/utils/cookie.js` | Cookie 读取(环境变量优先,文件次之),5分钟缓存,referrer 白名单校验 |
Expand All @@ -68,7 +77,7 @@ Bun.serve → CORS 处理 → logger 中间件 → error 中间件 → router

敏感操作(lrc、url、pic)使用 HMAC-SHA1 token 认证:
- token 计算: `HMAC-SHA1(METING_TOKEN, "${server}${type}${id}")`
- auth 函数在 `src/service/api.js:139`
- auth 函数已抽取到 `src/service/auth.js`(由 `src/service/api.js` 引入调用)
- 认证参数通过查询字符串 `token` 或 `auth` 传递

### 缓存策略
Expand All @@ -87,9 +96,10 @@ LRU 缓存(lru-cache),最多 1000 条,默认 TTL 30 秒:

### Cookie 管理

Cookie 支持两种来源(优先级从高到低):
1. 环境变量 `METING_COOKIE_{SERVER}`(如 `METING_COOKIE_NETEASE`)
2. 文件系统 `./cookie/{server}`
Cookie 支持三种来源(优先级从高到低):
1. 环境变量 `METING_COOKIE_{SERVER}`(如 `METING_COOKIE_NETEASE`,完整 Cookie)
2. 环境变量 `MUSIC_U` + 内置客户端指纹(`DEFAULT_COOKIES.netease`,MUSIC_U 从环境变量读取)
3. 文件系统 `./cookie/{server}`

通过 `METING_COOKIE_ALLOW_HOSTS` 限制哪些 referrer 来源可使用 Cookie。

Expand All @@ -103,10 +113,12 @@ Cookie 支持两种来源(优先级从高到低):
| `HTTPS_PORT` | HTTPS 端口 | `443` |
| `SSL_KEY_PATH` | HTTPS 私钥路径 | - |
| `SSL_CERT_PATH` | HTTPS 证书路径 | - |
| `METING_URL` | 公网访问地址(用于生成回调 URL) | - |
| `METING_URL` | 公网访问地址(用于生成回调 URL) | -(未设置时回退到 `https://${VERCEL_URL}`) |
| `VERCEL_URL` | Vercel 自动注入的域名 | -(仅 Vercel 环境存在) |
| `METING_TOKEN` | HMAC 签名密钥 | `token` |
| `METING_COOKIE_ALLOW_HOSTS` | Cookie referrer 白名单(逗号分隔) | `` (不限制) |
| `METING_COOKIE_{SERVER}` | 各平台 Cookie(NETEASE/TENCENT/KUGOU/BAIDU/KUWO) | - |
| `MUSIC_U` | 网易云登录凭证(浏览器 Cookie 中的 MUSIC_U 值,与内置客户端指纹拼成兜底 Cookie) | - |

## 开发注意事项

Expand Down
96 changes: 81 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
# Meting-API

基于 Hono.js 的多平台音乐 API 代理服务,封装 [@meting/core](https://www.npmjs.com/package/@meting/core) 提供的统一音乐 API。
基于原生 Web 标准 `Request`/`Response` 的多平台音乐 API 代理服务,可同时部署为 Bun 常驻进程或 Vercel Serverless 函数,封装 [@meting/core](https://www.npmjs.com/package/@meting/core) 提供的统一音乐 API。

## 特性

- 🎵 支持多个音乐平台:网易云、QQ音乐、酷狗、百度、酷我
- 🚀 基于 Hono.js 高性能框架
- 🚀 基于原生 Web 标准 Request/Response,零框架依赖
- ☁️ Vercel 一键部署
- 💾 内置 LRU 缓存机制,减少上游 API 调用
- 🔐 HMAC-SHA1 令牌鉴权,保护敏感接口
- 🐳 Docker 部署支持
- 📝 结构化 JSON 日志输出

## 改造说明

本项目由单进程 Bun 服务改造为「Bun 常驻进程 + Vercel Serverless」双适配架构:

- 请求处理逻辑抽为框架无关的 `createApp()`(`src/app.js`),Bun 与 Vercel 共用同一 handler
- 新增 RESTful 路径路由(`src/router.js`),传统查询串接口 `/api?server=&type=&id=` 完整保留
- 鉴权逻辑抽为纯函数 `src/service/auth.js`(HMAC-SHA1)
- 新增 Vercel 入口 `api/index.js`、`api/[...path].js` 与 `vercel.json`
- 列表 / 歌词 / 资源响应附带 `Cache-Control`,便于 CDN 边缘缓存
- 新增单元测试(`bun test`):鉴权、路由解析、歌词合并、核心契约

## 支持的平台

| 平台 | server 参数 | 说明 |
Expand All @@ -34,17 +46,17 @@

```bash
# 安装依赖
yarn install
bun install

# 配置环境变量(可选)
cp .env.example .env
# 编辑 .env 文件配置参数

# 开发模式(热重载)
yarn dev
bun run dev

# 生产模式
yarn start
bun run start
```

### Docker 部署
Expand Down Expand Up @@ -77,6 +89,23 @@ services:
restart: unless-stopped
```

### Vercel 部署

项目内置 Vercel Serverless 适配层,`api/` 目录会被自动识别为 Serverless Functions(Node.js Runtime,非 Edge)。

1. 将仓库导入 Vercel(或使用 CLI `vercel deploy`)
2. 配置环境变量:
- `METING_TOKEN`(**必填**):HMAC 签名密钥。默认值为 `token`,公开部署务必改掉,否则任何人都能算出 token
- `METING_URL`(可选):公网访问地址,未设置时自动回退到 `https://${VERCEL_URL}`(`VERCEL_URL` 由 Vercel 自动注入,无需手动配置)

**部署须知:**

- **依赖安装**:仓库只提交了 `bun.lock`,Vercel 默认用 `npm install`(依赖 `^` 范围可能与本地 Bun 版本不一致)。建议二选一:
- 在 `vercel.json` 添加 `"installCommand": "bun install"`(若启用 Bun)
- 或运行一次 `npm install --package-lock-only` 提交 `package-lock.json`
- **Cookie**:Serverless 环境只支持环境变量 `METING_COOKIE_{SERVER}`,`cookie/` 目录文件方式不生效
- **缓存**:LRU 缓存每实例独立、冷启动后重建;列表 / url / pic / lrc 响应已附带 `Cache-Control`,可借助 Vercel CDN 边缘缓存减少上游调用

## HTTPS 配置

### 开发环境
Expand All @@ -98,7 +127,7 @@ openssl req -x509 -nodes -days 365 \
HTTPS_ENABLED=true \
SSL_KEY_PATH=certs/local.key \
SSL_CERT_PATH=certs/local.crt \
yarn start
bun run start
```

### 生产环境
Expand Down Expand Up @@ -150,6 +179,7 @@ docker run -d \
| `METING_TOKEN` | HMAC 签名密钥 | `token` |
| `METING_COOKIE_ALLOW_HOSTS` | 允许使用 cookie 的 referrer 域名白名单(逗号分隔) | `` (空,不限制) |
| `METING_COOKIE_NETEASE` | 网易云音乐 Cookie | - |
| `MUSIC_U` | 网易云登录凭证(浏览器 Cookie 中的 MUSIC_U 值,与内置客户端指纹拼成兜底 Cookie) | - |
| `METING_COOKIE_TENCENT` | QQ音乐 Cookie | - |
| `METING_COOKIE_KUGOU` | 酷狗音乐 Cookie | - |
| `METING_COOKIE_BAIDU` | 百度音乐 Cookie | - |
Expand All @@ -163,6 +193,23 @@ docker run -d \
GET /api
```

### RESTful 接口

除了下述传统接口,项目还提供 RESTful 风格路由:

```
GET /api/:server/search?keywords=xxx
GET /api/:server/song/:id
GET /api/:server/album/:id
GET /api/:server/artist/:id
GET /api/:server/playlist/:id
GET /api/:server/lrc/:id (需 token)
GET /api/:server/url/:id (需 token)
GET /api/:server/pic/:id (需 token)
```

其中 `:server` 取值为 `netease`/`tencent`/`kugou`/`baidu`/`kuwo`。传统接口 `GET /api?server=&type=&id=` 仍然可用(演示页与 Meting.js 依赖它)。

### 请求参数

| 参数 | 类型 | 必填 | 说明 |
Expand Down Expand Up @@ -263,7 +310,7 @@ const token = generateToken('netease', 'url', '123456');

## Cookie 配置

部分音乐平台的 API 需要登录态才能访问完整数据。可以通过以下两种方式配置 Cookie:
部分音乐平台的 API 需要登录态才能访问完整数据。可以通过以下方式配置 Cookie:

### 方式一:环境变量(推荐)

Expand Down Expand Up @@ -293,10 +340,23 @@ cookie/

每个文件存储对应平台的 Cookie 字符串。

### 方式三:内置客户端指纹 + MUSIC_U 环境变量

项目在 `src/utils/cookie.js` 内置了网易云的客户端指纹(`os`/`appver`/`channel` 等公开信息),而登录凭证 `MUSIC_U` 从环境变量读取,二者自动拼成完整 Cookie:

```js
const DEFAULT_COOKIES = {
netease: 'os=pc; ...; MUSIC_U={MUSIC_U}; __remember_me=true'
}
```

使用时只需设置环境变量 `MUSIC_U=你的网易云登录凭证`(浏览器 Cookie 里的 `MUSIC_U` 值),即可解析 VIP 歌曲,无需把密钥写进代码。

### Cookie 优先级

1. 优先从环境变量读取(`METING_COOKIE_NETEASE` 等)
2. 环境变量不存在时从文件读取(`cookie/netease` 等)
1. 环境变量 `METING_COOKIE_NETEASE`(完整 Cookie,最高优先)
2. 环境变量 `MUSIC_U` + 内置客户端指纹(未配 `METING_COOKIE_NETEASE` 时生效)
3. 文件 `cookie/netease`(兜底,仅本地/非 Serverless 环境)

### Cookie 缓存

Expand Down Expand Up @@ -334,20 +394,26 @@ API 返回标准 HTTP 状态码:

### 代码规范

项目使用 ESLint Standard 规范:
项目使用 oxlint 进行代码检查:

```bash
bun run lint
```

运行单元测试:

```bash
yarn lint
bun test
```

### 技术栈

- **运行时**: Node.js 22+ (ES Module)
- **框架**: [Hono](https://hono.dev/) 4.x
- **核心库**: [@meting/core](https://www.npmjs.com/package/@meting/core) 1.5+
- **运行时**: Bun(本地常驻进程)+ Vercel Node.js Runtime(Serverless,非 Edge)
- **HTTP 服务**: 原生 fetch API(Web 标准 `Request`/`Response`,无框架)
- **核心库**: [@meting/core](https://www.npmjs.com/package/@meting/core) 1.6+
- **缓存**: lru-cache 11.x
- **日志**: pino (JSON 格式)
- **加密**: hash.js (HMAC-SHA1)
- **加密**: Node.js 内置 `node:crypto` (HMAC-SHA1)

## 许可证

Expand Down
6 changes: 6 additions & 0 deletions api/[...path].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createApp } from '../src/app.js'

const app = createApp()

export const GET = app
export const OPTIONS = app
Comment on lines +5 to +6
6 changes: 6 additions & 0 deletions api/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createApp } from '../src/app.js'

const app = createApp()

export const GET = app
export const OPTIONS = app
Comment on lines +5 to +6
Loading