一个最后提交停在 2021 年的 Go 下载工具,网页仍能访问相应内容,CLI 却在受保护音频接口上返回 403。
gcoredl-macos 基于这个旧项目改造,目标是把当前账号有权访问的机核有声书音频保存成带封面和 ID3 标签的 MP3。范围限定在 macOS 命令行,不做 GUI 和多用户系统。
改造过程中,403 解决之后又遇到了 401:浏览器用同一份令牌返回 200,CLI 反复重新登录仍然失败。最终定位到的差异,是旧隐藏输入路径把 199 字符截成了 128 字符。
本文按 API、凭证、文件处理和发布四个环节复盘。结果依据 2026-10-29 的项目记录,文末单独列出当前版本还没完成的验证。
保留 Go,把需要变化的部分拆开
原项目已有专辑模型和 ID3 功能,改造继续使用 Go,保留仓库历史、MIT License 与原作者版权声明。原先集中在 main.go 的逻辑,拆到命令、API、凭证、下载和标签层。
保留已有实现,减少了重做范围,也方便比较接口变化。引入 macOS Keychain 后,凭证层通过 cgo 调用 Security.framework,项目也因此收缩为 macOS 专用。这个选择适合当前 CLI 需求,项目没有进行 Go 与 Swift 的成本对比测试。
403:授权接口已经不能直接当音频地址
旧实现用 -auth 传入 auth_token,携带 Cookie 请求受保护音频接口,再跟随重定向。当前网页采用的流程已经变化:先请求 JSON 授权结果,取得短时音频 URL,再发起媒体下载。
记录中的请求形态如下,<appToken> 是占位符:
ini
GET /gapi/v1/medias/protected/radios/{id}?format=json
Authorization: Token token=<appToken>
这一步的返回值是授权结果,下载器需要解析其中的媒体 URL。新实现只在机核 API 请求中携带登录令牌,不把它继续发送到音频存储地址。受保护章节重试时,也重新获取媒体 URL,避免反复请求已经过期的地址。
章节解析则按关系数据确定顺序:先对 included 中的实体建索引,再遍历 relationships.published-audiobooks.data,不能直接拿实体数组的位置当章节编号。缺少节目或媒体对象时,再回退到节目接口查询。对应测试把实体数组与章节顺序反过来,检查输出是否仍然正确。
这些修改解决了协议解析问题,但真实下载只成功了一个公开章节,另外三个受保护章节仍返回 401。
401:客户端实际拿到的令牌不完整
用户重复复制并保存令牌后,CLI 的 auth-check 仍然失败。浏览器却能用同一份令牌得到 200,而且在不附带额外 Cookie 的情况下,只用 Authorization 头也能成功。
排查试过令牌解码、Cookie jar、显式 Cookie、Referer 和浏览器 User-Agent,都没有解决 401。随后对隐藏输入路径做独立实验,得到了下面的对比:
| 检查点 | 项目记录中的结果 |
|---|---|
| 浏览器使用的令牌 | 长度为 199,请求返回 200 |
| 独立测试输入 | 199 字符 |
经旧 /usr/bin/security -w 路径保存后回读 |
只剩 128 字符 |
客户端从未拿到完整令牌,继续调整请求头也无法补回丢失的数据。
修复后的流程用 term.ReadPassword 读取两次隐藏输入,比较两次结果,再写入钥匙串。写入后立即回读校验,并用新的 service 名称隔离历史截断值。
这个实验针对的是项目当时使用的输入路径,不能推广成所有系统版本和所有 security 用法都有相同问题。排查中值得复用的是数据比较方法:检查复制、读取、存储和回读后的值是否保持一致。敏感数据可使用独立测试值诊断,不需要把真实令牌输出到日志。
修复后,本地目录出现了四个最终 MP3 和封面,包括此前失败的三个受保护章节。这个结果发生在后续钥匙串 API 迁移之前,不能代替新版本的真实账号复测。
文件落盘:最终文件名要放在最后一步
只收到媒体响应,还没有完成一章音频的处理。下载器按下面的顺序执行:
媒体下载 → 临时文件 → 必要时转码 → ID3 与封面 → 最终改名
MP3 先写 .part;非 MP3 先写 .source.part,调用 ffmpeg 转换后再进入标签处理。下载、转换和标签都成功,才执行最终改名。重跑时跳过已经存在的最终文件,避免把中断产生的临时文件当成完成结果。
ID3 包括标题、专辑、作者、曲目号和封面,文件名另外处理路径字符与长度。已经是 MP3 的源文件不重复转码。
重试范围也需要明确:429 和部分服务端错误会重试,401、403、凭证缺失和未知媒体类型不会盲目重试。这里是章节级重试,没有 HTTP Range 断点续传。已有最终文件只按路径跳过,不检查大小和音频完整性。
发布:README 和 CI 也需要维护
README 最初写进了 199 与 128 字符的排障细节,反馈指出这些内容干扰了正常操作说明。随后文档收回到登录、授权检查和退出,故障经过留给复盘。
另一个问题出在 CI:任务整体成功,日志却出现红色 Error 标记,实际内容是旧 SecKeychain* 接口的弃用警告。后续凭证实现迁移到 SecItemUpdate、SecItemAdd、SecItemCopyMatching 和 SecItemDelete,并发布 v2.1.1。
素材记录显示,该版本完成竞态测试、Vet 和 Intel、ARM64 构建,Release 生成两种架构的程序与 SHA-256 校验表。但这次改动触及凭证读写,构建通过不能代替真实钥匙串与账号验证。
已有结果与待补验证
项目记录中的自动测试覆盖章节顺序、数据回退、授权请求、错误脱敏、下载重试、标签和跳过行为;修复凭证截断后也有四个 MP3 与封面的实际产物,系统识别到了 ID3 v2.4。
仍有几项没有完成:
v2.1.1迁移钥匙串 API 后,真实账号重新登录、授权与整张专辑下载。- Apple Silicon 真机登录与下载,目前只有 ARM64 构建验证。
- 第三方播放器完整试听、标签和封面显示,以及真实非 MP3 样本转换。
这些记录不支持"所有专辑兼容"或"长期稳定"的结论。工具依赖网页接口,后续接口变化仍可能导致失效。
这次改造中,403 指向协议流程,401 指向凭证输入,最终文件又依赖处理顺序。每个环节都有自己的验证方式;修复一个环节后,仍需重新走到最终结果。尤其当凭证实现再次变化时,历史下载成功与当前版本构建成功,需要用新的一次真实使用验证连接起来。
本项目为非官方工具。请仅处理本人账号有权访问、且依法可以下载和保存的内容,并遵守平台条款。不得用于绕过付费或技术保护措施,也不得未经授权分享音频、账号凭证或下载链接。