APlayer+Navidrome+LrcAPI博客播放器
背景
为了给博客添加音乐播放功能,我选择了APlayer这个HTML5播放器。作为一个没有编程经验的新手,我借助OpenAI的帮助完成了这个项目。
使用的工具和资源
OpenAI(ChatGPT你要是用不了你就自己想想办法)
APlayer(HTML5音乐播放器)
Navidrome(搭建教程自托管音乐服务)
edge浏览器插件:Live Stream Downloader(用于嗅探navidrome服务器上面的链接)
实现步骤
1. 在Halo后台添加播放器代码
在主题的扩展设置中添加以下代码:
<link rel="stylesheet" href="/APlayer.min.css">
<script src="/APlayer.min.js"></script>
<script src="/color-thief.js"></script>
<script src="https://unpkg.com/[email protected]/dist/Meting.min.js"></script>
<!-- MetingJS 音乐播放器容器 -->
<div id="aplayer" class="aplayer" data-id="playlist" data-server="netease" data-type="playlist"></div>
<script>
// APlayer 配置代码
const ap = new APlayer({
container: document.getElementById('aplayer'),
fixed: true,
autoplay: true,
theme: '#ee8243',
loop: 'all',
order: 'random',
preload: 'auto',
volume: 0.7,
lrcType: 3,
audio: [
{
name: '不如见一面',
artist: '海来阿木/单依纯',
url: 'https://nas.021800.xyz:8443/rest/stream?u=zhumao&t=e2b23b72e496a174203c216fd0ad1400&s=50acbc&f=json&v=1.8.0&c=NavidromeUI&id=f4fc6864440ea12c69cda880fba3ec21',
cover: 'https://nas.021800.xyz:8443/rest/getCoverArt?u=zhumao&t=e2b23b72e496a174203c216fd0ad1400&s=50acbc&f=json&v=1.8.0&c=NavidromeUI&id=f4fc6864440ea12c69cda880fba3ec21',
lrc: 'https://blog.021800.xyz/lyrics?title=%E4%B8%8D%E5%A6%82%E8%A7%81%E4%B8%80%E9%9D%A2&artist=%E6%B5%B7%E6%9D%A5%E9%98%BF%E6%9C%A8%2F%E5%8D%95%E4%BE%9D%E7%BA%AF'
},
// 可以继续添加更多歌曲
]
});
</script>2. 特别之处
使用Navidrome服务链接作为歌曲源
通过算法获取封面URL
使用API方式读取歌词
3. 生成Navidrome URL的工具
创建了一个HTML工具来生成Navidrome的歌曲URL、封面URL和歌词URL:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Generate URLs and Fetch Song Info</title>
</head>
<body>
<h1>Generate URLs and Fetch Song Info from Music Stream URL</h1>
<input type="text" id="musicUrl" placeholder="Enter music stream URL" size="100">
<button onclick="generateUrlsAndFetchSongInfo()">Generate URLs and Fetch Song Info</button>
<div id="metingJsContainer"></div>
<script>
async function generateUrlsAndFetchSongInfo() {
const musicUrl = document.getElementById('musicUrl').value;
// 解析 URL 参数
const urlParams = new URL(musicUrl).searchParams;
// 提取参数
const user = urlParams.get('u');
const token = urlParams.get('t');
const salt = urlParams.get('s');
const format = urlParams.get('f');
const version = urlParams.get('v');
const client = urlParams.get('c');
const id = urlParams.get('id');
// 基础 URL
const baseUrl = "https://自己的navidrome服务:4533/rest/";
// 构建封面图 URL 和歌曲信息 URL
const coverUrl = `${baseUrl}getCoverArt?u=${user}&t=${token}&s=${salt}&f=${format}&v=${version}&c=${client}&id=${id}`;
const songInfoUrl = `${baseUrl}getSong?u=${user}&t=${token}&s=${salt}&f=${format}&v=${version}&c=${client}&id=${id}`;
try {
// 获取并显示歌曲信息
const songInfoResponse = await fetch(songInfoUrl);
const songInfoData = await songInfoResponse.json();
const songName = songInfoData["subsonic-response"].song.title || "Unknown song name.";
const artist = songInfoData["subsonic-response"].song.artist || "Unknown artist.";
// 构建歌词 URL
const lyricsUrl = `https://自己不部署的lrcapi/lyrics?title=${encodeURIComponent(songName)}&artist=${encodeURIComponent(artist)}`;
// 生成 meting-js 组件代码
const metingJsHtml = `
{
name: '${songName}',
artist: '${artist}',
url: '${baseUrl}stream?u=${user}&t=${token}&s=${salt}&f=${format}&v=${version}&c=${client}&id=${id}',
cover: '${coverUrl}',
lrc: '${lyricsUrl}'
},`;
// 显示生成的 meting-js 组件代码
const preElement = document.createElement('pre');
preElement.textContent = metingJsHtml;
document.getElementById('metingJsContainer').appendChild(preElement);
} catch (error) {
console.error("Error fetching song info or lyrics:", error);
document.getElementById('metingJsContainer').textContent = "Error fetching song info.";
}
}
</script>
</body>
</html>遇到的问题和解决方案
CORS跨域问题:将LrcAPI直接部署到服务器解决
多个网站的问题:使用OpenResty实现反向代理
待解决的问题
全局播放功能尚未实现,希望能得到帮助-这里有二个方案都不完美
使用js代码会有停顿感代码如下
<div id="aplayer" class="aplayer" data-id="playlist" data-server="netease" data-type="playlist"></div>
<script>
// APlayer 配置代码
const ap = new APlayer({
container: document.getElementById('aplayer'),
fixed: true,
autoplay: true,
theme: '#ee8243',
loop: 'all',
order: 'random',
preload: 'auto',
volume: 0.7,
lrcType: 3,
audio: [
{
name: '一生所爱',
artist: '卢冠廷/莫文蔚',
url: 'https://nas.021800.xyz:8443/rest/stream?u=zhumao&t=a19bed028627fd2aca6035be08f6f414&s=49f2ca&f=json&v=1.8.0&c=NavidromeUI&id=b5275e4203d337b31ad96d0a4d0512e6',
cover: 'https://nas.021800.xyz:8443/rest/getCoverArt?u=zhumao&t=a19bed028627fd2aca6035be08f6f414&s=49f2ca&f=json&v=1.8.0&c=NavidromeUI&id=b5275e4203d337b31ad96d0a4d0512e6',
lrc: 'https://blog.021800.xyz/lyrics?title=%E4%B8%80%E7%94%9F%E6%89%80%E7%88%B1&artist=%E5%8D%A2%E5%86%A0%E5%BB%B7%2F%E8%8E%AB%E6%96%87%E8%94%9A'
},
// 可以继续添加更多歌曲
]
});
function doStuff() {
let flag = 0;
try {
ap.list;
flag = 1;
} catch {
setTimeout(doStuff, 50);
return;
}
if (flag) {
ap.lrc.hide();
const menuIcon = document.querySelector(".aplayer-icon-menu");
if (menuIcon) menuIcon.click();
const storedMusicIndex = localStorage.getItem("musicIndex");
if (storedMusicIndex !== null) {
ap.list.switch(storedMusicIndex);
}
const storedMusicTime = sessionStorage.getItem("musicTime");
if (storedMusicTime !== null) {
window.musict = storedMusicTime;
ap.setMode(sessionStorage.getItem("musicMode"));
if (sessionStorage.getItem("musicPaused") !== '1') {
ap.play();
}
let canSeek = true;
ap.on("canplay", function() {
if (canSeek) {
ap.seek(window.musict);
canSeek = false;
}
});
} else {
sessionStorage.setItem("musicPaused", 1);
ap.setMode("mini");
}
const storedVolume = sessionStorage.getItem("musicVolume");
if (storedVolume !== null) {
ap.audio.volume = Number(storedVolume);
}
ap.on("pause", function() {
sessionStorage.setItem("musicPaused", 1);
ap.lrc.hide();
});
ap.on("play", function() {
sessionStorage.setItem("musicPaused", 0);
ap.lrc.show();
});
ap.audio.onvolumechange = function() {
sessionStorage.setItem("musicVolume", ap.audio.volume);
};
setInterval(function() {
const musicIndex = ap.list.index;
const musicTime = ap.audio.currentTime;
localStorage.setItem("musicIndex", musicIndex);
sessionStorage.setItem("musicTime", musicTime);
sessionStorage.setItem("musicMode", ap.mode);
}, 100);
}
}
doStuff();
});
</script> 第二种方案只是能用但是很多bug 就是pjax方案代码如下
<script src="https://cdn.jsdelivr.net/npm/pjax/pjax.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const pjax = new Pjax({
elements: "a",
selectors: ["main","pjax"],
cacheBust: true
});
// 创建一个安全的 Proxy 对象
function createSafeProxy(obj) {
return new Proxy(obj || {}, {
get: () => 0
});
}
// 创建一个安全的 getClientRects 函数
function safeGetClientRects(element) {
return createSafeProxy(element && typeof element.getClientRects === 'function' ? element.getClientRects() : null);
}
// 替换原始的 getClientRects 函数
const originalGetClientRects = Element.prototype.getClientRects;
Element.prototype.getClientRects = function() {
return createSafeProxy(originalGetClientRects.apply(this, arguments));
};
document.addEventListener('pjax:complete', function() {
loadResources([
{ type: 'script', src: '/plugins/PluginHighlightJS/assets/static/highlight.min.js', onload: initHighlighting },
{ type: 'link', href: '/plugins/PluginHighlightJS/assets/static/styles/atom-one-dark.min.css', rel: 'stylesheet' },
{ type: 'link', href: '/plugins/PluginHighlightJS/assets/static/plugins/highlightjs-copy.min.css', rel: 'stylesheet' },
{ type: 'link', href: '/plugins/PluginHighlightJS/assets/static/plugins/override.css', rel: 'stylesheet' },
{ type: 'script', src: '/plugins/PluginHighlightJS/assets/static/plugins/highlightjs-copy.min.js' }
]);
// 确保 Fluid.events.registerScrollTopArrowEvent 存在并且是一个函数
if (typeof Fluid !== 'undefined' &&
typeof Fluid.events !== 'undefined' &&
typeof Fluid.events.registerScrollTopArrowEvent === 'function') {
// 调用原始函数
Fluid.events.registerScrollTopArrowEvent();
}
// 重新绑定窗口调整大小事件
jQuery(window).off('resize').on('resize', function() {
if (typeof Fluid !== 'undefined' &&
typeof Fluid.events !== 'undefined' &&
typeof Fluid.events.registerScrollTopArrowEvent === 'function') {
Fluid.events.registerScrollTopArrowEvent();
}
});
});
// 错误处理
document.addEventListener('pjax:error', function(event) {
console.error('Pjax error:', event.request.responseText);
// 添加友好的错误提示
alert('加载页面时发生错误,请稍后再试。');
window.location = event.triggerElement.href;
});
});
// 加载资源的函数
function loadResources(resources) {
resources.forEach(function(resource) {
const element = document.createElement(resource.type);
element.src = resource.src || '';
element.href = resource.href || '';
element.rel = resource.rel || '';
if (resource.onload) {
element.onload = resource.onload;
}
document.head.appendChild(element);
});
}
// 初始化代码高亮的函数
function initHighlighting() {
if (typeof hljs !== 'undefined') {
hljs.initHighlighting();
}
}
// 重新执行自执行函数
(async function (window, document) {
const typing = Fluid.plugins.typing;
const subtitle = document.getElementById('subtitle');
if (!subtitle || !typing) {
return;
}
const text = subtitle.getAttribute('data-typed-text');
const urlinfo = window.location.pathname;
urlinfo = decodeURIComponent(urlinfo);
if (true && urlinfo == '/') {
try {
const result = await $.ajax({
type: "GET",
url: 'https://international.v1.hitokoto.cn/'
});
// ... 处理Ajax响应的代码 ...
} catch (error) {
// ... 处理Ajax错误的代码 ...
typing(text);
}
} else {
typing(text);
}
})(window, document);
</script>结语
这个项目展示了如何将APlayer、Navidrome和自定义API结合使用,为博客添加音乐播放功能。尽管遇到了一些挑战,但通过不断学习和尝试,最终实现了预期的功能。
APlayer+Navidrome+LrcAPI博客播放器
http://blog.021800.xyz/2024/06/27/aplayer-navidrome-lrcapibo-ke-bo-fang-qi