1. 介绍
- Navidrome 是一个轻量级且功能强大的自托管音乐流媒体服务器,它可以帮助我们轻松管理和播放个人音乐库,GitHub地址:https://github.com/navidrome/navidrome
- APlayer是一款简洁而美观的音乐播放器,它能够通过 API 支持直接播放音频文件,APlayer 通常与MetingJS结合调用网易云音乐或者腾讯音乐的服务器,但是网易云音乐的版权太少,腾讯音乐上随便一首歌都需要会员。
如果将 APlayer 与自建的 Navidrome 服务结合,我们就能在网页中快速实现一个完备的音乐分享和播放系统。
秉持着代码能抄绝不自己写的原则,我在网上找了一圈,没有现成的代码可抄,貌似Navidrome有点小众了,没办法只能自己写了,好在应该不难,只要从Navidrome分享的链接中拿到歌曲信息,再传给APlayer解析就行了。
Navifrome GitHub地址:https://github.com/navidrome/navidrome
APlayer GitHub地址:https://github.com/DIYgod/APlayer
MetingJS GitHub地址:https://github.com/metowolf/MetingJS
2. 前置条件
- 你需要有一个已部署的 Navidrome 服务。如果你还没有部署 Navidrome,请参考 Navidrome 官方文档进行安装和配置。
- 确保你有一个能够访问到自建 Navidrome 服务器的 URL,并分享一个歌单,比如我分享的歌单的URL链接
https://music.aleft.top/share/5bwX7WCYrB。
3. 代码编写
3.1 嵌入 APlayer 音乐播放器
APlayer 是一款非常简洁的网页音乐播放器,支持丰富的配置和自定义选项。可以通过简单的 HTML 代码在网页中嵌入 APlayer 播放器。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>音乐播放器</title>
<!-- 引入 APlayer 的样式 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/aplayer/dist/APlayer.min.css" />
</head>
<body>
<div id="aplayer" style="width: 60%; margin: 0 auto;"></div>
<!-- 引入 APlayer 的脚本 -->
<script src="https://cdn.jsdelivr.net/npm/aplayer/dist/APlayer.min.js"></script>
<!-- 引入自定义脚本 -->
<script src="main.js"></script>
</body>
</html>
document.addEventListener("DOMContentLoaded", () => {
const ap = new APlayer({
container: document.getElementById('aplayer'),
mini: false,
autoplay: false,
theme: '#FADFA3',
loop: 'all',
order: 'random',
preload: 'auto',
volume: 0.5,
mutex: true,
listFolded: false,
listMaxHeight: 90,
lrcType: 1,
audio: [] // 动态加载歌曲
});
3.2 获取歌单信息
根据分享的歌单链接https://music.aleft.top/share/5bwX7WCYrB查看网页源代码,script标签中包含了歌单中所有歌曲的信息,类似下面的,其中id前加上https://music.aleft.top/share/s就会得到歌曲完整的url。
{
"id":"5bwX7WCYrB",
"description":"",
"downloadable":false,
"tracks":[
{
"id":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjcxODI5NTUsImlkIjoiZTVkNTE5ZTc1OWYyMWJlMWM5NzNhNDQ3MDNkZTAxMmIiLCJpc3MiOiJORCJ9.M63cGlrlVoTMp4xU1bLQ47irOGTwZoeNAWLJsLYmt_o",
"title":"以父之名",
"artist":"周杰伦",
"album":"叶惠美",
"updatedAt":"2024-12-30T04:10:27.307776212Z",
"duration":342.05
},
{
"id":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjcxODI5NTUsImlkIjoiMGY0MzYyNmE0MmE1NzcxMjc0NWYwN2I3OTRjYTBiZTQiLCJpc3MiOiJORCJ9.LY-nVH7Vcq_zT6Eqv71qhstuty62ftSNcMlz9r5C91U",
"title":"东风破",
"artist":"周杰伦",
"album":"叶惠美",
"updatedAt":"2024-12-30T04:10:27.320507287Z",
"duration":315.45
},
{
"id":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjcxODI5NTUsImlkIjoiNDQ4Y2ExNmE5NDljZDY5ZDFjMTA0ZmQ5NzU0ZTQ1MGUiLCJpc3MiOiJORCJ9.H41pIG8PBJ10yQfXgXJ5p_0H9R1uK3FuTWSBzWpMBvc",
"title":"发如雪",
"artist":"周杰伦",
"album":"十一月的萧邦",
"updatedAt":"2024-12-30T04:10:27.375885009Z",
"duration":301.69
}
]
}
下面要做的就是从网页元素中定位到我们需要的这个json文件,并进行解析,将信息传给APlayer。
fetch(url)
.then(response => response.text()) // 获取 HTML 内容
.then(html => {
// 正则表达式匹配 window.__SHARE_INFO__
const match_str = html.match(/window\.__SHARE_INFO__\s*=\s*"({.*?})"/);
const match = match_str[1].replace(/\\"/g, '"'); // 将转义的双引号还原
// 解析 JSON 数据
const shareInfo = JSON.parse(match);
if (shareInfo) {
// 提取歌曲信息,并提取封面和歌词
const song = shareInfo.tracks.map(track => {
name: track.title, // 歌曲名称
artist: track.artist, // 歌手
url: base_url + `${track.id}`, // 音频文件 URL 格式
cover: "", // 封面图像 URL (初始为空)
lrc: "", // 歌词 URL 或歌词文本 (初始为空)
};
ap.list.add(songs); // 添加到播放器
});
})
3.3 封面和歌词提取
其实完成到上一步,如果一切顺利的话,我们应该可以正确解析Navidrome的歌单了,不过播放器不能显示封面和歌词,这是因为我的Navidrome歌曲的封面和歌词是嵌入在歌曲文件中的,而APlayer不支持直接读取嵌入在文件中的封面和歌词信息,下面需要做的是将歌曲文件中的封面和歌词信息提取出来,再传给APlayer。
APlayer不支持直接读取内嵌封面和歌词,Navidrome支持内嵌歌词,不支持外挂歌词,两个完全相反,这里要提一下Github上的一个项目——music-tag-web,它可以从网易云音乐和腾讯音乐等平台上自动刮削歌曲的信息,内嵌到歌曲文件中,免去了手动改音乐标签的繁琐。Github地址:https://github.com/xhongc/music-tag-web
需要在html文件中引入jsmediatags库
<script src="https://cdn.jsdelivr.net/npm/jsmediatags"></script>
在js文件中添加提取封面和歌词的代码,APlayer歌词加载加载方式使用lrcType: 1
// 提取音频文件的封面和歌词
function fetchSongMetadata(url, song) {
return new Promise((resolve, reject) => {
jsmediatags.read(url, {
onSuccess: function(tag) {
// 提取封面图像
if (tag.tags.picture) {
const cover = tag.tags.picture.data;
const img = 'data:' + tag.tags.picture.format + ';base64,' + arrayBufferToBase64(cover);
song.cover = img; // 将封面图像添加到 song 对象中
}
// 提取歌词
if (tag.tags.lyrics) {
song.lrc = tag.tags.lyrics.lyrics; // 将歌词文本添加到 song 对象中
}
resolve(song); // 返回更新后的歌曲对象
},
onError: function(error) {
console.error("读取音频元数据失败:", error);
reject(error); // 提取失败时拒绝 Promise
}
});
});
}
// 将 ArrayBuffer 转换为 Base64
function arrayBufferToBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
const length = bytes.byteLength;
for (let i = 0; i < length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
至此,大功告成。
3.4 完整代码
包含两个文件,music.html和main.js
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>音乐播放器</title>
<!-- 引入 APlayer 的样式 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/aplayer/1.10.1/APlayer.min.css" />
</head>
<body>
<div id="aplayer" style="width: 60%; margin: 0 auto;"></div>
<!-- 引入 APlayer 的脚本 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/aplayer/1.10.1/APlayer.min.js"></script>
<!-- 引入 jsmediatags 库 -->
<script src="https://cdn.jsdelivr.net/npm/jsmediatags"></script>
<script src="main.js"></script>
</body>
</html>
const share_url = "https://music.aleft.top/share/5bwX7WCYrB"; // 歌单链接
const base_url = "https://music.aleft.top/share/s/"; // 分享链接
document.addEventListener("DOMContentLoaded", () => {
const ap = new APlayer({
container: document.getElementById('aplayer'),
mini: false,
autoplay: false,
theme: '#FADFA3',
loop: 'all',
order: 'random',
preload: 'auto',
volume: 0.5,
mutex: true,
listFolded: false,
listMaxHeight: 90,
lrcType: 1,
audio: [] // 动态加载歌曲
});
// 获取歌单信息
fetchPlaylist(share_url, ap);
});
// 从歌单 URL 获取数据并解析
function fetchPlaylist(url, ap) {
fetch(url)
.then(response => response.text()) // 获取 HTML 内容
.then(html => {
// 正则表达式匹配 window.__SHARE_INFO__
const match_str = html.match(/window\.__SHARE_INFO__\s*=\s*"({.*?})"/);
const match = match_str[1].replace(/\\"/g, '"'); // 将转义的双引号还原
// 解析 JSON 数据
const shareInfo = JSON.parse(match);
if (shareInfo) {
// 提取歌曲信息,并提取封面和歌词
const songsPromises = shareInfo.tracks.map(track => {
const song = {
name: track.title, // 歌曲名称
artist: track.artist, // 歌手
url: base_url + `${track.id}`, // 音频文件 URL 格式
cover: "", // 封面图像 URL (初始为空)
lrc: "", // 歌词 URL 或歌词文本 (初始为空)
};
// 使用 jsmediatags 提取封面和歌词
return fetchSongMetadata(song.url, song); // 返回 Promise
});
// 等待所有歌曲的封面和歌词提取完毕
Promise.all(songsPromises).then(songs => {
console.log("提取的歌曲信息:", songs);
ap.list.add(songs); // 添加到播放器
}).catch(error => {
console.error("提取歌曲信息失败:", error);
});
} else {
console.error("未找到歌单信息");
}
})
.catch(error => {
console.error("请求失败:", error);
});
}
// 提取音频文件的封面和歌词
function fetchSongMetadata(url, song) {
return new Promise((resolve, reject) => {
jsmediatags.read(url, {
onSuccess: function(tag) {
// 提取封面图像
if (tag.tags.picture) {
const cover = tag.tags.picture.data;
const img = 'data:' + tag.tags.picture.format + ';base64,' + arrayBufferToBase64(cover);
song.cover = img; // 将封面图像添加到 song 对象中
}
// 提取歌词
if (tag.tags.lyrics) {
song.lrc = tag.tags.lyrics.lyrics; // 将歌词文本添加到 song 对象中
}
resolve(song); // 返回更新后的歌曲对象
},
onError: function(error) {
console.error("读取音频元数据失败:", error);
reject(error); // 提取失败时拒绝 Promise
}
});
});
}
// 将 ArrayBuffer 转换为 Base64
function arrayBufferToBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
const length = bytes.byteLength;
for (let i = 0; i < length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
