/* ---- # 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;");