读完本文,你的 Android 应用可以通过 suchmusic:// 拉起 SuchMusic, 在用户授权后获取曲风画像、酷狗账号令牌、歌曲直链,或者让 SuchMusic 代播。 适用版本:SuchMusic 1.5.0 及以上。

能力说明

共四类能力,按需申请,用户逐项授权,随时可以撤销。

scope 能拿到什么 是否需要每次确认
genre.read 用户曲风画像:topGenres[{tag, weight}]、sampleSize、updatedAt 否,已授权可静默
url.resolve 指定歌曲的可播放直链 否,已授权可静默
playback.control 让 SuchMusic 播放、暂停或切歌 否,已授权可静默
account.token 酷狗账号 token、userId、nickname 是,每次都弹确认页
account.token 等价账号控制权, 因此不进入静默白名单,每次唤起都会要求用户在确认页上明确同意。 这是刻意的设计,不要指望它被静默续期。

接入前准备

接入采用「包名 + 签名指纹」白名单,每个应用都要经过审核。按下面四步拿到 client_id。

  1. 注册开放平台账户。用邮箱接收验证码完成验证后即可进入控制台。
  2. 在控制台点「新建应用」,填写应用名、包名、签名指纹、用途说明,并勾选需要的能力。 包名必须与安装包一致;签名指纹是发布签名的 SHA-256,64 位小写十六进制。
  3. 导出签名指纹(建议直接用发布包):
    apksigner verify --print-certs app-release.apk
    # 取 Signer #1 certificate SHA-256 digest,去掉冒号并转成小写
  4. 提交后平台分配 client_id(形如 sm_1a2b3c4d5e6f7a8b)。 审核通过后应用进入公开清单,此时才能真正被唤起。
调试签名与发布签名的指纹不同, 请提交发布签名。换签名或换包名都要重新提交审核, 未通过审核的 client_id 一律返回 client_mismatch。

唤醒协议

用自定义 scheme 唤起 SuchMusic 的授权页。

suchmusic://oauth/authorize?client_id={client_id}&package={packageName}&scopes={scopes}&state={state}&silent=1
参数 必填 说明
client_id 是 平台在审核通过后分配的标识
package 是 你的应用包名,必须与登记信息一致
scopes 是 逗号分隔的能力列表,重复项会自动去重
state 是 1 到 128 个字符,会原样回传,用于防止跨请求混淆
silent 否 传 1 表示请求静默续期,是否真的静默由 SuchMusic 判定
val uri = Uri.Builder()
    .scheme("suchmusic")
    .authority("oauth")
    .path("/authorize")
    .appendQueryParameter("client_id", "sm_1a2b3c4d5e6f7a8b")
    .appendQueryParameter("package", packageName)
    .appendQueryParameter("scopes", "genre.read,url.resolve")
    .appendQueryParameter("state", UUID.randomUUID().toString())
    .appendQueryParameter("silent", "1")   // 可选,请求静默续期
    .build()

startActivity(Intent(Intent.ACTION_VIEW, uri))

授权流程

  1. 你的应用用上面的 deep link 唤起 SuchMusic。
  2. SuchMusic 校验 client_id、包名与签名指纹。任一项不匹配就返回 client_mismatch,并且不写入任何授权记录。
  3. 用户看到确认页,逐项勾选要授予的能力。只突出新增或敏感的 scope。
  4. 用户点「允许」后,SuchMusic 用 explicit intent (com.lightningteam.suchmusic.CLIENT_CALLBACK) 把 code、port、state、scopes 投递给你的回调 Activity。 用户点「拒绝」则返回 access_denied,你的回调页不会被拉起。
  5. 你的应用拿 code 向本机回环端口换取数据。
  6. code 60 秒过期,只能使用一次;换取成功后服务立即关闭。

静默续期:再次唤起时带 silent=1,同时满足四条才会跳过确认页。

  1. 该 client_id 已有授权记录。
  2. 请求的 scopes 是已授权 scopes 的子集。
  3. 请求不含 account.token。
  4. 距上次静默调用不短于 5 秒。

静默完成时 SuchMusic 会发一条系统通知告知用户,例如「某某应用读取了你的曲风画像」。 这条通知是用户知情权的一部分,不要试图绕过。

数据交换

数据走本机回环 HTTP,不经过网络。端口由系统随机分配,在回调 intent 的 port 里给出。

POST http://127.0.0.1:{port}/v1/exchange
Authorization: Bearer {code}
Content-Type: application/json

请求体可以为空,此时返回本枚 code 覆盖的全部 scope;也可以按需指定:

{
  "request": {
    "url.resolve": {
      "song": {
        "id": "123456",
        "source": "netease",
        "name": "东风破",
        "artist": "周杰伦"
      }
    },
    "playback.control": {
      "action": "play",
      "song": { "id": "123456", "source": "netease", "name": "东风破" }
    },
    "account.token": { "platform": "lite" }
  }
}

成功响应:

{
  "client_id": "sm_1a2b3c4d5e6f7a8b",
  "scopes": ["genre.read", "url.resolve"],
  "issued_at": 1731600000,
  "data": {
    "genre.read": {
      "topGenres": [{ "tag": "流行", "weight": 0.42 }],
      "sampleSize": 128,
      "updatedAt": 1731599000
    },
    "url.resolve": {
      "url": "https://...",
      "source": "netease",
      "songId": "123456",
      "resolvedAt": 1731600000
    }
  }
}
  • 服务只在 127.0.0.1 监听,端口随机,不要写死。
  • 一次 code 只能交换一次,成功即关服;60 秒不用自动关服。
  • 请求体上限 32KB。
  • 未经过前台确认的 account.token 会返回 403 scope_not_granted。

回调声明

SuchMusic 不用 scheme 回跳,避免被抢注劫持,而是用 explicit intent 把结果投递给你的包。 因此你必须在 AndroidManifest.xml 里声明回调 Activity:

<activity
    android:name=".SuchMusicCallbackActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="com.lightningteam.suchmusic.CLIENT_CALLBACK" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

在回调 Activity 里取参数并立刻换数据:

class SuchMusicCallbackActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val code = intent.getStringExtra("code") ?: return finish()
        val port = intent.getIntExtra("port", 0)
        val state = intent.getStringExtra("state")
        val scopes = intent.getStringArrayExtra("scopes") ?: emptyArray()

        // 务必校验 state 与发起时一致
        // 拿到 code 后立刻换数据(code 60 秒过期,且只能使用一次)
        exchange(code, port, state)
        finish()
    }
}
没有声明这个 Activity 的应用会被判定为 no_callback_activity,授权不会落库。 这样做是为了避免「用户点了同意,数据却无处可去」。

错误码

error HTTP 含义 你该怎么做
invalid_request 400 深链参数缺失或格式错误 修正参数
client_mismatch — 未登记,或包名不符,或签名不符,或未安装 SuchMusic 检查登记信息
invalid_scope 400 scope 不合法 检查拼写
invalid_state 400 state 为空或超过 128 个字符 重新生成
rate_limited — 同一 client 每分钟超过 10 次 退避重试
access_denied — 用户拒绝授权 不要反复骚扰
no_callback_activity — 未声明回调 Activity 按「回调声明」一节声明
invalid_code / code_expired 401 code 无效、过期或已使用 重新走授权
scope_not_granted 403 请求了未授权的 scope,或令牌未经前台确认 重新申请该 scope
not_logged_in 400 用户未登录酷狗 提示用户先在 SuchMusic 里登录
resolve_failed 400 直链解析失败,通常是版权或参数问题 检查 song 字段
bad_request 400 请求体不是合法 JSON 对象 修正请求体

撤销授权

用户可以在 SuchMusic 的「设置 → 第三方授权」里撤销单个应用,或一次性全部撤销。撤销后:

  • 已授权的 scope 立即失效。
  • 该 client 未使用的 code 全部作废。
  • 进行中的回环服务立即关闭。
  • 你再调用必须重新走确认页。

不要假设授权是永久的,每次静默请求都可能收到需要前台确认的结果。

安全要求

  1. 不要把 code 或 token 写进日志、崩溃上报、埋点。
  2. 不要把换到的 token 上传到自己的服务器,除非你明确告知用户并取得同意。
  3. 不要伪造 package 参数,SuchMusic 会校验本机安装包与签名,伪造只会得到 client_mismatch。
  4. 不要轮询。需要最新数据时再唤起,频率受每分钟 10 次限制。
  5. 回调 Activity 保持 android:exported="true",其余敏感组件不要导出;回传的 state 必须校验。

调试建议

不需要写代码就能验证唤起是否成功,先用 adb 打一发:

adb shell am start -a android.intent.action.VIEW \
  -d "suchmusic://oauth/authorize?client_id={你的 client_id}&package={你的包名}&scopes=genre.read&state=test1"

联调清单:

  • 确认页出现,四项 scope 的文案与申请一致。
  • 点「拒绝」,你的回调页不应被拉起。
  • 点「允许」,回调页收到 code 与 port。
  • curl -X POST http://127.0.0.1:{port}/v1/exchange -H "Authorization: Bearer {code}" 返回 200。
  • 同一 code 再请求一次,应当返回 401。
  • 带 silent=1 再唤起,无确认页,收到系统通知。
  • 在设置里撤销授权,再带 silent=1 唤起,应重新弹确认页。

常见问题

现象 原因
唤起后无任何反应,或返回 client_mismatch SuchMusic 未安装;或 client_id/包名/签名指纹与登记信息不一致;或应用还没通过审核
用户点了允许,但应用没收到任何数据 回调 Activity 未声明 CLIENT_CALLBACK 的 intent-filter,或 android:exported 不为 true
换数据返回 401 code 超过 60 秒,或已经被使用过一次
换数据返回 403 scope_not_granted 请求了未授权的 scope,或 account.token 未经过前台确认
日志里频繁 rate_limited 同一 client 每分钟超过 10 次,改成按需唤起