APlayer+Navidrome+LrcAPI博客播放器

背景

为了给博客添加音乐播放功能,我选择了APlayer这个HTML5播放器。作为一个没有编程经验的新手,我借助OpenAI的帮助完成了这个项目。

使用的工具和资源

  • OpenAI(ChatGPT你要是用不了你就自己想想办法)

  • APlayer(HTML5音乐播放器)

  • Navidrome(搭建教程自托管音乐服务)

  • LrcAPI(歌词API项目)

  • 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. 特别之处

  1. 使用Navidrome服务链接作为歌曲源

  2. 通过算法获取封面URL

  3. 使用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>

遇到的问题和解决方案

  1. CORS跨域问题:将LrcAPI直接部署到服务器解决

  2. 多个网站的问题:使用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
作者
Administrator
发布于
2024年06月27日
更新于
2024年07月13日
许可协议