/* ----
# KPlayer 0.9
# By: Dreamer-Paul
# Last Update: 2023.5.22
一个简洁强大的网页音乐播放器。
本代码为奇趣保罗原创,并遵守 MIT 开源协议。欢迎访问我的博客:https://paugram.com
---- */
var KPlayer = function KPlayer (settings) {
var that = this;
// 状态记录
var current = {
id: 0,
last_id: 0,
playlist: [],
randlist: [],
lyric: [],
lyric_index: 0,
play_mode: 0, // 默认、循环、随机模式 normal(0) loop(1) random(2)
page_title: document.title
};
var icons = {
left: '',
right: '',
play: '',
pause: '',
none: '',
low: '',
mid: '',
max: '',
loop_all: '',
loop_single: '',
rand: '',
list: ''
};
// 小工具
var tools = {
// 拖拽文件名称识别
name: function (filename) {
filename = filename.replace(/\.ogg|\.mp3|\.wav|\.mp4?/, "");
var sp = filename.split(/\s\-\s|\s\–\s/);
return {title: sp[1], artist: sp[0]};
},
// 快捷创建对象
create: function (tag, set) {
var obj = document.createElement(tag);
if(!set) return obj;
if(set.class) obj.className = set.class;
if(set.text) obj.innerText = set.text;
if(set.html) obj.innerHTML = set.html;
return obj;
},
// 快捷添加对象
append: function (obj, child) {
for(var i = 0; i < child.length; i++){
obj.appendChild(child[i]);
}
}
};
// 元素模块
var elements = {
container: tools.create("kplayer"),
player: document.createElement("audio"),
buttons: { // 操作按钮
toggle: tools.create("div", { class: "control-item kico-toggle", html: icons.play }),
prev: tools.create("div", { class: "control-item kico-prev", html: icons.left }),
next: tools.create("div", { class: "control-item kico-next", html: icons.right }),
mode: tools.create("div", { class: "setting-item kico-mode", html: icons.loop_all }),
list: tools.create("div", { class: "setting-item kico-list", html: icons.list }),
volume: tools.create("div", { class: "setting-item kico-volume", html: icons.max })
},
details: { // 歌曲详情
title: tools.create("span", { class: "kico-title", text: "欢迎使用 Kico Player" }),
artist: tools.create("span", { class: "kico-artist", text: "奇趣保罗" }),
cover: tools.create("div", { class: "kp-cover" }),
time: tools.create("div", { class: "kp-time", text: "00:00" })
},
bar: { // 播放进度条
wrap: tools.create("div", { class: "kp-bar" }),
loaded: tools.create("div", { class: "loaded" }),
played: tools.create("div", { class: "played" })
},
playlist: tools.create("div", { class: "kp-list" }),
lyric: tools.create("span", { text: "欢迎使用 Kico Player" }),
list_items: []
};
// 事件
var events = {
onPlay: () => {
if (!interval) interval = setInterval(interval_fc, 500);
elements.buttons.toggle.innerHTML = icons.pause;
actions.title_change(true);
},
onPause: () => {
interval = clearInterval(interval);
elements.buttons.toggle.innerHTML = icons.play;
actions.title_change(false);
},
onProgress: () => {
const player = elements.player;
const percentage = player.buffered.length ? (
player.buffered.end(player.buffered.length - 1) / player.duration
) : 0;
elements.bar.loaded.style.width = percentage * 100 + "%";
},
onError: () => {
interval = clearInterval(interval);
elements.buttons.toggle.innerHTML = icons.play;
actions.title_change(false);
elements.details.title.innerText = ":(";
elements.details.artist.innerText = "发生了错误";
},
onEnd: () => {
// 列表和随机列表循环
if(current.play_mode === 0 || current.play_mode === 2){
this.next();
}
// 单曲循环
else if(current.play_mode === 1){
current.lyric_index = 0;
this.play();
elements.lyric.innerText = current.playlist[current.id].title + " (" + current.playlist[current.id].artist + ")";
}
}
}
var wrapper = {
wrap: tools.create("div", { class: "kp-header" }),
info: tools.create("div", { class: "kp-info" }),
ctrl: tools.create("div", { class: "kp-controls" }),
sets: tools.create("div", { class: "kp-settings" }),
lyric: tools.create("div", { class: "kp-lyrics" }),
drop: tools.create("div", { class: "kp-drop", html: "释放鼠标以添加歌曲" })
};
var interval;
var interval_fc = function () {
actions.update_time();
actions.update_bar();
actions.update_lyric_playing();
};
// 构造模块
var build = {
elements: function () {
// 歌手歌名
tools.append(wrapper.info, [elements.details.title, elements.details.artist]);
// 控制按钮
tools.append(wrapper.ctrl, [elements.buttons.prev, elements.buttons.toggle, elements.buttons.next]);
// 设置按钮
tools.append(wrapper.sets, [elements.buttons.volume, elements.buttons.mode, elements.buttons.list]);
// 整体框架
tools.append(wrapper.wrap, [elements.details.cover, elements.details.time, wrapper.info, wrapper.ctrl, wrapper.sets, elements.bar.wrap]);
wrapper.lyric.appendChild(elements.lyric);
tools.append(elements.container, [wrapper.wrap, elements.playlist, wrapper.lyric, wrapper.drop]);
settings.container.appendChild(elements.container); // 总添加
// 拖动播放
elements.container.ondragenter = function (e) {
e.preventDefault();
wrapper.drop.classList.add("active");
};
elements.container.ondragover = function (e) {
e.preventDefault();
wrapper.drop.classList.add("active");
};
elements.container.ondrag = function (e) {
e.preventDefault();
wrapper.drop.classList.add("active");
};
elements.container.ondragleave = function (e) {
e.preventDefault();
wrapper.drop.classList.remove("active");
};
elements.container.ondrop = function (e) {
e.preventDefault();
wrapper.drop.classList.remove("active");
var arr = [];
for(var i = 0; i < e.dataTransfer.items.length; i++){
var s = e.dataTransfer.items[i].getAsFile();
if (window.createObjectURL) {
url = window.createObjectURL(s)
}
else if (window.createBlobURL) {
url = window.createBlobURL(s)
}
else if (window.URL && window.URL.createObjectURL) {
url = window.URL.createObjectURL(s)
}
else if (window.webkitURL && window.webkitURL.createObjectURL) {
url = window.webkitURL.createObjectURL(s)
}
var n = tools.name(s.name);
arr.push({
title: n.title,
artist: n.artist,
link: url
});
}
that.add(arr);
that.jump(current.playlist.length - 1);
};
// 播放器
elements.player.volume = 1;
elements.player.addEventListener("play", events.onPlay);
elements.player.addEventListener("pause", events.onPause);
elements.player.addEventListener("progress", events.onProgress);
elements.player.addEventListener("error", events.onError);
elements.player.addEventListener("ended", events.onEnd);
// 按钮们
elements.buttons.toggle.addEventListener("click", that.toggle);
elements.buttons.prev.addEventListener("click", that.prev);
elements.buttons.next.addEventListener("click", that.next);
elements.buttons.mode.addEventListener("click", that.mode);
elements.buttons.list.addEventListener("click", that.toggle_list);
elements.buttons.volume.addEventListener("click", that.toggle_volume);
// 进度条
elements.bar.wrap.appendChild(elements.bar.loaded);
elements.bar.wrap.appendChild(elements.bar.played);
elements.bar.wrap.addEventListener("click", function (t) {
if (!elements.player.currentTime) return;
elements.player.currentTime = (t.offsetX / elements.bar.wrap.clientWidth) * elements.player.duration;
actions.update_bar();
actions.update_time();
current.lyric.length && actions.update_lyric();
});
},
playlist: function (item) {
function add(s) {
current.playlist.push(s);
current.randlist.push(s);
var id = current.playlist.length - 1;
var li = tools.create("div", { class: "list-item" });
li.innerHTML = "" + (id + 1) +
"" + current.playlist[id].title +
"" + current.playlist[id].artist + "";
li.onclick = function () {
if(current.id === id){
that.toggle();
}
else {
current.id = id;
that.jump(current.id);
}
};
elements.playlist.appendChild(li);
elements.list_items.push(li);
}
item.forEach(function (t) {
add(t);
});
},
randlist: function () {
for(var i = 0; i < current.randlist.length - 1; i++){
i++;
var a = calc.random(0, current.randlist.length - 1);
var b = calc.random(0, current.randlist.length - 1);
var cache = current.randlist[a];
current.randlist[a] = current.randlist[b];
current.randlist[b] = cache;
}
},
lyric: function (lyric, sub_lyric) {
if (!lyric) {
current.lyric = [];
elements.lyric.innerText = "纯音乐,请欣赏...";
return;
}
elements.lyric.innerText = `${current.playlist[current.id].title} (${current.playlist[current.id].artist})`;
var time, text, sub_text;
// var text = lyric.match(/\[[0-9]{2,}:[0-9]{2}\.[0-9]{2,}\](\S| )+/g).replace(/\[[0-9]{2,}:[0-9]{2}\.[0-9]{2,}\]/g);
time = lyric.match(/\d{2,}:\d{2,}.\d{1,4}/g);
text = lyric.match(/\d{1}\]+.*/g);
if(sub_lyric) sub_text = sub_lyric.match(/\d{1}\]+.*/g);
if(settings.debug) console.log(time, text, sub_text);
// 时间和歌词数量解析结果对的上
if (time && (time.length === text.length)) {
current.lyric = time.map((item, index) => {
const _range = item.match(/\d+/g);
const min = parseInt(_range[0] * 60);
const sec = parseInt(_range[1]);
const ms = parseFloat(_range[2].substr(0, 2) / 60);
const _lyric = {
time: min + sec + ms,
text: text[index].substr(2)
}
if (sub_text && text.length === sub_text.length) {
_lyric.sub_text = sub_text[index].substr(2);
}
return _lyric;
});
if(settings.debug) console.log(current.lyric);
}
else {
current.lyric = [{
time: 0,
text: "当前歌词不支持滚动"
}];
}
}
};
// 播放与暂停切换
this.play = function () {
elements.player.src && elements.player.play();
};
this.pause = function () {
elements.player.src && elements.player.pause();
};
this.toggle = function () {
if(elements.player.src){
elements.player.paused ? elements.player.play() : elements.player.pause();
}
else{
that.jump(current.id);
}
};
this.jump = function (id) {
if(id === undefined) return;
// 更新信息
current.id = id;
current.lyric_index = 0;
elements.details.title.innerText = current.playlist[current.id].title;
elements.details.artist.innerText = current.playlist[current.id].artist;
elements.details.cover.style.backgroundImage = current.playlist[current.id].cover ? "url('" + current.playlist[current.id].cover + "')" : "";
elements.player.src = current.playlist[current.id].link;
elements.player.play();
if("mediaSession" in navigator){
var _meta = {
title: current.playlist[current.id].title,
artist: current.playlist[current.id].artist,
};
if(current.playlist[current.id].album) _meta.album = current.playlist[current.id].album;
if(current.playlist[current.id].cover) _meta.artwork = [{ src: current.playlist[current.id].cover }]
navigator.mediaSession.metadata = new MediaMetadata(_meta);
navigator.mediaSession.setActionHandler("play", that.play);
navigator.mediaSession.setActionHandler("pause", that.pause);
navigator.mediaSession.setActionHandler("previoustrack", that.prev);
navigator.mediaSession.setActionHandler("nexttrack", that.next);
}
// 上次播放记录
if(elements.list_items[current.last_id]) elements.list_items[current.last_id].classList.remove("current");
elements.list_items[current.id].classList.add("current");
current.last_id = current.id;
// 如果有歌词则处理歌词
build.lyric(current.playlist[current.id].lyric, current.playlist[current.id].sub_lyric);
if(settings.debug) console.log("当前 ID:" + current.id);
};
// 上一首
this.prev = function () {
if(current.play_mode === 0 || current.play_mode === 1) {
current.id > 0 ? current.id-- : current.id = current.playlist.length - 1;
that.jump(current.id);
}
else if(current.play_mode === 2){
var a = current.randlist.indexOf(current.playlist[current.id]);
a === 0 ? current.id = current.randlist.length - 1 : current.id = current.playlist.indexOf(current.randlist[a - 1]);
that.jump(current.id);
}
};
// 下一首
this.next = function () {
current.lyric_index = 0;
if(current.play_mode === 0 || current.play_mode === 1){
current.id < current.playlist.length-1 ? current.id++ : current.id = 0;
that.jump(current.id);
}
else if(current.play_mode === 2){
var a = current.randlist.indexOf(current.playlist[current.id]);
a === current.randlist.length - 1 ? current.id = current.playlist.indexOf(current.randlist[0]) : current.id = current.playlist.indexOf(current.randlist[a + 1]);
that.jump(current.id);
}
};
// 播放模式
this.mode = function () {
var btn = elements.buttons.mode;
switch (current.play_mode){
case 0: current.play_mode = 1; btn.innerHTML = icons.loop_single; break;
case 1: current.play_mode = 2; btn.innerHTML = icons.rand; build.randlist(); break;
case 2: current.play_mode = 0; btn.innerHTML = icons.loop_all; break;
}
if(settings.debug) console.log("当前播放模式:" + current.play_mode);
};
// 添加歌曲
this.add = function (item) {
build.playlist(item);
};
// 移除歌曲
this.remove = function () {
if(current.playlist.length > 0){
if(current.randlist){
var del = current.randlist.indexOf(current.playlist[current.playlist.length - 1]);
current.randlist.splice(del, 1);
}
elements.playlist.removeChild(elements.list_items[elements.list_items.length - 1]);
current.playlist.pop();
elements.list_items.pop();
}
};
// 播放列表切换显示
this.toggle_list = function () {
elements.playlist.classList.toggle("show");
};
// 切换音量
this.toggle_volume = function () {
var btn = elements.buttons.volume;
var player = elements.player;
switch (player.volume){
case 1: player.volume = 0.75; btn.innerHTML = icons.mid; break;
case 0.75: player.volume = 0.50; btn.innerHTML = icons.low; break;
case 0.50: player.volume = 0.25; btn.innerHTML = icons.none; break;
case 0.25: player.volume = 1; btn.innerHTML = icons.max; break;
}
};
// 卸载播放器
this.destroy = function () {
events.onPause();
elements.player.pause();
elements.player.removeEventListener("play", events.onPlay);
elements.player.removeEventListener("pause", events.onPause);
elements.player.removeEventListener("progress", events.onProgress);
elements.player.removeEventListener("error", events.onError);
elements.player.removeEventListener("ended", events.onEnd);
elements.buttons.toggle.removeEventListener("click", that.toggle);
elements.buttons.prev.removeEventListener("click", that.prev);
elements.buttons.next.removeEventListener("click", that.next);
elements.buttons.mode.removeEventListener("click", that.mode);
elements.buttons.list.removeEventListener("click", that.toggle_list);
elements.buttons.volume.removeEventListener("click", that.toggle_volume);
elements.player = undefined;
elements.buttons = undefined;
elements.container.remove();
}
// 事件模块
var actions = {
// 拖拽进度条更新歌词
update_lyric: function () {
var num = 0;
current.lyric.forEach(function (t, index) {
if(elements.player.currentTime > t.time){
num = index;
return false;
}
});
current.lyric_index = num;
if(current.lyric[num].sub_text){
elements.lyric.innerHTML = current.lyric[num].text.toString() + "
" + current.lyric[num].sub_text.toString();
}
else{
elements.lyric.innerText = current.lyric[num].text.toString();
}
},
// 播放过程中更新歌词
update_lyric_playing: function () {
if(!current.lyric.length) return;
if(current.lyric[current.lyric_index] && elements.player.currentTime > current.lyric[current.lyric_index].time){
elements.lyric.innerText = current.lyric[current.lyric_index].text;
if(current.lyric[current.lyric_index].sub_text){
elements.lyric.innerHTML = current.lyric[current.lyric_index].text + "
" + current.lyric[current.lyric_index].sub_text;
}
else{
elements.lyric.innerText = current.lyric[current.lyric_index].text;
}
current.lyric_index++;
}
},
update_time: function () {
elements.details.time.innerText = calc.sec_time(elements.player.currentTime);
},
update_bar: function () {
elements.bar.played.style.width = (elements.player.currentTime / elements.player.duration) * 100 + "%";
},
title_change: function (stat) {
settings.title_change && current.playlist[current.id] && stat === true ? document.title = "▶ " + current.playlist[current.id].title + " - " + current.page_title : document.title = current.page_title;
}
};
// 计算函数
var calc = {
add_zero: function (num) {
return num < 10 ? "0" + num : num;
},
sec_time: function (second) {
if(isNaN(second)){
return "00:00";
}
else{
var min = parseInt(second / 60);
var sec = parseInt(second - min * 60);
var hours = parseInt(min / 60);
var minAdjust = parseInt(second / 60 - 60 * parseInt(second / 60 / 60));
return second >= 3600 ? `${calc.add_zero(hours)}:${calc.add_zero(minAdjust)}:${calc.add_zero(sec)}` : `${calc.add_zero(min)}:${calc.add_zero(sec)}`;
}
},
random: function (min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
};
// 执行函数
(function () {
build.elements();
build.playlist(settings.playlist);
if(settings.show_list === true){
that.toggle_list();
}
if(settings.debug){
console.log("MP3 兼容情况:" + elements.player.canPlayType("audio/mp3"));
console.log("OGG 兼容情况:" + elements.player.canPlayType("audio/ogg"));
console.log("WAV 兼容情况:" + elements.player.canPlayType("audio/wav"));
}
var user_agent = navigator.userAgent.toLowerCase().match(/mobile/g);
if(settings.autoplay === true && user_agent === null) that.jump(current.id);
})();
};
console.log("%c Kico Player %c https://paugram.com ","color: #fff; margin: 1em 0; padding: 5px 0; background: #3498db;","margin: 1em 0; padding: 5px 0; background: #efefef;");