关于 apple-music
Apple Music 集成指南,涵盖 AppleScript、UI 自动化和 MusicKit API。可用于播放控制、资料库与播放列表操作及目录搜索,并说明将目录歌曲加入播放列表前须先加入资料库。
主要功能
- 01通过 AppleScript 控制播放与资料库
- 02管理播放列表和曲目
- 03搜索并播放音乐目录
- 04集成 MusicKit API
- 05UI 自动化操作指南
适用场景
- 01控制播放并查看当前曲目
- 02创建播放列表并管理曲目
- 03搜索目录或将歌曲加入资料库
- 04播放目录曲目并切换 AirPlay 输出
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ epheterson/ applemusic-mcp --skill apple-music安装后在 Claude Code 中输入/apple-music确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSE.gitignore.secrets-allowlistMakefileREADME.mdCHANGELOG.mdSECURITY.mdRELEASING.mdconfig.example.jsonpyproject.tomlserver.json.github/dependabot.yml.github/workflows/codeql.ymlscorecard.ymlrelease.ymltest.ymlpublish.ymlscripts/check_live_env.pycheck_versions.pypreflight-ui.shpreflight.shintegration_test.pyintegration_test_applescript.pyvalidate_endpoints.pysrc/applemusic_mcp/__init__.py__main__.pyamp_api.pypaths.pyaudit_log.pymusickit_js.py详细介绍
Apple Music 集成
Apple Music 集成指南。提供三种方式:AppleScript(直接控制)、UI 自动化(无 API 的目录)和 MusicKit API(跨平台)。
适用场景
在用户要求以下操作时调用:
- 管理播放列表(创建、添加/移除曲目、列出)
- 控制播放(播放、暂停、跳过、调节音量)
- 播放 Apple Music URL(专辑、播放列表或歌曲)
- 搜索目录或资料库
- 将歌曲添加到资料库
- 访问收听历史或推荐内容
重要规则:优先使用资料库的工作流
不能直接将目录歌曲添加到播放列表。
歌曲必须先加入用户的资料库:
- ❌ 目录 ID → 播放列表(失败)
- ✅ 目录 ID → 资料库 → 播放列表(成功)
原因: 播放列表使用资料库 ID(i.abc123),而非目录 ID(1234567890)。
AppleScript 和 API 方式均适用此规则。
AppleScript(macOS)
无需设置。可立即与 Music 应用配合使用。
通过 Bash 运行:
osascript -e 'tell application "Music" to playpause'
osascript -e 'tell application "Music" to return name of current track'
多行脚本:
osascript <<'EOF'
tell application "Music"
set t to current track
return {name of t, artist of t}
end tell
EOF
可用操作
| 类别 | 操作 |
|---|---|
| 播放 | 播放、暂停、停止、继续、下一首、上一首、快进、快退、播放 URL |
| 播放器状态 | 播放器位置、播放器状态、音量、静音、随机播放开关/模式、歌曲重复播放 |
| 当前曲目 | 名称、艺人、专辑、时长、时间、评分、喜爱、不喜欢、流派、年份、曲目编号 |
| 资料库 | 搜索、列出曲目、获取曲目属性、设置评分 |
| 播放列表 | 列出、创建、删除、重命名、添加曲目、移除曲目、获取曲目 |
| AirPlay | 列出设备、选择设备、当前设备 |
曲目属性(只读)
tell application "Music"
set t to current track
-- Basic info
name of t -- "Hey Jude"
artist of t -- "The Beatles"
album of t -- "1 (Remastered)"
album artist of t -- "The Beatles"
composer of t -- "Lennon-McCartney"
genre of t -- "Rock"
year of t -- 1968
-- Timing
duration of t -- 431.0 (seconds)
time of t -- "7:11" (formatted)
start of t -- start time in seconds
finish of t -- end time in seconds
-- Track info
track number of t -- 21
track count of t -- 27
disc number of t -- 1
disc count of t -- 1
-- Ratings
rating of t -- 0-100 (20 per star)
loved of t -- true/false
disliked of t -- true/false
-- Content rating. DO NOT trust this for filtering.
-- On Apple Music (cloud) tracks this property is unset, and reading it
-- RAISES "A descriptor type mismatch occurred" rather than returning
-- false. Catching that and defaulting to false is what turns "I could
-- not tell" into "this is clean": on a real 463-track playlist Music.app
-- yielded zero explicit tracks where the catalog rated eight of them
-- explicit. If a caller asked for clean content, resolve the rating
-- against the catalog (`contentRating == "explicit"`, matching on name
-- AND artist) and report anything you could not verify as unknown.
explicit of t -- true/false, RAISES on most cloud tracks
-- Playback
played count of t -- 42
played date of t -- date last played
skipped count of t -- 3
skipped date of t -- date last skipped
-- IDs
persistent ID of t -- "ABC123DEF456"
database ID of t -- 12345
end tell
曲目属性(可写)
tell application "Music"
set t to current track
set rating of t to 80 -- 4 stars
set loved of t to true
set disliked of t to false
set name of t to "New Name" -- rename track
set genre of t to "Alternative"
set year of t to 1995
end tell
播放器状态属性
tell application "Music"
player state -- stopped, playing, paused, fast forwarding, rewinding
player position -- current position in seconds (read/write)
sound volume -- 0-100 (read/write)
mute -- true/false (read/write)
shuffle enabled -- true/false (read/write)
shuffle mode -- songs, albums, groupings
song repeat -- off, one, all (read/write)
current track -- track object
current playlist -- playlist object
current stream URL -- URL if streaming
end tell
播放命令
tell application "Music"
-- Play controls
play -- play current selection
pause
stop
resume
playpause -- toggle play/pause
next track
previous track
fast forward
rewind
-- Play specific content
play (first track of library playlist 1 whose name contains "Hey Jude")
play user playlist "Road Trip"
-- Settings
set player position to 60 -- seek to 1:00
set sound volume to 50 -- 0-100
set mute to true
set shuffle enabled to true
set song repeat to all -- off, one, all
end tell
匹配带排版标点符号/重音符号的标题
whose name contains "..." 会精确匹配字形——不同于 Music 的原生 search
命令,它不会将直引号(U+0027)与 Music 经常存储的弯引号(U+2019)视为相同,
也不会忽略重音符号。因此,whose name contains "That's a No No" 无法匹配存储为 That's a No No 的标题。以下是两种可靠的策略:
-- 1. Match the quote-free fragments instead of the apostrophe itself.
-- Works regardless of which apostrophe variant is stored or typed.
play (first track of library playlist 1 whose ¬
(name contains "That" and name contains "s a No No"))
-- 2. For accents/ellipsis/anything: bulk-fetch names in ONE Apple Event and
-- fold in-memory. `ignoring punctuation and diacriticals` uses Apple's
-- Unicode tables (curly quotes, ellipses, café≈cafe). Do NOT access
-- `name of t` per-iteration in a loop — that is one Apple Event per track
-- (~17s on a 12k library); fetch the whole list at once (~instant).
tell application "Music"
set allNames to (get name of every track of library playlist 1)
set idx to 0
ignoring punctuation and diacriticals
repeat with i from 1 to (count of allNames)
if ((item i of allNames) as text) contains "Fur Elise" then
set idx to i
exit repeat
end if
end repeat
end ignoring
if idx > 0 then play (track idx of library playlist 1)
end tell
资料库查询
tell application "Music"
-- All library tracks
every track of library playlist 1
-- Search by name
tracks of library playlist 1 whose name contains "Beatles"
-- Search by artist
tracks of library playlist 1 whose artist contains "Beatles"
-- Search by album
tracks of library playlist 1 whose album contains "Abbey Road"
-- Combined search
tracks of library playlist 1 whose name contains "Hey" and artist contains "Beatles"
-- By genre
tracks of library playlist 1 whose genre is "Rock"
-- By year
tracks of library playlist 1 whose year is 1969
-- By rating
tracks of library playlist 1 whose rating > 60 -- 3+ stars
-- Favorite (loved) tracks. Music.app renamed Loved -> Favorite and the
-- property with it: newer versions use `favorited`, older use `loved`,
-- and the unsupported name can fail INSIDE a whose-filter with "The
-- variable loved is not defined". Try `whose favorited is true` first,
-- fall back to `whose loved is true`, each wrapped in `try`.
tracks of library playlist 1 whose favorited is true
tracks of library playlist 1 whose loved is true -- legacy Music versions
-- Recently played (sort by played date)
tracks of library playlist 1 whose played date > (current date) - 7 * days
end tell
播放列表操作
tell application "Music"
-- List all playlists
name of every user playlist
-- Get playlist
user playlist "Road Trip"
first user playlist whose name contains "Road"
-- Create playlist
make new user playlist with properties {name:"New Playlist", description:"My playlist"}
-- Delete playlist. Match the name EXACTLY; a `contains` fallback here
-- turns "Work" into "Workout".
delete user playlist "Old Playlist"
-- Rename playlist
set name of user playlist "Old Name" to "New Name"
-- Get playlist tracks
every track of user playlist "Road Trip"
name of every track of user playlist "Road Trip"
-- Get the FIRST N, one Apple Event per property. Read the range off the
-- playlist itself. Do NOT stash the tracks in a variable first --
-- `set t to tracks of pl` materializes a plain list, and a property of a
-- plain list does not distribute: Music raises -1728 and names every
-- track of the playlist in the error message.
set pl to user playlist "Road Trip"
set n to 25
if n > (count of tracks of pl) then set n to (count of tracks of pl)
if n < 1 then return {} -- `tracks 1 thru 0` errors
get name of tracks 1 thru n of pl as list -- `as list`: n=1 returns a
-- bare value, not a list
-- Add track to playlist (must be library track)
set targetPlaylist to user playlist "Road Trip"
set targetTrack to first track of library playlist 1 whose name contains "Hey Jude"
duplicate targetTrack to targetPlaylist
-- Remove track from playlist. `contains` + `first` is a TRAP for anything
-- destructive: it deletes whichever match sorts first, irreversibly.
-- "Love" matches 376 tracks in a real library; "Alice" matches three
-- playlists including a 467-track one. Enumerate first and act only when
-- the target is unambiguous.
set hits to (every track of user playlist "Road Trip" whose name contains "Hey Jude")
if (count of hits) is 1 then
delete (item 1 of hits)
else
-- report the candidates and let the user choose; do not guess
end if
-- Playlist properties
duration of user playlist "Road Trip" -- total duration
time of user playlist "Road Trip" -- formatted duration
count of tracks of user playlist "Road Trip"
end tell
AirPlay
tell application "Music"
-- List AirPlay devices
name of every AirPlay device
-- Get current device
current AirPlay devices
-- Set output device
set current AirPlay devices to {AirPlay device "Living Room"}
-- Multiple devices
set current AirPlay devices to {AirPlay device "Living Room", AirPlay device "Kitchen"}
-- Device properties
set d to AirPlay device "Living Room"
name of d
kind of d -- computer, AirPort Express, Apple TV, AirPlay device, Bluetooth device
active of d -- true if playing
available of d -- true if reachable
selected of d -- true if in current devices
sound volume of d -- 0-100
end tell
字符串转义
始终转义用户输入:
def escape_applescript(s):
return s.replace('\\', '\\\\').replace('"', '\\"')
safe_name = escape_applescript(user_input)
script = f'tell application "Music" to play user playlist "{safe_name}"'
常见故障
osascript 的 stderr 消息对应少数几种环境状态。AppleScript 失败时,应先分类,再决定如何告知用户——直接显示原始 stderr 会造成误导;静默地转而使用 API 路径,则会泄露无关错误(例如,真正的问题是 Music.app 已关闭,却显示“未找到 Developer token”)。
| Stderr 信号 | 含义 | 告知用户的内容 |
|---|---|---|
(-609)、Connection is invalid、(-10810)、isn't running、Can't get application "Music" | Music.app 未运行,或在会话中途崩溃 | “Music.app 未运行。请打开后重试。” |
(-1743)、Not authorized、not allowed assistive access、assistive access | 宿主进程(Claude Desktop、Python CLI、Terminal)尚未获得 Music 的自动化权限 | “打开系统设置 → 隐私与安全性 → 自动化,找到运行此代码的应用,并启用 ‘Music’ 开关。” |
AppleScript timed out after 30 seconds | 30 秒子进程超时已触发——Music.app 卡住或仍在启动 | “Music.app 可能没有响应——请退出并重新打开。” |
syntax error、expected … but found … | 脚本本身格式错误(开发者错误) | 报告原始错误——这是我们的问题,不是用户的问题。 |
| 其他任何内容 | 逻辑层面的错误(未找到曲目、播放列表为空等) | 显示原始 stderr;如果有合理的回退方式,可以安全地转而使用 API。 |
不要只匹配 not allowed——Music.app 会发出“operation not allowed on smart playlists”等类似错误,这类逻辑层面的错误不应被归类为自动化权限拒绝。应匹配完整的 not allowed assistive 或 assistive access 字样。
不要只依据 is_available() 作出判断——它只检查 darwin + osascript exists。它无法告知 Music.app 是否正在运行,也无法判断是否已授予自动化权限。上述错误分类是判断这些状态的唯一信号。
限制
- 仅支持 macOS——不支持 Windows/Linux
- UI 功能需要显示器——无头环境或 Music.app 最小化时,UI 自动化无法运行
- Music.app 必须处于运行状态——只要
osascript在 PATH 中,is_available()就会返回 True,但每个tell application "Music"代码块都要求 Music.app 实际运行。全新启动后首次调用时,也可能触发自动化权限提示,用户需要批准一次。
UI 自动化(macOS)
在 0.15.0 中已移除添加到资料库/播放列表的 UI 自动化。 目录中的 添加到资料库和 auto_add→playlist 通过统一的 Apple Music API运行 (开发者令牌已生成或从 Apple 公共网页播放器获取,另加
media-user-token)。旧版 UI 添加路径容易受版本影响(在 macOS/Music.app 版本更新时会失效,#37)。若要启用 API 路径:applemusic-mcp login(或使用开发者令牌运行login --dev)。在 macOS 上,login默认从已登录的 Safari 读取 media-user-token,无需 Chrome/Playwright;前提是已启用 Safari → 设置 → 高级 → 开发 →“允许来自 Apple 事件的 JavaScript”;login --chrome则使用 Chrome 网页播放器(需要pip install 'applemusic-mcp[browser]')。在 macOS 以外的平台上,login会打开 Chrome (Playwright 默认随附)。以下 UI 原语仅用于 播放/从 URL 播放和目录搜索,不用于添加。引擎(
mode偏好设置/每次调用的engine=):auto(默认——播放使用原生 Music.app, Up Next 队列使用 Safari,macOS 上的数据使用 API;macOS 以外使用 Chrome)、native、safari、chrome、api。Safari 引擎通过相同的do JavaScript通道驱动已登录 Safari 的 MusicKit(原生 DRM, 无需 Chrome),用于播放/控制/now_playing/设置和完整队列——因此在 macOS 上, 无需 Playwright 即可使用整个网页播放器界面。使用queue会将其引擎设为当前活动引擎,因此playback control会连接到该引擎。
剩余的 UI 自动化通过 System Events(辅助功能 API)和 CoreGraphics(鼠标事件)控制 Music.app。
**要求:**连接显示器、Music.app 可见、已授予 System Events 辅助功能权限(系统设置 → 隐私与安全性 → 辅助功能)。
关键概念
System Events 通过辅助功能层级读取和点击 UI 元素:
tell application "System Events" to tell process "Music"
-- Main content area
scroll area 2 of splitter group 1 of window "Music"
-- Search field
text field 1 of UI element 1 of row 1 of outline 1 of scroll area 1 of splitter group 1 of window "Music"
end tell
CoreGraphics 鼠标事件(通过 JXA)生成真实鼠标输入——悬停、单击和双击:
// osascript -l JavaScript
ObjC.import("CoreGraphics");
var point = $.CGPointMake(x, y);
// hover (reveals hidden controls):
$.CGEventPost($.kCGHIDEventTap, $.CGEventCreateMouseEvent($(), $.kCGEventMouseMoved, point, 0));
// click: post LeftMouseDown then LeftMouseUp at the point.
// double-click: post two down/up pairs with the click-state field (1) set to 1 then 2.
重要:System Events 的
click(AXPress)不会触发 Music.app 的 自定义播放/随机播放按钮或曲目行——它会返回成功,但不会播放任何内容。 使用 System Events 读取元素矩形,然后在元素的屏幕坐标处使用 CoreGraphics 鼠标事件点击。单击只会选中曲目行;要播放曲目,请双击。
原生播放目录(非资料库)曲目
AppleScript 没有用于播放非资料库目录曲目的动词。请使用深层链接打开页面, 然后通过 CoreGraphics 驱动 UI:
open "music://…/album/<id>"(专辑/播放列表)或…?i=<songId>(单首曲目)- **专辑/播放列表:**找到
Play按钮,读取其位置和大小,然后在其中心位置使用 CoreGraphics 单击。 - **单首曲目:**按名称在曲目列表中匹配曲目行,使用 CoreGraphics 双击(这样只会播放该曲目,而非整张专辑)。
悬停技巧(每行控件)
将鼠标悬停在曲目行上之前,Music.app 会隐藏每首曲目的播放/“添加到资料库”控件:
- 通过 System Events 查找曲目 UI 元素的位置
- 通过 CoreGraphics 将鼠标移至该位置(生成真实的悬停事件)
- 隐藏的
checkbox(播放)和button(添加到资料库)会出现在辅助功能树中 - 在其坐标处通过 CoreGraphics 点击(不要使用 System Events AXPress)
通过 UI 搜索
- 设置搜索栏的值:
set value of searchField to "query" - 按 Return:
key code 36 - 等待结果加载(约 4 秒)
- 从
scroll area 2解析“热门结果”列表 - 每个结果都是一个
UI element,其description= 名称,静态文本则显示类型/艺人
**注意:**结果中的类型分隔符使用 Unicode 三分 em 空格(U+2004)+ 中点(U+00B7):Song꘎·꘎Radiohead
窗口恢复
Music.app 可能会在没有窗口的情况下运行。若要确保窗口存在:
tell application "Music" to activate
tell application "System Events" to tell process "Music"
if (count of windows) is 0 then
click menu item "Music" of menu "Window" of menu bar 1
end if
end tell
脆弱性
Apple 更新 Music.app 的布局时,UI 路径就会失效。请将路径集中定义为常量,并在 macOS 更新后进行测试。使用 Accessibility Inspector.app 查看当前层级结构。
搜索字段的位置在 macOS 15(边栏大纲行)与 macOS 26/Tahoe(工具栏组)之间发生了变化。服务会在运行时使用 AppleScript exists 检查探测工具栏元素,如果不存在则回退到边栏路径,因此无需硬编码即可支持这两个 OS 版本。如果 UI 搜索在新的 macOS 版本上失效,请先检查搜索字段路径。
工具路由(何时使用哪个顶层操作)
用户提出请求时,应根据他们要搜索的是自己的资料库还是目录来选择合适的 MCP 工具。这两者很容易混淆——如果在这里判断错误,用户可能会陷入“未找到 Developer token”的排查困境,而实际上该操作本可以在不需要 token 的情况下完成。
| 目标 | 使用 | 备注 |
|---|---|---|
| 查找用户已有的歌曲 | library(action='search', query='...') | 仅搜索本地资料库。macOS 上使用 AppleScript,其他平台使用 API。 |
| 按名称查找播放列表 | playlist(action='list', filter='alice') | 宽松匹配名称,返回匹配的播放列表及其 ID。此处不要使用 action='search'——该操作会搜索指定播放列表中的曲目,并且需要 playlist 参数。 |
| 列出用户某个流派的曲目 | library(action='search', query='Rock', types='genre') | 按曲目的流派字段筛选——仅限 macOS。不要将流派名称作为普通全文搜索进行路由:它不会检查流派字段,因此“Rock”会误匹配歌名为 “Rock Your Body” 的歌曲。无匹配项时会返回“No tracks found”;在 macOS 以外的平台上,会报告 API 不支持流派筛选。 |
| 在 Apple Music 的完整目录中查找歌曲 | catalog(action='search', query='...') | 优先尝试 API;在无需 token 的 macOS 环境下,会回退到 Music.app UI 搜索。 |
| 将目录中的歌曲添加到用户的资料库 | library(action='add', track='...') | 仅使用 API——需要 Developer token(生成或获取)以及 media-user-token(signin)。UI 自动化回退已在 0.15.0 中移除(见上面的说明)。 |
| 将歌曲(资料库中或目录中的歌曲)添加到播放列表 | playlist(action='add', track='...', auto_add=True) | 若歌曲尚未在资料库中,则必须设置 auto_add=True 才能搜索目录。默认值为 False,以避免意外写入资料库;对于“填充此播放列表”工作流,请将其设为 True。 |
| 浏览排行榜/推荐内容 | discover | 仅使用 API。此操作没有 UI 回退。 |
library(action='search') 每次返回一页结果:limit(默认值为 25)限制结果数量,offset 用于翻阅更多结果。文本标题会显示 start-end of total;因此,如果 end < total,则还有更多结果——将 offset 增加 limit 以获取下一页(与 action='browse' 类似)。
如果 library(action='search') 返回“No songs found in library”,这并不意味着需要设置 API token,而是提示你尝试使用 catalog(action='search') 或 playlist(action='add', auto_add=True)。无需 token 的 macOS 路径涵盖了这些操作。
组合流程(无需 API)
无需 API token 即可复现 applemusic-mcp 目录功能的操作方案。针对每个用户请求,先判断“是否需要访问目录?”——纯资料库和播放操作继续使用 AppleScript;只有目录查询才通过 UI 自动化执行。
将目录歌曲添加到用户的资料库
如果用户给出的是类似 "Silvera - GOJIRA" 的单个组合字符串,请先按 - 拆分,再进行搜索,以便目录查询获得干净的名称。
步骤 0 — 首先解析规范曲名(不要跳过此步骤)。 调用免费、无需令牌的 iTunes Search API(https://itunes.apple.com/search?term=<name>+<artist>&entity=song&country=<storefront>),并按艺人优先评分,选出最佳匹配:如果用户指定了艺人,那么由其他艺人演唱的结果就是错误曲目,即使曲名完全匹配也应拒绝,以免悄悄添加错误歌曲(例如,“Lemons”/“Brye”必须解析为 Brye 的“LEMONS (feat. Cavetown)”,而不是 Hairitage 的“Lemons”)。在下方的 UI 查询中使用解析出的规范曲名。这很重要,因为 Apple 的自动补全会受排名影响:模糊的查询(“Lemons”)会显示热门同名歌曲,并遗漏冷门曲目;而规范曲名(“LEMONS (feat. Cavetown)”)能让准确的结果行显示出来。
然后,根据版本采用不同界面:
- macOS 26 / 新版 Music(Music ≥ 1.6.x):搜索自动补全弹出层位于辅助功能树中,且目录深层链接已无法跳转。请使用弹出层:
- 使用规范曲名进行 UI 搜索(§ 通过 UI 搜索)
- 选择名称和艺人匹配的第一个
Song结果——不要回退到非 Song 结果;如果没有匹配的 Song,请正常失败。搜索状态过期可能导致点击错误结果。 - 点击该行 → 跳转到歌曲详情页
- 使用 CoreGraphics 将鼠标悬停在曲目行上——此时隐藏的“添加到资料库”按钮将变为可访问。如果找到的是“下载”按钮,则表示曲目已在资料库中(这是成功,而非失败)。
- 通过真实的 CoreGraphics 点击来点击“添加到资料库”(SwiftUI 按钮会忽略 AXPress)
- macOS 15 / 旧版 Music(Music ≤ 1.5.x):自动补全弹出层不在辅助功能树中,但对解析出的
music://…?i=…深层链接执行open会跳转到专辑页。跳转到解析出的 URL,找到高亮的曲目行,将鼠标悬停其上,然后点击该行的“添加到资料库”按钮。
最后:
6. 清空搜索框,确保下次调用从全新状态开始
7. 验证是否添加成功。对于 iCloud 资料库,弹出层 UI 是权威依据——即使后续步骤有所延迟,也应相信其成功结果。轮询 search_library / 直接查询 library playlist 1,直到曲目在本地可见(通常约 3 秒后首次出现;iCloud 可能会卡顿更久——最长等待约 18 秒)。如果 UI 已确认添加,但验证未找到曲目,这表示“仍在同步”,而不是“失败”。
将目录歌曲添加到指定播放列表
组合使用上文的**“添加到资料库”** → 然后使用 AppleScript duplicate 将新加入资料库的曲目复制到目标播放列表:
tell application "Music"
set targetTrack to first track of library playlist 1 ¬
whose name contains "Silvera" and artist contains "GOJIRA"
duplicate targetTrack to user playlist "Road Trip"
end tell
不要通过 UI 点击“添加到播放列表”菜单项——AppleScript 的 duplicate 路径更可靠。即使你拥有开发者令牌,也不要调用 POST /v1/me/library/playlists/{id}/tracks——对于任何并非通过 API 创建的播放列表(Music.app 中创建的播放列表默认为此类),它都会返回 HTTP 500。duplicate 适用于任何播放列表。
写入路由(优先采用官方路径)
服务器根据凭据和功能,而非播放模式选择写入路径:
- 官方路径——使用生成的开发者令牌调用 Apple Music 官方 API(
api.music.apple.com)。只要存在开发者令牌且该操作受支持,就优先采用此路径。 - Web——使用已登录的会话令牌调用 Web 播放器后端。仅用于公共 API 无法完成的操作(删除播放列表、添加到由 Music.app 创建的播放列表、将项目移出文件夹),或在没有开发者令牌时使用。
- 原生——在 macOS 上通过 AppleScript 操作本地 Music.app(无需令牌);在 Mac 上是默认写入方式。
因此,mode=web(一种播放选项)不会强制通过 Web 路径写入——持有开发者令牌的用户仍会通过官方 API 写入。无论模式如何,星级评分都需要 AppleScript(macOS)。每次写入都会报告所用路径(via Apple Music API / via web player / via Music.app);config(action="status") 会在 Writes: 行显示已解析的写入路径。
添加后验证
UI 路径在搜索状态过期时可能会静默点击错误的结果,而刚添加内容后,AppleScript 状态会短暂滞后。请始终验证预期曲目是否确实已添加:
tell application "Music"
set matches to (every track of user playlist "Road Trip" ¬
whose name contains "Silvera" and artist contains "GOJIRA")
return (count of matches) > 0
end tell
失败前,等待约 1 秒后重试一次。如果第二次验证仍失败,请相信验证结果——添加并未成功;应显示错误,而不是谎称成功。
MusicKit API
跨平台,但需要 Apple Developer 账户(每年 $99)并设置令牌。
身份验证
要求:
- Apple Developer 账户
- 从 开发者门户 获取 MusicKit 密钥(.p8 文件)
- 开发者令牌(JWT,最长 180 天)
- 用户音乐令牌(浏览器 OAuth)
生成开发者令牌:
import jwt, datetime
with open('AuthKey_XXXXXXXXXX.p8') as f:
private_key = f.read()
token = jwt.encode(
{
'iss': 'TEAM_ID',
'iat': int(datetime.datetime.now().timestamp()),
'exp': int((datetime.datetime.now() + datetime.timedelta(days=180)).timestamp())
},
private_key,
algorithm='ES256',
headers={'alg': 'ES256', 'kid': 'KEY_ID'}
)
**获取用户令牌:**通过浏览器 OAuth 访问 https://authorize.music.apple.com/woa
所有请求的请求头:
Authorization: Bearer {developer_token}
Music-User-Token: {user_music_token}
基础 URL: https://api.music.apple.com/v1
速率限制(批量循环前请阅读)
Apple 会在滚动约 60 分钟的时间窗口内进行限流,返回 HTTP 429 / 代码 42900,
且不会发送 Retry-After 或 X-Rate-Limit 请求头——无法观测剩余配额,
因此只有被拦截后才能知道触及了限制。
| 令牌 | 实际上限 | 说明 |
|---|---|---|
你自己的开发者令牌(login --dev,你的 .p8 文件) | 约 3600 次请求/小时,Apple 公布的数据 | 仅限你自己的配额 |
Apple 的公共 Web 播放器令牌(普通 login、无令牌/Safari 路径) | 每小时数百次,根据测量得出 | 由多人共享,不是每位用户各自独享——上限低得多,而且不能由你单独支配 |
可能造成的问题:
- 短暂等待没有用。 即使 15 分钟内没有流量,仍会收到 429。大约在突发流量结束一小时后才会解除限制,之后再发出几次请求就会再次触发限流。限流期间的每次重试都会增加计数,让你继续被拦截。
- 429 可能表现为空结果,而不是错误。 每首曲目搜索一次(“解析标题和艺术家 → 目录 ID”)是标准的导入方式,也是最快触发限流的方式;如果调用方将空搜索结果视为“未找到歌曲”,限流就会静默地产生误报。429 后的空结果应视为未知,绝不能视为不存在,也绝不能缓存。
- 批量操作请使用
login --dev。 你自己的开发者令牌拥有独立且大得多的配额。相关数据已在 #42 中报告并测量。
按 ISRC 批量解析,而不是逐曲搜索
如果调用方持有 ISRC(Spotify / Rekordbox / Plex 导出数据都包含 ISRC),
GET /v1/catalog/{storefront}/songs?filter[isrc]=ISRC1,ISRC2,… 每次请求可解析 25 个
并进行精确匹配——请求数约比逐曲搜索少 25 倍,也不会产生模糊匹配
错误。通过工具调用:catalog(action="resolve", isrcs="…", format="json")。
有两点需要注意,因为 filter[isrc] 是一个筛选器,而不是搜索:
- 未匹配项不会提示。 Apple 只会返回匹配到的结果,并省略其余项,因此只有将请求集合与
响应中的
attributes.isrc进行比较,才能得到未解析的 ISRC。请计算差集——不要假设响应包含全部结果。 - 将“已请求但未找到”与“从未请求”分开处理。 如果某个批次失败(429),剩余的 ISRC 状态是未知,而不是“目录中不存在”。把两者混为一谈,就会重现本节旨在提醒你避免的误判问题。
一个 ISRC 可能对应多首目录歌曲(不同地区发行版、重制版),因此应保留匹配数量,不要悄悄只取第一条。
没有 ISRC 时,写入前先试运行。 曲名匹配是模糊的,而且匹配失败不会提示:没有艺人信息时,Dont Let Me Down 会解析为 The Chainsmokers,而不是 The Beatles。
先解析整个列表,展示带置信度标记(精确匹配还是模糊匹配)的建议匹配结果,审核通过后再写入——不要对每首曲目逐一执行“搜索后添加”。通过工具调用:catalog(action="resolve", tracks=..., format="json"),此操作不会添加任何内容,并会返回可用于 playlist(action="add") 的 ids;也可以使用 playlist(action="add", ..., dry_run=True),预览要添加到特定播放列表的内容,同时与列表中已有曲目进行比对。请注意,这需要每首曲目发送一次请求(Apple 没有按曲名和艺人批量查询的接口),因此无法解决速率限制问题——应为此使用 ISRC。
手动操作:
resolved = {}
for i in range(0, len(isrcs), 25):
batch = isrcs[i : i + 25]
r = requests.get(
f"https://api.music.apple.com/v1/catalog/{storefront}/songs",
headers=headers,
params={"filter[isrc]": ",".join(batch)},
)
if r.status_code == 429:
break # later batches only extend the window
for song in r.json().get("data", []):
resolved.setdefault(song["attributes"]["isrc"], song["id"])
可用端点
目录(公开 — 仅限开发令牌)
| 端点 | 方法 | 描述 |
|---|---|---|
/catalog/{storefront}/search | GET | 搜索歌曲、专辑、艺人和播放列表 |
/catalog/{storefront}/songs/{id} | GET | 歌曲详情 |
/catalog/{storefront}/albums/{id} | GET | 专辑详情 |
/catalog/{storefront}/albums/{id}/tracks | GET | 专辑曲目 |
/catalog/{storefront}/artists/{id} | GET | 艺人详情 |
/catalog/{storefront}/artists/{id}/albums | GET | 艺人的专辑 |
/catalog/{storefront}/artists/{id}/songs | GET | 艺人的热门歌曲 |
/catalog/{storefront}/artists/{id}/related-artists | GET | 相似艺人 |
/catalog/{storefront}/playlists/{id} | GET | 播放列表详情 |
/catalog/{storefront}/charts | GET | 排行榜 |
/catalog/{storefront}/genres | GET | 所有流派 |
/catalog/{storefront}/search/suggestions | GET | 搜索自动补全 |
/catalog/{storefront}/stations/{id} | GET | 电台 |
资料库(需要用户令牌)
| 端点 | 方法 | 描述 |
|---|---|---|
/me/library/songs | GET | 资料库中的所有歌曲 |
/me/library/albums | GET | 资料库中的所有专辑 |
/me/library/artists | GET | 资料库中的所有艺人 |
/me/library/playlists | GET | 资料库中的所有播放列表 |
/me/library/playlists/{id} | GET | 播放列表详情 |
/me/library/playlists/{id}/tracks | GET | 播放列表曲目 |
/me/library/search | GET | 搜索资料库 |
/me/library | POST | 添加到资料库 |
/catalog/{sf}/songs/{id}/library | GET | 根据目录 ID 获取资料库 ID |
播放列表管理
| 端点 | 方法 | 描述 |
|---|---|---|
/me/library/playlists | POST | 创建播放列表 |
/me/library/playlists/{id}/tracks | POST | 向播放列表添加曲目 |
个性化
| Endpoint | Method | Description |
|---|---|---|
/me/recommendations | GET | 个性化推荐 |
/me/history/heavy-rotation | GET | 经常播放 |
/me/recent/played | GET | 最近播放 |
/me/recent/added | GET | 最近添加 |
评分
| Endpoint | Method | Description |
|---|---|---|
/me/ratings/songs/{id} | GET | 获取歌曲评分 |
/me/ratings/songs/{id} | PUT | 设置歌曲评分 |
/me/ratings/songs/{id} | DELETE | 移除评分 |
/me/ratings/albums/{id} | GET/PUT/DELETE | 专辑评分 |
/me/ratings/playlists/{id} | GET/PUT/DELETE | 播放列表评分 |
店面
| Endpoint | Method | Description |
|---|---|---|
/storefronts | GET | 所有店面 |
/storefronts/{id} | GET | 店面详情 |
/me/storefront | GET | 用户的店面 |
常用查询参数
| Parameter | Description | Example |
|---|---|---|
term | 搜索查询 | term=beatles |
types | 资源类型 | types=songs,albums |
limit | 每页结果数(最多 25 条) | limit=10 |
offset | 分页偏移量 | offset=25 |
include | 关联资源 | include=artists,albums |
extend | 附加属性 | extend=editorialNotes |
l | 语言代码 | l=en-US |
搜索示例
GET /v1/catalog/us/search?term=wonderwall&types=songs&limit=10
Response:
{
"results": {
"songs": {
"data": [{
"id": "1234567890",
"type": "songs",
"attributes": {
"name": "Wonderwall",
"artistName": "Oasis",
"albumName": "(What's the Story) Morning Glory?",
"durationInMillis": 258773,
"releaseDate": "1995-10-02",
"genreNames": ["Alternative", "Music"]
}
}]
}
}
}
以资料库优先的工作流(完整流程)
将目录中的歌曲添加到播放列表需要调用 4 次 API:
import requests
headers = {
"Authorization": f"Bearer {dev_token}",
"Music-User-Token": user_token
}
# 1. Search catalog
r = requests.get(
"https://api.music.apple.com/v1/catalog/us/search",
headers=headers,
params={"term": "Wonderwall Oasis", "types": "songs", "limit": 1}
)
catalog_id = r.json()['results']['songs']['data'][0]['id']
# 2. Add to library
requests.post(
"https://api.music.apple.com/v1/me/library",
headers=headers,
params={"ids[songs]": catalog_id}
)
# 3. Get library ID (catalog ID → library ID)
r = requests.get(
f"https://api.music.apple.com/v1/catalog/us/songs/{catalog_id}/library",
headers=headers
)
library_id = r.json()['data'][0]['id']
# 4. Add to playlist (library IDs only!)
requests.post(
f"https://api.music.apple.com/v1/me/library/playlists/{playlist_id}/tracks",
headers={**headers, "Content-Type": "application/json"},
json={"data": [{"id": library_id, "type": "library-songs"}]}
)
创建播放列表
POST /v1/me/library/playlists
Content-Type: application/json
{
"attributes": {
"name": "Road Trip",
"description": "Summer vibes"
},
"relationships": {
"tracks": {
"data": []
}
}
}
评分
# Love a song (value: 1 = love, -1 = dislike)
PUT /v1/me/ratings/songs/{id}
Content-Type: application/json
{"attributes": {"value": 1}}
限制
- 无法控制播放 - API 无法播放、暂停或跳过
- 播放列表编辑 - 只能修改通过 API 创建的播放列表
- 令牌管理 - 开发者令牌每 180 天过期
- 速率限制 - Apple 会限制请求频率
常见错误
❌ 在播放列表中使用目录 ID:
# WRONG
json={"data": [{"id": "1234567890", "type": "songs"}]}
修正方法: 先添加到资料库,获取资料库 ID,然后再添加。
❌ 通过 AppleScript 播放目录歌曲:
# WRONG
play track id "1234567890"
修正方法: 歌曲必须位于资料库中。
❌ 未转义的 AppleScript 字符串:
# WRONG
name = "Rock 'n Roll"
script = f'tell application "Music" to play playlist "{name}"'
修正方法: 转义引号。
❌ 令牌已过期: 开发者令牌最长有效期为 180 天。 修正方法: 检查过期时间,并处理 401 错误。
更简单的方式:applemusic-mcp
applemusic-mcp MCP 服务会自动处理所有这些复杂事项:AppleScript 转义、令牌管理、资料库优先工作流以及 ID 转换。
安装:
git clone https://github.com/epheterson/applemusic-mcp.git
cd applemusic-mcp && python3 -m venv venv && source venv/bin/activate
pip install -e .
配置 Claude Desktop:
{
"mcpServers": {
"Apple Music": {
"command": "/path/to/applemusic-mcp/venv/bin/python",
"args": ["-m", "applemusic_mcp"]
}
}
}
在 macOS 上,大多数功能都可以立即使用。如需使用目录功能或在 Windows/Linux 上使用,请参阅仓库 README。
| 手动操作 | applemusic-mcp |
|---|---|
| 添加歌曲需要调用 4 次 API | playlist(action="add", auto_add=True) |
| 复制 URL 并在 Music 中打开 | playback(action="play", url="...") |
| 将目录歌曲添加到资料库 | library(action="add")(统一 API — 开发者令牌或 Web 令牌) |
| 手动跟踪资料库变更 | library(action="snapshot") |
| AppleScript 转义 | 自动处理 |
| 令牌管理 | 自动处理并提供警告 |