方法转载自《如何给任何网站加背景音乐指南》
修改了原帖音频引用源,从
网易云音乐转到了阿里云OSS
源引用
<!-- 音乐播放器所用的文件 -->
<!-- 引入 APlayer 样式表 -->
<link rel="stylesheet" href="https://npm.elemecdn.com/aplayer@1.10.1/dist/APlayer.min.css">
<!-- 引入 APlayer JavaScript -->
<script src="https://npm.elemecdn.com/aplayer@1.10.1/dist/APlayer.min.js"></script>
<!-- 引入 MetingJS,用于加载网易云音乐歌单 -->
<script src="https://npm.elemecdn.com/meting@2.0.1/dist/Meting.min.js"></script>
<!-- 自定义播放器容器,设置为可见 -->
<div id="customize" style="position: fixed; bottom: 0; left: 0; width: 100%; z-index: 9999;">
<meting-js
fixed="true"
autoplay="false"
theme="#409EFF"
list-folded="true"
auto="https://music.163.com/#/playlist?id=596492605"
></meting-js>
</div>脚本下载:
具体功能如下:
fixed="true"播放器固定位置,不随页面滚动autoplay="false"加载时不自动播放theme="#409EFF"播放器主题颜色为蓝色list-folded="true"歌单默认折叠,用户需要点击展开auto=""网易云音乐歌单地址
将代码拷贝到页面即可看到播放器,对于 Wrodpress 来说,可以拷贝至主体文件包 header.php 中 <head> 标签内部,或者 footer.php 文件最后
不完美解决网易云音乐至阿里云OSS
至此,目前虽然页面上可以看到播放器,但是可能由于网易云音乐的版权限制导致所有歌单的音乐只能播放前数秒
为了解决这问题,我通过“豆包”重构了
Meting.min.js文件,搭配后端Node.js服务处理跨域请求,最终读取阿里云OSS对象存储列表以实现完整音乐播放
Node.js 服务端
const express = require('express');
const bodyParser = require('body-parser');
const OSS = require('ali-oss');
const cors = require('cors');
const multer = require('multer');
const app = express();
app.use(cors()); // 允许跨域
// ------------------- 博客音乐获取 -------------------
// 配置阿里云OSS
const clientMusic = new OSS({
region: 'oss-cn-beijing', // 你的OSS地域
accessKeyId: '', // 建议用RAM子账号,仅授予OSS只读权限
accessKeySecret: '',
bucket: '' // 你的Bucket名称
});
const ossPath = '' // 配置默认静态文件地址
// 封面支持的格式(优先级从高到低)
const coverExts = ['.jpg', '.jpeg', '.png', '.webp'];
// 歌词文件格式(固定lrc)
const lrcExt = '.lrc';
// 获取音乐列表的接口
app.get('/api/music/list', async (req, res) => {
try {
// 列举128chat/music目录下的所有文件
const result = await clientMusic.list({
prefix: 'music/',
delimiter: '/',
'max-keys': 100
});
// 提取所有文件的名称,方便快速查找封面/歌词
const allFileNames = result.objects.map(item => item.name);
// 过滤音乐文件(mp3/ogg/flac/wav/aac)
const musicExts = ['.mp3', '.ogg', '.flac', '.wav', '.aac'];
const musicList = result.objects
.filter(item => {
const ext = item.name.substring(item.name.lastIndexOf('.')).toLowerCase();
return musicExts.includes(ext);
})
.map(item => {
// 解析文件基础信息
const ext = item.name.substring(item.name.lastIndexOf('.')).toLowerCase();
const filePath = item.name.replace('music/', ''); // 相对路径
const fileBaseName = filePath.replace(ext, ''); // 去除扩展名的文件名
const [name = '未知歌曲', artist = '未知歌手'] = fileBaseName.split(' - ');
// 1. 自动匹配同名封面(优先jpg,其次png/webp)
let coverUrl = `${ossPath}/music/cover.jpg`; // 默认封面
for (const coverExt of coverExts) {
const coverPath = `music/${fileBaseName}${coverExt}`;
if (allFileNames.includes(coverPath)) {
coverUrl = `${ossPath}/${coverPath}`;
break; // 找到第一个匹配的封面就停止
}
}
// 2. 自动匹配同名LRC歌词
let lrcUrl = '';
const lrcPath = `music/${fileBaseName}${lrcExt}`;
if (allFileNames.includes(lrcPath)) {
lrcUrl = `${ossPath}/${lrcPath}`;
}
return {
name: name,
artist: artist,
url: `${ossPath}/${item.name}`, // 音频地址
cover: coverUrl, // 自动匹配的封面地址
lrc: lrcUrl, // 自动匹配的歌词地址(无则为空)
type: 'auto'
};
});
res.json({ code: 200, data: musicList });
} catch (err) {
console.error('获取OSS文件列表失败:', err);
res.json({ code: 500, message: '获取音乐列表失败', error: err.message });
}
});
// ------------------- 启动服务 -------------------
const port = 3000;
app.listen(port, () => {
console.log(`后端服务运行在 http://localhost:${port}`);
});Meting.min.js 脚本重构
"use strict";
function _objectSpread(a) {
for (var b = 1; b < arguments.length; b++) {
var c = null == arguments[b] ? {} : arguments[b],
d = Object.keys(c);
"function" == typeof Object.getOwnPropertySymbols && (d = d.concat(Object.getOwnPropertySymbols(c).filter(function(a) {
return Object.getOwnPropertyDescriptor(c, a).enumerable
}))), d.forEach(function(b) {
_defineProperty(a, b, c[b])
})
}
return a
}
function _defineProperty(a, b, c) {
return b in a ? Object.defineProperty(a, b, {
value: c,
enumerable: !0,
configurable: !0,
writable: !0
}) : a[b] = c, a
}
class MetingJSElement extends HTMLElement {
connectedCallback() {
window.APlayer && window.fetch && (this._init(), this._parse())
}
disconnectedCallback() {
this.lock || this.aplayer.destroy()
}
_camelize(a) {
return a.replace(/^[_.\- ]+/, "").toLowerCase().replace(/[_.\- ]+(\w|$)/g, (a, b) => b.toUpperCase())
}
_init() {
let a = {};
for (let b = 0; b < this.attributes.length; b += 1)
a[this._camelize(this.attributes[b].name)] = this.attributes[b].value;
let b = ["apiUrl", "lock", "lrcType", "defaultCover"];
this.meta = {};
for (var c = 0; c < b.length; c++) {
let d = b[c];
this.meta[d] = a[d], delete a[d]
}
this.config = a;
// 默认配置
this.meta.apiUrl = ""; // 自定义后端服务地址
this.meta.defaultCover = ""; // 默认音乐封面地址
this.meta.lrcType = this.meta.lrcType || 3; // 3表示解析LRC格式歌词
}
// 从后端接口获取音乐列表
_getMusicList() {
return fetch(this.meta.apiUrl)
.then(res => {
if (!res.ok) throw new Error("后端接口请求失败");
return res.json();
})
.then(data => {
if (data.code !== 200) throw new Error(data.message || "获取音乐列表失败");
// 补全默认配置
return data.data.map(item => ({
...item,
cover: item.cover || this.meta.defaultCover,
lrc: item.lrc || "" // 无歌词则为空字符串
}));
})
.catch(err => {
console.error("获取音乐列表失败:", err);
return [];
});
}
_parse() {
// 调用后端接口获取音乐列表
this._getMusicList().then(musicList => {
if (musicList.length === 0) {
console.warn("未找到音乐文件");
return;
}
this._loadPlayer(musicList);
});
}
_loadPlayer(a) {
let b = {
audio: a,
mutex: !0,
lrcType: this.meta.lrcType, // 关键:设置歌词解析类型
storageName: "metingjs",
cover: true, // 强制显示封面
autoplay: false,
// 歌词样式优化(可选)
lrc: {
show: true, // 显示歌词
fontSize: '14px',
lineHeight: '24px'
},
// 【修改1:歌单默认折叠】
listFolded: true,
// 【新增:开启列表循环(可选,保证最后一首播放完能回到第一首)】
listMaxHeight: '300px', // 可选:限制歌单展开高度
loop: 'all' // 修正:APlayer合法值为all(列表循环)/one(单曲)/none(不循环)
};
if (a.length) {
let a = _objectSpread({}, b, this.config);
// 处理布尔值配置
for (let b in a) ("true" === a[b] || "false" === a[b]) && (a[b] = "true" === a[b]);
let c = document.createElement("div");
a.container = c, this.appendChild(c), this.aplayer = new APlayer(a);
// 封面加载失败容错
this.aplayer.on('loadedmetadata', (audio, index) => {
const coverImg = this.aplayer.elements.cover;
coverImg.onerror = () => {
coverImg.src = this.meta.defaultCover;
};
// 歌词加载失败容错(无歌词时隐藏歌词区域)
const currentAudio = this.aplayer.list.audios[index];
if (!currentAudio.lrc) {
this.aplayer.elements.lrc.style.display = 'none';
} else {
this.aplayer.elements.lrc.style.display = 'block';
}
});
// 切换歌曲时更新歌词显示状态
this.aplayer.on('switch', (oldIndex, newIndex) => {
const currentAudio = this.aplayer.list.audios[newIndex];
if (!currentAudio.lrc) {
this.aplayer.elements.lrc.style.display = 'none';
} else {
this.aplayer.elements.lrc.style.display = 'block';
}
});
// 【修改2:修复播放结束自动切歌逻辑,替换无效的skip方法】
this.aplayer.on('ended', () => {
const currentIndex = this.aplayer.list.index;
const total = this.aplayer.list.audios.length;
if (total <= 1) return; // 只有一首时不处理
// 根据loop配置处理切歌逻辑
if (this.aplayer.options.loop === 'all') {
// 列表循环:最后一首切回第一首,否则切下一首
const nextIndex = (currentIndex + 1) % total;
this.aplayer.list.switch(nextIndex);
} else if (currentIndex < total - 1) {
// 非循环模式:只切到下一首(最后一首不处理)
this.aplayer.list.next();
}
// 自动播放下一首
this.aplayer.play();
});
}
}
}
console.log("\n %c MetingJS v2.0.1 (OSS封面+歌词版) %c https://github.com/metowolf/MetingJS \n", "color: #fadfa3; background: #030307; padding:5px 0;", "background: #fadfa3; padding:5px 0;"),
window.customElements && !window.customElements.get("meting-js") && (window.MetingJSElement = MetingJSElement, window.customElements.define("meting-js", MetingJSElement));最后仍存在的一个问题是,切换到下一首后无法实现自动播放,当歌单分别都手动触发后才可以实现自动无缝播放
暂时记录,后期如果修复将更新本文内容
