Anni 音频仓库协议
版本信息
⚠️️ 在协议版本迈入
1.0之前,任何改动都有可能发生。
最后修改:2022 年 05 月 10 日 协议版本:0.5.0
定义
Anni 音频仓库(Anni Audio Library,以下简称音频仓库)是指实现了 Anni 音频仓库协议中定义 API 的服务端应用程序。其核心功能有三:
- 对用户身份的鉴定
- 对分享的权限认证及限制
- 向用户分发资源(包括音频和封面)
跨域约定
Anni 音频仓库协议约定所有请求的返回中都包含头部:
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Headers: Authorization
并建议带有:
Access-Control-Allow-Origin: *
各实现可以允许用户选择能够进行跨域请求的域名。
返回状态码
Anni 音频仓库协议使用了以下 HTTP 状态码。
| 状态码 | 含义 |
|---|---|
400 | 请求非法 |
200 | 请求成功 |
403 | Token 校验失败或权限不足 |
404 | 资源不存在 |
鉴权
对请求者的身份鉴定通过 JWT(JSON Web Token) 的形式进行(下文称作 Token)。本协议不限制签名的方式,主要针对 Claim 部分作出规定。各实现需要对请求者的身份进行鉴权,以判断是否有对应资源的权限。
协议规定了两种 Token:用户 Token 和分享 Token。用户 Token 的使用者为音频仓库的正式用户,部分用户拥有创建分享 Token 的权限;分享 Token 的使用者为任意用户,可以在分享 Token 的限定范围内获取 Anni 音频仓库中的资源。下文中除特殊说明外,限定的访问范围均为有用户 Token 和有对应资源访问权限的分享 Token。
Anni 音频仓库协议允许以下两种鉴权方式中的任意一种。客户端可以根据需要选用其中任意一种调用。
Authorization 头部
当使用 Authorization 头部时,客户端需要将合法的 JWT 携带于 Authorization 头部中,不需要增加 Bearer 前缀。
Anni 音频仓库能否正确处理带 Bearer 的请求属于未定义行为。
该方式对所有请求均有效。
?auth=
和 Authorization 类似,客户端也可以选择将 JWT 夹带在 URL 的 query 中。仅对 GET 请求可用。
用户身份鉴定
以下为用户 Token(User Token) 的 Claim 部分样例:
{
// Token 签发的时间
"iat": 1615788479,
// JWT 类型,必须为 user
"type": "user",
// 用户标识,注意不要包含用户信息,只要能标记用户即可
"user_id": "rua",
// 当 Token 支持分享时,会携带以下信息
"share": {
// 可选,用于标识 key
"key_id": "409e2d92-d2c5-4dff-81e6-fe832e1b06d0",
// secret 字段用于对分享 Token 签名
"secret": "c398313c-b086-47ce-9a91-edfa5c93bd73",
// 可选,表示允许签署的分享范围,省略则表示无限制
// 结构与 /albums 的返回相同
// 该字段若存在则必须为数组,其他情况均未定义。
"scope": [],
// @deprecated `scope` 的别名,不建议使用。
// 协议进入 1.0 后会将该字段删除。
"allowed": []
}
}
音频仓库信息
获取音频仓库信息的 API 定义为 GET /info,所有用户都可以请求。
返回结果如下例:
{
// 音频仓库的版本描述
// 用于客户端展示
"version": "Annil v0.1.0",
// 当前运行的 Annil 音频仓库协议版本
// 用于客户端比对其支持的能力
"protocol_version": "0.2.1",
// 音频仓库最近一次数据更新时间
// 用于客户端缓存 `/albums` 请求结果
"last_update": 1639631487
}
获取可用专辑
由于音频仓库整合了多个音频后端,因此继承了所有音频后端的可用音频数据。
获得可用专辑的 API 定义为 GET /albums,仅合法的用户 Token 可以正常请求。
响应内容包含当前仓库所有可用的专辑 album_id 列表,如下例:
// 以下 UUID 不代表实际内容
[
// KSLA-0155~9/2018-12-29
"4992994f-9ede-56c2-b614-83bf89840e10",
// TS-0003/2015-05-20
"c12705be-6f2d-5195-aa53-5325ce9b780a",
// KSLA-0178/2020-12-16
"896a00d4-f93d-57b2-aa0a-52b1cf521c63"
]
Anni 音频仓库协议建议音频仓库在响应中携带 ETag 头部,以便客户端缓存。
客户端实现指导
Anni 音频仓库建议,客户端每次启动时携带 If-None-Match: <previous-etag-value> 头部请求 GET /albums ,并判断:
- 若响应状态码为
304,则无需更新; - 若响应状态码为
200,则根据返回结果自动更新索引,并向用户提示。
特别地,第一次启动时客户端无需携带 If-None-Match: * 头部,直接请求 GET /albums 并自动更新索引,并向用户提示。
资源分享
Project Anni 的设计目的是 Self Host 使用,因此存在验证用户身份的环节,非认证用户无法访问仓库中的资源。但是对部分资源的分享需求是真实存在的。因此 Anni 音频仓库草案提出了细粒度的资源共享方案。
资源共享通过 JWT 的方式进行鉴权。
签署 Token
用户 Token 的 Claim 部分定义了 share 字段,用于存储资源分享所需的相关凭据。
客户端需以 share.secret 字段作为 key,对分享 Token 进行 HS256 签名。
分享 Token(Share Token)
Header 部分如下例:
{
"typ": "JWT",
"alg": "HS256",
// 分享 Key 的 ID,当用户 Token 中携带时须包括在分享 Token 内
"kid": "409e2d92-d2c5-4dff-81e6-fe832e1b06d0"
}
Claim 部分如下例:
{
// 该 Token 的签发时间,即分享的开始时间
"iat": 1615788479,
// 该 Token 的失效时间,即分享的结束时间
// 服务端有权拒绝向不存在该字段的 Token 提供资源,播放器实现需考虑到这一点并相应地对用户作出提示
"exp": 1615874879,
// JWT 类型,必须为 share
"type": "share",
// 分享的音频,以 JSON Object 的形式表示
// 作为 key 的字符串表示该分享允许访问者访问的专辑 album_id
// value 表示该分享允许访问者访问的唱片号(disc_id)和音轨号(track_id)
"audios": {
// KSLA-0155~9/2018-12-29
"4992994f-9ede-56c2-b614-83bf89840e10": {
// Disc 2(KSLA-0156), Track 2
"2": [2],
// Disc 3(KSLA-0157), Track 3
"3": [3]
},
// KSLA-0178/2020-12-16
"896a00d4-f93d-57b2-aa0a-52b1cf521c63": {
"1": [1, 2, 3]
}
}
}
资源分发
音频仓库接收用户的请求,从可用的音频后端获取数据,并将获得的数据转发给用户。
由于 Anni 约定中的各专辑均带有封面信息,因此资源分发同时支持音频和封面。
音频分发
音频分发基于音频所在 Disc 的 album_id、 disc_id 和 track_id。用户获取音频的前提是其提供的 Token 允许其访问对应的资源。
获得音频的 API 定义为 GET /{album_id}/{disc_id}/{track_id}。通过 HEAD /{album_id}/{disc_id}/{track_id} 可以只获取 Header 信息。
音频偏好
考虑到客户端的不同网络场景和带宽因素,以下的参数可以向音频仓库表明客户端的格式偏好。
| 参数名 | 说明 | 可选项 |
|---|---|---|
quality | 客户端期望的音频质量。 | low、medium、high、lossless |
需要注意的是,客户端传递的参数仅为客户端偏好,对音频仓库仅有指导意义,不代表实际结果。
音频仓库在对参数进行处理时,应以枚举而非字符串拼接的形式进行,以防止命令注入。
请求范围
为了达到更好的播放效果,Annil 实现可以选择实现 HTTP 请求范围。
当音频仓库支持某一音频资源的单一范围请求时,HEAD 和 GET 请求的返回中必须带有 Accept-Ranges: bytes。当该头部不存在,或为 none 时,表示音频仓库不支持该音频文件的单一范围请求。
Anni 音频仓库协议暂不支持多重范围请求。
当客户端向音频仓库通过 GET 请求某一范围时,下文所述的返回头部不保证可用。
返回头部
当成功获取音频时,返回的头部会携带如下额外信息:
| Key | 含义 |
|---|---|
Content-Type | 音频的 MIME Type,根据情况可能为 audio/flac、audio/mp3 等 |
X-Origin-Type | 源文件的 MIME Type,当与 Content-Type 不同时表明音频经过了转码 |
X-Origin-Size | 源文件的大小,单位为字节(Byte) |
X-Duration-Seconds | 音频的长度,单位为秒 |
X-Audio-Quality | 音频的实际音质,可选值为 low、medium、high、lossless |
封面分发
封面分发基于音频所在专辑的 album_id 和可选的 disc_id。获取封面无需 Token。
获得封面的 API 定义为 GET /{album_id}/{disc_id}/cover 或 GET /{album_id}/cover,默认 disc_id 为 1。
返回头部
当成功获取封面时,返回的头部会携带如下额外信息:
| Key | 含义 |
|---|---|
Content-Type | 图片的 MIME Type,当前定义中只能是 image/jpeg |
管理接口
Annil 定义了如下的管理接口,供音频仓库的管理员使用。
以下所有 API 均为可选,对接时须确认各接口是否可用,以及公网可访问性等细节。接口仅包含最小定义,在满足下文定义行为的前提下,各实现可以自行添加部分参数以实现功能扩展。
访问管理接口时需要包含 Authorization 头部,该头部的获取方式不在本文规定范围之内。
管理接口不应允许跨域访问。
签署用户 Token
签署用户 Token 的 API 定义为 POST /admin/sign。
{
"user_id": "rua",
"share": true
}
返回结果为签名后的用户 Token。
资源热更新
热更新 Annil 中各个 Provider 中资源的 API 定义为 POST /admin/reload。
热更新的过程是同步的,更新结束后请求才会返回。