首先,通过分析酷狗在线音乐的报文,可以找到它的LRC歌词。
然后,查看它的调用堆栈,可以找到发起该请求的JS源码。
去掉报文前缀,其实它的内部就是一个JSON
代码运行结果
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<style>
* {
margin: 0;
padding: 0;
}
.container {
display: flex;
}
.lrc-container {
overflow-x: hidden;
border: 1px solid black;
}
.lrc-panel {
width: 30rem;
height: 22rem;
line-height: 250%;
display: inline-block;
overflow-y: scroll;
box-sizing: border-box;
}
.lrc-panel>div {
text-align: center;
}
.play-over {
color: blue;
}
#QQAudio, #KugouAudio {
width: 100%;
}
#KugouAudio {
display: none;
}
</style>
<script>
//心跳-王心语(酷狗lrc格式)
const response1 = `jQuery191017512709795911285_1543801309287({"status":1,"err_code":0,"data":{"hash":"AE2F1C6F855C8A670D2B6DDCD19AA805","timelength":239000,"filesize":3839115,"audio_name":"\u90d1\u5408\u60e0\u5b50 - \u5411\u4f60\u5954\u8dd1","have_album":1,"album_name":"\u5411\u4f60\u5954\u8dd1","album_id":"2017710","img":"http:\/\/imge.kugou.com\/stdmusic\/20170419\/20170419190138997417.jpg","have_mv":1,"video_id":"616926","author_name":"\u90d1\u5408\u60e0\u5b50","song_name":"\u5411\u4f60\u5954\u8dd1","lyrics":"[00:00.20]\u60e0\u5b50 - \u5411\u4f60\u5954\u8dd1\r\n[00:01.41]\u4f5c\u8bcd:\u79cb\u6c34\r\n[00:02.27]\u4f5c\u66f2:\u795e\u9a6c\u5c0f\u58ee\r\n[00:22.28]\u8d70\u8fc7\u90a3\u8857\u89d2\r\n[00:24.36]\u4f3c\u4e4e\u8fd8\u662f\u90a3\u719f\u6089\u7684\u5473\u9053\r\n[00:30.98]\u95ea\u70c1\u7684\u706f\u5149\r\n[00:33.21]\u76ee\u7779\u7740\u6211\u90a3\u4ec5\u5b58\u7684\u9a84\u50b2\r\n[00:39.14]\u970e\u90a3\u95f4\u7684\u9519\u89c9\r\n[00:41.44]\u524d\u9762\u8fd8\u662f\u4f60\u5ba0\u6eba\u7684\u5fae\u7b11\r\n[00:47.04]\u6211\u60f3\u5b66\u4f1a\u5954\u8dd1\r\n[00:49.83]\u5c31\u53ef\u4ee5\u8df3\u8fdb\u4f60\u7684\u6000\u62b1\r\n[00:57.31]\u5b64\u72ec\u7684\u8857\u9053\r\n[00:59.44]\u604d\u60da\u662f\u4f60\u7275\u6211\u624b\u7684\u70ed\u95f9\r\n[01:05.91]\u6a21\u7cca\u7684\u53cc\u773c\r\n[01:08.08]\u8033\u8fb9\u662f\u4f60\u5531\u60c5\u6b4c\u7684\u66f2\u8c03\r\n[01:14.01]\u6211\u548c\u6e05\u98ce\u4f4e\u5583\r\n[01:16.54]\u53ef\u5426\u7a0d\u53bb\u6211\u6700\u771f\u7684\u601d\u5ff5\r\n[01:22.05]\u6211\u548c\u65f6\u95f4\u8d5b\u8dd1\r\n[01:24.98]\u6015\u4f60\u56de\u5934\u65f6\u770b\u6211\u4e0d\u5230\r\n[01:30.34]\u6df1\u7231\u6211\u7684\u4eba\u548c\u6211\u7231\u7684\u4eba\r\n[01:34.53]\u5230\u4e86\u6700\u540e\u7ec8\u7a76\u54ea\u4e00\u4e2a\r\n[01:36.78]\u4f1a\u66f4\u91cd\u8981\r\n[01:39.20]\u90a3\u4e00\u79cd\u65b9\u5f0f\r\n[01:41.33]\u8ba9\u6211\u6289\u62e9\r\n[01:43.19]\u5176\u5b9e\u5e76\u4e0d\u53ef\u9760\r\n[01:48.05]\u6b64\u65f6\u7684\u6b64\u523b\r\n[01:50.12]\u53ea\u60f3\u8eab\u65c1\u6709\u4f60\r\n[01:51.99]\u9759\u9759\u7684\u966a\u7740\u6211\r\n[01:53.47]\u4e00\u8d77\u5929\u8352\u5730\u8001\r\n[01:57.06]\u4e0d\u77e5\u9053\r\n[01:57.70]\u8fd9\u6837\u7684\u8868\u767d\r\n[02:01.01]\u4f1a\u4e0d\u4f1a\u592a\u65e9\r\n[02:24.55]\u5b64\u72ec\u7684\u8857\u9053\r\n[02:26.75]\u604d\u60da\u662f\u4f60\u7275\u6211\u624b\u7684\u70ed\u95f9\r\n[02:33.33]\u6a21\u7cca\u7684\u53cc\u773c\r\n[02:35.50]\u8033\u8fb9\u662f\u4f60\u5531\u60c5\u6b4c\u7684\u66f2\u8c03\r\n[02:41.53]\u6211\u548c\u6e05\u98ce\u4f4e\u5583\r\n[02:43.68]\u53ef\u5426\u7a0d\u53bb\u6211\u6700\u771f\u7684\u601d\u5ff5\r\n[02:49.21]\u6211\u548c\u65f6\u95f4\u8d5b\u8dd1\r\n[02:52.30]\u6015\u4f60\u56de\u5934\u65f6\u770b\u6211\u4e0d\u5230\r\n[02:57.71]\u6df1\u7231\u6211\u7684\u4eba\u548c\u6211\u7231\u7684\u4eba\r\n[03:01.66]\u5230\u4e86\u6700\u540e\u7ec8\u7a76\u54ea\u4e00\u4e2a\r\n[03:04.08]\u4f1a\u66f4\u91cd\u8981\r\n[03:06.41]\u90a3\u4e00\u79cd\u65b9\u5f0f\r\n[03:08.58]\u8ba9\u6211\u6289\u62e9\r\n[03:10.35]\u5176\u5b9e\u5e76\u4e0d\u53ef\u9760\r\n[03:15.26]\u6b64\u65f6\u7684\u6b64\u523b\r\n[03:17.19]\u53ea\u60f3\u8eab\u65c1\u6709\u4f60\r\n[03:19.32]\u9759\u9759\u7684\u966a\u7740\u6211\r\n[03:20.70]\u4e00\u8d77\u5929\u8352\u5730\u8001\r\n[03:24.29]\u4e0d\u77e5\u9053\r\n[03:25.14]\u8fd9\u6837\u7684\u8868\u767d\r\n[03:28.38]\u4f1a\u4e0d\u4f1a\u592a\u65e9\r\n[03:33.16]\u6211\u7684\u5c0f\u5c0f\u613f\u671b\r\n[03:35.79]\u5c31\u662f\u548c\u4f60\u6162\u6162\u53d8\u8001\r\n","author_id":"669356","privilege":0,"privilege2":"0","play_url":"http:\/\/fs.w.kugou.com\/201812031036\/ee33f53d08cda71a6d64a940a1a878e6\/G095\/M0A\/13\/1F\/P5QEAFj3Q6GAIywPADqUi71UxSw776.mp3","authors":[{"is_publish":"1","author_id":"669356","avatar":"20170421150041803.jpg","author_name":"\u90d1\u5408\u60e0\u5b50"}],"bitrate":127}});
`;
//体面-于文文(酷狗lrc格式)
const response2 = `jQuery19103209198362507233_1543889299163({"status":1,"err_code":0,"data":{"hash":"238B786A3F93C42E3D212953E1CE96C3","timelength":282080,"filesize":4525288,"audio_name":"\u4e8e\u6587\u6587 - \u4f53\u9762","have_album":1,"album_name":"\u4f53\u9762","album_id":"8149034","img":"http:\/\/imge.kugou.com\/stdmusic\/20171221\/20171221090821129644.jpg","have_mv":1,"video_id":"636232","author_name":"\u4e8e\u6587\u6587","song_name":"\u4f53\u9762","lyrics":"[00:00.56]\u4e8e\u6587\u6587 - \u4f53\u9762\r\n[00:01.88]\u4f5c\u8bcd\uff1a\u5510\u606c\r\n[00:02.94]\u4f5c\u66f2\uff1a\u4e8e\u6587\u6587\r\n[00:05.97]\u7f16\u66f2\uff1a\u90d1\u6960\r\n[00:07.60]\u5236\u4f5c\u4eba\uff1a\u90d1\u6960\r\n[00:08.80]\u548c\u58f0\u7f16\u5199\uff1a\u4e8e\u6587\u6587\r\n[00:24.12]\u522b\u5806\u780c\u6000\u5ff5\u8ba9\u5267\u60c5\u53d8\u5f97\u72d7\u8840\r\n[00:35.43]\u6df1\u7231\u4e86\u591a\u5e74\u53c8\u4f55\u5fc5\u6bc1\u4e86\u7ecf\u5178\r\n[00:44.26]\u90fd\u5df2\u6210\u5e74\u4e0d\u62d6\u4e0d\u6b20\r\n[00:49.97]\u6d6a\u8d39\u65f6\u95f4\u662f\u6211\u60c5\u613f\r\n[00:55.61]\u50cf\u8c22\u5e55\u7684\u6f14\u5458\r\n[00:59.06]\u773c\u770b\u7740\u706f\u5149\u7184\u706d\r\n[01:06.84]\u6765\u4e0d\u53ca\u518d\u8f70\u8f70\u70c8\u70c8\r\n[01:12.50]\u5c31\u4fdd\u7559\u544a\u522b\u7684\u5c0a\u4e25\r\n[01:18.17]\u6211\u7231\u4f60\u4e0d\u540e\u6094\r\n[01:21.66]\u4e5f\u5c0a\u91cd\u6545\u4e8b\u7ed3\u5c3e\r\n[01:29.38]\u5206\u624b\u5e94\u8be5\u4f53\u9762\r\n[01:32.66]\u8c01\u90fd\u4e0d\u8981\u8bf4\u62b1\u6b49\r\n[01:36.50]\u4f55\u6765\u4e8f\u6b20\r\n[01:38.28]\u6211\u6562\u7ed9\u5c31\u6562\u5fc3\u788e\r\n[01:42.17]\u955c\u5934\u524d\u9762\u662f\r\n[01:44.15]\u4ece\u524d\u7684\u6211\u4eec\u5728\u559d\u5f69\r\n[01:48.32]\u6d41\u7740\u6cea\u58f0\u5636\u529b\u7aed\r\n[01:52.02]\u79bb\u5f00\u4e5f\u5f88\u4f53\u9762\r\n[01:55.11]\u624d\u6ca1\u8f9c\u8d1f\u8fd9\u4e9b\u5e74\r\n[01:59.02]\u7231\u5f97\u70ed\u70c8\r\n[02:00.85]\u8ba4\u771f\u4ed8\u51fa\u7684\u753b\u9762\r\n[02:04.68]\u522b\u8ba9\u6267\u5ff5\u6bc1\u6389\u4e86\u6628\u5929\r\n[02:09.29]\u6211\u7231\u8fc7\u4f60\u5229\u843d\u5e72\u8106\r\n[02:36.85]\u6700\u719f\u6089\u7684\u8857\u4e3b\u89d2\u5374\u6362\u4e86\u4eba\u6f14\r\n[02:48.16]\u6211\u54ed\u5230\u54fd\u54bd\r\n[02:50.09]\u5fc3\u518d\u75db\u5c31\u5f53\u7834\u8327\r\n[02:57.02]\u6765\u4e0d\u53ca\u518d\u8f70\u8f70\u70c8\u70c8\r\n[03:02.58]\u5c31\u4fdd\u7559\u544a\u522b\u7684\u5c0a\u4e25\r\n[03:08.22]\u6211\u7231\u4f60\u4e0d\u540e\u6094\r\n[03:11.96]\u4e5f\u5c0a\u91cd\u6545\u4e8b\u7ed3\u5c3e\r\n[03:19.55]\u5206\u624b\u5e94\u8be5\u4f53\u9762\r\n[03:22.68]\u8c01\u90fd\u4e0d\u8981\u8bf4\u62b1\u6b49\r\n[03:26.57]\u4f55\u6765\u4e8f\u6b20\r\n[03:28.28]\u6211\u6562\u7ed9\u5c31\u6562\u5fc3\u788e\r\n[03:32.33]\u955c\u5934\u524d\u9762\u662f\r\n[03:34.46]\u4ece\u524d\u7684\u6211\u4eec\u5728\u559d\u5f69\r\n[03:38.44]\u6d41\u7740\u6cea\u58f0\u5636\u529b\u7aed\r\n[03:41.99]\u79bb\u5f00\u4e5f\u5f88\u4f53\u9762\r\n[03:45.33]\u624d\u6ca1\u8f9c\u8d1f\u8fd9\u4e9b\u5e74\u7231\u5f97\u70ed\u70c8\r\n[03:51.04]\u8ba4\u771f\u4ed8\u51fa\u7684\u753b\u9762\r\n[03:54.78]\u522b\u8ba9\u6267\u5ff5\u6bc1\u6389\u4e86\u6628\u5929\r\n[03:59.46]\u6211\u7231\u8fc7\u4f60\u5229\u843d\u5e72\u8106\r\n[04:05.44]\u518d\u89c1\u4e0d\u8d1f\u9047\u89c1\r\n","author_id":"86820","privilege":8,"privilege2":"1000","play_url":"http:\/\/fs.w.kugou.com\/201812040959\/4db465946beab6c09ed897827d768ef4\/G123\/M09\/1E\/13\/uw0DAFo7CmOAckvsAEUM6BGXWvo816.mp3","authors":[{"is_publish":"1","author_id":"86820","avatar":"20180920194639307.jpg","author_name":"\u4e8e\u6587\u6587"}],"bitrate":128}});`;
//百度百科LRC标准格式
const response3 = `
[ti:依赖] ——ti.=title, 标题,即歌曲名
[ar:蔡健雅] ——ar.=artist,艺术家,即歌手名
[al:MY SPACE] ——al.=album,专辑,即歌曲被收录的专辑
[by:Chapter Chang] ——by somebody,即LRC歌词文件的制作者
[offset:0] ——补偿时值。500=0.5秒,正负值分别提前和延长相应的时间(其值多为500的倍数)
[00:00.50]蔡健雅 - 依赖
[00:07.94]词、曲:蔡健雅、陶晶莹
[00:11.60]关了灯把房间整理好
[00:15.48]凌晨三点还是睡不著
[00:19.64]你应该是不在 所以把电话挂掉
[00:30.39]在黑暗手表跟着心跳
[00:34.57]怎么慢它停也停不了
[00:39.70]我应该只是心情不好
[00:45.00]那又怎样
[00:48.50]但本来是这样
[01:21.36]朋友们对我的安慰
[01:25.20]都是同样的一个话题
[01:29.73]我一定要变得更坚强
[01:34.68]说很简单
[00:38.50]但是做却很难
[00:53.00][01:43.88][02:11.23]虽然无所谓写在脸上
[00:58.21][01:48.44][02:15.79]我还是舍不得让你离开
[01:02.97][01:53.50][02:20.60]虽然闭着眼假装听不到
[01:07.84][01:58.23][02:25.11][02:33.15]你对爱 已不再 想依赖
`;
class MediaPlayer {
getKugouData(response) {
//JSON.parse转json字符串时遇到一些特殊字符需要先转义
response = response.replace(/\r\n/g, "\\r\\n");
const data = response.match(/\{.+\}/m);
const jsonData = JSON.parse(data);
return jsonData.data;
}
parseKugouLrc(response) {
const data = this.getKugouData(response);
const lrc = this.parseLrc(data.lyrics);
lrc.ti = data.song_name;
lrc.ar = data.author_name;
return lrc;
}
parseLrc(data) {
let result = { lyrics: new Array() };
//箭号表达式的this指向它父对象的上下文,如果要用到arguments的话就不能用箭头表达式
data.split(/\r\n|\n/).forEach((line)=>{
//过滤掉空行,\s匹配任何不可见字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]
if(!/^\s*$/.test(line)) {
const arr = line.split("]");
for(let i = 0; i < arr.length; i++) {
if(arr[i].indexOf("ti") != -1) {
//ti.=title, 标题,即歌曲名
result.ti = arr[i].substr(1);
break;
} else if(arr[i].indexOf("ar") != -1) {
//ar.=artist,艺术家,即歌手名
result.ar = arr[i].substr(1);
break;
} else if(arr[i].indexOf("al") != -1) {
//al.=album,专辑,即歌曲被收录的专辑
result.al = arr[i].substr(1);
break;
} else if(arr[i].indexOf("by") != -1) {
//by somebody,即LRC歌词文件的制作者
result.by = arr[i].substr(1);
break;
} else if(arr[i].indexOf("offset") != -1) {
//补偿时值。500=0.5秒,正负值分别提前和延长相应的时间(其值多为500的倍数)
result.offset = arr[i].substr(1);
break;
} else if(arr[i].indexOf("[") == 0) {
//两种情况“[mm:ss]”或“[mm:ss.ff]”(分钟数:秒数.百分之一秒数),这里需要把它们统一转换为毫秒
//string.substr(start [, length ])
//string.substring(start, end)
const mm = parseInt(arr[i].substr(1, 2));
const ss = parseFloat(arr[i].substr(4));
const time = mm * 60 + ss;
const lrc = arr[arr.length-1];
if(lrc.indexOf("[" == -1)) result.lyrics.push([time, lrc]);
else result.lyrics.push([time]);
} else {
//歌词
}
}
}
});
result.lyrics.sort((a,b)=>{
return a[0] - b[0];
});
return result;
}
constructor (responseLrc) {
this.lyrics = this.parseKugouLrc(responseLrc);
this.index = -1;
this.lrcPanel = document.getElementsByClassName("lrc-panel")[0];
this.fraction = 0.5; //当前歌词的基准线
this.audio = document.getElementById("QQAudio");
}
init () {
this.lrcPanel.innerHTML = "";
//使用父布局来隐藏子元素的滚动条
const container = document.getElementsByClassName("lrc-container")[0];
//offsetWidth:获取到的值是width+padding+border;
//clientWidth: 获取对象可见内容的宽度,不包括滚动条,不包括边框;
container.style.width = this.lrcPanel.clientWidth - 8 + "px";
this.lyrics.lyrics.forEach((lrc)=>{
const div = document.createElement("div");
div.innerText = lrc[1];
this.lrcPanel.appendChild(div);
});
//通过设置padding,将当前歌词定位于基准线上
const panelHeight = this.lrcPanel.clientHeight;
const childHalfHeight = this.lrcPanel.firstChild.offsetHeight / 2;
this.fractionTop = panelHeight * this.fraction - childHalfHeight;
this.lrcPanel.style.paddingTop = this.fractionTop + "px";
this.lrcPanel.style.paddingBottom = panelHeight * (1-this.fraction) - childHalfHeight + "px";
/*
最简单的方法是当时间改变的时候做判断
但是存在很大的优化空间,比如通常index会出现在下一位,没必要全部比较。
这里呈现的只是最简单的lrc格式,qrc(腾讯)和krc(酷狗)都能精确到字,如果每次都逐个比较会消耗很多CPU资源
但是涉及到状态机又比较麻烦,需要记录onSeeked,用audio自带的control不太好弄,用自己的seekbar倒是方便记录状态
*/
this.audio.ontimeupdate=()=>{
if(this.audio.ended){
return;
}
let reachIndex = -2;
let offset = 0;
if(this.lyrics.offset != undefined) this.lyrics.offset / 1000;
const len = this.lyrics.lyrics.length;
const children = this.lrcPanel.children;
for(let i=0; i<len; i++) {
//duration 歌曲总时长,以秒计数,浮点型。currentTime 当前时间
if(this.audio.currentTime < this.lyrics.lyrics[i][0]) {
reachIndex = i - 1;
break;
}
};
if(reachIndex == -2) reachIndex = len - 1;
if(reachIndex != this.index) {
this.index = reachIndex;
for(let i=0; i<children.length; i++) {
if(i == reachIndex) children[i].className = "play-over";
else children[i].removeAttribute("class");
}
this.lrcPanel.scrollTo(0, children[reachIndex < 0 ? 0 : reachIndex].offsetTop - this.fractionTop);
}
};
let title = this.lyrics.ti + "_" + this.lyrics.ar + "_";
setInterval(()=>{
document.title = title;
title = title.substr(1) + title.charAt(0);
}, 1000);
}
}
window.onload = ()=>{
const player = new MediaPlayer(response2);
player.init();
};
/*
这里是对纯前端跨域访问的尝试,很不幸
首先酷狗不支持跨域访问。
其次,尝试过通过酷狗的JS用JSONP来获取,但是由于它的内部混淆了,然后报了一些错,而且时间不允许,就没接下去做了
不过本次失败的经验告诉大家,纯前端想跨域访问是不可能的,还是靠后端吧。
fetch用来取代ajax,下面展示了fetch的两种写法
//这是es6的async/await语法的fetch
async function fetchLrc() {
const url = "http://www.kugou.com/yy/index.php?r=play/getdata&hash=CB7EE97F4CC11C4EA7A1FA4B516A5D97";
const header = new Headers({
// 'Accept': '* /*',
// 'Accept-Encoding': 'gzip, deflate, br',
// 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
// 'Connection': 'keep-alive',
// 'Cookie': 'kg_mid=64cd73a46bbf114fc40a917…62f509129360d6bb3d=1543801310',
// 'Host': 'wwwapi.kugou.com',
// 'Referer': 'http://www.kugou.com/song/fh5rj81.html?frombaidu?frombaidu',
// 'TE': 'Trailers',
// 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; …) Gecko/20100101 Firefox/63.0',
});
const option = {
method: 'GET',
mode: 'cors', //请求的模式,主要用于跨域设置
headers: header,
credentials: "include", //是否发送Cookie omit, same-origin
}
//const response = await fetch(url, option);
const response = fetch("https://www.baidu.com");
console.log(response);
if(response.code == 200) {
const data = await response.json();
document.write(data);
}
}
//这是基于promise语法的fetch
fetch('http://www.kugou.com/yy/index.php?r=play/getdata&hash=CB7EE97F4CC11C4EA7A1FA4B516A5D97', {mode: 'cors'})
.then(response => response.json())
.then(data => {
console.log(data)
});
*/
</script>
</head>
<body>
<div class="container">
<div class="lrc-container">
<div class="lrc-panel"></div>
<!--
autoplay 如果出现该属性,则音频在就绪后马上播放。
controls 如果出现该属性,则向用户显示控件,比如播放按钮。
loop 如果出现该属性,则每当音频结束时重新开始播放。
preload 如果出现该属性,则音频在页面加载时进行加载,并预备播放。如果使用 "autoplay",则忽略该属性。
src 要播放的音频的 URL。
-->
<!-- QQ -->
<audio id="QQAudio" autoplay="false" loop controls src="http://isure.stream.qqmusic.qq.com/C400000Md1wq0vnwzE.m4a?guid=4355126464&vkey=14B628F0C84C04446DDF79BDDAD479694510C6F7CF7CAC545D7564E50C6E5A0B073DDE8BB6456C019145D9F0A36FA5B22B7901E36B7139E1&uin=0&fromtag=66">
<source src="http://isure.stream.qqmusic.qq.com/C400000Md1wq0vnwzE.m4a?guid=4355126464&vkey=14B628F0C84C04446DDF79BDDAD479694510C6F7CF7CAC545D7564E50C6E5A0B073DDE8BB6456C019145D9F0A36FA5B22B7901E36B7139E1&uin=0&fromtag=66">
<p class="myAudiohide">你的浏览器不支持<code>audio</code>标签.</p>
</audio>
<!-- 酷狗 -->
<audio id="KugouAudio" src="http://fs.w.kugou.com/201812040959/4db465946beab6c09ed897827d768ef4/G123/M09/1E/13/uw0DAFo7CmOAckvsAEUM6BGXWvo816.mp3" preload="auto">
<p class="myAudiohide">你的浏览器不支持<code>audio</code>标签.</p>
</audio>
</div>
</div>
</body>
</html>