将 Spotify 当前播放接口替换为 Last.fm
之前博客里有一个 spotify.datehoer.com 的小接口,用来给页面展示当前正在听的歌。最开始的实现是后端拿 Spotify refresh token 换 access token,然后请求 Spotify Web API 的 /me/player/currently-playing。
这次迁移的原因不是 Nginx、DNS 或服务部署问题,而是 Spotify 上游直接拒绝了请求。
问题现象
后端请求 Spotify token 是成功的:
TOKEN_STATUS 200
TOKEN_SCOPE user-read-currently-playing
但是继续请求播放器接口时,几个相关接口都返回 403:
/currently-playing -> 403
/player -> 403
/recently-played -> 403
Spotify 返回的原文是:
Active premium subscription required for the owner of the app.
When the subscription status changes, it can take a few hours before requests are allowed again.
意思是创建这个 Spotify Developer App 的 owner 账号需要有效 Premium。后端拿 token 没问题,但是读取播放状态时会被 Spotify 拒绝。最终表现就是接口只能返回默认的 Not Playing。
替换方案
后来改成使用 Last.fm。
只要 Spotify 已经关联 Last.fm,播放记录会被同步到 Last.fm。后端请求 Last.fm 的 user.getRecentTracks,看第一条记录是否带有:
{
"@attr": {
"nowplaying": "true"
}
}
如果有,就认为当前正在播放;如果没有,就返回 Not Playing。
这样做的好处是:
- 不再依赖 Spotify Premium。
- 不再需要 Spotify refresh token。
- 读最近播放、排行榜、用户统计都可以通过 Last.fm 的公开接口完成。
- 原来的
/currently-playing返回结构可以保持不变,前端不用改。
部署位置
服务部署在:
/srv/projects/spotify_listening
主程序:
/srv/projects/spotify_listening/spotify_listening_app.py
systemd 服务:
spotify-listening.service
监听地址:
127.0.0.1:7723
Nginx/Cloudflare 对外域名:
https://spotify.datehoer.com
这次确认后,Cloudflare 的 spotify.datehoer.com A 记录指向:
107.173.237.30
之前迁移时也有一台 107.174.71.101,但这次最终使用的是 107.173.237.30 这台机器上的 /srv/projects/spotify_listening。
需要注意:这个目录权限是 root:root 700,普通用户直接 ls /srv/projects/spotify_listening 会看不到里面内容,所以容易误以为项目没有迁移过来。
当前接口
接口文档
FastAPI 自带 Swagger 文档:
https://spotify.datehoer.com/docs
OpenAPI JSON:
https://spotify.datehoer.com/openapi.json
另外加了一个轻量接口目录:
https://spotify.datehoer.com/endpoints
用于快速查看当前有哪些接口、参数和用途。
当前播放
GET /currently-playing
这个接口保留了原来前端使用的字段:
{
"isPlaying": true,
"title": "Heartbeat",
"artist": "Marcus & Martinus",
"album": "Together",
"albumImageUrl": "https://lastfm.freetls.fastly.net/i/u/300x300/xxx.png",
"songUrl": "https://www.last.fm/music/..."
}
如果当前没有正在播放,则返回:
{
"isPlaying": false,
"title": "Not Playing",
"artist": "Last.fm",
"album": "Last.fm",
"albumImageUrl": "https://oss.datehoer.com/blog/Spotify_Logo.png",
"songUrl": ""
}
最近播放
GET /recent-tracks?limit=10
参数:
limit: 1-50,默认 10
返回最近 scrobble 的歌曲列表。第一首如果是当前播放,会有:
{
"nowPlaying": true,
"playedAt": null
}
排行榜
GET /top/tracks?period=7day&limit=10
GET /top/artists?period=7day&limit=10
GET /top/albums?period=7day&limit=10
排行榜接口的 period 支持:
overall
7day
1month
3month
6month
12month
limit 支持:
1-50,默认 10
Loved Tracks
GET /loved-tracks?limit=10
返回 Last.fm 用户喜欢的歌曲。
Profile
GET /profile
返回 Last.fm 用户信息和统计,例如:
{
"name": "johwueubu",
"url": "https://www.last.fm/user/johwueubu",
"playcount": 100,
"artistCount": 20,
"trackCount": 50,
"albumCount": 30
}
Summary
GET /summary?period=7day&limit=5
一次性返回:
profile
currentlyPlaying
recentTracks
topTracks
topArtists
topAlbums
这个接口适合首页、卡片组件、dashboard 一类场景,避免前端同时请求多个接口。
Health
GET /health
用于服务监控:
{
"ok": true,
"source": "last.fm",
"user": "johwueubu",
"checkedAt": "2026-05-22T01:06:28Z"
}
实现要点
核心逻辑是请求 Last.fm:
def lastfm_request(method, **params):
request_params = {
"method": method,
"user": LASTFM_USERNAME,
"api_key": LASTFM_API_KEY,
"format": "json",
**params,
}
response = requests.get(LASTFM_API_URL, params=request_params, timeout=10)
response.raise_for_status()
return response.json()
当前播放使用:
data = lastfm_request("user.getrecenttracks", limit=1)
然后判断第一首歌:
nowplaying = track.get("@attr", {}).get("nowplaying") == "true"
如果 nowplaying 为 true,就转换成前端兼容的旧格式;否则返回 Not Playing。
注意事项
Last.fm API key 可以用于公开读取接口,但 shared secret 不应该放在前端或公开文章里。当前这个服务只需要读取公开数据,不需要签名写操作,所以没有必要暴露 shared secret。
另外,Last.fm 的接口在当前播放存在时,user.getRecentTracks 可能会在请求 limit=2 时返回当前播放加两条历史记录。服务层已经做了裁剪,确保 /recent-tracks?limit=2 最终只返回 2 条。
验证
迁移后测试:
curl https://spotify.datehoer.com/currently-playing
curl https://spotify.datehoer.com/recent-tracks?limit=2
curl "https://spotify.datehoer.com/summary?period=7day&limit=2"
/docs、/openapi.json、/endpoints 都已经可以访问。
最终结论:这个接口现在已经从 Spotify Web API 切换到了 Last.fm。实时播放依赖 Last.fm scrobble 状态,不再受 Spotify Premium 限制。