{
  "name": "music-direct-link-worker",
  "version": "1.0.0",
  "platforms": [
    "netease",
    "qq"
  ],
  "auth": {
    "how": "Authorization: Bearer <ADMIN_TOKEN>  /  X-Admin-Token  /  ?token=",
    "protected": [
      "/api/token",
      "/api/refresh"
    ],
    "public": [
      "/api/song",
      "/api/url",
      "/api/lyric",
      "/api/playlist",
      "/api/search",
      "/healthz",
      "/openapi.json"
    ]
  },
  "openapi": "/openapi.json",
  "endpoints": {
    "POST /api/token?platform=netease|qq": {
      "auth": true,
      "desc": "上传 token, 用于首次部署或登录态失效后重新导入",
      "body": {
        "cookie": "cookie 串 / JSON 对象 / cookies.txt 全文 (也接受铺平 {MUSIC_U, uin, qm_keyst}...)",
        "clientUserId": "可选, 网易云 uid"
      },
      "example": "curl -X POST \"https://<worker>/api/token?platform=netease\" \\\n  -H \"Authorization: Bearer $ADMIN_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"MUSIC_U\":\"<802字符>\"}'"
    },
    "GET /api/token": {
      "auth": true,
      "desc": "查看各平台 token 配置状态(长度脱敏, 不回显原文)"
    },
    "POST /api/refresh?platform=netease|qq": {
      "auth": true,
      "desc": "手动触发刷新, 响应里带 tokenValid 表示当前 token 是否正常",
      "body": {
        "alsoMusickey": "true 时 QQ 同时刷 musickey"
      }
    },
    "GET /api/song?platform=..&id=..": {
      "auth": false,
      "desc": "音乐基础信息: 曲名 / 作者 / 专辑 / 封面 / 音质列表"
    },
    "GET /api/url?platform=..&id=..&quality=..": {
      "auth": false,
      "desc": "取直链。默认 quality=netease:exhigh / qq:flac(无损需显式传 lossless|hires|flac)",
      "note": "★ 网易云默认给 exhigh(320k 免费档)而不是 lossless —— 无损是 VIP 档, 匿名态或非会员必然拿不到, 当默认会让人误以为接口坏了。★ 失败时看 authIssue 字段: true 表示登录态失效/无权限(与音质无关, 需重传 token), false 且 retryable=true 表示限流/风控, 退避后重试。songCode 是上游歌曲级 code(-110=登录态)。"
    },
    "GET /api/lyric?platform=..&id=..": {
      "auth": false,
      "desc": "歌词 + 逐字歌词。only=lrc(默认) / yrc(网易云逐字) / qrc(QQ逐字) / both",
      "note": "id 语义: 网易云=歌曲id, QQ=songMID。★ 网易云: 上游一次把 lrc 和 yrc 都发回来了, only 只是本地裁剪, 不会多花请求; 逐字歌词只有部分歌曲有(众包上传), hasWordByWord=false 就退回普通 lrc。★ QQ: 逐字歌词(QRC)已在本地解密(逆 QQMusic_Lyric.dll 的三轮 DES + zlib), wordByWordLyric 里直接给明文: meta(歌名/歌手/专辑) + lines[].words[] 逐字时间轴; encrypted:true 表示上游是密文, decrypted:true 表示已解开。默认只给可用的 LRC。"
    },
    "GET /api/playlist?platform=..&id=..&limit=1000": {
      "auth": false,
      "desc": "歌单详情 + 曲目列表。id: 网易云歌单id / QQ disstid。limit 上限 1000",
      "note": "两家平台的 offset 参数在服务端都是废的(永远返回最前 N 首), 所以超长歌单目前拿不全; 响应用 truncated 标明"
    },
    "GET /api/search?platform=..&q=..&type=..": {
      "auth": false,
      "desc": "搜索。网易云 type: song / album / artist / playlist / user / mv / lyric / djradio ; QQ type: all / singer / album / songlist / mv / lyric / user",
      "note": "★ 网易云 type 码反直觉: 歌单是 1000、歌手是 100。★ QQ 搜索必须带 remoteplace(Worker 内部已固定), 否则 code=0 但全部分类为空。"
    }
  },
  "searchTypes": {
    "netease": [
      {
        "name": "song",
        "code": 1,
        "returns": "songs"
      },
      {
        "name": "album",
        "code": 10,
        "returns": "albums"
      },
      {
        "name": "artist",
        "code": 100,
        "returns": "artists"
      },
      {
        "name": "playlist",
        "code": 1000,
        "returns": "playlists"
      },
      {
        "name": "user",
        "code": 1002,
        "returns": "userprofiles"
      },
      {
        "name": "mv",
        "code": 1004,
        "returns": "mvs"
      },
      {
        "name": "lyric",
        "code": 1006,
        "returns": "songs"
      },
      {
        "name": "djradio",
        "code": 1009,
        "returns": "djRadios"
      }
    ],
    "qq": [
      {
        "name": "all",
        "code": 0,
        "returns": "song/zhida"
      },
      {
        "name": "singer",
        "code": 1,
        "returns": "singer"
      },
      {
        "name": "album",
        "code": 2,
        "returns": "album"
      },
      {
        "name": "songlist",
        "code": 3,
        "returns": "songlist"
      },
      {
        "name": "mv",
        "code": 4,
        "returns": "mv"
      },
      {
        "name": "lyric",
        "code": 7,
        "returns": "song"
      },
      {
        "name": "user",
        "code": 8,
        "returns": "user"
      }
    ]
  },
  "quality": {
    "netease": [
      "64acc(流畅,free)",
      "standard(标准,free)",
      "higher(标准,free)",
      "exhigh(极高,free)",
      "lossless(无损,vip)",
      "hires(高解析度无损,vip)",
      "dolby(杜比全景声,svip)",
      "jyeffect(高清臻音,vip)",
      "jymaster(超清母带,svip)",
      "sky(沉浸环绕声,svip)",
      "vivid(臻音全景声,svip)"
    ],
    "qq": [
      "aac48(48K AAC)",
      "aac96(96K AAC)",
      "aac192(192K AAC)",
      "mp3(128K MP3)",
      "mp3320(320K MP3)",
      "flac(无损 FLAC)",
      "ogg(OGG)"
    ]
  },
  "notes": [
    "网易云续期: 服务端不校验 refreshToken 的值, 传任意非空值即可换新 MUSIC_U, 可无限重复",
    "网易云 MUSIC_U 失效是静默降级(account=null), 不会报错; 此时所有取流返回 code=-110",
    "QQ refresh_token 每次刷新都会轮换, 必须立刻落库; 但可连续刷新(2026-10-03 实测 CF 出口连刷 3 次均 code=0)",
    "QQ musickey 由 QQRefreshToken 顺带下发, 长度 101->162 后稳定, 无需 alsoMusickey",
    "定时任务每天跑一次, 见 wrangler.toml 的 crons"
  ],
  "docs": "README.md"
}