Anni 音频仓库协议

版本信息

⚠️️ 在协议版本迈入 1.0 之前,任何改动都有可能发生。

最后修改:2022 年 05 月 10 日 协议版本:0.5.0

定义

Anni 音频仓库(Anni Audio Library,以下简称音频仓库)是指实现了 Anni 音频仓库协议中定义 API 的服务端应用程序。其核心功能有三:

  1. 对用户身份的鉴定
  2. 对分享的权限认证及限制
  3. 向用户分发资源(包括音频和封面)

跨域约定

Anni 音频仓库协议约定所有请求的返回中都包含头部:

Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Headers: Authorization

建议带有:

Access-Control-Allow-Origin: *

各实现可以允许用户选择能够进行跨域请求的域名。

返回状态码

Anni 音频仓库协议使用了以下 HTTP 状态码。

状态码含义
400请求非法
200请求成功
403Token 校验失败或权限不足
404资源不存在

鉴权

对请求者的身份鉴定通过 JWTJSON 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 夹带在 URLquery 中。仅对 GET 请求可用。

用户身份鉴定

以下为用户 TokenUser 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

用户 TokenClaim 部分定义了 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 约定中的各专辑均带有封面信息,因此资源分发同时支持音频和封面。

音频分发

音频分发基于音频所在 Discalbum_iddisc_idtrack_id。用户获取音频的前提是其提供的 Token 允许其访问对应的资源。

获得音频的 API 定义为 GET /{album_id}/{disc_id}/{track_id}。通过 HEAD /{album_id}/{disc_id}/{track_id} 可以只获取 Header 信息。

音频偏好

考虑到客户端的不同网络场景和带宽因素,以下的参数可以向音频仓库表明客户端的格式偏好。

参数名说明可选项
quality客户端期望的音频质量。lowmediumhighlossless

需要注意的是,客户端传递的参数仅为客户端偏好,对音频仓库仅有指导意义,不代表实际结果。

音频仓库在对参数进行处理时,应以枚举而非字符串拼接的形式进行,以防止命令注入。

请求范围

为了达到更好的播放效果,Annil 实现可以选择实现 HTTP 请求范围

当音频仓库支持某一音频资源的单一范围请求时,HEADGET 请求的返回中必须带有 Accept-Ranges: bytes。当该头部不存在,或为 none 时,表示音频仓库不支持该音频文件的单一范围请求。

Anni 音频仓库协议暂不支持多重范围请求。

当客户端向音频仓库通过 GET 请求某一范围时,下文所述的返回头部不保证可用

返回头部

当成功获取音频时,返回的头部会携带如下额外信息:

Key含义
Content-Type音频的 MIME Type,根据情况可能为 audio/flacaudio/mp3
X-Origin-Type源文件的 MIME Type,当与 Content-Type 不同时表明音频经过了转码
X-Origin-Size源文件的大小,单位为字节(Byte
X-Duration-Seconds音频的长度,单位为秒
X-Audio-Quality音频的实际音质,可选值为 lowmediumhighlossless

封面分发

封面分发基于音频所在专辑的 album_id 和可选的 disc_id。获取封面无需 Token

获得封面的 API 定义为 GET /{album_id}/{disc_id}/coverGET /{album_id}/cover,默认 disc_id1

返回头部

当成功获取封面时,返回的头部会携带如下额外信息:

Key含义
Content-Type图片的 MIME Type,当前定义中只能是 image/jpeg

管理接口

Annil 定义了如下的管理接口,供音频仓库的管理员使用。

以下所有 API 均为可选,对接时须确认各接口是否可用,以及公网可访问性等细节。接口仅包含最小定义,在满足下文定义行为的前提下,各实现可以自行添加部分参数以实现功能扩展。

访问管理接口时需要包含 Authorization 头部,该头部的获取方式不在本文规定范围之内。

管理接口不应允许跨域访问。

签署用户 Token

签署用户 TokenAPI 定义为 POST /admin/sign

{
  "user_id": "rua",
  "share": true
}

返回结果为签名后的用户 Token

资源热更新

热更新 Annil 中各个 Provider 中资源的 API 定义为 POST /admin/reload

热更新的过程是同步的,更新结束后请求才会返回。