├── .gitignore
├── .idea
├── jsLibraryMappings.xml
├── misc.xml
├── modules.xml
├── request-song-robot.iml
└── workspace.xml
├── README.md
├── package.json
├── src
├── getsong
│ └── index.js
├── server
│ ├── index.js
│ └── static
│ │ ├── core.js
│ │ ├── index.html
│ │ └── style.css
└── utils
│ ├── Crypto.js
│ ├── index.js
│ └── songs.js
└── upload
├── 1471696540554.png
├── 1471697286239.png
├── 1471697765689.png
├── 1471705339720.png
└── gif3.gif
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | .DS_Store
3 | node_modules/
4 | .idea/
5 | src/log.log
--------------------------------------------------------------------------------
/.idea/jsLibraryMappings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/request-song-robot.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | true
152 | DEFINITION_ORDER
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 | project
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 | project
396 |
397 |
398 | true
399 |
400 | bdd
401 |
402 | DIRECTORY
403 |
404 | false
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 | 1471571585421
432 |
433 |
434 | 1471571585421
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 | 1471701889286
447 |
448 |
449 |
450 | 1471701889287
451 |
452 |
453 | 1471705366045
454 |
455 |
456 |
457 | 1471705366046
458 |
459 |
460 | 1471705441913
461 |
462 |
463 |
464 | 1471705441914
465 |
466 |
467 | 1471707307715
468 |
469 |
470 |
471 | 1471707307715
472 |
473 |
474 | 1471710082427
475 |
476 |
477 |
478 | 1471710082427
479 |
480 |
481 | 1471715898337
482 |
483 |
484 |
485 | 1471715898337
486 |
487 |
488 | 1471717987202
489 |
490 |
491 |
492 | 1471717987202
493 |
494 |
495 | 1471745242840
496 |
497 |
498 |
499 | 1471745242840
500 |
501 |
502 | 1471828798490
503 |
504 |
505 |
506 | 1471828798490
507 |
508 |
509 | 1471837852253
510 |
511 |
512 |
513 | 1471837852253
514 |
515 |
516 | 1471839056103
517 |
518 |
519 |
520 | 1471839056103
521 |
522 |
523 | 1472044520327
524 |
525 |
526 |
527 | 1472044520327
528 |
529 |
530 |
531 |
532 |
533 |
534 |
535 |
536 |
537 |
538 |
539 |
540 |
541 |
542 |
543 |
544 |
545 |
546 |
547 |
548 |
549 |
550 |
551 |
552 |
553 |
554 |
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 |
563 |
564 |
565 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 |
583 |
584 |
585 |
586 |
587 |
588 |
589 |
590 |
591 |
592 |
593 |
594 |
595 |
596 |
597 |
598 |
599 |
600 |
601 |
602 |
603 |
604 |
605 |
606 |
607 |
608 |
609 |
610 |
611 |
612 |
613 |
614 |
615 |
616 |
617 |
618 |
619 |
620 |
621 |
622 |
623 |
624 |
625 |
626 |
627 |
628 |
629 |
630 |
631 |
632 |
633 |
634 |
635 |
636 |
637 |
638 |
639 |
640 |
641 |
642 |
643 |
644 |
645 |
646 |
647 |
648 |
649 |
650 |
651 |
652 |
653 |
654 |
655 |
656 |
657 |
658 |
659 |
660 |
661 |
662 |
663 |
664 |
665 |
666 |
667 |
668 |
669 |
670 |
671 |
672 |
673 |
674 |
675 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
685 |
686 |
687 |
688 |
689 |
690 |
691 |
692 |
693 |
694 |
695 |
696 |
697 |
698 |
699 |
700 |
701 |
702 |
703 |
704 |
705 |
706 |
707 |
708 |
709 |
710 |
711 |
712 |
713 |
714 |
715 |
716 |
717 |
718 |
719 |
720 |
721 |
722 |
723 |
724 |
725 |
726 |
727 |
728 |
729 |
730 |
731 |
732 |
733 |
734 |
735 |
736 |
737 |
738 |
739 |
740 |
741 |
742 |
743 |
744 |
745 |
746 |
747 |
748 |
749 |
750 |
751 |
752 |
753 |
754 |
755 |
756 |
757 |
758 |
759 |
760 |
761 |
762 |
763 |
764 |
765 |
766 |
767 |
768 |
769 |
770 |
771 |
772 |
773 |
774 |
775 |
776 |
777 |
778 |
779 |
780 |
781 |
782 |
783 |
784 |
785 |
786 |
787 |
788 |
789 |
790 |
791 |
792 |
793 |
794 |
795 |
796 |
797 |
798 |
799 |
800 |
801 |
802 |
803 |
804 |
805 |
806 |
807 |
808 |
809 |
810 |
811 |
812 |
813 |
814 |
815 |
816 |
817 |
818 |
819 |
820 |
821 |
822 |
823 |
824 |
825 |
826 |
827 |
828 |
829 |
830 |
831 |
832 |
833 |
834 |
835 |
836 |
837 |
838 |
839 |
840 |
841 |
842 |
843 |
844 |
845 |
846 |
847 |
848 |
849 |
850 |
851 |
852 |
853 |
854 |
855 |
856 |
857 |
858 |
859 |
860 |
861 |
862 |
863 |
864 |
865 |
866 |
867 |
868 |
869 |
870 |
871 |
872 |
873 |
874 |
875 |
876 |
877 |
878 |
879 |
880 |
881 |
882 |
883 |
884 |
885 |
886 |
887 |
888 |
889 |
890 |
891 |
892 |
893 |
894 |
895 |
896 |
897 |
898 |
899 |
900 |
901 |
902 |
903 |
904 |
905 |
906 |
907 |
908 |
909 |
910 |
911 |
912 |
913 |
914 |
915 |
916 |
917 |
918 |
919 |
920 |
921 |
922 |
923 |
924 |
925 |
926 |
927 |
928 |
929 |
930 |
931 |
932 |
933 |
934 |
935 |
936 |
937 |
938 |
939 |
940 |
941 |
942 |
943 |
944 |
945 |
946 |
947 |
948 |
949 |
950 |
951 |
952 |
953 |
954 |
955 |
956 |
957 |
958 |
959 |
960 |
961 |
962 |
963 |
964 |
965 |
966 |
967 |
968 |
969 |
970 |
971 |
972 |
973 |
974 |
975 |
976 |
977 |
978 |
979 |
980 |
981 |
982 |
983 |
984 |
985 |
986 |
987 |
988 |
989 |
990 |
991 |
992 |
993 |
994 |
995 |
996 |
997 |
998 |
999 |
1000 |
1001 |
1002 |
1003 |
1004 |
1005 |
1006 |
1007 |
1008 |
1009 |
1010 |
1011 |
1012 |
1013 |
1014 |
1015 |
1016 |
1017 |
1018 |
1019 |
1020 |
1021 |
1022 |
1023 |
1024 |
1025 |
1026 |
1027 |
1028 |
1029 |
1030 |
1031 |
1032 |
1033 |
1034 |
1035 |
1036 |
1037 |
1038 |
1039 |
1040 |
1041 |
1042 |
1043 |
1044 |
1045 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 点歌机器人 (来自网易云音乐)
2 |
3 | 偶然的机会,发现了B站的点歌机器人,觉得挺好玩的就自己做了一个简易版点歌机器人,预览如下:
4 |
5 | 
6 |
7 |
8 | hhh
9 | test break
10 |
11 | ## 功能
12 |
13 | 1. 使用websocket,支持多人同时点歌,发送弹幕聊天
14 | 2. 具有搜索suggestion,用户体验更佳
15 | 3. 点击mv视频右上角可以缩小放大,不影响用户其他操作
16 | 4. 具有mv的资源,优先播放mv
17 | 5. 对于未播放的已点歌曲,可以进行取消
18 | 6. ...
19 |
20 | ## 其他说明
21 |
22 | 由于是实时多人点歌,所以不能够跳过当前播放歌曲,也不能跳跃播放,Mv只能够重头开始播放,mp3能够根据线上其他用户的播放进度进行同步
23 |
24 | **音乐资源均来自网易云音乐,该程序仅用于个人学习,不得用于任何商业用途**
25 |
26 | 关于网易云音乐的接口规则,我就不多说了,因为关于商业机密,可能吃官司的,有兴趣的可以私下找我
27 |
28 | ## 技术沉淀
29 |
30 | 
31 | 如上图,网易云音乐的请求参数是做了加密处理的。
32 | 关于网易云音乐请求参数的加密方法,简单提下
33 | ```js
34 |
35 | aesRsaEncrypt: function (text) {
36 | var secKey = createSecretKey(16);
37 | return {
38 | params: aesEncrypt(aesEncrypt(text, nonce), secKey),
39 | encSecKey: rsaEncrypt(secKey, pubKey, modulus)
40 | }
41 | }
42 | ```
43 |
44 | 
45 |
46 | `secKey`为本地随机生成的密文,通过rsa非对称加密算法加密,然后网易服务器通过约定好的与`pubKey`对应的另一个因数进行解密,得到`secKey`, 然后通过两次aes逆运算就能得到`text`,也就是真实的参数了。
47 |
48 | 这样做的好处不言而喻,不法分子很难破解抓取到的请求数据
49 | 但服务器负担加重了,每次提供服务前,还得先去破解一番
50 |
51 | 另外!网易还做了一点安全措施,调用接口得到音乐url是有时间限制的!!!
52 |
53 | 
54 |
55 | 所以,不能够在点歌的时候就把音乐url抓取下来保存,必须得有用户需要播放的时候再抓取url
56 |
57 | **怎么伪造浏览器请求报头中的`Referer`字段?**
58 |
59 | 而且云音乐的mvurl不支持外链访问,所以我只好做个代理,转发视频数据流了,但这样做的不好就是mv播放不能跳跃播放(如最上方动图所示)
60 | 如果需要实时的播放视频流, 好像就要牵涉到流媒体传输服务器了, 需要应用层`RTSP`协议, 具体也不是很清楚, 有时间的话再好好看看
61 | 还有希望能加上歌词
62 | `express`中静态资源已经实现了
63 |
64 | ```javascript
65 |
66 | let url = req.url
67 | let q = URL.parse(req.url, true).query
68 | if(url.startsWith(SUFFIX)) {
69 | if(q.id!=0)
70 | gs.getMvUrl(q.id)
71 | .then(json => {
72 | if(json.hurl || json.murl) {
73 | res.writeHead(200, {'Content-Type': u.suffix2Type('mp4')});
74 | var s = gs.getStream(json.hurl || json.murl)
75 | s.on('error', (err) => {
76 | s.close && s.close()
77 | console.error(err)
78 | res.end()
79 | })
80 | //传递MV视频数据流
81 | s.pipe(res)
82 | } else {
83 | res.writeHead(500);
84 | res.end('Error '+JSON.stringify(json))
85 | }
86 | })
87 | else {
88 | res.writeHead(500);
89 | res.end('Error')
90 | }
91 | return
92 | }
93 | ```
94 |
95 | ## 最后在上个预览
96 |
97 | 
98 |
99 | ## 源码与使用
100 |
101 | [song-robot](https://github.com/moyuyc/request-song-robot)
102 |
103 | ```
104 | npm i song-robot -g
105 | song-robot -p 9888
106 | open http://localhost:9888
107 | ```
108 |
109 | ## 参考资料
110 |
111 | referer
112 | https://zh.wikipedia.org/zh/HTTP%E5%8F%83%E7%85%A7%E4%BD%8D%E5%9D%80
113 |
114 | 网易云api破解
115 | http://qianzewei.com/2015/12/10/%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90api%E6%95%B4%E7%90%86/#
116 |
117 | node crypto
118 | https://nodejs.org/api/crypto.html
119 |
120 | 输入框光标变色
121 | http://jsfiddle.net/8k1k0awb/
122 |
123 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "song-robot",
3 | "version": "1.0.37",
4 | "description": "点歌机器人, 来自网易云音乐",
5 | "main": "index.js",
6 | "dependencies": {
7 | "big-integer": "^1.6.15",
8 | "cheerio": "^0.20.0",
9 | "dateformat": "^1.0.12",
10 | "minimist": "^1.2.0",
11 | "request": "^2.74.0",
12 | "socket.io": "^1.4.8"
13 | },
14 | "devDependencies": {},
15 | "scripts": {
16 | "start": "node src/server/index.js",
17 | "test": "echo \"Error: no test specified\" && exit 1"
18 | },
19 | "author": "moyuyc",
20 | "license": "ISC",
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/moyuyc/request-song-robot.git"
24 | },
25 | "keywords": [
26 | "songrobot"
27 | ],
28 | "bugs": {
29 | "url": "https://github.com/moyuyc/request-song-robot/issues"
30 | },
31 | "homepage": "https://github.com/moyuyc/request-song-robot#readme",
32 | "bin": {
33 | "song-robot": "src/server/index.js"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/getsong/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by Moyu on 16/8/19.
3 | */
4 | const URL = require('url')
5 |
6 | const util = require('../utils')
7 |
8 | const GET_SONG_URL = "http://music.163.com/weapi/cloudsearch/get/web?csrf_token="
9 | const GET_SONGURL_URL = "http://music.163.com/weapi/song/enhance/player/url?csrf_token="
10 | const crypto = util.Crypto
11 | // getSongs('Sugar Maroon').then(console.log)
12 | function getSongs(text) {
13 | return util
14 | .spider({
15 | url: GET_SONG_URL,
16 | method: 'POST',
17 | headers: {
18 | 'Referer': 'http://music.163.com/search/'
19 | },
20 | form: crypto.aesRsaEncrypt( JSON.stringify({s: text, type: '1'}))
21 | }, 'json')
22 | .then(json => {
23 | if(json.code==200) {
24 | json.result = json.result.songs.map((song) => {
25 | return {
26 | name: song.name,
27 | id: song.id,
28 | pic: song.al,
29 | author: song.ar.map(art=>{
30 | return art.name
31 | }).join(','),
32 | mv: song.mv>0 ? song.mv : null
33 | }
34 | })
35 | }
36 | return json
37 | })
38 | .catch(console.error)
39 | }
40 |
41 | function getSongUrl(id) {
42 | return util.spider({
43 | url: GET_SONGURL_URL,
44 | method: 'POST',
45 | headers: {
46 | 'Referer': 'http://music.163.com/search/'
47 | },
48 | form: crypto.aesRsaEncrypt( JSON.stringify({ids: [id], br: 128000}))
49 | }, 'json')
50 | .then(json => {
51 | if(json.code==200) {
52 | json.data = {
53 | url: json.data[0].url,
54 | id: json.data[0].id
55 | }
56 | }
57 | return json
58 | })
59 | }
60 |
61 | function getStream(ops) {
62 |
63 | return util.spiderStream(ops)
64 | }
65 |
66 |
67 | // getLyric(426502151).then(x=>console.log(x))
68 | function getLyric(songid) {
69 | return util.spider({
70 | url: `http://music.163.com/weapi/song/lyric?csrf_token=`,
71 | method: 'POST',
72 | form: crypto.aesRsaEncrypt( JSON.stringify({id: songid, os:'osx', lv: -1, kv: -1, tv: 1}))
73 | }, 'json')
74 | .then(x => {
75 | if(x.code == 200 && !x.nolyric)
76 | return {
77 | code: 200,
78 | lrc: x.lrc.lyric
79 | // tlrc: x.tlyric.lyric
80 | }
81 | return {
82 | code: 500
83 | }
84 | })
85 | }
86 |
87 | function getMvUrl(id) {
88 | return util.spider({
89 | url: `http://music.163.com/mv?id=${id}`,
90 | method: 'GET',
91 | headers: {
92 | Referer: 'http://music.163.com/'
93 | }
94 | }, 'jq').then($ => {
95 | let json = {}
96 | let embed = $('embed')
97 | if(embed.length!=0){
98 | embed.attr('flashvars').split('&').forEach(x=>{
99 | let i = x.indexOf('=')
100 | json[x.substring(0, i)] = x.substring(i+1)
101 | })
102 | }
103 | return json
104 | })
105 | }
106 | //coverImg=http://p3.music.126.net/go6fIIio9GgTcUw4V9tfYg==/2495891495054232.jpg
107 | // "hurl=http://v4.music.126.net/20160820235551/2cdb91ea93917fb668cb58394a3097f9/web/cloudmusic/YDAwIDVgJTQwNDUgICEwIQ==/mv/==/288118/d200ceeb902399e0f503374c6e792eb3.mp4&" +
108 | // "murl=http://v4.music.126.net/20160820235551/ece7b823f0e9dd4575ec7d704238eda9/web/cloudmusic/YDAwIDVgJTQwNDUgICEwIQ==/mv/288118/174cdc4bf70d5830521ee06c560ade56.mp4&
109 |
110 | function getSongSuggest(s) {
111 | return util.spider({
112 | url: "http://music.163.com/weapi/search/suggest/web?csrf_token=",
113 | method: 'POST',
114 | headers: {
115 | Referer: 'http://music.163.com/search/'
116 | },
117 | form: crypto.aesRsaEncrypt(JSON.stringify({s: s}))
118 | }, 'json').then(json=>{
119 | if(json.code == 200) {
120 | json.songs = json.result.songs
121 | delete json.result
122 | json.songs.map(x=>{
123 | return {
124 | name: x.name,
125 | id: x.id,
126 | mv: x.mvid,
127 | author: x.artists.map(art=>{
128 | return art.name
129 | }).join(',')
130 | }
131 | })
132 | }
133 | return json;
134 | })
135 | }
136 |
137 | function forwardRequest (req, res, url) {
138 | var urlAsg = URL.parse(url, true);
139 | var headers = req.headers;
140 | var urlOptions = {
141 | host: urlAsg.host,
142 | port: urlAsg.port || 80,
143 | path: urlAsg.path,
144 | method: req.method,
145 | headers: { range: headers.range }
146 | // rejectUnauthorized: false
147 | };
148 |
149 | var forward_request = require('http').request(urlOptions, function(response) {
150 | var code = response.statusCode;
151 | if(code === 302 || code === 301) {
152 | var location = response.headers.location;
153 | console.log('location', location);
154 | response.destroy();
155 | forward_request.abort();
156 | forwardRequest(req, res, location);
157 | return;
158 | }
159 | res.writeHead(code, response.headers);
160 | response.pipe(res)
161 | });
162 |
163 | forward_request.on('error', function(e) {
164 | console.error('problem with request: ' + e.message);
165 | });
166 |
167 | req.pipe(forward_request)
168 | }
169 |
170 | module.exports = {
171 | getSongs,
172 | getSongUrl: getSongUrl,
173 | getMvUrl,
174 | getStream,
175 | getSongSuggest,
176 | getLyric,
177 | forwardRequest
178 | }
179 |
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /**
3 | * Created by Moyu on 16/8/19.
4 | */
5 |
6 | var http = require('http')
7 | var app = http.createServer(handler)
8 | var io = require('socket.io')(app);
9 | var fs = require('fs');
10 | var URL = require('url');
11 | var p = require('path');
12 | var dateFormat = require('dateformat');
13 | var argv = require('minimist')(process.argv.slice(2))
14 |
15 | var gs = require('../getsong')
16 | var u = require('../utils')
17 | const SUFFIX = '/api/mv'
18 | const SUG_SUFFIX = '/api/sug'
19 | const songs = u.songs
20 |
21 | const log = fs.createWriteStream(p.resolve(__dirname, '../log.log'), {flags: 'a'})
22 | function Log(text) {
23 | console.log(text)
24 | log.write(text + '\r\n')
25 | }
26 |
27 | app.listen(argv.p || 9888, () => {
28 | console.log(`http://localhost:${app.address().port}`)
29 | });
30 |
31 | function handler (req, res) {
32 | let url = req.url
33 | console.log(url)
34 | let q = URL.parse(req.url, true).query
35 |
36 | if(url.startsWith(SUFFIX)) {
37 |
38 | if(q.id!=0) {
39 | gs.getMvUrl(q.id)
40 | .then(json => {
41 | if(json.hurl || json.murl ) {
42 |
43 | gs.forwardRequest(req, res, json.hurl || json.murl);
44 |
45 | } else {
46 | res.writeHead(500);
47 | res.end('Error '+JSON.stringify(json))
48 | }
49 | })
50 | } else {
51 | res.writeHead(500);
52 | res.end('Error')
53 | }
54 |
55 | return
56 |
57 | } else if(url.startsWith(SUG_SUFFIX)) {
58 | gs.getSongs(q.s)
59 | .then(json=>{
60 | res.end(JSON.stringify(json))
61 | })
62 | return
63 | } else if(url.startsWith('/api/song')) {
64 | if(q.id!= null && q.id>0) {
65 | gs.getSongUrl(q.id)
66 | .then(x=>{
67 | if(x.code!=200){
68 | res.end('Error: '+x.msg)
69 | return
70 | }
71 | return gs.getStream(x.data.url)
72 | .then(s => {
73 | s.on('error', (err) => {
74 | s.close && s.close()
75 | console.error(err)
76 | res.end()
77 | })
78 | res.writeHead(200, {'Content-Type': 'audio/mpeg'})
79 | s.pipe(res)
80 | })
81 | })
82 | } else {
83 | res.writeHead(500);
84 | res.end('Error')
85 | }
86 | return
87 | }
88 | // if(url=="/stream") {
89 | // res.writeHead(200, {
90 | // 'Content-Type': 'text/event-stream',
91 | // 'Cache-Control': 'no-cache'
92 | // })
93 | // setInterval(function () {
94 | // res.write("data: " + Date.now()+"\n\n")
95 | // }, 1000)
96 | // return;
97 | // }
98 | let queryIndex = url.lastIndexOf('?')
99 | if(queryIndex >= 0) {
100 | url = url.slice(queryIndex)
101 | }
102 | let filename = url=='/'?'index.html':url.substring(1)
103 | let dotIndex = filename.lastIndexOf('.')
104 | let ext
105 | if(dotIndex >= 0) {
106 | ext = filename.substring(dotIndex+1)
107 | }
108 | fs.readFile(p.resolve(__dirname, 'static', filename),
109 | function (err, data) {
110 | if (err) {
111 | res.writeHead(500, {'Content-Type': u.suffix2Type(ext)});
112 | return res.end(`Error loading ${filename}.`);
113 | }
114 | res.writeHead(200, {'Content-Type': u.suffix2Type(ext)});
115 | res.end(data);
116 | });
117 | }
118 |
119 | io.on('connection', function (socket) {
120 | socket.emit('login')
121 | socket._id = socket.id.substring(2)
122 | socket
123 | .on('login', (name) => {
124 | socket.emit('initSongs', songs.toJSON().map(x=>{
125 | return Object.assign(x, {
126 | isSelf: x.userid==socket._id
127 | })
128 | }))
129 | fixTime(socket)
130 | socket.name = ( (name!=null&&name!='') ? name : makeName() )
131 | broadcast('message', {welcome: true, text: socket.name})
132 | socket.on('bullet', (data) => {
133 | if(socket.lastSend && (Date.now()-socket.lastSend)<5000) {
134 |
135 | socket.emit('bullet', Object.assign(data, {isSelf: true, forbid: true}))
136 |
137 | } else {
138 | socket.lastSend = Date.now()
139 |
140 | socket.emit('bullet', Object.assign({}, data, {isSelf: true}))
141 | socket.broadcast.emit('bullet', data)
142 |
143 | broadcast('message', {name: socket.name, text: data.val})
144 |
145 | let flag = matchSong(data.val)
146 | if(flag) {
147 | getFirstSong(flag).then((json) => {
148 | if(json.code==200) {
149 | requestSongWorker(json.song, socket)
150 | } else {
151 | socket.emit('putSong', json)
152 | }
153 | })
154 | }
155 | }
156 | }).on('playEnd', function (id) {
157 | songs.remove(id)
158 | playSon(socket)
159 | }).on('deleteSong', function (id) {
160 | let success = songs.deleteSelfSong(socket._id, id)
161 | if(success) {
162 | broadcast('deleteSong', { isSelf: success, id: id })
163 | // socket.emit('play', songs.getFirst())
164 | }else {
165 | socket.emit('deleteSong', { isSelf: success, id: id })
166 | }
167 | }).on('reqsong', function (song) {
168 | const v = '点歌 ' + song.name+' - '+song.author;
169 | socket.emit('bullet', {isSelf: true, val: v});
170 | socket.broadcast.emit('bullet', {val: v});
171 | requestSongWorker(song, socket);
172 | })
173 | }).on('disconnect', ()=> {
174 | broadcast('message', {bye: true, text: socket.name})
175 | // songs.removeUserSongs(socket._id)
176 | }).on('play', () => {
177 | if(Object.keys(socket.server.sockets.sockets).length>1) {
178 | socket.playTimer = setTimeout(function () {
179 | playSon(socket)
180 | }, 5000)
181 | socket.broadcast.emit('currentTime', socket.id)
182 | }
183 | else {
184 | playSon(socket)
185 | }
186 | }).on('currentTime', (json) => {
187 | let findId = Object.keys(socket.server.sockets.sockets).find((x) => {
188 | return x == json.id
189 | })
190 | delete json.id
191 | if(findId) {
192 | clearTimeout(socket.server.sockets.sockets[findId].playTimer)
193 | playSon(socket.server.sockets.sockets[findId], json)
194 | }
195 |
196 | })
197 | });
198 |
199 |
200 | function broadcast() {
201 | io.emit.apply(io, arguments)
202 | }
203 |
204 | function matchSong(text) {
205 | if(/^点歌 (.+)$/.test(text)) {
206 | return RegExp.$1.trim()
207 | }
208 | }
209 |
210 | function getFirstSong(title) {
211 | return gs.getSongs(title).then(json => {
212 | if(json.code==200) {
213 | json.song = json.result[0]
214 | delete json.result
215 | return json
216 | }
217 | return json
218 | })
219 | }
220 |
221 | function fixTime(socket) {
222 | let song = songs.getFirst()
223 | if(Object.keys(socket.server.sockets.sockets).length>1 && !!song) {
224 | socket.playTimer = setTimeout(function () {
225 | playSon(socket)
226 | }, 5000)
227 | socket.broadcast.emit('currentTime', {
228 | socketID: socket.id,
229 | songID: song.id
230 | })
231 | }
232 | else
233 | playSon(socket)
234 | }
235 |
236 | const makeName = (() => {
237 | let id = 0
238 | return () => {
239 | return `游客${id++}号`
240 | }
241 | })()
242 |
243 | const requestSongWorker = (song, socket, callback) => {
244 | let id = song.id
245 | if(songs.exists(id)) {
246 | socket.emit('putSong', {code: 500, message: song.name + '已经在点歌列表中'})
247 | } else {
248 | Log(`${dateFormat(Date.now(), 'yyyy-mm-dd HH:MM:ss')},${socket.name},${socket._id},${song.name}`)
249 | Object.assign(song, {username: socket.name, userid: socket._id})
250 | if(song.mv!=null) {
251 | songs.add(Object.assign({}, song, {
252 | mvurl: SUFFIX + '?id=' + song.mv,
253 | mv: song.mv
254 | }))
255 | socket.broadcast.emit('putSong', {code: 200, song: song})
256 | song.isSelf = true
257 | socket.emit('putSong', {code: 200, song: song})
258 | if(songs.size() == 1) {
259 | broadCastPlaySon()
260 | }
261 | } else {
262 | gs.getSongUrl(id).then((x) => {
263 | if(x.code!=200) {
264 | socket.emit('putSong', {code: 500, message: x.msg})
265 | } else if(x.data.url == null) {
266 | socket.emit('putSong', {code: 500, message: '该歌曲无数据'});
267 | } else {
268 | songs.add(Object.assign({}, song, x.data))
269 | socket.broadcast.emit('putSong', {code: 200, song: song})
270 | song.isSelf = true
271 | socket.emit('putSong', {code: 200, song: song})
272 | if(songs.size() == 1) {
273 | broadCastPlaySon()
274 | }
275 | }
276 | })
277 | }
278 | }
279 | }
280 |
281 |
282 | function broadCastPlaySon(opt = {}) {
283 | _playSon(null, opt)
284 | }
285 | function _playSon(socket, opt) {
286 | let fn = socket ? socket.emit.bind(socket) : broadcast
287 | let first = songs.getFirst()
288 | if(first) {
289 | if(first.mv>0) {
290 | fn('play', Object.assign(opt, first))
291 | } else {
292 | gs.getSongUrl(first.id)
293 | .then(x=>{
294 | gs.getLyric(first.id)
295 | .then(y => {
296 | fn('play', Object.assign(opt, first, x.data, {lyric: y}));
297 | })
298 | })
299 | }
300 | }
301 | else
302 | fn('play')
303 | }
304 | function playSon(socket, opt = {}) {
305 | _playSon(socket, opt)
306 | }
307 |
--------------------------------------------------------------------------------
/src/server/static/core.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by Moyu on 16/8/19.
3 | */
4 |
5 | // var source = new EventSource("/stream");
6 | // source.onerror = function (error) {
7 | // console.error("error", error)
8 | // }
9 | // source.onmessage = function (event) {
10 | // console.log(event.data);
11 | // }
12 |
13 | !function (w, d) {
14 | var socket = io();
15 | var ipt = d.querySelector('.ipt-container input')
16 | var tip = d.querySelector('.tips')
17 | var container = d.querySelector('.container')
18 | var msgs = d.querySelector('.msg-items')
19 | var songs = d.querySelector('.songs-items')
20 | var videoC = d.querySelector('.video-c')
21 | var audio = d.querySelector('audio')
22 | var video = d.querySelector('video')
23 | var currentPlay = d.querySelector('#currentPlay')
24 | var suggest = d.querySelector('.suggest')
25 | var time = d.querySelector('#musicBox span')
26 | var musicBox = d.querySelector('#musicBox')
27 | var range = d.querySelector('#musicBox input')
28 | var lyricDom = d.querySelector('.lyric')
29 | var color = d.querySelector('input[type=color]')
30 | var lyricC = d.querySelector('#lyric-c')
31 | var title = d.head.querySelector('title')
32 | setProps(title, title.innerText)
33 | localStorage['bullet'] && insertStyle(localStorage['bullet'])
34 | localStorage['word'] && insertStyle(localStorage['word'])
35 |
36 |
37 | /* common begin */
38 | function bindEventListener(ele, type, fn, bub) {
39 | bub = bub || false
40 | ele[type] = fn.bind(ele)
41 | ele.addEventListener(type, fn, bub)
42 | }
43 | function appendBullet(val, isSelf) {
44 | function randDuration(low, delta) {
45 | return low + (Math.random()*delta);
46 | }
47 | function randTop(el, low) {
48 | var bound = el.clientHeight - 20
49 | return low + (Math.random()*(bound - low));
50 | }
51 | var span = d.createElement('span')
52 | span.className = 'bullet-text ' + (isSelf?'isSelf':'')
53 | span.innerText = val
54 | if(!video.paused && !video.ended) {
55 | span.style.top = randTop(videoC, 20)+'px'
56 | span.style.animationDuration = (videoC.clientWidth < 800 ? randDuration(5, 2) : randDuration(videoC.clientWidth * .01, 2)) +'s'
57 | videoC.appendChild(span)
58 | // var all = container.querySelectorAll('.bullet-text')
59 | // all && all.forEach(a=>{
60 | // a.remove()
61 | // })
62 | } else {
63 | span.style.top = randTop(container, 90)+'px'
64 | span.style.animationDuration = randDuration(5, 2)+'s'
65 | container.appendChild(span)
66 | // var all = videoC.querySelectorAll('.bullet-text')
67 | // all && all.forEach(a=>{
68 | // a.remove()
69 | // })
70 | }
71 | }
72 | function binarySearch(ar, compare_fn) {
73 | var m = 0;
74 | var n = ar.length - 1;
75 | while (m <= n) {
76 | var k = (n + m) >> 1;
77 | var cmp = compare_fn(ar[k]);
78 | if (cmp > 0) {
79 | m = k + 1;
80 | } else if(cmp < 0) {
81 | n = k - 1;
82 | } else {
83 | return k;
84 | }
85 | }
86 | return -1;
87 | }
88 | function getCurrentWords(lrc, begin, sec, wordnum, curPos) {
89 | if(curPos==null)
90 | curPos = parseInt(wordnum/2)
91 | var tmp = lrc.slice(begin)
92 | if(tmp.length===0) return null
93 | var i = tmp.findIndex(function (word) {
94 | return word[0] >= sec
95 | })
96 | i = i>=0 ? i+begin-1 : lrc.length-1
97 | // if(i>0) {
98 | // if(sec - lrc[i-1][0] >= lrc[i][0] - sec){
99 | //
100 | // } else{
101 | // i--
102 | // }
103 | // }
104 | // console.log(i)
105 | var rlt = new Array(wordnum)
106 | begin = i - curPos
107 | for(var j=0; j=0 ? i : 0
137 | var time = word.substring(1, i).match(/(\d+):(\d+)\.(\d+)/)
138 | time = time && time.length>=4 && (parseInt(time[1]*60) + parseInt(time[2]) + parseFloat((time[3]*.001).toFixed(3)))
139 | return [time, word.substring(i+1)]
140 | })
141 | }
142 | lyricDom.innerHTML = ''
143 | setProps(lyricDom, {
144 | id: id,
145 | lrc: getFormattedLyric(lyric.lrc),
146 | hlLyric: null
147 | })
148 |
149 | }
150 | function setTip(v) {
151 | tip.innerText = v
152 | tip.timer!=null && clearTimeout(tip.timer)
153 | tip.timer = setTimeout(function () {
154 | tip.innerText = ''
155 | }, 2000)
156 | }
157 | function setTitle(text) {
158 | title.innerText = text
159 | }
160 | function setCurrentPlay(song) {
161 | var s, info
162 | if(song) {
163 | info = '\uD83D\uDC49 '+ song.name + ' - ' + song.author + ' \uD83D\uDC48'
164 | s = '正在播放: ' + info
165 | setTitle(info)
166 | } else {
167 | s = ''
168 | setTitle(getProps(title))
169 | }
170 | currentPlay.innerText = s
171 | setProps(currentPlay, {
172 | song: song
173 | })
174 | }
175 | function appendMsg(msg) {
176 | function mkP(text) {
177 | var span = d.createElement('p')
178 | // span.className = cls
179 | span.innerText = text
180 | return span
181 | }
182 | var li = d.createElement('li')
183 | li.appendChild(mkP(msg.welcome ? ('欢迎: '+msg.text) : msg.bye ? ('Bye: '+msg.text) : (msg.name+': '+msg.text)))
184 | msgs.appendChild(li)
185 | msgs.scrollTop = msgs.scrollHeight
186 | }
187 | function appendSong(song) {
188 | function mkP(text, id, close) {
189 | var span = d.createElement('p')
190 | span.innerText = text
191 | span.id = 'p'+id;
192 | if(close) {
193 | var s = d.createElement('span')
194 | s.className = 'btn-close'
195 | span.appendChild(s)
196 | s.innerText = 'X'
197 | }
198 | return span
199 | }
200 | var li = d.createElement('li')
201 | li.appendChild(mkP(song.username+' 点歌 '+song.name+' - '+song.author, song.id, song.isSelf))
202 | songs.appendChild(li)
203 | songs.scrollTop = songs.scrollHeight
204 | }
205 | function removeSelector(el, parent) {
206 | parent = parent || d
207 | var ele = parent.querySelector(el)
208 | ele && ele.remove()
209 | }
210 | function setProps(el, props) {
211 | if(typeof props === 'object' || typeof props === 'undefined')
212 | el.props = Object.assign(el.props||{}, props)
213 | else
214 | el.props = props
215 | }
216 | function getProps(el, key) {
217 | return key==null ? el.props : (el.props && el.props[key])
218 | }
219 | function toggleMiniVideo() {
220 | if(videoC.classList.contains('mini')) {
221 | setProps(videoC, {
222 | miniStyle: {
223 | right: videoC.style.right,
224 | top: videoC.style.top,
225 | width: videoC.style.width
226 | }
227 | })
228 | videoC.style.right = videoC.style.top = videoC.style.width = ''
229 | } else {
230 | var style = getProps(videoC, 'miniStyle')
231 | if(style) {
232 | for(var k in style) {
233 | videoC.style[k] = style[k]
234 | }
235 | }
236 | }
237 |
238 | videoC.classList.toggle('mini')
239 | var mini = videoC.querySelector('.mini')
240 | mini.classList.toggle('vertical')
241 | videoC.querySelector('.mini.second').classList.toggle('show')
242 | }
243 | function fixVideoCloseBtn() {
244 | videoC.style.height = w.getComputedStyle(video).height
245 | videoC.style.maxHeight = video.style.maxHeight = w.innerHeight+'px'
246 | }
247 | function playSong(song) {
248 | if(!song.id)
249 | return
250 | setCurrentPlay(song)
251 | if(song.mv > 0) {
252 | var all = videoC.querySelectorAll('.bullet-text')
253 | all = [].slice.call(all)
254 | all && all.forEach(a=>{
255 | a.remove()
256 | })
257 | videoC.style.display = 'block'
258 | musicBox.style.display = 'none'
259 | video.src = song.mvurl
260 | video.dataset['sid'] = song.id
261 | if(song.pic) {
262 | video.poster = song.pic.picUrl
263 | container.style.backgroundImage='url("'+song.pic.picUrl+'")'
264 | }
265 | if(song.curTime)
266 | video.currentTime = song.curTime
267 | video.play()
268 | lyricDom.style.display = 'none'
269 | } else {
270 | videoC.style.display = 'none'
271 | musicBox.style.display = 'block'
272 | audio.src = song.url
273 | audio.dataset['sid'] = song.id
274 | if(song.curTime)
275 | audio.currentTime = song.curTime
276 | audio.play()
277 | lyricDom.style.display = ''
278 | container.style.backgroundImage='url("'+song.pic.picUrl+'")'
279 | song.lyric && song.lyric.code==200 && setLyric(song.lyric, song.id)
280 | }
281 | let hl = songs.querySelector('.hl')
282 | let active = songs.querySelector('#p'+song.id)
283 | hl && hl!==active && hl.remove()
284 | songs.querySelector('#p'+song.id)
285 | .classList.add('hl')
286 | }
287 | var SUG_URL = '/api/sug'
288 | function get(url, callback, type) {
289 | var xhr = new XMLHttpRequest()
290 | xhr.open('GET', url, true)
291 | xhr.send(null)
292 | xhr.onreadystatechange = function() {
293 | if (xhr.readyState==4 && xhr.status==200) {
294 | var t = xhr.responseText
295 | switch (type) {
296 | case 'json':
297 | t = JSON.parse(t)
298 | break
299 | }
300 | callback(t)
301 | }
302 | }
303 | }
304 | function setSuggests(songs) {
305 | suggest.innerHTML = ''
306 | function createLi(song, active) {
307 | var li = d.createElement('li')
308 | li.innerText = song.name+' - '+song.author
309 | setProps(li, song)
310 | li.className = active?'active':''
311 | return li
312 | }
313 | songs.forEach(function (x, i) {
314 | suggest.appendChild(createLi(x, i==0))
315 | })
316 | }
317 | function suggestIsShow () {
318 | return suggest.style.visibility=='visible'
319 | }
320 | function getTimeStr(sec) {
321 | var m = parseInt(sec/60)
322 | var s = parseInt(sec - m*60)
323 | return m+':'+s
324 | }
325 | function showSuggest (val) {
326 | if(val=='') return;
327 | if(/^点歌 (.+)$/.test(val)) {
328 | val = RegExp.$1.trim()
329 | get(SUG_URL+'?s='+val, function (json) {
330 | if(json.code!=200)
331 | setTip(json.message)
332 | else {
333 | suggest.style.visibility = 'visible'
334 | setSuggests(json.result)
335 | }
336 | }, 'json')
337 | }
338 | }
339 | function getCurrentSong() {
340 | return getProps(currentPlay, 'song') || {}
341 | }
342 | function insertStyle(css) {
343 | var h = d.head || d.querySelector('head')
344 | var sty = d.createElement('style')
345 | sty.setAttribute('type', 'text/css')
346 | sty.innerText = css
347 | h.appendChild(sty)
348 | }
349 | /* common end */
350 | /* events begin */
351 | bindEventListener(d.body, 'mousedown', function (e) {
352 | var targ = e.target
353 | if(videoC.classList.contains('mini')) {
354 | if(targ === video) {
355 | videoC.mouseDownMove = true
356 | videoC.offset = {
357 | x: e.offsetX,
358 | y: e.offsetY
359 | }
360 | videoC.classList.add('moving')
361 | return
362 | } else if(targ.classList.contains('resize-left')) {
363 | videoC.mouseDownResize = true
364 | return
365 | }
366 | }
367 | if(targ.classList.contains('word')) {
368 | lyricDom.mouseDownMove = true
369 | lyricDom.offset = {
370 | x: e.offsetX,
371 | y: e.offsetY
372 | }
373 | lyricDom.classList.add('moving')
374 | }
375 | }, false)
376 | bindEventListener(d.body, 'mouseup', function (e) {
377 | videoC.mouseDownMove = false
378 | videoC.mouseDownResize = 0
379 | lyricDom.mouseDownMove = false
380 | videoC.classList.remove('moving')
381 | lyricDom.classList.remove('moving')
382 | })
383 | bindEventListener(d.body, 'mousemove', function (e) {
384 | var x = e.clientX, y = e.clientY
385 | function moveHandler(ele) {
386 | ele.style.width = w.getComputedStyle(ele).width
387 | if(w.getComputedStyle(ele).position!='fixed')
388 | ele.style.position = 'fixed'
389 | var offsetx = ele.offset.x, offsety = ele.offset.y
390 | ele.style.right = (w.innerWidth- ele.clientWidth - (x-offsetx))+'px'
391 | ele.style.top = (y-offsety)+'px'
392 | }
393 | if(videoC.mouseDownMove) {
394 | moveHandler(videoC)
395 | } else if(lyricDom.mouseDownMove){
396 | moveHandler(lyricDom)
397 | lyricDom.classList.add('moved')
398 | }else if(videoC.mouseDownResize) {
399 | var MIN_WIDTH = 140
400 | var MAX_WIDTH = w.innerWidth - 10
401 | var currentWidth = videoC.clientWidth
402 | var left = videoC.getBoundingClientRect().left
403 | var delta = left - x
404 |
405 | var computedWidth = currentWidth+delta
406 | videoC.style.width = (computedWidth>MIN_WIDTH ? computedWidth=0)
430 | next = ( find + (e.keyCode==38?-1:1) ) % l
431 | else
432 | next = 0
433 | next = next>=0?next:l+next
434 | active && active.classList.remove('active')
435 | if(suggest.children[next]) {
436 | var h = [].slice.call(suggest.children, 0, next).reduce((p,n)=>{
437 | return p+n.clientHeight
438 | }, 0)
439 | suggest.scrollTop = h
440 | suggest.children[next].classList.add('active')
441 | }
442 | }
443 | })
444 | bindEventListener(ipt, 'blur', function () {
445 | setTimeout(function () {
446 | suggest.style.visibility = 'hidden'
447 | }, 100)
448 | })
449 | bindEventListener(ipt, 'focus', function () {
450 | var val = this.value.trim()
451 | showSuggest(val)
452 | })
453 | bindEventListener(ipt, 'input', function (e) {
454 | var val = this.value.trim()
455 | showSuggest(val)
456 | })
457 |
458 | bindEventListener(audio, 'ended', function (e) {
459 | audio.pause()
460 | audio.src = ''
461 | musicBox.style.display = 'none'
462 | setCurrentPlay(null)
463 | container.style.backgroundImage = ''
464 | socket.emit('playEnd', audio.dataset.sid)
465 | removeSelector('.hl', songs)
466 | lyricDom.style.display = 'none'
467 | lyricDom.innerHTML = ''
468 | })
469 |
470 |
471 | bindEventListener(audio, 'timeupdate', function (e) {
472 | musicBox.style.display = 'block'
473 | var s = getTimeStr(audio.currentTime) + ' - ' + getTimeStr(audio.duration)
474 | time.innerText = s
475 | var p = getProps(lyricDom)
476 | if(p && p.id == getCurrentSong().id) {
477 | var bg = hlSec = 0
478 | if(p.hlLyric && p.hlLyric.hlIndex!=null) {
479 | bg = p.hlLyric.hlIndex>=0?p.hlLyric.hlIndex:0
480 | hlSec = p.lrc[bg] && p.lrc[bg][0] || 0
481 | }
482 | if(/* audio.currentTime > hlSec && */!p.rendering) {
483 | // add lock
484 | bg = audio.currentTime > hlSec ? bg : 0
485 | setProps(lyricDom, {rendering: true})
486 | var hlLyric = getCurrentWords(p.lrc, bg, audio.currentTime, 3)
487 | // p.tlrc ? getCurrentWords(p.tlrc, bg, audio.currentTime, 3) : undefined
488 | if(hlLyric){
489 | p.hlLyric = hlLyric
490 | renderHlLyric(hlLyric.rlt, hlLyric.hlPos)
491 | }
492 | setProps(lyricDom, {rendering: false})
493 | }
494 | }
495 | })
496 | bindEventListener(video, 'timeupdate', function (e) {
497 | // console.log('video timeupdate: ', video.currentTime)
498 | })
499 | bindEventListener(video, 'ended', function (e) {
500 | video.pause()
501 | video.src = ''
502 | videoC.style.display = 'none'
503 | setCurrentPlay(null)
504 | container.style.backgroundImage = ''
505 | socket.emit('playEnd', video.dataset.sid)
506 | removeSelector('.hl', songs)
507 | })
508 | bindEventListener(videoC, 'click', function(e) {
509 | var target = e.target
510 | if(target.classList.contains('btn-close') || target.parentElement.classList.contains('btn-close')) {
511 | toggleMiniVideo()
512 | fixVideoCloseBtn()
513 | }
514 | })
515 | bindEventListener(w, 'resize', function (e) {
516 | fixVideoCloseBtn()
517 | })
518 | bindEventListener(video, 'resize', function (e) {
519 | fixVideoCloseBtn()
520 | })
521 | bindEventListener(songs, 'click', function (e) {
522 | var t = e.target
523 | if(t.classList.contains('btn-close')) {
524 | var sid = t.parentElement.id.substring(1)
525 | socket.emit('deleteSong', sid)
526 | }
527 | })
528 | bindEventListener(suggest, 'click', function (e) {
529 | var t = e.target
530 | if(t.tagName == 'LI') {
531 | socket.emit('reqsong', getProps(t))
532 | }
533 | })
534 | bindEventListener(range, 'change', function (e) {
535 | audio.volume = range.value / 100
536 | })
537 | bindEventListener(container, 'click', function (e) {
538 | var t = e.target
539 | if(t.classList.contains('btn-bullet-color')) {
540 | setProps(color, 'bullet')
541 | color.click()
542 | }
543 | if(t.classList.contains('btn-word-color')) {
544 | setProps(color, 'word')
545 | color.click()
546 | }
547 | })
548 | bindEventListener(color, 'change', function (e) {
549 | if(getProps(this) == 'bullet') {
550 | var s = "main .bullet-text {color: "+this.value+";}"
551 | localStorage['bullet'] = s
552 | insertStyle(s)
553 | }
554 | if(getProps(this) == 'word') {
555 | var s = ".container .lyric .word {color: "+this.value+";}"
556 | localStorage['word'] = s
557 | insertStyle(s)
558 | }
559 | })
560 | /* events end */
561 |
562 | audio.volume = 1
563 | /* socket.io begin */
564 | socket
565 | .on('login', function () {
566 | var name = prompt('输入名字: ', '')
567 | songs.innerHTML = ''
568 | socket.emit('login', (name==null?'':name).trim())
569 | videoC.style.display = 'none'
570 | musicBox.style.display = 'none'
571 | audio.pause()
572 | video.pause()
573 | setCurrentPlay(null)
574 | container.style.backgroundImage = ''
575 | }).on('bullet', function (data) {
576 | if(data.forbid) {
577 | setTip('5s内不能多次发送消息')
578 | } else {
579 | ipt.value = ''
580 | appendBullet(data.val, data.isSelf)
581 | }
582 | }).on('message', function (msg) {
583 | appendMsg(msg)
584 | }).on('initSongs', function (songs) {
585 | songs.innerHTML = ''
586 | songs.forEach(function (song) {
587 | appendSong(song)
588 | })
589 | }).on('putSong', function (data) {
590 | if(data.code==200) {
591 | appendSong(data.song)
592 | } else {
593 | setTip('错误: ' + data.message)
594 | }
595 | }).on('play', function (song) {
596 | if(song) {
597 | playSong(song)
598 | } else {
599 | lyricDom.style.display = 'none'
600 | lyricDom.innerHTML = ''
601 | setTip('现在还没人点歌哦')
602 | }
603 | }).on('currentTime', function (idObj) {
604 | if(getCurrentSong() && getCurrentSong().id == idObj.songID) {
605 | var curTime = 0
606 | if(!audio.paused)
607 | curTime = audio.currentTime
608 | else if(!video.paused)
609 | curTime = video.currentTime
610 | socket.emit('currentTime', {
611 | id: idObj.socketID,
612 | curTime: curTime
613 | })
614 | }
615 | }).on('deleteSong', function (json) {
616 | if(json.isSelf) {
617 | removeSelector('#p'+json.id, songs)
618 | }else {
619 | setTip('不能删除不是你点的歌曲')
620 | }
621 | })
622 | /* socket.io end */
623 | }(window, document)
--------------------------------------------------------------------------------
/src/server/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 点歌机器人 -- Moyu
6 |
7 |
8 |
9 |
10 |
11 | 3:11
12 |
13 |
14 |
15 |
20 |
21 |
点歌机器人 -- moyu
22 |
23 |
24 |
26 |
27 |
28 |
29 |
弹幕
30 |
歌词
31 |
32 |
33 |
34 |
35 |
42 |
43 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/server/static/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | font-family: "Abadi MT Condensed Light";
5 | user-select: none;
6 | }
7 |
8 | .row-c {
9 | margin-right: auto;
10 | margin-left: auto;
11 | /*overflow: hidden;*/
12 | padding-left: 15px;
13 | padding-right: 15px;
14 | }
15 | body {
16 |
17 | background-color: #E9E9E9;
18 | }
19 |
20 | main {
21 | /*width: 100%;*/
22 | /*margin: auto;*/
23 | position: absolute;
24 | top: 0;
25 | bottom: 0;
26 | left: 0;
27 | right: 0;
28 | }
29 |
30 | h1 {
31 | text-align: center;
32 | border-bottom: 1px solid #ccc;
33 | margin-top: 10px;
34 | padding-bottom: 4px;
35 | margin-bottom: 10px;
36 | margin-left: 20px;
37 | margin-right: 20px;
38 | }
39 |
40 | main .container {
41 | background-position: center center;
42 | background-size: auto 100%;
43 | position: relative;
44 | overflow: hidden;
45 | background-color: #eeeeee;
46 | height: 73%;
47 | border-radius: 8px;
48 | box-shadow:4px 4px 5px #aaaaaa;
49 | }
50 | .container .lyric.moved {
51 | z-index: 300;
52 | }
53 |
54 | .container .lyric.moving {
55 | background-color: rgba(56, 56, 56, .5);
56 | }
57 | .container .lyric:hover {
58 | background-color: rgba(56, 56, 56, .5);
59 | }
60 | #lyric-c,
61 | .container .lyric {
62 | padding-top: 5px;
63 | padding-bottom: 5px;
64 | position: absolute;
65 | width: 100%;
66 | height: 60px;
67 | bottom: 5px;
68 | }
69 | .container .lyric {
70 | cursor: move;
71 | text-align: center;
72 | bottom: 0px;
73 | -webkit-user-select: none;
74 | -moz-user-select: none;
75 | -ms-user-select: none;
76 | user-select: none;
77 | }
78 |
79 | .container .lyric .word.hl {
80 | /*color: #880000;*/
81 | opacity: 1;
82 | }
83 | .container .btn-bullet-color,
84 | .container .btn-word-color {
85 | position: absolute;
86 | bottom: 3px;
87 | font-size: 14px;
88 | color: #660000;
89 | border: 1px solid #660000;
90 | cursor: pointer;
91 | border-radius: 3px;
92 | z-index: 50;
93 | }
94 | .container .btn-bullet-color {
95 | left: 3px;
96 | }
97 | .container .btn-word-color {
98 | right: 3px;
99 | }
100 | .container .lyric .word {
101 | font-family: Consolas, Menlo, Monaco, monospace;
102 | height: 18px;
103 | font-size: 16px;
104 | padding-top: 2px;
105 | display: block;
106 | color: #880000;
107 | opacity: .5;
108 | }
109 |
110 | .video-c:not(.mini) .resize {
111 | display: none;
112 | }
113 |
114 | .video-c .resize {
115 | cursor: ew-resize;
116 | position: absolute;
117 | height: 100%;
118 | background-color: transparent;
119 | }
120 |
121 | .video-c .resize-left {
122 | width: 2px;
123 | left: 0;
124 | }
125 |
126 | .video-c .resize-right {
127 | width: 1px;
128 | right: 0;
129 | }
130 |
131 | .col-2 {
132 | width: 25%;
133 | }
134 | .col-8 {
135 | width: 75%;
136 | }
137 | .row{
138 | /* 向外左右延伸15px */
139 | height: 100%;
140 | margin-right: -15px;
141 | margin-left: -15px;
142 | }
143 | /* 防止子元素为float,父元素的高度为0 */
144 | .row:before,
145 | .row:after {
146 | content: " ";
147 | display: table;
148 | }
149 | .row:after {
150 | clear: both;
151 | }
152 | .col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9{
153 | padding-left: 15px;
154 | padding-right: 15px;
155 | float: left;
156 | min-height: 10px;
157 | height: 100%;
158 | box-sizing: border-box;
159 | }
160 | .col-6 {
161 | width: 50%;
162 | }
163 | main {
164 | /*background-color: #eeeeee;*/
165 | overflow-y: scroll;
166 | }
167 | .ipt-container {
168 | margin-top: -30px;
169 | width: 80%;
170 | margin-left: 50%;
171 | transform: translate(-50%, 50%);
172 | position: relative;
173 | }
174 | .ipt-container .suggest{
175 | /*display: none;*/
176 | visibility: hidden;
177 | z-index: 299;
178 | list-style: none;
179 | position: absolute;
180 | height: 90px;
181 | overflow-x: hidden;
182 | overflow-y: scroll;
183 | border-radius: 4px;
184 | box-shadow: 2px 2px 3px #aaa;
185 | }
186 | .ipt-container .suggest li:hover,
187 | .ipt-container .suggest li.active,
188 | .ipt-container .suggest li:active {
189 | background-color: #eee;
190 | }
191 | .ipt-container .suggest li {
192 | cursor: pointer;
193 | font-family: Consolas, Menlo, Monaco, monospace;
194 | border: 1px solid black;
195 | border-top: none;
196 | padding: 4px 10px;
197 | background-color: #fff;
198 | }
199 |
200 | main .suggest,
201 | main input {
202 | box-sizing: border-box;
203 | outline: none;
204 | display: block;
205 | width: 100%;
206 | height: 30px;
207 | }
208 | main .tips {
209 |
210 | display: inline-block;
211 | margin-top: 6px;
212 | color: #ff0000;
213 | }
214 | main input {
215 | font-size: 18px;
216 | padding-left: 10px;
217 | padding-right: 10px;
218 | border-radius: 4px;
219 | border: 1px solid black;
220 | -webkit-user-select: text;
221 | -moz-user-select: text;
222 | -ms-user-select: text;
223 | user-select: text;
224 | color: steelblue;
225 | text-shadow: 0px 0px 0px #000;
226 | -webkit-text-fill-color: transparent;
227 | }
228 | main input:focus {
229 | box-shadow: 1px 1px 1px #aaaaaa;
230 | }
231 | input::-webkit-input-placeholder {
232 | text-shadow: none;
233 | -webkit-text-fill-color: initial;
234 | }
235 |
236 | main .bullet-text {
237 | position: absolute;
238 | font-size: 20px;
239 | color: white;
240 | white-space: nowrap;
241 | display: inline-block;
242 | font-family: Consolas;
243 | transform: translateX(-100%);
244 |
245 | animation-name: r2l;
246 | animation-iteration-count: 1;
247 | animation-timing-function: linear;
248 | }
249 | main .bullet-text.isSelf {
250 | padding: 4px;
251 | border: 2px dashed darkred;
252 | }
253 | @keyframes r2l {
254 | 0% {
255 | transform: translateX(0%);
256 | left: 100%;
257 | }
258 | 100% {
259 | left: 0;
260 | transform: translateX(-100%);
261 | }
262 | }
263 |
264 | .songs-items, .msg-items {
265 | list-style: none;
266 | margin-top: 70px;
267 |
268 | height: 64%;
269 | overflow-y: scroll;
270 | background-color: #eeeeea;
271 | border: 1px solid #111111;
272 | border-radius: 5px;
273 | padding: 10px;
274 | }
275 | .songs-items .btn-close {
276 | margin-left: 5px;
277 | height: 18px;
278 | width: 18px;
279 | color: #111111;
280 |
281 | position: absolute;
282 | right: -24px;
283 | top: 0px;
284 | text-align: center;
285 | border: 1px solid black;
286 | cursor: pointer;
287 | }
288 | .songs-items p {
289 | margin-top: 5px;
290 | margin-bottom: 5px;
291 | position: relative;
292 | margin-right: 25px;
293 | }
294 | .songs-items .hl .btn-close{
295 | display: none;
296 | }
297 | .msg-items li .name {
298 | display: block;
299 | margin-right: 6px;
300 | float: left;
301 | }
302 | .msg-items li .text {
303 | display: block;
304 | float: left;
305 | }
306 | .msg-items li:after {
307 | content: " ";
308 | display: table;
309 | clear: both;
310 | }
311 | .msg-items li {
312 | vertical-align: text-top;
313 | }
314 |
315 | .hl {
316 | transition: transform 1.5s;
317 | color: #880000;
318 | /*transform: scale(2);*/
319 | }
320 | main .video-c {
321 | position: fixed;
322 | margin: auto;
323 | left: 0;
324 | right: 0;
325 | top: 0;
326 | bottom: 0;
327 | display: none;
328 | height: auto;
329 | width: 100%;
330 | background-color: rgba(0, 0, 0, .7);
331 | overflow-x: hidden;
332 | z-index: 200;
333 | box-shadow: 3px 3px 4px #aaa;
334 | /*max-height: 560px;*/
335 | }
336 | main audio {
337 | display: none;
338 | }
339 | main video {
340 | width: 100%;
341 | height: auto;
342 | position: absolute;
343 | top: 50%;
344 | transform: translateY(-50%);
345 | }
346 | main .video-c.moving {
347 | border: 2px dashed #dae17b;
348 | }
349 | .video-c .btn-close {
350 | position: absolute;
351 | top: 10px;
352 | right: 10px;
353 | color: black;
354 | text-align: center;
355 | font-size: 23px;
356 | font-weight: bolder;
357 | width: 24px;
358 | height: 24px;
359 | line-height: 24px;
360 | border: 1px solid red;
361 | cursor: pointer;
362 | }
363 | .video-c .btn-close .mini {
364 | position: absolute;
365 | left: 0;
366 | right: 0;
367 | top: 0;
368 | bottom: 0;
369 | margin: auto;
370 | display: inline-block;
371 | height: 3px;
372 | background-color: red;
373 | width: 18px;
374 | }
375 | .video-c .btn-close .mini.second {
376 | display: none;
377 | }
378 | .show {
379 | display: inline-block!important;
380 | }
381 | .video-c .btn-close .mini.vertical {
382 | transform: rotate(-90deg);
383 | }
384 | .video-c.mini {
385 | /*transition: all 1s;*/
386 | cursor: move;
387 | margin: 0;
388 | width: 30%;
389 | top: 2px;
390 | right: 4px;
391 | left: auto;
392 | bottom: auto;
393 | }
394 | #currentPlay {
395 | color: #880000;
396 | position: fixed;
397 | display: inline-block;
398 | top: 10px;
399 | left: 10px;
400 | z-index: 300;
401 | }
402 |
403 | #musicBox {
404 | position: fixed;
405 | display: none;
406 | z-index: 300;
407 | top: 10px;
408 | right: 20px;
409 | }
410 | #musicBox span {
411 | margin-right: 10px;
412 | }
--------------------------------------------------------------------------------
/src/utils/Crypto.js:
--------------------------------------------------------------------------------
1 | var crypto = require('crypto');
2 | var bigInt = require('big-integer');
3 |
4 | function addPadding(encText, modulus) {
5 | var ml = modulus.length;
6 | for (i = 0; ml > 0 && modulus[i] == '0'; i++)ml--;
7 | var num = ml - encText.length, prefix = '';
8 | for (var i = 0; i < num; i++) {
9 | prefix += '0';
10 | }
11 | return prefix + encText;
12 | }
13 |
14 |
15 | function aesEncrypt(text, secKey) {
16 | var cipher = crypto.createCipheriv('AES-128-CBC', secKey, '0102030405060708');
17 | return cipher.update(text, 'utf-8', 'base64') + cipher.final('base64');
18 | }
19 |
20 | /**
21 | * RSA Encryption algorithm.
22 | * @param text {string} - raw data to encrypt
23 | * @param exponent {string} - public exponent
24 | * @param modulus {string} - modulus
25 | * @returns {string} - encrypted data: reverseText^pubKey%modulus
26 | */
27 | function rsaEncrypt(text, exponent, modulus) {
28 | var rText = '', radix = 16;
29 | for (var i = text.length - 1; i >= 0; i--) rText += text[i];//reverse text
30 | var biText = bigInt(new Buffer(rText).toString('hex'), radix),
31 | biEx = bigInt(exponent, radix),
32 | biMod = bigInt(modulus, radix),
33 | biRet = biText.modPow(biEx, biMod);
34 | return addPadding(biRet.toString(radix), modulus);
35 | }
36 |
37 | function createSecretKey(size) {
38 | var keys = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
39 | var key = "";
40 | for (var i = 0; i < size; i++) {
41 | var pos = Math.random() * keys.length;
42 | pos = Math.floor(pos);
43 | key = key + keys.charAt(pos)
44 | }
45 | return key;
46 | }
47 | var modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7';
48 | var nonce = '0CoJUm6Qyw8W8jud';
49 | var pubKey = '010001';
50 | var Crypto = {
51 | MD5: function (text) {
52 | return crypto.createHash('md5').update(text).digest('hex');
53 | },
54 | MD564: function (text) {
55 | return crypto.createHash('md5').update(text).digest().toString("base64");
56 | },
57 | aesRsaEncrypt: function (text) {
58 | var secKey = createSecretKey(16);
59 | return {
60 | params: aesEncrypt(aesEncrypt(text, nonce), secKey),
61 | encSecKey: rsaEncrypt(secKey, pubKey, modulus)
62 | }
63 | }
64 | };
65 | module.exports = Crypto;
66 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by Moyu on 16/8/19.
3 | */
4 | const request = require('request')
5 | const $ = require('cheerio')
6 |
7 |
8 | module.exports = {
9 | spider: (options, type) => {
10 | return new Promise((resolve, reject) => {
11 | request(options, function (err, res, body) {
12 | if (err) {
13 | reject(err)
14 | }else {
15 | switch (type) {
16 | case 'json':
17 | body = JSON.parse(body);
18 | break
19 | case 'jq':
20 | body = $.load(body);
21 | break
22 | }
23 | resolve(body)
24 | }
25 | })
26 | })
27 | },
28 | spiderStream: (options) => {
29 | return request(options)
30 | },
31 | Crypto: require('./Crypto'),
32 |
33 | suffix2Type: function (suffix) {
34 | return {
35 | css: 'text/css',
36 | html: 'text/html',
37 | js: 'application/javascript',
38 | mp4: 'video/mp4'
39 | }[suffix] || 'text/plain'
40 | },
41 | songs: require('./songs')
42 | }
43 |
--------------------------------------------------------------------------------
/src/utils/songs.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by Moyu on 16/8/19.
3 | */
4 |
5 | // let ;
6 | /*
7 | {
8 | id: '',
9 | pic: {},
10 | name: '',
11 | username: '',
12 | userid: ''
13 | }
14 | */
15 | let songs = []
16 | // const gs = require('../getsong')
17 | let songsMap = {}
18 |
19 | module.exports = {
20 | getFirst: () => {
21 | return songsMap[songs[0]]
22 | },
23 | deleteSelfSong:function (userid, id) {
24 | if(songsMap[id] && songsMap[id].userid==userid) {
25 | this.remove(id)
26 | return true
27 | }
28 | return false
29 | }
30 | ,
31 | exists: (id) => {
32 | return songsMap[id]!=null
33 | },
34 | add: (song) => {
35 | songs.push(song.id)
36 | songsMap[song.id] = song
37 | },
38 | remove: (id) => {
39 | if(songsMap[id]!=null) {
40 | delete songsMap[id]
41 | let i = songs.findIndex(x=>{
42 | return x == id
43 | })
44 | i>=0 && songs.splice(i, 1)
45 | }
46 | },
47 |
48 | toJSON: () => {
49 | return songs.map(id=>{
50 | let x = songsMap[id]
51 | return {
52 | id: x.id,
53 | name: x.name,
54 | username: x.username,
55 | userid: x.userid,
56 | author: x.author,
57 | }
58 | })
59 | },
60 | size: () => {
61 | return songs.length
62 | }
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/upload/1471696540554.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imcuttle/request-song-robot/00a7d1518de906371f5f367c19dca1837b690c03/upload/1471696540554.png
--------------------------------------------------------------------------------
/upload/1471697286239.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imcuttle/request-song-robot/00a7d1518de906371f5f367c19dca1837b690c03/upload/1471697286239.png
--------------------------------------------------------------------------------
/upload/1471697765689.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imcuttle/request-song-robot/00a7d1518de906371f5f367c19dca1837b690c03/upload/1471697765689.png
--------------------------------------------------------------------------------
/upload/1471705339720.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imcuttle/request-song-robot/00a7d1518de906371f5f367c19dca1837b690c03/upload/1471705339720.png
--------------------------------------------------------------------------------
/upload/gif3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imcuttle/request-song-robot/00a7d1518de906371f5f367c19dca1837b690c03/upload/gif3.gif
--------------------------------------------------------------------------------