需求
在博客首页展示我正在听的音乐,包括:
- 歌曲名、歌手
- 专辑封面
- 实时同步
网易云 api
申请不到api,能申请到可以试试
Spotify 弃之
最开始想到的是 Spotify 官方 API,流程清晰,文档完善。但web api 需要 Premium 会员。免费账号拿不到实时播放状态,放弃。
Last.fm
Last.fm 是一个音乐社交平台,支持 Scrobble(播放记录同步):
- Web Scrobbler 浏览器插件可以监听 YouTube、Bilibili、网页版网易云等多个平台的播放状态,自动同步到 Last.fm
Web Scrobbler 在插件市场有多个,这个可以work,有的不能用
- Last.fm 提供免费 API
user.getRecentTracks,可以获取最近播放 - 完全免费,不需要会员
绑定步骤
- 注册 Last.fm 账号
- 申请 API Key,填写应用名即可
- 在cloudfare pages 环境变量中添加
NEXT_PUBLIC_LASTFM_API_KEY和NEXT_PUBLIC_LASTFM_USERNAME
music-key
- 安装 Web Scrobbler 浏览器插件(支持 Edge、Chrome、Firefox)
- 在插件中登录 Last.fm 账号
- 播放支持的平台(YouTube、Bilibili、网页版网易云等),去 Last.fm 个人主页确认记录已同步
Last.fm
实现
客户端获取数据
由于是静态博客,服务器端没有 Node.js 运行时环境。选择在客户端直接请求 Last.fm API:
async function getLastfmTrack() {
const username = process.env.NEXT_PUBLIC_LASTFM_USERNAME
const apiKey = process.env.NEXT_PUBLIC_LASTFM_API_KEY
if (!username || !apiKey) return null
const res = await fetch(
`https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${username}&api_key=${apiKey}&format=json&limit=1`
)
const data = await res.json()
const track = data.recenttracks?.track?.[0]
if (!track) return null
return {
title: track.name,
artist: track.artist?.['#text'],
albumArt: track.image?.[3]?.['#text'] || null,
dateUts: track.date?.uts ? Number(track.date.uts) : Math.floor(Date.now() / 1000),
}
}
封面图片 Fallback
Last.fm 的封面图依赖用户提交的 metadata,很多歌曲没有封面。借助 iTunes Search API 作为 fallback:
async function getItunesArtwork(title: string, artist: string) {
const term = encodeURIComponent(`${title} ${artist}`)
const res = await fetch(
`https://itunes.apple.com/search?term=${term}&entity=song&limit=1`
)
const data = await res.json()
const artwork = data.results?.[0]?.artworkUrl100
if (!artwork) return null
// Convert 100x100 to 600x600 for higher quality
return artwork.replace('100x100', '600x600')
}
播放时间气泡
用一个时间戳气泡展示这首歌是多久之前播放的:
function getTimeAgo(uts: number) {
const diff = Math.floor(Date.now() / 1000) - uts
if (diff < 60) return { label: 'now', isNow: true }
if (diff < 3600) return { label: `${Math.floor(diff / 60)}m ago`, isNow: false }
if (diff < 86400) return { label: `${Math.floor(diff / 3600)}h ago`, isNow: false }
return { label: `${Math.floor(diff / 86400)}d ago`, isNow: false }
}
这里的时间戳 uts 来自 Last.fm API 返回的真实播放时间 track.date?.uts,而不是当前时间。这样气泡显示的就是「这首歌 X 分钟前在网易云播放的」。
无数据时的占位符
在没有获取到数据之前(API 还在请求中或请求失败),播放器显示骨架屏占位符:
{isLoaded && lastfmTrack ? (
<NowPlaying favoriteSong={lastfmTrack} latestPostDate={latestPostDate} />
) : (
<NowPlayingLoading />
)}
NowPlayingLoading 是一个纯 UI 占位符组件,包含专辑封面槽位、歌曲名和歌手名的骨架块,以及底部的状态栏占位。
最终效果
首页顶部显示最近播放的歌曲,附带专辑封面和 X 分钟前 气泡。封面优先从 Last.fm 获取(基本获取不到),缺失则从 iTunes 自动补全。数据在客户端获取,请求完成前显示骨架屏占位符。
总结
整个方案不需要服务器端代码,不依赖后端服务,纯客户端实现。缺点是首次加载时封面可能有延迟(依赖 iTunes API 响应),优点是部署简单、维护成本低。
如果你也在做个人博客的音乐同步需求,Last.fm + iTunes Fallback 是个可行且免费的方案。