Skip to content

Commit a2302c0

Browse files
committed
wip(163musichot): 新增支持歌词同步展示
1 parent 29b8302 commit a2302c0

File tree

7 files changed

+270
-112
lines changed

7 files changed

+270
-112
lines changed

README.md

+16
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@
44

55
- [博客地址:https://lzw.me](https://lzw.me)
66

7+
## 开发与预览
8+
9+
```bash
10+
npm i -g @lzwme/sserver
11+
12+
git clone https://github.com/lzwme/blog-examples.git
13+
cd blog-examples
14+
pnpm install
15+
16+
# src 预览。注意,部分涉及 PHP 的模块,需要全局安装 PHP 环境
17+
cd src
18+
ss -a
19+
20+
# examples 下具体参见各示例目录下的 README.md 文件说明
21+
```
22+
723
## 资源列表
824

925
### [工具包(发布至NPM)](./packages)

src/x/163musichot/index.html

+12-8
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
<title>网易云音乐热评墙 - 志文工作室</title>
2525
<meta name="description" content="网易云热评墙,热评音乐在线随心听! " />
2626
<meta name="keywords" content="云音乐,网易云音乐评论" />
27-
<link rel="stylesheet" href="style.css" />
28-
<script src="../lib/jquery/jquery.min.js"></script>
27+
<link rel="stylesheet" href="style.css?v=001" />
28+
<script crossorigin="anonymous" src="https://npm.elemecdn.com/jquery/dist/jquery.min.js"></script>
2929
</head>
3030

3131
<body>
@@ -37,16 +37,19 @@
3737
<div class="content">
3838
<div class="inner">
3939
<h1 id="title"></h1>
40+
<div id="lrcUpdate"></div>
4041
<p id="yh"></p>
4142
<p id="pl"></p>
43+
<div id="copyComment" onClick="copyComment()">复制</div>
44+
<div id="nextComment">下一条</div>
4245
</div>
4346
</div>
4447
<nav>
4548
<ul>
46-
<li><a href="javascript:;" onClick="bf()" id="bf">播放</a></li>
49+
<li><a href="javascript:;" onClick="bf()" id="bfBtn">播放</a></li>
4750
<li><a href="javascript:;" onClick="Nextone()">下一首</a></li>
48-
<li><a href="javascript:;" onClick="copyUrl2()">复制热评</a></li>
49-
<li><a href="javascript:;" onClick="getHotList()">热歌榜</a></li>
51+
<li><a href="javascript:;" id="lrcBtn">看歌词</a></li>
52+
<li><a href="javascript:;" onClick="getHotList()" id="hotBtn">热歌榜</a></li>
5053
</ul>
5154
</nav>
5255
</header>
@@ -55,10 +58,11 @@ <h1 id="title"></h1>
5558
您的浏览器不支持 audio 元素。
5659
</audio>
5760
</section>
61+
<pre id="lrc"></pre>
5862
<footer id="footer">
5963
<p class="copyright ">
6064
<a target="_blank" href="https://lzw.me/links">
61-
<span class="cp">Copyright © 志文工作室; 2008-<span id="currentYear"></span>, All Rights Reserved.</span>
65+
<span class="cp">Copyright © 志文工作室; 2001-<span id="currentYear"></span>, All Rights Reserved.</span>
6266
</a>
6367
</p>
6468
</footer>
@@ -67,6 +71,6 @@ <h1 id="title"></h1>
6771
<div id="bg"></div>
6872
<div id="toplistContain"></div>
6973
</body>
70-
<script src="../lib/utils/h5-common.js?v=20230425-2"></script>
71-
<script src="main.js"></script>
74+
<script src="https://lzw.me/x/lib/utils/h5-common.js?v=001"></script>
75+
<script src="main.js?v=001"></script>
7276
</html>

src/x/163musichot/main.js

+149-50
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,61 @@ h5CommInit();
33
const el = {
44
mp3: $('#mp3'),
55
pl: $('#pl'),
6+
nextComment: $('#nextComment'),
67
yh: $('#yh'),
7-
bf: $('#bf'),
8+
bf: $('#bfBtn'),
89
img: $('#img'),
910
toplistContain: document.getElementById('toplistContain'),
11+
/** 看歌词按钮 */
12+
lrcBtn: $('#lrcBtn'),
13+
hotBtn: $('#hotBtn'),
14+
lrc: document.getElementById('lrc'),
15+
lrcUpdate: $('#lrcUpdate'),
1016
};
11-
var hotList;
12-
var dataJson;
1317

14-
$(function () {
15-
var aud = el.mp3[0];
16-
aud.onended = function () {
18+
const urlParams = h5Utils.getUrlParams();
19+
const cache = {
20+
playList: new Map(),
21+
data: new Map(),
22+
/** 歌单 ID */
23+
pid: +(urlParams.pid || urlParams.ca) || 3778678,
24+
};
25+
let hotList;
26+
let dataJson;
27+
28+
$(async function () {
29+
initEvets();
30+
if (cache.pid) await getHotList(true, cache.pid).catch(e => console.log(e));
31+
const mid = +urlParams.id;
32+
if (mid) playMp3(mid);
33+
else Nextone();
34+
});
35+
36+
function initEvets() {
37+
const audio = el.mp3[0];
38+
audio.onended = () => Nextone();
39+
audio.addEventListener('timeupdate', updateLyric);
40+
audio.addEventListener('error', ev => {
41+
console.log(ev.message || ev);
42+
if (dataJson) h5Utils.toast(`[${dataJson.name}] 播放异常`, { icon: 'error' });
1743
Nextone();
18-
};
44+
});
45+
audio.addEventListener('pause', () => {
46+
el.bf.html('播放');
47+
el.img.removeClass('play-rotate').addClass('pause');
48+
});
49+
audio.addEventListener('play', () => {
50+
el.bf.html('暂停');
51+
el.img.addClass('play-rotate').removeClass('pause');
52+
});
1953

20-
el.pl.on('click', function () {
54+
el.nextComment.on('click', function () {
2155
if (dataJson) {
2256
if (dataJson.hotComments) {
2357
updateComment();
2458
} else {
2559
$.getJSON('../iapi/163music/hot.php?type=comment&id=' + dataJson.id).then(function (d) {
26-
if (d.hotComments) {
60+
if (dataJson && d.hotComments) {
2761
dataJson.hotComments = d.hotComments;
2862
updateComment();
2963
}
@@ -32,6 +66,15 @@ $(function () {
3266
}
3367
});
3468

69+
el.lrcBtn.on('click', async function () {
70+
if (el.lrc.style.display === 'block') {
71+
el.lrc.style.display = 'none';
72+
} else {
73+
const lrc = await getLrc();
74+
if (lrc) el.lrc.style.display = 'block';
75+
}
76+
});
77+
3578
el.toplistContain.addEventListener(
3679
'click',
3780
ev => {
@@ -54,23 +97,16 @@ $(function () {
5497
},
5598
false
5699
);
100+
}
57101

58-
const mid = +h5Utils.getUrlParams().id;
59-
if (mid) playMp3(mid);
60-
else Nextone();
61-
});
62-
102+
/** 播放音乐 */
63103
function bf() {
64-
var audio = el.mp3[0];
104+
const audio = el.mp3[0];
65105
if (audio !== null) {
66106
if (audio.paused) {
67107
audio.play();
68-
el.bf.html('暂停');
69-
el.img.addClass('play-rotate').removeClass('pause');
70108
} else {
71109
audio.pause();
72-
el.bf.html('播放');
73-
el.img.removeClass('play-rotate').addClass('pause');
74110
}
75111
}
76112
}
@@ -95,7 +131,7 @@ async function Nextone(json) {
95131
$('#title').html(json.data.name + '(' + json.data.artistsname + ')');
96132
el.mp3.attr('src', json.data.url);
97133
el.mp3.attr('controls', 'controls');
98-
el.img.attr('src', json.data.picurl);
134+
el.img.attr('src', String(json.data.picurl).replace('http:', 'https:'));
99135
// $("#img2").attr("src", json.data.avatarurl);
100136
el.yh.html('来自网易云用户「@' + json.data.nickname + '」的评论:');
101137
el.pl.html(json.data.content);
@@ -105,34 +141,38 @@ async function Nextone(json) {
105141
history.pushState(null, '', u.toString());
106142

107143
dataJson = json.data;
144+
cache.data.set(json.data.id, json.data);
145+
$('#header').css({ display: 'flex' });
146+
147+
await getLrc();
108148
}
109149

110-
function copyUrl2() {
111-
var Url2 = document.getElementById('pl').innerText;
112-
var oInput = document.createElement('input');
113-
oInput.value = Url2;
114-
document.body.appendChild(oInput);
115-
oInput.select(); // 选择对象
116-
document.execCommand('Copy'); // 执行浏览器复制命令
117-
oInput.style.display = 'none';
118-
h5Utils.alert('复制成功');
150+
function copyComment() {
151+
var comment = document.getElementById('pl').innerText;
152+
if (!comment) return h5Utils.toast('没有内容', { icon: 'warning' });
153+
if (h5Utils.copy(comment)) h5Utils.toast('复制成功!');
154+
else h5Utils.toast('复制失败', { icon: 'error' });
119155
}
120156
function updateComment() {
121-
var hotComments = dataJson.hotComments;
157+
const hotComments = dataJson.hotComments;
122158
if (dataJson.commentIdx == null) dataJson.commentIdx = 0;
123159
else dataJson.commentIdx += 1;
124160
if (dataJson.commentIdx >= hotComments.length) dataJson.commentIdx = 0;
125161

126-
var rndItem = hotComments[dataJson.commentIdx];
162+
const rndItem = hotComments[dataJson.commentIdx];
127163
el.yh.html('来自网易云用户「@' + rndItem.user.nickname + '」的评论:');
128164
el.pl.text(rndItem.content);
129165
}
130166

131-
async function getHotList(isPreLoad) {
167+
async function getHotList(isPreLoad, id = 3778678) {
132168
if (!hotList) {
133-
const d = await fetch(`../iapi/163music/hot.php?type=playlist`).then(d => d.json());
169+
const d = await fetch(`../iapi/163music/api.php?type=toplist&id=${id}`).then(d => d.json());
134170
if (d.code !== 200) return h5Utils.alert(d.errmsg || '获取失败!');
135171
hotList = d;
172+
if (hotList.result.name) {
173+
el.hotBtn.text(hotList.result.name);
174+
el.hotBtn.attr('title', hotList.result.name);
175+
}
136176
if (isPreLoad) return hotList;
137177
}
138178

@@ -141,7 +181,7 @@ async function getHotList(isPreLoad) {
141181
htmllist.push(
142182
`<li>`,
143183
`<span class="left">`,
144-
`<span class="idx">${idx}</span>`,
184+
`<span class="idx">${idx + 1}</span>`,
145185
n.ablum ? `<img src="${n.ablum.picUrl}">` : '',
146186
`<span class="tilte">${n.name} - <span class="author">${n.artists ? n.artists.map(m => m.name).join(',') : ''}</span></span>`,
147187
`</span>`,
@@ -157,24 +197,83 @@ async function getHotList(isPreLoad) {
157197

158198
async function playMp3(id) {
159199
if (!hotList) await getHotList(true);
160-
const item = hotList.result.tracks.find(d => d.id == id);
161-
if (!item) return false;
162-
163-
const comments = await $.getJSON(`../iapi/163music/hot.php?type=comment&id=${id}`);
164-
dataJson = {
165-
id,
166-
commentIdx: 0,
167-
hotComments: comments.hotComments,
168-
name: item.name,
169-
url: `https://music.163.com/song/media/outer/url?id=${id}.mp3`,
170-
picurl: item.album.picUrl,
171-
artistsname: item.artists[0].name,
172-
nickname: comments.hotComments[0].user.nickname,
173-
avatarurl: comments.hotComments[0].user.avatarUrl,
174-
content: comments.hotComments[0].content,
175-
};
200+
const item = hotList.result.tracks.find(d => d.id == id) || hotList.result.tracks[0];
201+
202+
dataJson = cache.data.get(id);
203+
if (!dataJson) {
204+
const comments = await $.getJSON(`../iapi/163music/hot.php?type=comment&id=${id}`);
205+
const comment = comments.hotComments[0];
206+
dataJson = {
207+
id,
208+
commentIdx: 0,
209+
hotComments: comments.hotComments,
210+
name: item.name,
211+
url: `https://music.163.com/song/media/outer/url?id=${id}.mp3`,
212+
picurl: item.album.picUrl,
213+
artistsname: item.artists[0].name,
214+
nickname: comment ? comment.user.nickname : '',
215+
avatarurl: comment ? comment.user.avatarUrl : '',
216+
content: comment ? comment.content : '',
217+
};
218+
219+
cache.data.set(id, dataJson);
220+
}
176221

177222
Nextone({ data: dataJson });
178223
updateComment();
179224
return true;
180225
}
226+
227+
async function getLrc() {
228+
const data = dataJson;
229+
if (!data) return;
230+
231+
if (!data.lrc) {
232+
const d = await $.getJSON('../iapi/163music/api.php?type=lrc&id=' + data.id);
233+
if (d.lrc) {
234+
data.lrc = d.lrc;
235+
data.lrc.list = [];
236+
String(data.lrc.lyric)
237+
.split('\n')
238+
.forEach(line => {
239+
const r = line.match(/\[([\d:.]+)\](.+)/);
240+
if (r) {
241+
const [_a, timeStr, text] = r.map(d => d.trim());
242+
const [min, sec] = timeStr.split(':').map(d => +d);
243+
const time = Number((min * 60 + sec).toFixed(3));
244+
data.lrc.list.push({ text, time });
245+
}
246+
});
247+
248+
el.lrc.innerHTML = data.lrc.lyric || '暂无歌词';
249+
}
250+
}
251+
252+
return data.lrc;
253+
}
254+
255+
// 播放歌曲的函数
256+
function updateLyric() {
257+
if (!dataJson || !dataJson.lrc) return;
258+
259+
var currentTime = this.currentTime; // 获取当前歌曲播放时间
260+
var currentLyricIndex = -1;
261+
const lyrics = dataJson.lrc.list;
262+
263+
// 查找当前歌词索引
264+
for (var i = 0; i < lyrics.length; i++) {
265+
if (currentTime >= lyrics[i].time) {
266+
currentLyricIndex = i;
267+
} else {
268+
break; // 找到当前时间对应的歌词后退出循环
269+
}
270+
}
271+
272+
// 显示当前歌词
273+
const lyricText = [currentLyricIndex - 1, currentLyricIndex, currentLyricIndex + 1]
274+
.filter(i => lyrics[i])
275+
.map(i => `<div class="lyric-line ${i == currentLyricIndex ? 'current' : ''}">${lyrics[i].text}</div>`)
276+
.join('');
277+
278+
if (el.lrcUpdate.html() !== lyricText) el.lrcUpdate.html(lyricText);
279+
}

0 commit comments

Comments
 (0)