chore(release): v2.0.0 —— Python 版版本号与 README 对齐
- pyproject/app.__init__/OpenAPI 统一 2.0.0(main.py 改为读取 __version__) - README 切换为 v2.0(Python)说明:uv 快速开始、app/ 结构、音源加载未移植、已知限制更新
This commit is contained in:
1 parent
dfc06bf4de
commit
e168f43b81
5 files changed
+64
-71
No files matched your search
@@ -1,6 +1,10 @@
|
|||||||
# 音乐服务器(MusicServer)
|
# 音乐服务器(MusicServer)
|
||||||
|
|
||||||
多平台音乐聚合 API 服务器,纯 Node.js 实现,无需浏览器环境。
|
多平台音乐聚合 API 服务器,无需浏览器环境。
|
||||||
|
|
||||||
|
> **v2.0(Python 版,当前默认分支)**:服务端已全量迁移到 Python 3.12 + FastAPI,14 个平台、
|
||||||
|
> 各平台登录通道与 Node 版 v1.x 功能对齐(外部音源脚本加载暂未移植)。
|
||||||
|
> Node/TypeScript 实现(v1.x)保留在 `master` 分支。
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
@@ -30,17 +34,21 @@
|
|||||||
|
|
||||||
> 能力位以 `GET /sources` 返回为准(含 `albumSearch`/`playlist`/`parse` 等新增能力位)。
|
> 能力位以 `GET /sources` 返回为准(含 `albumSearch`/`playlist`/`parse` 等新增能力位)。
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始(v2.0 Python)
|
||||||
|
|
||||||
|
依赖 [uv](https://docs.astral.sh/uv/)(Python 3.12,见 `.python-version`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm install
|
uv sync # 安装依赖(FastAPI/uvicorn/aiohttp/cryptography/qrcode 等)
|
||||||
pnpm start # 默认端口 16688,可用 PORT 环境变量修改(需 Node 22.18+,直接运行 .ts)
|
uv run uvicorn app.main:app --host 0.0.0.0 --port 16688
|
||||||
pnpm dev # 开发模式:node --watch 热重载,改动 src/、sdk/ 代码自动重启
|
# 默认端口 16688;也可用 PORT 环境变量 + --port $PORT
|
||||||
pnpm test # 类型检查(tsc)+ 冒烟测试(自动启动/关闭本地服务器)
|
uv run ruff check app && uv run ruff format --check app # 规范检查
|
||||||
pnpm typecheck # 仅类型检查,不运行
|
uv run pyright # 类型检查(基础模式)
|
||||||
```
|
```
|
||||||
|
|
||||||
环境变量(`PORT`、`SOURCE_*` 等)可写入项目根目录的 `.env` 文件(已 gitignore),启动时自动加载;已存在的真实环境变量优先于 `.env` 文件值。
|
环境变量(`PORT`、`AUTH_KEY`、`RATE_LIMIT`、`DATA_DIR` 等)可写入项目根目录的 `.env` 文件(已 gitignore),启动时自动加载;已存在的真实环境变量优先于 `.env` 文件值。登录凭据持久化在 `data/auth.json`(0600,原子写入)。
|
||||||
|
|
||||||
|
Node/TypeScript 版(v1.x,`master` 分支):`pnpm install && pnpm start`(需 Node 22.18+,直接运行 `.ts`)。
|
||||||
|
|
||||||
## HTTP 接口
|
## HTTP 接口
|
||||||
|
|
||||||
@@ -48,9 +56,9 @@ pnpm typecheck # 仅类型检查,不运行
|
|||||||
|
|
||||||
| 接口 | 说明 |
|
| 接口 | 说明 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `GET /search/:source/:keyword[/:page]` | 搜索歌曲:平台 id 或 all(聚合,排除 xm/me/qt/lr);第 1 页可省略页码段,下一页 `/search/kw/稻香/2`;?singer= 附加歌手、?limit= 每页条数 |
|
| `GET /search/:source/:keyword[/:page]` | 搜索歌曲:平台 id 或 all(聚合,排除 xm/me/qt/lr/apple);第 1 页可省略页码段,下一页 `/search/kw/稻香/2`;?singer= 附加歌手、?limit= 每页条数 |
|
||||||
| `GET /find/:source/:keyword` | 精确匹配搜索(source 为平台 id 或 all;?singer= ?albumName= ?interval= 辅助匹配,all 时 ?excludeSource= 跳过平台) |
|
| `GET /find/:source/:keyword` | 精确匹配搜索(source 为平台 id 或 all;?singer= ?albumName= ?interval= 辅助匹配,all 时 ?excludeSource= 跳过平台) |
|
||||||
| `GET /url/:source/:id?quality=320k` | 获取播放地址(quality: 64k/128k/320k/flac/flac24bit)。取流链:外部音源(启用时)→ SDK 直连 → 酷我兜底(跨平台匹配,响应 `via` 字段标记实际命中通道) |
|
| `GET /url/:source/:id?quality=320k` | 获取播放地址(quality: 64k/128k/320k/flac/flac24bit)。取流链:SDK 直连 → 酷我兜底(跨平台匹配,响应 `via` 字段标记实际命中通道) |
|
||||||
| `GET /stream/:source/:id` | 加密流解密代理(`mg` 咪咕 Z3D/I3D 加密流)。取流响应返回相对地址时使用,服务端下载上游密流并在本地解密回明文音频 |
|
| `GET /stream/:source/:id` | 加密流解密代理(`mg` 咪咕 Z3D/I3D 加密流)。取流响应返回相对地址时使用,服务端下载上游密流并在本地解密回明文音频 |
|
||||||
| `GET /lyric/:source/:id` | 获取歌词(返回 lyric/tlyric/yrc) |
|
| `GET /lyric/:source/:id` | 获取歌词(返回 lyric/tlyric/yrc) |
|
||||||
| `GET /pic/:source/:id` | 获取封面 |
|
| `GET /pic/:source/:id` | 获取封面 |
|
||||||
@@ -107,7 +115,7 @@ pnpm typecheck # 仅类型检查,不运行
|
|||||||
- **5sing**:`<songId>|<songType>`(如 `932557|yc`,songType 为 yc 原创 / fc 翻唱 / bz 伴奏),URL 路径需编码 `|` 为 `%7C`
|
- **5sing**:`<songId>|<songType>`(如 `932557|yc`,songType 为 yc 原创 / fc 翻唱 / bz 伴奏),URL 路径需编码 `|` 为 `%7C`
|
||||||
- **apple**:数字歌曲 id(搜索结果产出);取流固定为 `preview` 试听片段(完整曲为 DRM,不在支持范围)
|
- **apple**:数字歌曲 id(搜索结果产出);取流固定为 `preview` 试听片段(完整曲为 DRM,不在支持范围)
|
||||||
|
|
||||||
`/url` 在外部音源与平台直连都失败时,自动降级到**酷我**兜底:按歌名/歌手/时长匹配同一首歌后走 kw 通道(匿名可拿无损)。kg 可由 hash 反查歌曲信息;其它平台需要兜底时给 `/url` 传 `name`/`singer`/`interval` 查询参数。
|
`/url` 在平台直连失败时,自动降级到**酷我**兜底:按歌名/歌手/时长匹配同一首歌后走 kw 通道(匿名可拿无损)。kg 可由 hash 反查歌曲信息;其它平台需要兜底时给 `/url` 传 `name`/`singer`/`interval` 查询参数。
|
||||||
|
|
||||||
## 调用鉴权与限流
|
## 调用鉴权与限流
|
||||||
|
|
||||||
@@ -132,72 +140,57 @@ per-IP 滑动窗口限流,`RATE_LIMIT` 为每 IP 每分钟允许的请求数
|
|||||||
|
|
||||||
## 外部音源
|
## 外部音源
|
||||||
|
|
||||||
设置 `SOURCE_ENABLED=1` 后取流优先走外部音源(默认未设置时直接走 SDK 官方直连接口;是否启用由服务端在 `src/server.ts` 决策)。两种脚本都放 `sources/` 目录(不入库),按文件名混排成候选链:取流失败自动切换下一个,某平台连续失败 3 次的脚本降权冷却 5 分钟;整条音源链失败后回落 SDK 直连、再失败走酷我兜底(见上文取流链)。音源候选链只覆盖 kw/kg/tx/wy。音源模式下登录态不参与取流,登录自动续期不会启动(手动 `POST /login/refresh` 仍可用)。详见 [docs/source-ivm.md](docs/source-ivm.md) 与 [docs/source-simple.md](docs/source-simple.md)。
|
**v2.0 未移植外部音源加载**(`SOURCE_ENABLED` / `sources/*.js` / `*.mjs` 候选链与 isolated-vm 沙箱不在本分支)。
|
||||||
|
需要时请使用 `master` 分支的 v1.x(详见该分支 README 与 `docs/source-ivm.md`、`docs/source-simple.md`)。
|
||||||
|
|
||||||
**社区音源脚本(`.js`)** —— 通用格式,isolated-vm 硬隔离沙箱执行(详见 `src/source-ivm.ts` 头注释),文件名排序即优先级;`SOURCE_SCRIPT_PATH` 可改为指定目录外单脚本,`SOURCE_LOG=0` 关闭请求日志,`SOURCE_ALLOW_ALL_HTTP=1` 放开 HTTP 白名单(调试用)。
|
## 项目结构(v2.0)
|
||||||
|
|
||||||
**简单 API 脚本(`.mjs`)** —— 进程内直接加载、无沙箱的第二套极简契约,只需导出一个 `getMusicUrl`,适合自己写的取流脚本(运行于服务器进程内,请只放自己审计过的代码):
|
|
||||||
|
|
||||||
```js
|
|
||||||
// sources/99-my-api.mjs —— dev 热重载模式下保存即自动重启生效
|
|
||||||
export const sources = ['kw', 'kg', 'tx', 'wy'] // 可选,默认四平台
|
|
||||||
export const timeout = 15000 // 可选,单次取流超时 ms(默认 15000)
|
|
||||||
|
|
||||||
export async function getMusicUrl({ source, id, songInfo, quality, signal }) {
|
|
||||||
// source: 平台 id(kw/kg/tx/wy);id: 歌曲 id(songmid/hash)
|
|
||||||
// quality: 128k/320k/flac/flac24bit
|
|
||||||
// signal: 取消/超时信号(AbortSignal),fetch(url, { signal }) 透传即可支持取消
|
|
||||||
const res = await fetch(`https://your-api/get?source=${source}&id=${id}&quality=${quality}`, { signal })
|
|
||||||
const { data } = await res.json()
|
|
||||||
return data.url // 播放地址字符串,或 { type, url }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
```
|
```
|
||||||
src/ HTTP 应用层(Fastify 服务器,全部 TypeScript 源码)
|
app/ Python 应用(FastAPI,Python 3.12)
|
||||||
server.ts HTTP 服务器入口(pnpm start 实际启动文件)
|
main.py HTTP 入口(装配中间件/异常处理/OpenAPI/路由 + lifespan)
|
||||||
routes.ts 业务路由(JSON Schema 校验 + OpenAPI 元数据)
|
config.py 环境变量、.env 加载、端口/鉴权/限流/数据目录
|
||||||
routes-login.ts 登录路由(wy 扫码/手机号、kg 扫码/短信/账密+风控验证/token、tx 扫码/cookie、
|
routes/
|
||||||
bili 扫码、xm/me cookie 绑定、lr 账密)
|
music.py 业务路由(搜索/取流/歌词/封面/榜单/歌单/专辑/歌手/解析/流代理)
|
||||||
source.ts 外部音源候选链(SOURCE_ENABLED 时 /url 取流优先走 sources/ 脚本,失败回落 SDK)
|
login.py 登录路由(wy/kg/tx/bili 扫码与短信账密、xm/me cookie、lr 账密)
|
||||||
source-ivm.ts 社区音源脚本加载器(isolated-vm 硬隔离沙箱)
|
refresh.py 登录态自动续期(启动后延时首轮 + 每 24h,kg/tx)
|
||||||
source-simple.ts 简单 API 音源脚本加载器(.mjs 进程内直载)
|
system.py /health、/sources
|
||||||
http/
|
http/
|
||||||
api-error.ts 业务错误类型(404/502 等 → 统一 JSON 响应)
|
api_error.py 业务错误(统一 JSON 响应)
|
||||||
auth-gate.ts 调用秘钥鉴权(AUTH_KEY,全路径防扫描;/stream/* 可改用短时签名)
|
auth_gate.py 调用秘钥鉴权(AUTH_KEY,全路径防扫描;/stream/* 可用短时签名)
|
||||||
rate-limit.ts per-IP 滑动窗口限流(RATE_LIMIT,默认 120/分钟)
|
rate_limit.py per-IP 滑动窗口限流(RATE_LIMIT)
|
||||||
stream-sign.ts /stream/* 流地址短时签名(HMAC 派生,10 分钟有效)
|
envelope.py 成功响应信封 { code: 0, data }
|
||||||
migu-stream.ts 咪咕加密流票据登记与逐块解密管道
|
stream_sign.py /stream/* 流地址短时签名(HMAC,10 分钟)
|
||||||
|
mg_stream.py 咪咕加密流票据与逐块解密管道
|
||||||
openapi/
|
openapi/
|
||||||
components.ts OpenAPI 共享 schema(与路由校验同源)
|
components.py OpenAPI 共享 schema
|
||||||
ui.ts ReDoc 页面模板
|
ui.py ReDoc 页面
|
||||||
sdk/ 音乐聚合 SDK(仅官方平台:平台模块 + 官方直连取流 + 凭据 + 基础设施;14 个平台)
|
sdk/
|
||||||
index.ts 平台聚合入口(通用工具转发,实现在 common/utils.ts)
|
__init__.py 平台注册表、聚合搜索、跨平台精确匹配
|
||||||
apis/ 各平台官方 URL 直连取流实现(14 平台)
|
types.py 契约类型(SongInfo / MusicUrlResult 等)
|
||||||
auth/ 登录态与登录协议(store.ts 持久化凭据到 data/auth.json;wy.ts 扫码/手机号、kg.ts 扫码/短信/token/续期、tx.ts 扫码/cookie/续期)
|
auth_store.py 凭据持久化(data/auth.json,0600 + 原子替换)
|
||||||
common/ SDK 基础设施
|
common/ request(aiohttp 直连,不跟随 302、保留多值 Set-Cookie)、
|
||||||
request.ts HTTP 请求(可取消、自动 JSON 解析、gzip/deflate 解压)
|
utils(size_formate/format_play_time/as_dict 等)、qrcode
|
||||||
utils.ts 工具函数(sizeFormate/formatPlayTime/dateFormat 等)
|
auth/ 登录协议(wy/kg/kg_simulate/tx/lr/bili)
|
||||||
message.ts 请求错误消息
|
apis/ 各平台官方直连取流(14 平台,含 CDN 选址/档位降级)
|
||||||
kg/ kw/ tx/ wy/ migu/ qian/ joox/ bili/ 5sing/ apple/ xm/ me/ qt/ lr/
|
platforms/ 平台模块(每平台一个包,MODULE: Platform + 子模块)
|
||||||
平台模块(搜索/歌词/榜单/歌单/专辑/链接解析等;migu 含 Z3D 流解密,bili 含扫码登录)
|
docs/
|
||||||
apis/ kw.ts kg.ts tx.ts wy.ts migu.ts qian.ts joox.ts bili.ts 5sing.ts apple.ts …
|
PORTING.md TS → Python 平台移植契约(目录/子模块协议/签名差异/验收要求)
|
||||||
test/
|
pyproject.toml uv 项目定义(依赖、ruff、pyright、pytest 配置)
|
||||||
smoke-test.ts 冒烟测试(自动启动/关闭本地服务器,验证各平台接口)
|
tests/ pytest 目录(预留:TS 版 smoke 测试尚未移植)
|
||||||
source-simple-test.ts 简单 API 音源脚本加载器测试(离线)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> 分层依赖严格单向:`src` → `sdk`,`sdk` 内部自包含(不反向依赖 src)。源码统一为 TypeScript(import 一律写 `.ts` 后缀),由 Node ≥22.18 的原生类型剥离直接运行——**无编译产物**,改完即跑;tsc 仅用于类型检查(`pnpm typecheck`)。代码只使用可剥离的 TS 语法(无 enum/namespace/参数属性),新增代码也须遵守。
|
> 分层依赖单向:`routes` → `sdk`,`sdk` 内部自包含。全源码 `.py` 由 uv 管理的 CPython 3.12 直接运行,
|
||||||
|
> 无编译产物;`ruff` 负责风格与静态检查,`pyright` 负责类型检查。TS 版对照见 `master` 分支。
|
||||||
|
|
||||||
## 已知限制
|
## 已知限制
|
||||||
|
|
||||||
- **kg 取流**:匿名仅能获取部分免费资源,VIP/付费歌需 `/login/kg` 登录(扫码/短信/token 均可)后取流,成功响应解析见 `sdk/apis/kg.ts` 头注释。
|
- **kg 取流**:匿名仅能获取部分免费资源,VIP/付费歌需 `/login/kg` 登录(扫码/短信/token 均可)后取流,成功响应解析见 `app/sdk/apis/kg.py` 头注释。
|
||||||
- **kw 取流**:主通道 mobi.s 严格校验「返回 bitrate == 请求档位且为明文格式」,档位缺失时沿降级链换档(flac24bit→flac→320k→128k);web playUrl 回退通道匿名恒 128k。
|
- **kw 取流**:主通道 mobi.s 严格校验「返回 bitrate == 请求档位且为明文格式」,档位缺失时沿降级链换档(flac24bit→flac→320k→128k);web playUrl 回退通道匿名恒 128k。
|
||||||
- **mg 取流**:匿名 PQ(128k) 永远可用,但候选链会优先探测更高档位——多数曲目实际能匿名拿到 SQ/ZQ 真无损直链(上游同款 tone 路径重写 + 逐档探测);Z3D/I3D 加密候选经 `/stream/mg` 解密后同样可播。HQ/SQ 返回「登录后免费试听」时自动降级到可用的最高档。
|
- **mg 取流**:匿名 PQ(128k) 永远可用,但候选链会优先探测更高档位——多数曲目实际能匿名拿到 SQ/ZQ 真无损直链(上游同款 tone 路径重写 + 逐档探测);Z3D/I3D 加密候选经 `/stream/mg` 解密后同样可播。HQ/SQ 返回「登录后免费试听」时自动降级到可用的最高档。
|
||||||
- **apple 取流**:只有 preview 试听片段(约 30–90s,AAC/m4a),返回的 `type` 固定为 `preview`,不要当作完整曲。
|
- **apple 取流**:只有 preview 试听片段(约 30–90s,AAC/m4a),返回的 `type` 固定为 `preview`,不要当作完整曲。
|
||||||
- **bili 取流**:匿名最高 192k;flac/Hi-Res 需 `/login/bili/qr` 登录大会员账号。CDN 有约 1 小时的 `deadline` 签名,取流结果不宜缓存过久。
|
- **bili 取流**:匿名最高 192k;flac/Hi-Res 需 `/login/bili/qr` 登录大会员账号。CDN 有约 1 小时的 `deadline` 签名,取流结果不宜缓存过久。
|
||||||
- **5sing 歌单详情**:依赖 5sing 网页(`5sing.kugou.com/.../dj/...html`)解析曲目;2026-10-02 实测该站点网页层整体 502(搜索/取流/歌词等手机端 API 不受影响)。上游同样依赖该页面,待站点恢复即自动可用。
|
- **xm 取流**:签名依赖 `hdaa.shuzilm.cn` 的设备上报;请求层必须直连(不走系统代理,Node 版语义一致)。
|
||||||
|
- **5sing 歌词**:2026-10-23 实测匿名 `newget` 不返回 `dynamicWords`(搜索结果与多个关键词均如此,Node 版同样),拿不到时接口如实报错。
|
||||||
|
- **5sing 歌单详情**:依赖 5sing 网页(`5sing.kugou.com/.../dj/...html`)解析曲目;2026-10-02 曾整体 502,2026-10-23 实测已恢复。
|
||||||
- **link 解析**:`/parse/*` 走各平台网页链接解析(需上游有对应实现);未实现的平台返回 404。
|
- **link 解析**:`/parse/*` 走各平台网页链接解析(需上游有对应实现);未实现的平台返回 404。
|
||||||
- 平台接口随时可能变更,遇失效时可对照 `sdk/apis/` 内的接口文档调整。
|
- 平台接口随时可能变更,遇失效时可对照 `app/sdk/apis/` 内的接口文档调整。
|
||||||
+1
-1
@@ -9,4 +9,4 @@
|
|||||||
app/main.py FastAPI 应用入口
|
app/main.py FastAPI 应用入口
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "1.0.0"
|
__version__ = "2.0.0"
|
||||||
+3
-3
@@ -22,7 +22,7 @@ from fastapi.exceptions import RequestValidationError
|
|||||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||||
|
|
||||||
from app import config
|
from app import __version__, config
|
||||||
from app.http.api_error import ApiError
|
from app.http.api_error import ApiError
|
||||||
from app.http.auth_gate import auth_gate
|
from app.http.auth_gate import auth_gate
|
||||||
from app.http.rate_limit import make_rate_limiter
|
from app.http.rate_limit import make_rate_limiter
|
||||||
@@ -71,7 +71,7 @@ def create_app() -> FastAPI:
|
|||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title=API_TITLE,
|
title=API_TITLE,
|
||||||
description=API_DESCRIPTION,
|
description=API_DESCRIPTION,
|
||||||
version="1.0.0",
|
version=__version__,
|
||||||
lifespan=_lifespan,
|
lifespan=_lifespan,
|
||||||
openapi_url="/openapi.json",
|
openapi_url="/openapi.json",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
@@ -186,7 +186,7 @@ def _install_openapi(app: FastAPI) -> None:
|
|||||||
def custom_openapi():
|
def custom_openapi():
|
||||||
if app.openapi_schema:
|
if app.openapi_schema:
|
||||||
return app.openapi_schema
|
return app.openapi_schema
|
||||||
schema = get_openapi(title=API_TITLE, version="1.0.0", description=API_DESCRIPTION, routes=app.routes)
|
schema = get_openapi(title=API_TITLE, version=__version__, description=API_DESCRIPTION, routes=app.routes)
|
||||||
components = schema.setdefault("components", {}).setdefault("schemas", {})
|
components = schema.setdefault("components", {}).setdefault("schemas", {})
|
||||||
for name, definition in COMPONENT_SCHEMAS.items():
|
for name, definition in COMPONENT_SCHEMAS.items():
|
||||||
components.setdefault(name, definition)
|
components.setdefault(name, definition)
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "music-server"
|
name = "music-server"
|
||||||
version = "1.0.0"
|
version = "2.0.0"
|
||||||
description = "多平台音乐聚合服务器"
|
description = "多平台音乐聚合服务器"
|
||||||
requires-python = ">=3.12,<3.13"
|
requires-python = ">=3.12,<3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "music-server"
|
name = "music-server"
|
||||||
version = "1.0.0"
|
version = "2.0.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
|
|||||||
Reference in new issue
Block a user