用 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 请求。把响应解析和错误提示写好,后面做歌单工具、推荐页面都能直接用。