Skip to content

Releases: AMDL-Web/apple-music-downloader

Release v2.0.0-rc.5

Release v2.0.0-rc.5 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 01 Nov 00:54
99c62ac

v2.0.0-rc.5

2.0.0 的第五个候选版本。它是 GitHub 预发布:安装脚本默认不装它,Docker 镜像的 latest 和 2.0 标签也不会指向它。想试的话,先备份数据库,读完 升级到 2.0,别直接升级正在用的安装。

装命令行和服务端:curl -fsSL https://raw.githubusercontent.com/AMDL-Web/apple-music-downloader/dev/deploy/install/install.sh | sh -s -- --version v2.0.0-rc.5

相对 rc.4 没有破坏性变更。

升级前看一眼

  • 第一次启动会跑两个新迁移,现有数据全部保留:job_items.download_bytes(现有行为 0),以及 jobs.queue_order(按创建顺序给现有任务编号,排队任务保持原来的先后)。
  • 迁移过的数据库,rc.4 及更早的版本打不开(桌面端本机模式的数据库也一样)。要退回旧版,只能用升级前备份的 amdl.db。如果在这一版里新建或改过 webhook,hooks.yaml 也要换回备份,或者删掉其中的 preset、method、body 键,否则旧版服务端不认识这些键,起不来。
  • 从源码克隆运行、并且在仓库里的 configs/hooks.yaml 写过 hooks 的:这个文件改名为模板 configs/hooks.example.yaml,configs/hooks.yaml 不再入库。拉取前先把文件拷走,git checkout -- configs/hooks.yaml,拉取后再拷回。Docker 卷里的 hooks.yaml 不受影响。

服务端

  • 下载队列可以调整运行顺序:新增 POST /api/v1/downloads/{job_id}/move,任务已不在排队时返回 409 job_not_queued。排队任务按保存下来的顺序开始,新任务和重试排在最后,重启时被中断的任务排在最前。排队中的任务带 queue_order,GET /api/v1/downloads?sort=queue_order 按运行顺序列出,每次移动记一条 job_moved 事件。
  • 解密密钥改为在音轨下载的同时预取,不再在解密阶段干等 wrapper 逐个返回。带宽不是瓶颈时整张专辑明显更快:一张 12 首 ALAC 的专辑从约 51–55 秒降到约 39–41 秒;受带宽限制的专辑变化不大。等密钥时状态显示为 waiting_decrypt。
  • hooks 可以通过 API 编辑:总开关,新建、修改、删除 webhook,发送测试。写入时就地改写 hooks.yaml,注释和其他条目保留,所以它所在的目录要可写。命令类 hook 只读,仍需在 hooks.yaml 里改。
  • webhook 新增预设:ntfy、Bark、Telegram 通知,Plex、Jellyfin、Emby 刷新媒体库,以及自定义请求。手写的 webhook 也能用新键 method(GET、POST、PUT)和 body(基于 payload 的模板);payload 的 job 新增 title 和 artist_name。
  • 搜索支持歌单,结果里新增 playlists 分组。
  • 每个条目报告已下载的字节数 download_bytes,下载途中就能算平均速度。
  • 修复:低内存模式下同一任务的音轨互相清掉断点目录,导致多一次下载重试。
  • 修复:音轨从保存进入打标签时进度倒退,速度和剩余时间估算因此重置。

桌面端

  • 队列可以拖动排序,也可以用 Alt(macOS 为 Option)加方向键,或任务菜单里的 Run next、Run last。
  • 搜索加入歌单,可按分组翻页。首页去掉了音质和覆盖开关:从搜索或专辑页下载时,只在真有选择时问一次(无损还是杜比全景声,跳过还是替换已有文件);粘贴链接直接按服务器设置下载。设置里打开 Skip files already downloaded 就总是跳过。
  • 把 Apple Music 链接拖进窗口任意位置即可下载。
  • 设置里可以开关和编辑 webhook,从预设新建时只填几项值。
  • 设置合并为一整页连续滚动;编解码器优先级去掉了箭头,改为拖进或拖出 Not used 一栏。
  • 专辑页和任务详情页统一布局;已结束的任务可以删除,已保存的文件留在磁盘上。
  • 下载中就显示平均速度;剩余时间在任务后半程不再停在「-」。
  • 队列默认打开 All;连接状态移到侧栏底部;托盘面板随内容变化立即贴合高度。

控制台

  • Queue 可以拖动排序,也可以用 Alt+方向键、Alt+Home/End,或行菜单里的 Move to top、Move to bottom;默认打开 All。每行末尾的 … 菜单可以看详情、重试或取消、复制链接,也能单独删除已结束的任务(文件保留在磁盘上)。
  • 下载前弹窗询问音质和已有文件的处理方式,取代原来的 Overwrite 开关。
  • 搜索支持歌单,Show more 单看一个分组并随滚动翻页。
  • Settings › Hooks 可以编辑 webhook,添加时可选预设。
  • 编解码器优先级改为在 In use 和 Not used 之间拖动。
  • 任务抽屉显示平均速度和剩余时间;排队任务不再显示 Resolving;进度数字变化时不再抖动。

终端界面、CLI 和 MCP

  • 终端界面重做:开场横幅显示服务器、wrapper 和磁盘状态,任务有 #1、#2 编号;新增 /queue、/quality、/copy,支持 NO_COLOR。
  • 终端界面也有平均速度、下载前的选择和分页搜索。
  • CLI 新增 jobs move、hooks presets、hooks switch、hooks set、hooks test、hooks delete;MCP 新增 move_job;搜索都支持歌单。

这个候选版本不含的

  • 桌面安装包仍未签名,每个系统第一次打开时会拦一次,怎么放行见文末的「Desktop app」一节和 安装文档。
  • Homebrew 和 Scoop 从 2.0.1 开始发布;这一版的 Release 附件里仍有渲染好的 amdl.rb 和 amdl.json。

致谢

@LYJW131、@claude

Desktop app

amdl-desktop-* are the desktop app for macOS (.dmg, Apple silicon arm64 or Intel amd64), Windows (-setup-*.exe) and Linux (.deb or .tar.gz). They are not signed yet, so each system asks once:

  • macOS: drag AMDL to Applications and open it. When macOS says it cannot check the app, open System Settings › Privacy & Security and choose Open Anyway, or run xattr -dr com.apple.quarantine /Applications/AMDL.app.
  • Windows: when SmartScreen stops the installer, choose More info › Run anyway.

docs/install.md has the details.

Release v2.0.0-rc.4

Release v2.0.0-rc.4 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 31 Oct 07:59
2aa13f4

v2.0.0-rc.4

2.0.0 的第四个候选版本。这是第一个附带桌面端安装包的候选版本,也就是说 2.0 会和桌面端一起发布。它是 GitHub 预发布:安装脚本默认不装它,Docker 镜像的 latest 和 2.0 标签也不会指向它。想试的话,先备份数据库,读完 升级到 2.0,别直接升级正在用的安装。

装命令行和服务端:curl -fsSL https://raw.githubusercontent.com/AMDL-Web/apple-music-downloader/dev/deploy/install/install.sh | sh -s -- --version v2.0.0-rc.4

相对 rc.3 没有破坏性变更。

桌面端

  • Release 附件里的 amdl-desktop-* 就是桌面端,覆盖 macOS(Apple 芯片和 Intel 各一个 .dmg)、Windows(安装程序)和 Linux(.deb 和 .tar.gz)。
  • 第一次打开时选择下载在哪里跑:局域网里的一台服务器,或者这台电脑本身。选这台电脑的话,设置页会检查并安装需要的 ffmpeg 和 wrapper。安装包本身不带 ffmpeg 和 QEMU。
  • 安装包暂时没有签名,每个系统第一次打开时会拦一次。怎么放行见文末的「Desktop app」一节和 安装文档。

其他改动

  • 控制台在手机上的底栏改成五格:Overview、Queue、新建下载、Settings、Logs,新建下载的加号在正中间。Logs 单独占一格,不用再从 Settings 进。

这个候选版本不含的

  • Homebrew 和 Scoop 从 2.0.1 开始发布;这一版的 Release 附件里仍有渲染好的 amdl.rb 和 amdl.json。

致谢

@LYJW131、@claude

Desktop app

amdl-desktop-* are the desktop app for macOS (.dmg, Apple silicon arm64 or Intel amd64), Windows (-setup-*.exe) and Linux (.deb or .tar.gz). They are not signed yet, so each system asks once:

  • macOS: drag AMDL to Applications and open it. When macOS says it cannot check the app, open System Settings › Privacy & Security and choose Open Anyway, or run xattr -dr com.apple.quarantine /Applications/AMDL.app.
  • Windows: when SmartScreen stops the installer, choose More info › Run anyway.

docs/install.md has the details.

Release v2.0.0-rc.3

Release v2.0.0-rc.3 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 31 Oct 07:07
872f85f

v2.0.0-rc.3

2.0.0 的第三个候选版本。rc.2 之后的主要变化:重复下载已有的专辑不再卡住,wrapper 的 media-user-token 由后端自己读取,新增查询专辑曲目的接口。它是 GitHub 预发布:安装脚本默认不装它,Docker 镜像的 latest 和 2.0 标签也不会指向它。想试的话,先备份数据库,读完 升级到 2.0,别直接升级正在用的安装。

装这一版:curl -fsSL https://raw.githubusercontent.com/AMDL-Web/apple-music-downloader/dev/deploy/install/install.sh | sh -s -- --version v2.0.0-rc.3

相对 rc.2 没有破坏性变更。

新功能

  • 后端直接读 wrapper 自己维护的 media-user-token(MUSIC_TOKEN 文件),每次用时现读,读不到才用 catalog.media_user_token。用 amdl wrapper install 装在本机的 wrapper 自动读取;容器部署用新配置键 wrapper.data_dir 或环境变量 AMDL_WRAPPER_DATA_DIR 指向挂载的 wrapper 数据目录。目前只支持一个 wrapper 账号,wrapper-manager 也只读一个。
  • GET /api/v1/albums/{album_id}:返回专辑信息和按碟号、曲号排好的曲目。命令行是 amdl albums get,MCP 是 get_album。

主要修复

  • 重复下载已经下过的专辑:已在磁盘上的曲目先判定跳过,不再去取封面和歌词。同时把发往 wrapper 的并发连接限制在 wrapper-lite 能同时处理的数量以内,原来一次开十几个连接,部分曲目会卡约 30 秒才超时重试。实测 12 首的专辑重复提交,3 秒内全部跳过。
  • 进度条各阶段的权重按实测耗时重新分配:下载约占 77%,解密约 9%,原来下载完成时进度条才走到 60%。
  • 本机 wrapper 的系统镜像改为多连接分段下载,支持断点续传,下载完校验 sha256。原来单连接只有几十 KB/s。
  • 控制台:任务进度更易读,替换草稿后可以撤销,任务完成时弹出提示;清掉一批没再用到的样式。

这个候选版本不含的

  • 桌面端会和 2.0.0 一起发布,这个候选版本还没有附带它的安装包。
  • Homebrew 和 Scoop 从 2.0.1 开始发布;这一版的 Release 附件里仍有渲染好的 amdl.rb 和 amdl.json。

致谢

@LYJW131、@claude

Release v2.0.0-rc.2

Release v2.0.0-rc.2 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 31 Oct 00:02
d2f2b3a

v2.0.0-rc.2

2.0.0 的第二个候选版本。rc.1 之后,新装的 Mac 用户、每天用控制台的人、用命令行和 MCP 的 agent 三种用法各做了三轮真实使用,这一版带上了由此修好的问题。它是 GitHub 预发布:安装脚本默认不装它,Docker 镜像的 latest 和 2.0 标签也不会指向它。想试的话,先备份数据库,读完 升级到 2.0,别直接升级正在用的安装。

装这一版:curl -fsSL https://raw.githubusercontent.com/AMDL-Web/apple-music-downloader/dev/deploy/install/install.sh | sh -s -- --version v2.0.0-rc.2

相对 rc.1 的破坏性变更

升级指南里各有一行。

  • Apple 查无此 id 时,POST /lookup、GET /search、GET /artists/{artist_id}/albums 和 POST /quality 返回 404 catalog_not_found,原来是 502 upstream_error;命令行退出码为 3。
  • GET /config、PUT /config 和 POST /config/reload 只返回 catalog.media_user_token 的末 4 位;PUT 收到这种打码值时保留已存的 token。
  • GET /wrapper/status 的 status 改名为 logged_in。
  • 取消已经 completed 或 failed 的任务返回 409 job_not_cancellable,命令行退出码为 4。
  • 命令行:--wait 和 jobs wait 在任务结束时就返回,hook 还在跑时结果里带 hooks_running,已结束的任务不再报超时;任务摘要的 album_progress 只留 total、finished、percent。
  • MCP:没有服务端也能启动,参数校验和错误码与命令行一致。

主要修复

  • 只有 AAC 的专辑查音质不再报 502,控制台写明会回退到 AAC-LC。
  • amdl download --watch 在任务结束后约 0.05 秒返回,原来要多等 6 到 9 秒。
  • 控制台:列表和 Overview 显示任务的实际进度(任务新增 progress 字段);链接不存在的失败任务不再提供 Retry;从书签带进来的链接不再并进上次的草稿。
  • wrapper:各处提示统一说明怎么查看和修改 wrapper 地址;wrapper install、update、uninstall 支持 --yes;非终端下的 wrapper login 提示改用 --password-stdin。
  • amdl cli setup 给出的片段在连远程服务时总带上 --server;配置键拼错时给出最接近的键。
  • 安装脚本下载失败时说清原因;版本号不带 v 也能装。
  • 帮助文本只用 ASCII,Windows 旧代码页的控制台不再乱码。

这个候选版本不含的

  • 桌面端不在 2.0.0 里,签名账号到位后单独发布。
  • Homebrew 和 Scoop 从 2.0.1 开始发布;这一版的 Release 附件里仍有渲染好的 amdl.rb 和 amdl.json。

致谢

@LYJW131、@claude

Release v1.7.2

Choose a tag to compare

@github-actions github-actions released this 04 Aug 16:30
31ae33c

v1.7.2

v1.7.1...v1.7.2

新增 | New

文档 | Docs

  • link the Renovate attribution at the app instead of a dead mention @LYJW131, Claude Opus 5

其他 | Other

Release v1.7.1

Choose a tag to compare

@github-actions github-actions released this 02 Aug 10:36
5414424

v1.7.1

维护版本:接上 Renovate 自动升级依赖,并把它开出的第一批升级合了进来。没有功能变更,没有 API 变更,没有数据库迁移。

之所以发一个补丁号而不是把这些留在 dev:镜像会变(alpine 3.22 → 3.24、golang 1.25.12 → 1.26.5),链进二进制的依赖也会变。这两样都是发出去才算数的东西。

依赖升级

从 到
github.com/Eyevinn/mp4ff v0.48.0 v0.55.0
google.golang.org/grpc v1.82.1 v1.83.0
modernc.org/sqlite v1.53.0 v1.55.0
golang.org/x/time v0.14.0 v0.15.0
alpine(运行镜像) 3.22 3.24
golang(构建镜像) 1.25.12-alpine 1.26.5-alpine
go.mod 的 go 指令 1.25.12 1.26.5
actions/checkout、actions/setup-go v6 v7

Action 那一条顺带修了个不一致:actions/checkout 此前在 ci.yml 等文件里是 v6、在 release.yml 和 weekly-snapshot.yml 里是 v7,现在全仓库统一。

go.mod 的 Go 版本必须跟着构建镜像走

这两行此前精确相等,都是 1.25.12。Renovate 只认识 Dockerfile 里的 golang 镜像,把它升到 1.26.5-alpine 之后,go.mod 仍然写着 go 1.25.12——而 CI 的 actions/setup-go 用的是 go-version-file: go.mod。

结果是发出去的二进制由 Go 1.26.5 编译,CI 却只测过 1.25.12。Go 向后兼容,构建不会失败,所以这个偏差不会以任何形式报错,只是 CI 从此覆盖不到实际出货的那套 toolchain。

go.mod 的 go 指令因此一并升到 1.26.5,两边重新对齐,CI 测的就是出货的。这一行上方加了注释说明它和 Dockerfile 的绑定关系。整套 CI 检查(gofmt、build、vet、test、race、govulncheck)都在 1.26.5 下重跑过。

这也意味着从源码构建本项目现在需要 Go 1.26.5 或更高。

mp4ff v0.55 改了加密 API,影响只落在测试里

InitProtectData.ProtFunc 变成非导出字段,EncryptFragment 的签名从只返回 error 变成返回 ([]byte, error)。生产解密路径走的是 DecryptFragment,签名没动——所以 CI 里 go build 一路是过的,挂掉的是把测试文件也纳入编译的 go vet。

internal/media/mp4_codec_roundtrip_test.go 两处适配:

  • ALAC 夹具改用 ExtractInitProtectData,从它自己手工拼好的那个受保护 init segment 里把数据读回来。这里不能改用 InitProtect:它会在夹具刚写好的 sinf/tenc 上再叠一层,而这正是当初要用结构体字面量构造的原因。
  • EncryptFragment 返回的 IV 现在链式传给下一个分片,不再让每个分片都从同一个基准 IV 重新开始——那等于 AES-CTR 密钥流复用,新签名的存在就是为了避免它。断言不受影响:解密自始至终用的是从各个 senc 盒子里抓的每样本 IV,从来不是基准 IV。

验证是带 ffmpeg 真跑的,夹具真造、真解密。alac 与 alac-multifrag 都过,而多分片正是 IV 链式传递唯一会改变行为的场景。

Renovate

配置在 .github/renovate.json5,覆盖 gomod、Dockerfile 基础镜像、workflow action 三个生态。每周一 09:00(Asia/Shanghai)扫一次,PR 一律开向 dev,跟着下一次发版进 main,不绕过发版流程。

三个坑写在配置注释里,这里再记一遍:

  • 配置只从默认分支读。 官方原话是 "Renovate always uses the config from the repository's default branch, even if that configuration specifies baseBranchPatterns"。所以这份文件必须待在 main 上,尽管它开的 PR 全部指向 dev。
  • config:recommended 会把 Go 依赖标成 fix(deps)。 它带了 :semanticPrefixFixDepsChoreOthers,而 .github/scripts/generate-changelog.js 按前缀分类,fix 归进「修复 | Fix」。依赖升级不是 bug 修复,所以 packageRules 里把类型显式钉成 chore,actions 组钉成 ci。
  • minimumReleaseAge: "3 days"。 新版本发布满三天才升,躲开当天就被撤回的版本,也给投毒的包留出被发现的时间。

托管版还有一个开关不在仓库里:Mend 组织设置的 Silent 模式。开着的时候 Renovate 照常扫描算结果,但不往 GitHub 写任何东西,PR 全堆在 developer.mend.io 上等手动放行——看起来和「Renovate 没跑」完全一样。已关闭,同时打开 Require config file,把作用范围限制在确实有 Renovate 配置的仓库,避免组织里其他仓库一起收到依赖 PR。

bot 开的 PR 不再触发 AI 审查

依赖升级 PR 的 diff 全是版本号,Claude 和 Codex 读不出什么,两次审查纯烧额度。两个 workflow 的 job 级 if: 各加了一条 github.event.pull_request.user.type != 'Bot',用的是通用判断而非只匹配 renovate[bot],以后接别的 bot 自动生效。人开的 PR 不受影响。

安全网一点没动:ci.yml(build、vet、test、race、govulncheck)和 CodeQL 在 bot PR 上照常跑。上面 mp4ff 那个破坏性改动就是被它们拦下来的。

顺带修好的:DCO 必需检查一直绑错了 app

不是代码改动,但值得记一笔。dev 和 main 的分支保护都要求一个名为 DCO 的检查来自 GitHub Actions(app 15368),而 DCO 实际是 probot 的 dco app(app 1861)发出的。来源对不上,这个必需检查就永远不算满足——仓库里任何 PR 都合不了,只能靠管理员绕过。同一份规则里的 Build and test 绑 15368 是对的,错的只有 DCO 这一条。

拖到现在才暴露,是因为按仓库约定改动一直直接 push 到 dev,Renovate 这三个是史上第一批以 dev 为目标的 PR。两个分支的规则现已指向正确的 app。


Release v1.7.0

Choose a tag to compare

@github-actions github-actions released this 31 Jul 05:46
051743b

v1.7.0

资料库同步:后端定期检查已登录的 Apple Music 资料库,把新加进去的曲目所属的整张专辑作为普通下载任务提交。手机上收藏一首歌,NAS 那边自动把专辑下下来。

默认关闭。 这是唯一一个会自己创建下载任务的功能,升级到这个版本的安装必须显式开启。

本次没有破坏性变更。新增一张数据库表,纯追加(CREATE TABLE IF NOT EXISTS),不触碰任何现有表和数据。

新功能:资料库同步

新增配置段 library_sync,两个键,都可以在运行时改(PUT /api/v1/config、iOS 设置页):

  • enabled:开关,默认 false。关掉是真的停止轮询,而不只是丢弃结果。
  • interval_minutes:轮询间隔,1–1440,默认 15。改了立即生效——轮询循环按固定短节拍走、每拍判断是否到点,所以把间隔从 24 小时改成 5 分钟不会等到明天才生效。

依赖 catalog.media_user_token:个人资料库只有订阅令牌读得到,developer token 单独不行。该字段为空时监视器保持空转并说明原因,不会每轮报错刷屏。

两个新端点:

  • GET /api/v1/library-sync:状态(开关、间隔、锚点条目数、最近一轮结果)。功能关闭时同样返回,客户端不必猜测开关状态。
  • POST /api/v1/library-sync/reset:清空锚点。

Apple 不给曲目的加入时间,所以水位线是位置而不是时间

这是整个功能里唯一需要解释的设计。

library-songs 没有 dateAdded 属性——extend= 和 fields[library-songs] 都要不出来。library-albums 有 dateAdded,但那是该专辑第一首歌入库的时刻,之后往同一张专辑补歌它不会变:一张 2024 年加入、今天补了一首的专辑,读出来仍然是 2024。sort=-dateModified 直接被 400 拒绝。

能用的只有一条:sort=-dateAdded 返回的顺序是准的。

所以监视器记住最新 50 首曲目的 id 当锚点,每轮倒序翻页直到重新看见锚点里最老那一首,中间凡是锚点没有的即为新增。不依赖任何时钟,不受时钟漂移影响,而且是精确判断而非近似。

首次启用只落锚、不提交任何专辑——开启那一刻库里已有的东西一律算「已有」,补下整个历史资料库从来不是这个功能的意图。

两个容易做错的地方,值得单独写出来:

  • 每轮会多读 25 首,这样普通的新增一次请求就能解决。这多出来的部分按定义比所有锚点条目都老,它们不在锚点里只是因为锚点只记固定条数——所以「是否在锚点集合里」不能单独用来判断新旧,边界是锚点最老那一首的位置。把「不在锚点里」当成「是新的」,会导致每一轮都重复提交约 25 张老专辑。
  • 锚点最老那首如果已经从资料库里消失,就没有可比的边界了。这时重新落锚并且不提交任何东西,而不是把扫到的几百张全部塞进队列。

开销

稳态一轮通常只有 1 次 Apple 请求(锚点窗口一页就够),每张新专辑另加 1 次——资料库 id(l.xxx)不能直接下载,要换成目录链接。按 15 分钟一轮、平时无新增算,一天约 100 次请求。

提交走的是同一条路

监视器调用 jobs.Manager.SubmitBatch,不是自己打自己的 HTTP API。所以它提交的任务和你手动提交的完全一致:同一套去重、同一套校验、同一套 hooks。目录读取用后端内部的 developer token(sign(..., nil),不带 origin claim),与 /api/v1/developer-token 签给浏览器前端的那份不同。

重置是「忘记历史」,不是「重放历史」

POST /api/v1/library-sync/reset 清空锚点之后,下一轮会以当时的资料库重新落锚,并且不提交任何专辑。适用于监视器对「已经有了」的认知与实际情况脱节时。

想重新下载某张专辑,请直接 POST /api/v1/downloads 提交它——这个接口做不到那件事,也不应该做到。

客户端

iOS 设置页新增「资料库同步」(开关 + 间隔)。旧后端不返回 library_sync 段时,App 不会把这一段发出去(后端的 PUT 会拒绝未知字段并返回 400,无条件发送会让每一次保存设置都失败),设置页那一行显示「后端不支持」并把开关置灰。

Web 前端本次未跟进:amdl-frontend/src/api/types.ts 与设置页仍无此字段。前端保存设置不受影响(PUT 是合并语义,省略即保留),只是在 Web 上看不到也改不了这个功能。


feat(librarysync): submit albums for newly added library songs — @LYJW131, @claude

Release v1.6.2

Choose a tag to compare

@github-actions github-actions released this 29 Jul 11:46
9183ce9

v1.6.2

补上 v1.6.1 漏发的那一半 —— 真正设置 waiting_download / waiting_decrypt 的代码;并给进度事件加上时间下限,几十首的专辑不再把事件流打满。

本次没有破坏性变更,也没有数据库迁移。

修正:v1.6.1 声明了两个永远不会出现的状态

v1.6.1 发布了枚举值和它的文档,但没有发布设置它们的代码。API 因此声明了两个不存在的状态,客户端为它们准备的分支成了死代码。行为上是失效,不是损坏。

这次把 set 调用一并发出去了。

修正:进度事件没有时间下限

此前唯一的节流是看值不看时间的:progressChanged 按整数百分比变化触发,把单个曲目的单个计量器封顶在约 101 次 —— 但对两次之间的间隔没有任何约束。一首三秒传完的曲目会在这三秒内把一百次全部打出去,再乘以并行的曲目数,而每一次都是两次 SQLite 写加一次发布。

24 首专辑、部署中的并发(下载 8 / 解密 32)实测:

平均事件/秒 计量器峰值/秒 每曲目进度事件
之前 43.5 74 153.8
之后 19.4 34 67.6

progressGate 是按曲目的前沿触发闸门,作用在持久化那一步。它把旧的判断拆成两半:一次性的流水线里程碑永不延迟,两个进度计量器才受限。

它刻意不使用后台定时器。 一个在曲目终态之后才触发的定时器,会把一个已经完成的曲目在所有客户端上倒退回去。取而代之的是:闸门包住 reporter,任何非进度事件都会先把暂存的进度值冲刷出去,再发自己 —— 所以状态切换前的最后一个进度值不会丢。合并发生在写库之前而不是推送的路上,因此断线重连按 last_event_id 回放的客户端读到的历史与实时流逐字节一致。

全事件峰值几乎没变(129 → 112),因为那是启动瞬间每个曲目各自宣告前四个状态转换的爆发 —— 而那正是绝不能延迟的部分。

新增运行时配置 download.progress_event_interval_ms,默认 500,0 表示关闭时间合并,可热更新。它不提供逐任务覆盖:事件流是所有订阅者共享的,不该由某一个任务决定别人的事件率。


  • feat(media): emit the waiting_* states, and put a floor under progress events — @LYJW131, @claude

Release v1.6.1

Choose a tag to compare

@github-actions github-actions released this 29 Jul 11:11
2b1fedb

v1.6.1

把「在并发池里排队」和「正在传输」拆成两个状态,客户端不再需要猜一个不动的进度条到底是慢还是堵;另外修正取消一个不存在的任务时返回 500 而不是 404。

本次没有破坏性变更,也没有数据库迁移。

新增:waiting_download 与 waiting_decrypt

一个处于 downloading 的曲目此前在做两件很不一样的事:真的在搬字节,或者在全局下载池里等号。decrypting 对解密池同理。客户端没有任何办法区分,于是一个排队中的曲目看起来就是一个卡住的传输 —— 而这恰恰是「池子饱和」被误读成「任务挂死」的方式。

现在排队的那一半有了自己的名字。两个传输态保持原义,并且只有在池子放行之后才开始。

queued → resolving → waiting_download → downloading
                   → waiting_decrypt  → decrypting → remuxing → tagging → saving → completed

这是对 item status 枚举的追加,不知道新值的客户端仍然看到一条合法的流水线。之所以按补丁号发:在这个枚举上做分支的只有两个手写镜像,而两边都已经跟上了 —— iOS 的 JobItemStatus 本来就有这两个 case,amdl-frontend 的 ItemStatus 与 ACTIVE_ITEM 随本次一并补齐。没有第三方客户端会因为不认识新值而把排队中的曲目判成已结束。

修正:取消不存在的任务返回 500

POST /downloads/{id}/cancel 在任务不存在时把 db.ErrJobNotFound 当成内部错误,返回 500。客户端因此分不清「这个任务已经没了」和「服务端出问题了」—— 前者该安静地把本地那一行删掉,后者该重试并告警。

现在返回 404。


  • feat(api): distinguish waiting for a concurrency slot from transferring — @LYJW131, @claude
  • fix(api): answer 404 when cancelling a job that does not exist — @LYJW131, @claude

Release v1.6.0

Choose a tag to compare

@github-actions github-actions released this 28 Jul 16:25
df5e2e4

v1.6.0

新增 artist_url,让客户端能直接链到艺人页而不是把名字丢进搜索;修正去重键漏掉「目的地」这一半,导致同一内容下到不同目录被误判为重复;修掉两个并发写同一输出文件时会互相踩的暂存文件;升级 grpc 关闭一个已知漏洞。

本次没有破坏性变更,也没有数据库迁移。

新增:artist_url

想链到艺人页的客户端此前无路可走。artist_name 是展示字符串,唯一的办法是把它丢进 Apple Music 搜索 —— 落在结果页上,遇到重名还会落错人。

Apple 在解析集合时本来就会把 artist 资源一并 included 进来,所以读它的 attributes.url 不增加任何请求。这个值现在贯穿领域模型、存储与 API。

只有 artist_name 确实是艺人的类型才有这个字段 —— 单曲、专辑、艺人。歌单与电台留空,因为它们的展示名是策展人或提供方标签,不是艺人。此外在输入解析完成之前、在该字段存在之前建立的历史任务上、以及目录响应恰好没有 artist 关系的少数情况下,同样为空。客户端必须按可选字段处理,并保留原有的兜底路径。

修正:去重键漏掉了目的地

两个写向不同目录的任务本来就不是彼此的重复,但去重键没有表达这一点。canonical_key 是 type:storefront:adamID,配一条覆盖 queued/running 行的唯一部分索引 —— 于是把一张专辑下到某处、在它还没跑完时再下到另一处,会被判为重复而拒绝。而且 API 层没有任何绕过手段:请求级 force 已经不存在,overrides.force_overwrite 只管是否覆盖已有输出文件。

键上现在追加一段「规范化后的实际 downloads_dir」的哈希。

  • 规范化是必须的:<root>/./music/ 与 <root>/music 是同一个目录且后端两者都接受,直接哈希原始字符串等于让调用方换个写法就能绕过自己的去重。
  • 哈希而非内嵌路径:键更短、不把文件系统路径写进索引、也不会被目录名里的 : 撞坏解析。

键同时是被读回来用的:parseJobInput 从键里恢复 adam id,而不是重新解析原始输入 —— 否则当前的 album_track_url_mode 会把一条以单曲身份提交的 album?i= 链接悄悄变成整张专辑任务。该解析现在移到 jobs.ParseCanonicalKey,与构造键的代码放在一起,且采用「剥掉尾部定宽 hex 段」而不是按固定段数切分。按固定段数会在反方向引入一个新 bug:: 在 URL 路径段里是合法字符,含冒号的 adam id 今天能正确解析,固定段数切分会把它静默截断。

兼容性:三段式旧键照常解析,无需迁移。跨升级仍在队列里的任务保留旧键,不会与新提交的同目的地任务去重 —— 结果是漏一次去重,绝不会下错内容,且该窗口随这些任务进入终态而关闭。

修正:暂存文件在并发写同一输出时互相踩

copyIntoPlace 此前暂存于 dst + ".part" —— 名字由目标路径推导,而非由写入方推导。两个指向同一输出的任务因此共用一个暂存文件:互相截断、交错写入,后改名的那个会在前一个已经把共享文件改走之后撞上 ENOENT。

两个写入方想要同一个输出是常态而非奇例 —— 同一首曲目经由专辑任务与歌单任务各下一次就会发生,而且没有任何去重键会把这两者分开。processOutputLocks 在单进程内串行化了它们,但跨进程没有,且那把锁是调度层面的优化,不是文件布局的性质。

它只在 EXDEV 回退路径上可达,也就是 temp_dir 与 downloads_dir 位于不同文件系统时 —— 而这正是容器化部署的常见形态,下载根是 bind mount 而临时目录不是。

暂存名现在按调用唯一(os.CreateTemp,模式 "<name>.*.part",保留输出自身的名字,好让硬杀留下的孤儿文件能说清自己属于哪首曲目)。两个由此形态强制的细节:

  • 名字会被裁剪到长度上限以内,且按 rune 边界裁 —— APFS 拒绝非法 UTF-8 的文件名。不裁的话,给一个刚好放得下的名字再加上唯一化后缀,会让下载在最后一步失败,而此时字节已经取回、解密、remux 并打完标签。
  • 清理扫描改为遍历目录而不是 glob。safeName 不剥 [ 与 ],一首名为 Song [Live] 的曲目会让 filepath.Glob 把方括号读成字符类,从而匹配到别的曲目的暂存文件。扫描同时覆盖新旧两种命名,因为已部署的实例可能已经存有旧格式的孤儿。

安全:grpc 升级

govulncheck 报告 GO-2026-6061 命中 google.golang.org/grpc@v1.81.1,v1.82.1 已修。它是可达的,不是死代码:调用图经 internal/wrapper/client.go(每次下载都走的解密路径)与 internal/hooks/dispatcher.go 抵达。升级后 govulncheck 报告零漏洞。

验证

  • gofmt、go build ./...、go vet ./... 通过。
  • Go 测试通过:go test -count=1 ./...(13 个包),-race 在 internal/media、internal/jobs、internal/db 上通过。
  • govulncheck ./... 报告 No vulnerabilities found。
  • 本次四个提交逐个 checkout 验证均可独立编译。
  • 端到端:隔离实例(simulate 模式,非默认端口,hooks 指向不存在的文件)上,同一张专辑分别下到两个不同目录均被接受并产生两个独立任务,同目的地重复提交仍返回 duplicate_active;<root>/./music/ 与 <root>/backup/../music 两种拼写正确归并到同一任务。

提交明细

  • feat(catalog): expose the artist's own Apple Music page as artist_url — @LYJW131
  • fix(jobs): fold the destination into the canonical dedup key — @LYJW131、@claude
  • build(deps): bump google.golang.org/grpc to v1.82.1 for GO-2026-6061 — @LYJW131、@claude
  • fix(media): give each writer its own staging file — @LYJW131、@claude