效率工具

用 Spotify 官方嵌入播放器制作可切换的推荐列表

不需要登录、不需要 token,做一个“我的推荐”页面:Python 脚本用 Spotify oEmbed 把一组分享链接转成标题、封面和 URI;页面用官方的 iFrame API 嵌入播放器,点击列表就能切换内容,并显示播放进度。

BINBHO · 图文教程

不需要登录、不需要 token,做一个“我的推荐”页面:Python 脚本用 Spotify oEmbed 把一组分享链接转成标题、封面和 URI;页面用官方的 iFrame API 嵌入播放器,点击列表就能切换内容,并显示播放进度。

先看清成品

本机真实调用了 oEmbed:两个链接分别返回 “Never Gonna Give You Up” 和 “Whenever You Need Somebody”;一个不存在的 ID 返回 404,被跳过;一个非 Spotify 链接被拒绝;一个 intl-ja 形式的重复链接被识别出来(见调试示例)。用无头 Chrome 打开页面,播放器触发了 ready 事件,点击第二项后 iframe 变成了 embed/album/6eUW0wxWtzkFdaEFsTJto6。pytest 2 个测试通过。没有测试实际播放:未登录时 Embed 一般只能播放预览片段,具体以官方说明为准。

注意:Spotify 订阅价格以官方页面为准,本文不列数字。官方开发者文档写明:开发模式(Development Mode)应用的所有者必须有有效的 Spotify Premium,每个应用最多 5 个授权用户,并且只开放部分接口,用于个人和非商业的学习项目。不要下载或录制音频,不要绕过 DRM 或地区限制,不要共享账号;只有你加入名单的用户才能登录你的应用。

做完之后你会得到什么?

成果 说明
build_list.py 链接 → URI,去掉 ?si= 和 intl-xx,用 oEmbed 取标题和封面,去重
index.html iFrame API 播放器,加上可以点击的列表和进度显示
e2e.py 检查 ready 事件和切换功能

需要准备哪些环境?

项目 要求 说明
Python 3.8 以上 只用标准库
静态服务器 任意 页面需要用 fetch 读取 items.json

文件结构

31-spotify-C-src/
├── build_list.py
├── test_build.py
├── links.txt
├── items.json      # 本机生成的结果
├── index.html
└── e2e.py

第 1 步:生成列表

build_list.py:

"""用 Spotify oEmbed(无需 token)把一组链接转成 items.json:标题、封面、URI。
用法:python build_list.py links.txt items.json"""
import json, re, sys, urllib.parse, urllib.request, urllib.error

PAT = re.compile(r"^https://open\.spotify\.com/(?:intl-[a-z-]+/)?(track|album|playlist|episode|show)/([A-Za-z0-9]{22})")

def to_uri(link):
    m = PAT.match(link.strip())
    if not m: raise ValueError(f"不是 Spotify 链接:{link}")
    return f"spotify:{m.group(1)}:{m.group(2)}", f"https://open.spotify.com/{m.group(1)}/{m.group(2)}"

def oembed(url, opener=urllib.request.urlopen):
    q = "https://open.spotify.com/oembed?" + urllib.parse.urlencode({"url": url})
    with opener(urllib.request.Request(q, headers={"User-Agent": "embed-list/1.0"}), timeout=15) as r:
        return json.load(r)

def build(links, fetch=oembed):
    items, errors, seen = [], [], set()
    for link in links:
        try:
            uri, clean = to_uri(link)
            if uri in seen: errors.append(f"{link.strip()} → 重复"); continue
            seen.add(uri)
            d = fetch(clean)
            items.append({"uri": uri, "title": d.get("title", uri), "thumb": d.get("thumbnail_url", "")})
        except (ValueError, urllib.error.HTTPError) as e:
            errors.append(f"{link.strip()} → {e}")
    return items, errors

if __name__ == "__main__":
    links = [l for l in open(sys.argv[1], encoding="utf-8") if l.strip() and not l.startswith("#")]
    items, errors = build(links)
    json.dump(items, open(sys.argv[2], "w", encoding="utf-8"), ensure_ascii=False, indent=1)
    print(f"{len(items)} 条写入 {sys.argv[2]}")
    for e in errors: print("SKIP", e)

links.txt:

# 每行一个 open.spotify.com 链接(分享菜单 → 复制链接)
https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT?si=abc123
https://open.spotify.com/intl-ja/track/4cOdK2wGLETKBW3PvgPWqT
https://open.spotify.com/album/6eUW0wxWtzkFdaEFsTJto6
https://open.spotify.com/track/0000000000000000000000
https://music.example.com/song/1
$ python build_list.py links.txt items.json
2 条写入 items.json
SKIP https://open.spotify.com/intl-ja/track/4cOdK2wGLETKBW3PvgPWqT → 重复
SKIP https://open.spotify.com/track/0000000000000000000000 → HTTP Error 404: Not Found
SKIP https://music.example.com/song/1 → 不是 Spotify 链接:https://music.example.com/song/1

第 2 步:页面

index.html:

<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>我的推荐歌单</title>
<style>body{font-family:sans-serif;max-width:760px;margin:24px auto}ul{list-style:none;padding:0;display:flex;gap:12px;flex-wrap:wrap}
button.item{display:flex;gap:8px;align-items:center;border:1px solid #ddd;background:#fff;padding:6px;cursor:pointer}button.item img{width:40px;height:40px}
button.item[aria-current=true]{border-color:#1db954}</style></head>
<body>
<h1>我的推荐</h1>
<div id="embed"></div>
<p id="status">加载中…</p>
<ul id="list"></ul>
<script>
const fmt = (ms) => { const s = Math.floor(ms / 1000); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; };
window.onSpotifyIframeApiReady = async (IFrameAPI) => {
  const items = await (await fetch("items.json")).json();
  if (!items.length) { document.getElementById("status").textContent = "items.json 为空"; return; }
  IFrameAPI.createController(document.getElementById("embed"), { uri: items[0].uri, width: "100%", height: 352 }, (ctrl) => {
    window.__ctrl = ctrl;
    ctrl.addListener("ready", () => { window.__ready = true; document.getElementById("status").textContent = "播放器已就绪"; });
    ctrl.addListener("playback_update", (e) => {
      const { position, duration, isPaused } = e.data;
      document.getElementById("status").textContent = `${isPaused ? "暂停" : "播放中"} ${fmt(position)} / ${fmt(duration)}`;
    });
    const ul = document.getElementById("list");
    items.forEach((it, i) => {
      const b = document.createElement("button"); b.className = "item"; b.setAttribute("aria-current", i === 0);
      const img = document.createElement("img"); img.src = it.thumb; img.alt = "";
      const span = document.createElement("span"); span.textContent = it.title;
      b.append(img, span);
      b.onclick = () => { ctrl.loadUri(it.uri); ul.querySelectorAll("button").forEach((x) => x.setAttribute("aria-current", x === b)); };
      const li = document.createElement("li"); li.append(b); ul.append(li);
    });
  });
};
</script>
<script src="https://open.spotify.com/embed/iframe-api/v1" async></script>
</body>
</html>

要点(来自 iFrame API 参考文档):

  • 先定义 window.onSpotifyIframeApiReady,再加载 https://open.spotify.com/embed/iframe-api/v1。
  • IFrameAPI.createController(element, options, callback) 会把 element 替换成播放器 iframe。
  • controller.loadUri(uri) 用来切换内容;事件有 ready、playback_started、playback_update(e.data 里有 position、duration、isPaused)。

第 3 步:测试

python -m pytest -q          # 2 passed
pip install playwright && python e2e.py

本机输出:

status: 播放器已就绪
iframe: https://open.spotify.com/embed/track/4cOdK2wGLETKBW3PvgPWqT
after click: https://open.spotify.com/embed/album/6eUW0wxWtzkFdaEFsTJto6

调试示例

同一首歌出现了两次

第一版脚本没有去重。links.txt 里有一个带 ?si= 的链接和一个 intl-ja 的链接,看起来不同,其实是同一首歌。运行后 items.json 里有两条一模一样的 “Never Gonna Give You Up”。正则先把两种链接都还原成 spotify:track:4cOdK2wGLETKBW3PvgPWqT,再按 URI 去重,第二条就被标记为“重复”跳过了。测试里也加了这种情况。

常见问题

现象 原因 处理
页面一直显示“加载中…” onSpotifyIframeApiReady 定义得太晚 先定义回调,再加载脚本(带 async)
items.json 读不到 用 file:// 打开了页面 用静态服务器打开
只能播放片段 访客没有登录 Spotify 正常现象,引导访客去 Spotify 收听完整版
oEmbed 返回 404 链接错了,或者内容已下架 重新复制分享链接

模板规则

  • 嵌入只用官方的 Embed 和 iFrame API,不要下载或转存音频。
  • 链接先规范成 URI,再去重。
  • 外部数据用 textContent 输出。
  • 出错的链接跳过并说明原因,不要让整个列表生成失败。

项目资源

用途 地址
iFrame API 参考 https://developer.spotify.com/documentation/embeds/references/iframe-api
Embeds 总览 https://developer.spotify.com/documentation/embeds
oEmbed https://developer.spotify.com/documentation/embeds/reference/oembed
设计与品牌指南 https://developer.spotify.com/documentation/design
开发者条款 https://developer.spotify.com/terms

嵌入播放器是最轻量的接入方式,不需要应用审核,也不受开发模式名单的限制。用 iFrame API 控制切换和显示进度,一个静态页面就够用了。