开发技巧

用 Apple Music API 搜索曲库并导出 Markdown

用 Apple Music API 在曲库里搜歌,结果整理成 Markdown 表格(歌名、歌手、专辑、时长、链接),可以直接贴进笔记或文章。只需要 A 篇的开发者令牌,不需要用户登录。

BINBHO · 图文教程

用 Apple Music API 在曲库里搜歌,结果整理成 Markdown 表格(歌名、歌手、专辑、时长、链接),可以直接贴进笔记或文章。只需要 A 篇的开发者令牌,不需要用户登录。

先看清成品

pytest 跑了 2 个测试,全部通过:用一个按官方 SearchResponse 结构写的 fixture,验证请求地址、参数和 Authorization 头;验证时长格式化;歌名里有 | 时会被转义;storefront 和 limit 的校验;401 时给出可读的提示。用一个无效令牌请求真实接口,返回 HTTP/2 401、content-length: 0,脚本按预期给出了提示。没有用有效令牌拿到过真实的搜索结果。

注意:Apple Music API 和 MusicKit 要求你是 Apple Developer Program 会员(会员费以官方页面为准,本文不列数字);完整播放还需要听众自己有 Apple Music 订阅。只使用你所在地区的 storefront,不要用它绕过地区限制;不要下载、录制或转存音频;不要共享 Apple ID 或 .p8 私钥。

做完之后你会得到什么?

成果 说明
search_catalog.py 搜索、解析,输出 Markdown 表格
参数校验 storefront 必须是两位小写字母,limit 范围 1–25
容错 缺少时长等字段时不报错

需要准备哪些环境?

项目 要求 说明
开发者令牌 用 A 篇的脚本生成 放进环境变量 APPLE_MUSIC_DEV_TOKEN
Python 3.9 以上 pip install -r requirements.txt

文件结构

32-apple-music-B-src/
├── search_catalog.py
├── test_search.py
├── fixtures/search.json
└── requirements.txt

第 1 步:接口

项目 内容(官方参考文档 Search for Catalog Resources)
地址 GET https://api.music.apple.com/v1/catalog/{storefront}/search
必填参数 term、types(例如 songs、albums、artists、playlists)
limit 默认 5,最大 25
返回 results.songs.data[],每一项的 attributes 里有 name、artistName、albumName、durationInMillis、url

storefront 填你自己所在国家或地区的代码。不同 storefront 的曲库不一样,不要用别的地区的 storefront 去查看你所在地区没有上架的内容。

第 2 步:脚本

search_catalog.py:

"""在 Apple Music 曲库里按关键词搜歌,导出 Markdown 表格(歌名、歌手、专辑、时长、链接)。
接口:GET https://api.music.apple.com/v1/catalog/{storefront}/search?term=...&types=songs&limit=...
limit 默认 5,最大 25(官方参考文档)。只需要开发者令牌,不需要用户登录。
用法:APPLE_MUSIC_DEV_TOKEN=... python search_catalog.py cn "周杰伦 晴天" 10"""
import os, re, sys
import requests

API = "https://api.music.apple.com"

def search(term, storefront, limit=10, token=None, api=API, session=requests):
    if not re.fullmatch(r"[a-z]{2}", storefront):
        raise ValueError("storefront 是两位小写国家/地区代码,比如 cn、us、jp")
    if not 1 <= limit <= 25: raise ValueError("limit 范围是 1–25")
    r = session.get(f"{api}/v1/catalog/{storefront}/search",
                    params={"term": term, "types": "songs", "limit": limit},
                    headers={"Authorization": f"Bearer {token}"}, timeout=15)
    if r.status_code == 401: raise PermissionError("401:开发者令牌无效、过期,或者签名用的密钥不对")
    r.raise_for_status()
    songs = r.json().get("results", {}).get("songs", {}).get("data", [])
    return [row(s) for s in songs]

def row(s):
    a = s.get("attributes", {})
    ms = a.get("durationInMillis") or 0
    return {"name": a.get("name", ""), "artist": a.get("artistName", ""), "album": a.get("albumName", ""),
            "length": f"{ms // 60000}:{ms // 1000 % 60:02d}", "url": a.get("url", ""), "id": s.get("id", "")}

def to_markdown(rows):
    esc = lambda x: str(x).replace("|", "\\|")
    out = ["| # | 歌名 | 歌手 | 专辑 | 时长 | 链接 |", "| ---: | :--- | :--- | :--- | ---: | :--- |"]
    out += [f"| {i} | {esc(r['name'])} | {esc(r['artist'])} | {esc(r['album'])} | {r['length']} | [打开]({r['url']}) |"
            for i, r in enumerate(rows, 1)]
    return "\n".join(out)

if __name__ == "__main__":
    sf, term = sys.argv[1], sys.argv[2]
    n = int(sys.argv[3]) if len(sys.argv) > 3 else 10
    print(to_markdown(search(term, sf, n, token=os.environ["APPLE_MUSIC_DEV_TOKEN"])))

第 3 步:测试

fixtures/search.json:

{"results":{"songs":{"href":"/v1/catalog/cn/search?limit=2&term=demo&types=songs","data":[
 {"id":"100000001","type":"songs","attributes":{"name":"示例歌曲 | 现场版","artistName":"示例歌手","albumName":"示例专辑","durationInMillis":269000,"url":"https://music.apple.com/cn/album/x/1?i=100000001"}},
 {"id":"100000002","type":"songs","attributes":{"name":"没有时长的歌","artistName":"示例歌手","albumName":"示例专辑","url":"https://music.apple.com/cn/album/x/1?i=100000002"}}
]}}}

test_search.py:

import json, pathlib, pytest
from search_catalog import search, to_markdown

class Resp:
    def __init__(self, code, body=None): self.status_code, self._b = code, body
    def json(self): return self._b
    def raise_for_status(self):
        if self.status_code >= 400: raise RuntimeError(self.status_code)

class Sess:
    def __init__(self, resp): self.resp, self.calls = resp, []
    def get(self, url, **kw): self.calls.append((url, kw)); return self.resp

FIX = json.loads(pathlib.Path(__file__).with_name("fixtures").joinpath("search.json").read_text())

def test_parse_and_request():
    s = Sess(Resp(200, FIX))
    rows = search("demo", "cn", 2, token="T", session=s)
    url, kw = s.calls[0]
    assert url.endswith("/v1/catalog/cn/search") and kw["params"] == {"term": "demo", "types": "songs", "limit": 2}
    assert kw["headers"]["Authorization"] == "Bearer T"
    assert rows[0]["length"] == "4:29" and rows[1]["length"] == "0:00"
    assert "示例歌曲 \\| 现场版" in to_markdown(rows)

def test_validation_and_401():
    with pytest.raises(ValueError): search("x", "CN", token="T", session=Sess(Resp(200, FIX)))
    with pytest.raises(ValueError): search("x", "cn", 50, token="T", session=Sess(Resp(200, FIX)))
    with pytest.raises(PermissionError): search("x", "cn", token="bad", session=Sess(Resp(401)))
pip install -r requirements.txt
python -m pytest -q      # 2 passed

第 4 步:真实搜索(本机没有有效令牌,未执行)

export APPLE_MUSIC_DEV_TOKEN=$(python ../32-apple-music-A-src/make_token.py AuthKey_XXXX.p8 KEYID TEAMID 1)
python search_catalog.py cn "晴天" 5

预期输出一张 5 行的 Markdown 表格,每一行最后都有一个指向 music.apple.com 的链接。

调试示例

令牌无效时,响应体是空的

$ curl -i 'https://api.music.apple.com/v1/catalog/cn/search?term=a&types=songs' -H 'Authorization: Bearer x'
HTTP/2 401
content-length: 0

如果直接调用 r.json(),会得到一个让人看不懂的 JSON 解析错误。所以脚本先判断 401,再给出中文提示:

PermissionError: 401:开发者令牌无效、过期,或者签名用的密钥不对

C 篇在浏览器里也遇到了同样的问题。

常见问题

现象 原因 处理
结果为空 这个 storefront 里没有这首歌,或者关键词太长 换个关键词;不要改用别的地区
400 types 写错了 只用参考文档里列出的取值
429 请求太快 降低频率
表格错位 歌名里有 ` `

模板规则

  • 令牌从环境变量读取,不写进代码。
  • storefront 用你自己所在的地区。
  • 先判断状态码,再解析响应体。

项目资源

用途 地址
Search for Catalog Resources https://developer.apple.com/documentation/applemusicapi/search-for-catalog-resources-(by-type)
Storefronts and Localization https://developer.apple.com/documentation/applemusicapi/storefronts-and-localization
Generating Developer Tokens https://developer.apple.com/documentation/applemusicapi/generating-developer-tokens
Apple 媒体服务条款 https://www.apple.com/legal/internet-services/itunes/

曲库搜索是 Apple Music API 最基础的用法:一个令牌,一个 GET 请求。把响应解析和错误提示写好,后面做歌单工具、推荐页面都能直接用。