APlayer+Navidrome网页音乐播放器
本文最后更新于 159 天前,其中的信息可能已经有所发展或是发生改变。

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.htmlmain.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);
 }
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇