mcpField
A
apple-music

apple-music

github.com/epheterson其他12351 次安装5 天前更新
代码仓库

关于 apple-music

Apple Music 集成指南,涵盖 AppleScript、UI 自动化和 MusicKit API。可用于播放控制、资料库与播放列表操作及目录搜索,并说明将目录歌曲加入播放列表前须先加入资料库。

主要功能

  1. 01通过 AppleScript 控制播放与资料库
  2. 02管理播放列表和曲目
  3. 03搜索并播放音乐目录
  4. 04集成 MusicKit API
  5. 05UI 自动化操作指南

适用场景

  1. 01控制播放并查看当前曲目
  2. 02创建播放列表并管理曲目
  3. 03搜索目录或将歌曲加入资料库
  4. 04播放目录曲目并切换 AirPlay 输出

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/epheterson/applemusic-mcp --skill apple-music

安装后在 Claude Code 中输入/apple-music确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

LICENSE
.gitignore
.secrets-allowlist
Makefile
README.md
CHANGELOG.md
SECURITY.md
RELEASING.md
config.example.json
pyproject.toml
server.json
.github/
dependabot.yml
.github/workflows/
codeql.yml
scorecard.yml
release.yml
test.yml
publish.yml
scripts/
check_live_env.py
check_versions.py
preflight-ui.sh
preflight.sh
integration_test.py
integration_test_applescript.py
validate_endpoints.py
src/applemusic_mcp/
__init__.py
__main__.py
amp_api.py
paths.py
audit_log.py
musickit_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 seconds30 秒子进程超时已触发——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:

  1. open "music://…/album/<id>"(专辑/播放列表)或 …?i=<songId>(单首曲目)
  2. **专辑/播放列表:**找到 Play 按钮,读取其位置和大小,然后在其中心位置使用 CoreGraphics 单击。
  3. **单首曲目:**按名称在曲目列表中匹配曲目行,使用 CoreGraphics 双击(这样只会播放该曲目,而非整张专辑)。

悬停技巧(每行控件)

将鼠标悬停在曲目行上之前,Music.app 会隐藏每首曲目的播放/“添加到资料库”控件:

  1. 通过 System Events 查找曲目 UI 元素的位置
  2. 通过 CoreGraphics 将鼠标移至该位置(生成真实的悬停事件)
  3. 隐藏的 checkbox(播放)和 button(添加到资料库)会出现在辅助功能树中
  4. 在其坐标处通过 CoreGraphics 点击(不要使用 System Events AXPress)

通过 UI 搜索

  1. 设置搜索栏的值:set value of searchField to "query"
  2. 按 Return:key code 36
  3. 等待结果加载(约 4 秒)
  4. 从 scroll area 2 解析“热门结果”列表
  5. 每个结果都是一个 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):搜索自动补全弹出层位于辅助功能树中,且目录深层链接已无法跳转。请使用弹出层:
    1. 使用规范曲名进行 UI 搜索(§ 通过 UI 搜索)
    2. 选择名称和艺人匹配的第一个 Song 结果——不要回退到非 Song 结果;如果没有匹配的 Song,请正常失败。搜索状态过期可能导致点击错误结果。
    3. 点击该行 → 跳转到歌曲详情页
    4. 使用 CoreGraphics 将鼠标悬停在曲目行上——此时隐藏的“添加到资料库”按钮将变为可访问。如果找到的是“下载”按钮,则表示曲目已在资料库中(这是成功,而非失败)。
    5. 通过真实的 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)并设置令牌。

身份验证

要求:

  1. Apple Developer 账户
  2. 从 开发者门户 获取 MusicKit 密钥(.p8 文件)
  3. 开发者令牌(JWT,最长 180 天)
  4. 用户音乐令牌(浏览器 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}/searchGET搜索歌曲、专辑、艺人和播放列表
/catalog/{storefront}/songs/{id}GET歌曲详情
/catalog/{storefront}/albums/{id}GET专辑详情
/catalog/{storefront}/albums/{id}/tracksGET专辑曲目
/catalog/{storefront}/artists/{id}GET艺人详情
/catalog/{storefront}/artists/{id}/albumsGET艺人的专辑
/catalog/{storefront}/artists/{id}/songsGET艺人的热门歌曲
/catalog/{storefront}/artists/{id}/related-artistsGET相似艺人
/catalog/{storefront}/playlists/{id}GET播放列表详情
/catalog/{storefront}/chartsGET排行榜
/catalog/{storefront}/genresGET所有流派
/catalog/{storefront}/search/suggestionsGET搜索自动补全
/catalog/{storefront}/stations/{id}GET电台
资料库(需要用户令牌)
端点方法描述
/me/library/songsGET资料库中的所有歌曲
/me/library/albumsGET资料库中的所有专辑
/me/library/artistsGET资料库中的所有艺人
/me/library/playlistsGET资料库中的所有播放列表
/me/library/playlists/{id}GET播放列表详情
/me/library/playlists/{id}/tracksGET播放列表曲目
/me/library/searchGET搜索资料库
/me/libraryPOST添加到资料库
/catalog/{sf}/songs/{id}/libraryGET根据目录 ID 获取资料库 ID
播放列表管理
端点方法描述
/me/library/playlistsPOST创建播放列表
/me/library/playlists/{id}/tracksPOST向播放列表添加曲目
个性化
EndpointMethodDescription
/me/recommendationsGET个性化推荐
/me/history/heavy-rotationGET经常播放
/me/recent/playedGET最近播放
/me/recent/addedGET最近添加
评分
EndpointMethodDescription
/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播放列表评分
店面
EndpointMethodDescription
/storefrontsGET所有店面
/storefronts/{id}GET店面详情
/me/storefrontGET用户的店面

常用查询参数

ParameterDescriptionExample
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 次 APIplaylist(action="add", auto_add=True)
复制 URL 并在 Music 中打开playback(action="play", url="...")
将目录歌曲添加到资料库library(action="add")(统一 API — 开发者令牌或 Web 令牌)
手动跟踪资料库变更library(action="snapshot")
AppleScript 转义自动处理
令牌管理自动处理并提供警告

评论

支持 Markdown