Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6946dc8e14 | ||
|
|
22b78779ca | ||
|
|
35607f5050 | ||
|
|
60af95ac06 | ||
|
|
46073e5f50 | ||
|
|
bf12abe2f1 | ||
|
|
1491b91829 | ||
|
|
63f669d7f8 | ||
|
|
d525204dda | ||
|
|
6eb31984fd | ||
|
|
9faeb0aa25 | ||
|
|
f6db3e76e9 | ||
|
|
9163d48f0a | ||
|
|
1454ee216c | ||
|
|
83ee2cda34 | ||
|
|
3c352b5752 | ||
|
|
60ee927e32 | ||
|
|
eb07a525eb | ||
|
|
6fc4231142 | ||
|
|
fa69736300 | ||
|
|
73a5e9d788 | ||
|
|
dc6f56e8a8 | ||
|
|
2c9da8507d | ||
|
|
1db354464f | ||
|
|
395a80e74b | ||
|
|
6550c7c827 | ||
|
|
c959866436 | ||
|
|
e3a1c041b6 | ||
|
|
e2324e26da | ||
|
|
f01e770c35 | ||
|
|
abb56ec014 | ||
|
|
0300c1b51e | ||
|
|
9896242333 | ||
|
|
e3812be97b | ||
|
|
e7ff0da4ba | ||
|
|
65261fa658 | ||
|
|
cb5b2e7d0f | ||
|
|
f58fd4b50d | ||
|
|
1a5f91a192 | ||
|
|
50cc26f297 | ||
|
|
c92613b19e | ||
|
|
7aacc8990b | ||
|
|
8e934789a1 | ||
|
|
d26e6b0615 | ||
|
|
a9c33de182 | ||
|
|
d0935f6e42 | ||
|
|
3e0337ab94 | ||
|
|
5cf43c48ef | ||
|
|
68567f4a14 | ||
|
|
d0d71bb4f9 | ||
|
|
ec7eff1697 | ||
|
|
1882c3c31e | ||
|
|
f21f5677e8 | ||
|
|
fc5a76d5e2 | ||
|
|
fbc34a1846 | ||
|
|
3ef5ea341b | ||
|
|
4195c41171 | ||
|
|
232cc3d916 | ||
|
|
1b4af30a49 | ||
|
|
39e0e0718d | ||
|
|
01d391ac91 | ||
|
|
85d70ba9dd | ||
|
|
3cfecbcc1f | ||
|
|
c6e9746ed6 | ||
|
|
d6961ecb4d | ||
|
|
02242eecf6 | ||
|
|
269456def3 | ||
|
|
086e379f2c | ||
|
|
38803a18ae | ||
|
|
3980d09bcd | ||
|
|
64d4847350 | ||
|
|
cf74e90c1d | ||
|
|
a366983e99 | ||
|
|
6175d55f89 | ||
|
|
15fa49a2e8 | ||
|
|
f5fe69c863 | ||
|
|
b6be789ccd | ||
|
|
0395d087d9 | ||
|
|
3f679c0d87 | ||
|
|
ba9c6deaee | ||
|
|
2c36daf07a | ||
|
|
5b5f380cd5 | ||
|
|
ff7c5518d1 | ||
|
|
bbb2b9d4a0 | ||
|
|
3188238678 | ||
|
|
8e60324452 | ||
|
|
c9909cddd3 | ||
|
|
dbdbba3f98 | ||
|
|
27aa9a01cb | ||
|
|
30e522018f | ||
|
|
7b2507e43d | ||
|
|
3647fc5fa0 | ||
|
|
8521c198d3 | ||
|
|
4d8c4daa14 | ||
|
|
2e3cf4d451 | ||
|
|
288459cddb | ||
|
|
c1b8a381f3 | ||
|
|
2dd12b88ae | ||
|
|
b6a1f9ce0a | ||
|
|
5991e41c5d | ||
|
|
f61554714c | ||
|
|
266cd8d059 | ||
|
|
89a70b14d4 | ||
|
|
a9332af453 | ||
|
|
cffe68d9ff | ||
|
|
75ec2de66f | ||
|
|
0bd023c188 | ||
|
|
2c82c9b7be | ||
|
|
1320e5780b | ||
|
|
bde1f0864b | ||
|
|
e891287f1a | ||
|
|
59241631db | ||
|
|
29ef4f68c2 | ||
|
|
ee25eb17a6 | ||
|
|
31d2ec9225 | ||
|
|
b45bdd411f | ||
|
|
c14b06f6c3 | ||
|
|
b027aca40a | ||
|
|
b1729f19aa | ||
|
|
755489546b | ||
|
|
92df6e13a0 | ||
|
|
1ad8ab088f | ||
|
|
a09f126ab2 | ||
|
|
872bae1b43 | ||
|
|
83c527af01 | ||
|
|
84efe5d758 | ||
|
|
cfa475bf94 | ||
|
|
4087bafd7d | ||
|
|
8951e32a0e | ||
|
|
33ccc83615 | ||
|
|
2d6173b2aa | ||
|
|
ba7d1a8574 | ||
|
|
26d55255e0 | ||
|
|
4489f10f63 | ||
|
|
2047bdfa65 | ||
|
|
bc9d7b949f | ||
|
|
f08f192328 | ||
|
|
b04b9d51fb | ||
|
|
c91f5f3714 | ||
|
|
7fceec12e4 | ||
|
|
1df147edab | ||
|
|
6d1f75f229 | ||
|
|
2a0b54e442 | ||
|
|
a19418bbb6 | ||
|
|
ce51da9150 | ||
|
|
27002d7665 | ||
|
|
43d1c72f8f | ||
|
|
1d7e9cb6d8 | ||
|
|
609c910d5a | ||
|
|
2b69e9dda5 | ||
|
|
796c2803df | ||
|
|
ab00919a93 | ||
|
|
db2ca44bbc | ||
|
|
6c79f7495b | ||
|
|
fd0608adef | ||
|
|
f6fc452666 | ||
|
|
748d5da8fe | ||
|
|
6613fbf50d | ||
|
|
c8493b93ad | ||
|
|
b93578a879 | ||
|
|
de54c7fe97 | ||
|
|
2e707e2f5e | ||
|
|
2994e2fadd | ||
|
|
eb08ed4ba3 | ||
|
|
d5fedb5c54 | ||
|
|
f2f78b2774 | ||
|
|
871414f2e6 | ||
|
|
2d3f15f0e5 | ||
|
|
598dd8bd34 | ||
|
|
30bd839809 | ||
|
|
d4ee3b88f8 | ||
|
|
77eb154028 | ||
|
|
140ea858c4 | ||
|
|
f7f8c54236 | ||
|
|
bcda215a6c | ||
|
|
ca6834b83f | ||
|
|
c8fba0f7d1 | ||
|
|
546a84b1b8 | ||
|
|
205ac7292f | ||
|
|
1aba68561d | ||
|
|
bd2fdff5ae | ||
|
|
4af8323694 | ||
|
|
a5a1a91dfa | ||
|
|
ee43878834 | ||
|
|
9d18639464 | ||
|
|
fe28194445 | ||
|
|
b14cb9bb24 | ||
|
|
06739b2d6d | ||
|
|
8639c9d0f1 | ||
|
|
bc4a0d4916 | ||
|
|
4e00b94ee6 | ||
|
|
9019a2a4a5 | ||
|
|
3a10a4d753 | ||
|
|
f39aafaff4 | ||
|
|
2a8fb72d1d | ||
|
|
ea6ab4f6f7 | ||
|
|
6fe19a71e3 | ||
|
|
abd3a79ecd | ||
|
|
044ef355c1 | ||
|
|
2944ca42ba | ||
|
|
d4d465e731 | ||
|
|
f49d3cac19 | ||
|
|
2f2755ac8c | ||
|
|
f46c0d4a84 | ||
|
|
14ee86e15b | ||
|
|
5e820187a1 | ||
|
|
781299c1c5 | ||
|
|
8fc6ed1a76 | ||
|
|
8bb68accee | ||
|
|
55858d89d9 | ||
|
|
597ac1498f | ||
|
|
398f410109 | ||
|
|
97980b5ac3 | ||
|
|
65ff21526a | ||
|
|
fb052fdecd | ||
|
|
b6453a1c1b | ||
|
|
0c3da3bcb7 | ||
|
|
80fbd7a620 | ||
|
|
a4c9c0edaf | ||
|
|
19e51bc39a | ||
|
|
a914b5bc11 | ||
|
|
5eeeaeee8a | ||
|
|
1ab9def4d5 | ||
|
|
446ad4c871 | ||
|
|
316826dd81 | ||
|
|
11a240cb18 | ||
|
|
df2d0e6431 | ||
|
|
a1436e1d64 | ||
|
|
1f87ccf080 | ||
|
|
323b1014d2 | ||
|
|
d2351f6f30 | ||
|
|
dd653c07e9 | ||
|
|
173496f305 | ||
|
|
b45739b621 | ||
|
|
f63068b386 | ||
|
|
927b79030b | ||
|
|
6d6eee13b9 | ||
|
|
a7f086cf8f | ||
|
|
a38d689de4 | ||
|
|
4ffb20c4ff | ||
|
|
84c19ee237 | ||
|
|
f3d95f17b4 | ||
|
|
6d33efee47 | ||
|
|
acc458f6cb | ||
|
|
d78a20ea11 | ||
|
|
2cbd17a6d7 | ||
|
|
240d8042fb | ||
|
|
f9f6db3fee | ||
|
|
89e7c98412 | ||
|
|
16bf878135 | ||
|
|
8dcbe4db9d | ||
|
|
a1fbcf7008 | ||
|
|
c9c881ba7d | ||
|
|
0e0777d175 | ||
|
|
765734cd2d | ||
|
|
7f3be34fdd | ||
|
|
00af0c281d | ||
|
|
001f206a31 | ||
|
|
4d9045d597 | ||
|
|
e001de3efa | ||
|
|
6ae1ccd5e3 | ||
|
|
cb72918c55 | ||
|
|
ba16f2f0ab | ||
|
|
a76576af23 | ||
|
|
72526ba20d | ||
|
|
db05eddb9c | ||
|
|
fb6804364a | ||
|
|
9e4626e905 | ||
|
|
6c8f7be2a5 | ||
|
|
cf97ed45b7 | ||
|
|
1d640a789f | ||
|
|
22e47582c4 | ||
|
|
ed3959f545 | ||
|
|
771c2f9d52 | ||
|
|
f5b7fa2d26 | ||
|
|
b798f1383b | ||
|
|
6a1b7d987c | ||
|
|
db95ee1987 | ||
|
|
edb9fbba09 | ||
|
|
ede6534ebf | ||
|
|
15c6ea7266 | ||
|
|
a719631607 | ||
|
|
e7d9d0fdf2 | ||
|
|
27bba9ce55 | ||
|
|
27562092b1 | ||
|
|
053640fd33 | ||
|
|
f7ad6b7d61 | ||
|
|
da6f590632 | ||
|
|
96148385c4 | ||
|
|
348b7569b8 | ||
|
|
ea3acc0b14 | ||
|
|
09f6b862fe | ||
|
|
0535b88b24 | ||
|
|
9fe3a0f491 | ||
|
|
b73685ace3 | ||
|
|
f16c92e825 | ||
|
|
abd0c9ca91 | ||
|
|
4f4aa134d5 | ||
|
|
365450c26e | ||
|
|
24928fc0b4 | ||
|
|
8787bc6bb6 | ||
|
|
442d0a8f81 | ||
|
|
79edb14a03 | ||
|
|
ceb2044500 | ||
|
|
11e1c62cc3 | ||
|
|
1261f83a27 | ||
|
|
5125847ec4 | ||
|
|
a2befaad75 | ||
|
|
7def05ed30 | ||
|
|
7ece12c131 | ||
|
|
29ecf0ba11 | ||
|
|
a1d0a7b699 | ||
|
|
96f1a59d20 | ||
|
|
06d514141a | ||
|
|
e3a451780a | ||
|
|
598c887be8 | ||
|
|
b598978628 | ||
|
|
771777b669 | ||
|
|
c6bc299bbc | ||
|
|
974e17580b | ||
|
|
fae5c63c68 | ||
|
|
0ee7b55f99 | ||
|
|
0c99618380 | ||
|
|
de01a55aa2 | ||
|
|
6e63dce9d3 | ||
|
|
cecc04e60e | ||
|
|
34da6ddd6c | ||
|
|
2d4250931d | ||
|
|
b018be8695 | ||
|
|
fb2eb82a86 | ||
|
|
76d8e662f7 | ||
|
|
f5e3c8ae1b | ||
|
|
9925b4da91 | ||
|
|
9711915f7a | ||
|
|
4fdc4c1abc | ||
|
|
8246736140 | ||
|
|
a051864303 | ||
|
|
b91092d0c6 | ||
|
|
8d4b790f2b | ||
|
|
bf62107b17 | ||
|
|
2b662ae7c7 | ||
|
|
f617b066c9 | ||
|
|
fc66dc4de2 | ||
|
|
50093704b5 | ||
|
|
d6dea0895b | ||
|
|
83e51505d1 | ||
|
|
b06599a088 | ||
|
|
bf3b21af8d | ||
|
|
288d6fe63f | ||
|
|
eadff324d1 | ||
|
|
0b706626c8 | ||
|
|
35f79bb0ea | ||
|
|
646617dd8e | ||
|
|
05bdc89393 | ||
|
|
45609a8806 | ||
|
|
0ff650cd50 | ||
|
|
614e733fd1 | ||
|
|
f35c198c4e | ||
|
|
6ae1964685 | ||
|
|
b6a135e8b9 | ||
|
|
f4ac52756e | ||
|
|
9758f53058 | ||
|
|
5435ed70f3 | ||
|
|
950f9a2365 | ||
|
|
2d5f4630e9 | ||
|
|
88712e02a1 | ||
|
|
ffd5c095a9 | ||
|
|
007890be90 | ||
|
|
0e7ce27b3c | ||
|
|
eedd30295c | ||
|
|
ed28a571a6 | ||
|
|
879edd7fef | ||
|
|
90b9883691 | ||
|
|
e04bfbf3b4 | ||
|
|
ea8d2f56f7 | ||
|
|
455cb1929b | ||
|
|
07edbfed6d | ||
|
|
ff0dcc0b29 | ||
|
|
48dfefc039 | ||
|
|
fc85d6d6c7 | ||
|
|
88ca6938be | ||
|
|
8c8683557e | ||
|
|
ae3ed350a8 | ||
|
|
1fff1d559c | ||
|
|
a71eaf76f0 | ||
|
|
d45124dfbb | ||
|
|
5689016228 | ||
|
|
e84325625d | ||
|
|
0e18cb7fbe | ||
|
|
2113f7f78b | ||
|
|
5aaa2bc105 | ||
|
|
b796e87106 | ||
|
|
c5d29c72e9 | ||
|
|
bffc4aa59c | ||
|
|
a09c519cc2 | ||
|
|
9032a2e504 | ||
|
|
71272bf448 | ||
|
|
6d8b50da59 | ||
|
|
5a242a300e | ||
|
|
75549b6117 | ||
|
|
3433c33b7e | ||
|
|
a0b09afc30 | ||
|
|
a2d675cdd6 | ||
|
|
e6c3e95737 | ||
|
|
b25732e820 | ||
|
|
7e2683c660 | ||
|
|
9d3828b3c1 | ||
|
|
18c9ebe51e | ||
|
|
784eb1110e | ||
|
|
8a4db72ed1 | ||
|
|
016fe7c285 | ||
|
|
b970ed9704 | ||
|
|
8ee4167087 | ||
|
|
61983799fe | ||
|
|
e549d85c39 | ||
|
|
8750653418 | ||
|
|
20a626d413 | ||
|
|
c9a422a0d5 | ||
|
|
0dab5e55ba | ||
|
|
8d49669b26 | ||
|
|
c702774bb8 | ||
|
|
9447e7bb60 | ||
|
|
dd9ec456bf | ||
|
|
c1011b8602 | ||
|
|
0fc4057a81 | ||
|
|
377970c73d | ||
|
|
e9bad5d27e | ||
|
|
eae4c1f53c | ||
|
|
7e51c9cd5b | ||
|
|
422722e0a7 | ||
|
|
a0d4a3c27a | ||
|
|
2469823df2 | ||
|
|
9310aa450a | ||
|
|
02505b72d4 | ||
|
|
83cc9a7566 | ||
|
|
8de3e9fa66 | ||
|
|
656a69e07e | ||
|
|
f7132eba63 | ||
|
|
93ebc9cf51 | ||
|
|
b42e16f91e | ||
|
|
58e9865b70 | ||
|
|
bd8c049576 | ||
|
|
c38a8a0816 | ||
|
|
9006fdc938 | ||
|
|
157a058f74 | ||
|
|
9754f341ea | ||
|
|
2553caf13a | ||
|
|
90d2a9ad93 | ||
|
|
5e3a7ba027 | ||
|
|
f99564d1b4 | ||
|
|
2501ca2101 | ||
|
|
0d2e3232f3 | ||
|
|
46718c29f1 | ||
|
|
d78a4990fe | ||
|
|
6d6bd1ad77 | ||
|
|
d23b4a3653 | ||
|
|
1c5d203323 | ||
|
|
8d6fa6b6ca | ||
|
|
8c140fe6d3 | ||
|
|
7e38d10383 | ||
|
|
a75565929b | ||
|
|
e933c00ee8 | ||
|
|
7d2d715dbc | ||
|
|
1c1447b49a | ||
|
|
caf6432c08 | ||
|
|
dd7e109b92 | ||
|
|
1123672a86 | ||
|
|
fef44babf0 | ||
|
|
d5795aef08 | ||
|
|
5f7c752707 | ||
|
|
2d367aaf4f | ||
|
|
577e4b9aad | ||
|
|
ef7f998636 | ||
|
|
47a43bc4a9 | ||
|
|
6dd52cf95f | ||
|
|
6aa4418ecb | ||
|
|
dea65db2b3 | ||
|
|
5b30e1b222 | ||
|
|
076ee17286 | ||
|
|
5d07cdf071 | ||
|
|
b6310e1b24 | ||
|
|
15a2e1bcdf | ||
|
|
1c1c850b51 | ||
|
|
9b941ef17d | ||
|
|
b821ce2b76 | ||
|
|
3da8ff1d00 | ||
|
|
da0f9f6fd7 | ||
|
|
780f30f0b1 | ||
|
|
683b470ac0 | ||
|
|
8437f9a7cd | ||
|
|
cc0377ba7c | ||
|
|
eb92cef0b2 | ||
|
|
7b6e9d90e0 | ||
|
|
2bb5269dfa | ||
|
|
d55fb43f0a | ||
|
|
4fe71a4903 | ||
|
|
52892d2f03 | ||
|
|
fab8b50229 | ||
|
|
a9e0626b12 | ||
|
|
22f21eda3f | ||
|
|
fc3e62a69e | ||
|
|
c7c5769c05 | ||
|
|
9c9a439bc3 | ||
|
|
4ad08e0222 | ||
|
|
7201908b30 | ||
|
|
2ae029fb2a | ||
|
|
efd7a7b39c | ||
|
|
54496fc407 | ||
|
|
143fddd066 | ||
|
|
8f0d4bf490 | ||
|
|
d2f8f659f4 | ||
|
|
98c49de0aa | ||
|
|
18ca74214e | ||
|
|
dfcdc04fbb | ||
|
|
571c35da55 | ||
|
|
2be88c6a86 | ||
|
|
4b3cf00c8c | ||
|
|
9203e8a7d0 | ||
|
|
a34d4cfa2f | ||
|
|
0f458b1784 | ||
|
|
4c6d2910ca | ||
|
|
a42d2e491e | ||
|
|
35e1807c4c | ||
|
|
22ac50621c | ||
|
|
06362c4751 | ||
|
|
b8f9110f69 | ||
|
|
185031ddce | ||
|
|
daa377ffb9 | ||
|
|
32531f1331 | ||
|
|
516b906d9b | ||
|
|
61a1a17318 | ||
|
|
1c3050d6ef | ||
|
|
574364ae41 | ||
|
|
6a541a33a1 | ||
|
|
5c1f3aee5a | ||
|
|
a505114140 | ||
|
|
b5e67199bc | ||
|
|
caf2ae1c5f | ||
|
|
39f335efd7 | ||
|
|
fa5c733895 | ||
|
|
19e927d6ec | ||
|
|
7eccaa2c07 | ||
|
|
50ab628751 | ||
|
|
737e226471 | ||
|
|
ef7b1a935b | ||
|
|
be75a7b1b9 | ||
|
|
399cf2dc97 | ||
|
|
f8e839db2f | ||
|
|
efde4c308b | ||
|
|
52b2335749 | ||
|
|
af3846658a | ||
|
|
b4a9c8ece1 | ||
|
|
f7bba48508 | ||
|
|
4f4727e6bb | ||
|
|
d7a64982f9 | ||
|
|
b143af56c4 | ||
|
|
4755df842e | ||
|
|
6ac0baeffb | ||
|
|
18812abf66 | ||
|
|
262be06e67 | ||
|
|
43391930b3 | ||
|
|
0140eefce5 | ||
|
|
a89d2e6b5d | ||
|
|
afa12efcbb | ||
|
|
d3a5530d60 | ||
|
|
74e64d1594 | ||
|
|
d3d84eb5c0 | ||
|
|
7046219cd4 | ||
|
|
0912ae1458 | ||
|
|
d5ba853e3e | ||
|
|
dfa0639317 | ||
|
|
08ca5e8026 | ||
|
|
a8756990f7 | ||
|
|
ca54d662ae | ||
|
|
4d201f20eb | ||
|
|
0816c7411e | ||
|
|
bddb6dcbb5 | ||
|
|
255161f995 | ||
|
|
51eb568ea9 | ||
|
|
bea03fd60e | ||
|
|
75aa4c7b1b | ||
|
|
626209284d | ||
|
|
10bcd07fbd | ||
|
|
f37a8dae41 | ||
|
|
542d8573fc | ||
|
|
aa2e832ea3 | ||
|
|
932095663a | ||
|
|
a1f1a39300 | ||
|
|
4deeb52134 | ||
|
|
e42ac5b5b6 | ||
|
|
bd23bf1e9b | ||
|
|
e439a537fa | ||
|
|
ca14c983bb | ||
|
|
56f0317529 | ||
|
|
224f6e2cf8 | ||
|
|
336dc2a65a | ||
|
|
ddc9b58ed9 | ||
|
|
488a30b16f | ||
|
|
d42ec9acda | ||
|
|
bb7f0734f5 | ||
|
|
5506e31c43 | ||
|
|
a526741a55 | ||
|
|
8d1f196cf6 | ||
|
|
8a9b040092 | ||
|
|
715770246c | ||
|
|
9f242fc658 | ||
|
|
4127b047c9 | ||
|
|
f00dd4d363 | ||
|
|
4056015771 | ||
|
|
119d301e24 | ||
|
|
76506e65b7 | ||
|
|
c64ec3bc21 | ||
|
|
6114012be7 | ||
|
|
fa269f7f07 | ||
|
|
18e3566194 | ||
|
|
38e53640eb | ||
|
|
cdb1fb767d | ||
|
|
d1565a1848 | ||
|
|
b21fc110a0 | ||
|
|
e5db2139bc | ||
|
|
005a45309c | ||
|
|
957213bb93 | ||
|
|
39ad34fa0b | ||
|
|
38e5a8742c | ||
|
|
00d3594814 | ||
|
|
c3dbccf52a | ||
|
|
70db662026 | ||
|
|
37bad1cc33 | ||
|
|
1bc0b8f177 | ||
|
|
7e869aa919 | ||
|
|
1f6c8928b2 | ||
|
|
10d8ef5f43 | ||
|
|
b6f6a87a86 | ||
|
|
7fe9d36b73 | ||
|
|
2e933b3d61 | ||
|
|
8c52f4de1b | ||
|
|
52c6021d89 | ||
|
|
6741d9a920 | ||
|
|
dd2365bcb7 | ||
|
|
846b7e5fbc | ||
|
|
312434bc99 | ||
|
|
a7f508f3cf | ||
|
|
05c705f495 | ||
|
|
2e03ad008b | ||
|
|
08f4c3b3df | ||
|
|
ee96cc09a5 | ||
|
|
17c7317183 | ||
|
|
ff0206ee25 | ||
|
|
954093864d | ||
|
|
d7cf9d1c2f | ||
|
|
c2621a5870 | ||
|
|
e528f93ef7 | ||
|
|
ac99ba5822 | ||
|
|
5a0bdfb171 | ||
|
|
7f21fbcda1 | ||
|
|
8913131739 | ||
|
|
8c70630d2d | ||
|
|
e1c4ed4268 | ||
|
|
c0366c5da5 | ||
|
|
695b599e7d | ||
|
|
c44faaa224 | ||
|
|
511837df3a | ||
|
|
6b28f34b5b | ||
|
|
d20ec1af22 | ||
|
|
a892488320 | ||
|
|
092ef8b316 | ||
|
|
c505cf455f | ||
|
|
f741bb358b | ||
|
|
3935bd7b41 | ||
|
|
0764c52ffb | ||
|
|
653aad88c4 | ||
|
|
df9f2da995 | ||
|
|
6170e15e0d | ||
|
|
65be4e30e7 | ||
|
|
3022f2a47a | ||
|
|
35b2c8454b | ||
|
|
77a0dcda3b | ||
|
|
83eda822dd | ||
|
|
4606e248b2 | ||
|
|
8157758820 | ||
|
|
84953f2c7a | ||
|
|
789e26be6a | ||
|
|
3e5c052f98 | ||
|
|
0ff32acdc1 | ||
|
|
422191cea4 | ||
|
|
9d63df78c5 | ||
|
|
7eca25d390 | ||
|
|
5e3c84e6c3 | ||
|
|
de4f06196e | ||
|
|
23cceebdbd | ||
|
|
9debdc0bbc | ||
|
|
03fab7f2d7 | ||
|
|
ebfc130983 | ||
|
|
1917316718 | ||
|
|
0a83b98faf | ||
|
|
02c2a792e4 | ||
|
|
28e071fa94 | ||
|
|
b7aa9f934c | ||
|
|
4d7c51831b | ||
|
|
bebe44fb38 | ||
|
|
167d7a4c30 | ||
|
|
aa95a6185d | ||
|
|
4fc46a046c | ||
|
|
9240541331 | ||
|
|
764960a17d | ||
|
|
18cc8ce66b | ||
|
|
65463bf726 | ||
|
|
1946797d70 | ||
|
|
9f1bb9d1f6 | ||
|
|
f52f2988c9 | ||
|
|
715e22b389 | ||
|
|
848f899afe | ||
|
|
0015cf27cf | ||
|
|
5e7beb8203 | ||
|
|
d8abac46d2 | ||
|
|
8743e216c9 | ||
|
|
6a6c6e3dea | ||
|
|
51e1e3b6f9 | ||
|
|
f268e7b870 | ||
|
|
d73effd34a | ||
|
|
47772fe902 | ||
|
|
a106997d40 | ||
|
|
c71adfada5 | ||
|
|
4036dd9f93 | ||
|
|
7872d77d50 | ||
|
|
6a8144f227 | ||
|
|
912998b212 | ||
|
|
372883c35f | ||
|
|
985a054658 | ||
|
|
23516ca856 | ||
|
|
ff34f8ab11 | ||
|
|
4e14ca2786 | ||
|
|
f8dad7ec22 | ||
|
|
8ff9b82e36 | ||
|
|
1fadb1b98b | ||
|
|
f2ab58d3bb | ||
|
|
4994acd8a5 | ||
|
|
521f284b79 | ||
|
|
99f6c915d4 | ||
|
|
fe0ee3fba1 | ||
|
|
5adc11edfd | ||
|
|
8debae2c21 | ||
|
|
a85797665d | ||
|
|
61cdcdeee7 | ||
|
|
4e39afebeb | ||
|
|
07f5842230 | ||
|
|
ba3e8caf11 | ||
|
|
175932d0ab | ||
|
|
e05700004f | ||
|
|
b4ca93bda8 | ||
|
|
4d2343967d | ||
|
|
2af22a506e | ||
|
|
aa54bd8a24 | ||
|
|
ccaa4bc8d5 | ||
|
|
80d411f5b1 | ||
|
|
06dec2e79b | ||
|
|
ae032b72a4 | ||
|
|
88056db7bd | ||
|
|
4977423e3a | ||
|
|
20c9dd4125 | ||
|
|
beb8919315 | ||
|
|
3cca8d2924 | ||
|
|
b4f6ba0884 | ||
|
|
feaf083dfe | ||
|
|
cc10e81228 | ||
|
|
ae38be14ec | ||
|
|
56166c3157 | ||
|
|
890344aa97 | ||
|
|
15151ff8ac | ||
|
|
0d812fd0b0 | ||
|
|
6a3cf6fa2d | ||
|
|
3b908b34a5 | ||
|
|
020e825701 | ||
|
|
15492b996c | ||
|
|
6592ffaf19 | ||
|
|
dfc104e876 | ||
|
|
be156c5d22 | ||
|
|
b9e3b8f016 | ||
|
|
7a0321bcef | ||
|
|
0cc771244f | ||
|
|
9b7545b3f1 | ||
|
|
eb5c40bb33 | ||
|
|
647396f38c | ||
|
|
f6e69243ed | ||
|
|
86faba5817 | ||
|
|
d47714175f | ||
|
|
7c196a0bc5 | ||
|
|
6abee06600 | ||
|
|
92098e4b9a | ||
|
|
33cca95cb3 | ||
|
|
82ab334d69 | ||
|
|
0b0baa52bf | ||
|
|
b079f7d1ba | ||
|
|
71abe6faaf | ||
|
|
3e9f97cdb8 | ||
|
|
e8222ce3c6 | ||
|
|
10df8ecc3c | ||
|
|
7bb0eaaf7e | ||
|
|
53ec7ee8e1 | ||
|
|
9074a695a0 | ||
|
|
41bf7d2c9c | ||
|
|
ddb3042988 | ||
|
|
e174de0448 | ||
|
|
1591d0df6f | ||
|
|
7faff01f5b | ||
|
|
b1a9c93ebb | ||
|
|
b8898523e5 | ||
|
|
47bd9175ee | ||
|
|
b3ca43267a | ||
|
|
9e5c9372fe | ||
|
|
c0a82ece79 | ||
|
|
1d93513570 | ||
|
|
6589094d99 | ||
|
|
cff6bcf766 | ||
|
|
6bd63eb061 | ||
|
|
bef1b1c9ee | ||
|
|
065d4aa111 | ||
|
|
3027c704e2 | ||
|
|
1a3a9d3df6 | ||
|
|
338ba1ea49 | ||
|
|
c43bb9cb15 | ||
|
|
b0a9fe014d | ||
|
|
844dabeeff | ||
|
|
d7201c1b38 | ||
|
|
08dbeb1061 | ||
|
|
4eef73db71 | ||
|
|
f4130b0dd8 | ||
|
|
08334d85d8 | ||
|
|
d5250e8590 | ||
|
|
65765edafe | ||
|
|
598b7eff5f | ||
|
|
606819489a | ||
|
|
9505a4ede9 | ||
|
|
7e13a3361d | ||
|
|
9ce5055aad | ||
|
|
896560d554 | ||
|
|
bc3541f1a7 | ||
|
|
9f5f178d68 | ||
|
|
78640ffae3 | ||
|
|
81a218a2b1 | ||
|
|
22fbe0e3ed | ||
|
|
321e9c68f0 | ||
|
|
7c86728612 | ||
|
|
ad2608494f | ||
|
|
557874dcc2 | ||
|
|
c2c53ade74 | ||
|
|
28da98d102 | ||
|
|
15260bc154 | ||
|
|
b0701be845 | ||
|
|
3cbad92d2f | ||
|
|
684c581fe0 | ||
|
|
ff74170bf2 | ||
|
|
9cba287739 | ||
|
|
5195a56a79 | ||
|
|
878cc7fffc | ||
|
|
f803f774f1 | ||
|
|
faf35edef4 | ||
|
|
31e95721a7 | ||
|
|
064a128fd9 | ||
|
|
5001b283ee | ||
|
|
cdf1abdba8 | ||
|
|
9b67a09f4e | ||
|
|
c3996147e8 | ||
|
|
4050efc01c | ||
|
|
0ee9527980 | ||
|
|
8a1c84afc1 | ||
|
|
8cc6fe8676 | ||
|
|
6f6501cf10 | ||
|
|
02192b5863 | ||
|
|
d638be3455 | ||
|
|
0bc2a75879 | ||
|
|
da88795b86 | ||
|
|
530d331eb2 | ||
|
|
61d468ebbd | ||
|
|
61c8f2406c | ||
|
|
0fef46f7d1 | ||
|
|
ce6e29a7ec | ||
|
|
2752ad5270 | ||
|
|
4c643c7d36 | ||
|
|
8fa03ecd27 | ||
|
|
247081174a | ||
|
|
969d5d59c2 | ||
|
|
114b92b68b | ||
|
|
39708e48cb | ||
|
|
39d3fb83eb | ||
|
|
7a566765d9 | ||
|
|
6681f11ce0 | ||
|
|
61a0c0ebe1 | ||
|
|
d665429d77 | ||
|
|
b02d4d5e04 | ||
|
|
91a3ba758d | ||
|
|
e7c153d9e9 | ||
|
|
d5b2952299 | ||
|
|
36942a350c | ||
|
|
69a71cb710 | ||
|
|
c412f53f11 | ||
|
|
7061a9ea82 | ||
|
|
9e0c900f6f | ||
|
|
4e434a2691 | ||
|
|
5bf53028ef | ||
|
|
fc9630aa39 | ||
|
|
fb04e512c0 | ||
|
|
0ac00325c8 | ||
|
|
7985690987 | ||
|
|
6d335bd85f | ||
|
|
5daa11bdb6 | ||
|
|
3a914fb240 | ||
|
|
2bd00f932f | ||
|
|
8ea1dc5d8a | ||
|
|
9ebdb6a1d8 | ||
|
|
9579e709f1 | ||
|
|
cdee107ced | ||
|
|
644756d301 | ||
|
|
9e29c66c63 | ||
|
|
a23eda52da | ||
|
|
b30b8a75ce | ||
|
|
d2c1cf215d | ||
|
|
f643dd5e73 | ||
|
|
a0500ec648 | ||
|
|
4a941dd5e9 | ||
|
|
10a0751b00 | ||
|
|
775cda8e24 | ||
|
|
231a0ad6fe | ||
|
|
845c357aa3 | ||
|
|
4200833a30 | ||
|
|
8ecc3ce150 | ||
|
|
68ca8bdf88 | ||
|
|
0d4f4d671e | ||
|
|
168451712f | ||
|
|
fffdd57d69 | ||
|
|
96db47b4fe | ||
|
|
6ab08670ab | ||
|
|
95efccf0a6 | ||
|
|
79646fda5a | ||
|
|
8331a3f9f4 | ||
|
|
5221b9e671 | ||
|
|
0f27e22656 | ||
|
|
8d7c147696 | ||
|
|
873eb58899 | ||
|
|
5f4f5f8268 | ||
|
|
b43c0871ed | ||
|
|
96cab0cafe | ||
|
|
4c4c2d8233 | ||
|
|
9803e2e5a1 | ||
|
|
136f14db49 | ||
|
|
80961900f9 | ||
|
|
9f0ea1dabc | ||
|
|
9022f52156 | ||
|
|
615e1c1748 | ||
|
|
ba38d005bf | ||
|
|
96f84b0ace | ||
|
|
2f479f9622 | ||
|
|
8b7b45ca4a | ||
|
|
e15e7ea664 | ||
|
|
793c276e4e | ||
|
|
9e9c77541a | ||
|
|
6c3743959e | ||
|
|
ac02283a83 | ||
|
|
b931bfde40 | ||
|
|
42a64dc0e1 | ||
|
|
7385f05809 | ||
|
|
b8a19a2ee8 | ||
|
|
9fe5238ab8 | ||
|
|
107ba8b266 | ||
|
|
365391c081 | ||
|
|
0f694fb2a2 | ||
|
|
cd88b5924a | ||
|
|
0f72d6043a | ||
|
|
8aad07e683 | ||
|
|
7c7080ce3b | ||
|
|
1c05f8f57d | ||
|
|
a8a20ae48b | ||
|
|
5942e265bf | ||
|
|
f5cade95e4 | ||
|
|
8da05fe6f9 | ||
|
|
ffa1a0f0e6 | ||
|
|
0660928dc5 | ||
|
|
d608f100d1 | ||
|
|
51bbb25a0f | ||
|
|
ec36bc27b0 | ||
|
|
53ba3e175f | ||
|
|
0e658f9b5a | ||
|
|
28289beb8d | ||
|
|
ffeb4dc1c2 | ||
|
|
ddd2163e80 | ||
|
|
58865f5e7d | ||
|
|
6916b8fe63 | ||
|
|
b6642c958e | ||
|
|
2cab8b1bf3 | ||
|
|
52856371a4 | ||
|
|
7fb81da3ad | ||
|
|
d8bdb4002d | ||
|
|
077a347d41 | ||
|
|
ced768962a | ||
|
|
22e4da431d | ||
|
|
4eb4952262 | ||
|
|
0e12a4239b | ||
|
|
ffdc2f0137 | ||
|
|
54195037a4 | ||
|
|
04ab092dac | ||
|
|
f9073f27c6 | ||
|
|
7179935a39 | ||
|
|
4cb8177255 | ||
|
|
2859471bfd | ||
|
|
359ca4c538 | ||
|
|
f47c7269dd | ||
|
|
3961e358f1 | ||
|
|
ff5ace990c | ||
|
|
d602d5fa61 | ||
|
|
bbe19891a2 | ||
|
|
96f4d9adf5 | ||
|
|
801fc5a7ea | ||
|
|
e602cdd550 | ||
|
|
f79f707e6c | ||
|
|
7874fcb6c9 | ||
|
|
a5dfadc1ad | ||
|
|
9d5a79eb7f | ||
|
|
f081e8ae9e | ||
|
|
fa7efa28e4 | ||
|
|
2ecde76413 | ||
|
|
338ae58dc7 | ||
|
|
214415a119 | ||
|
|
509755a33b | ||
|
|
375a2e3d9c | ||
|
|
e42d19c2c2 | ||
|
|
3b8071b50e | ||
|
|
462af30e25 | ||
|
|
7f644bf61f | ||
|
|
c2d62cddfe | ||
|
|
60ffcd5f0a | ||
|
|
1cdc09226f | ||
|
|
122b6f765b | ||
|
|
45bfab349b | ||
|
|
25d1e3ae88 | ||
|
|
6a293401c2 | ||
|
|
3b04746278 | ||
|
|
8af9f78abc | ||
|
|
788d2f0e6f | ||
|
|
f2e6e6a258 | ||
|
|
c579ef887a | ||
|
|
7a2dd90960 | ||
|
|
35cfbbb2b6 | ||
|
|
36e8573ac6 | ||
|
|
a95f2270a9 | ||
|
|
af6d787af1 | ||
|
|
c2ba980a5a | ||
|
|
a27af8dbae | ||
|
|
5bd3b97081 | ||
|
|
63ff22bf34 | ||
|
|
4102a705fb | ||
|
|
b0e12093e5 | ||
|
|
b1beb10acf | ||
|
|
26506ad48c | ||
|
|
f2ec40d36e | ||
|
|
0475f3b61d | ||
|
|
ea86e5bbeb | ||
|
|
5494359948 | ||
|
|
e994bb836e | ||
|
|
f8ff652b36 | ||
|
|
369edad75e | ||
|
|
629eda2db6 | ||
|
|
806898ab81 | ||
|
|
8e7c7c5531 | ||
|
|
af27f1a319 | ||
|
|
055775bf75 | ||
|
|
83be4cc744 | ||
|
|
8fa42fe169 | ||
|
|
6c4899e38d | ||
|
|
f5437a34d2 | ||
|
|
dc6a303793 | ||
|
|
c2d738822e | ||
|
|
f23fbae6aa | ||
|
|
eba9641aa0 | ||
|
|
aded3f3ea5 | ||
|
|
a82f930297 | ||
|
|
e0eec75f08 | ||
|
|
97bd7fedf8 | ||
|
|
296082afcc | ||
|
|
ecb2f8e4b8 | ||
|
|
8804cf0ebc | ||
|
|
e9668a33d1 | ||
|
|
fbea33dfbd | ||
|
|
1ec1950cf8 | ||
|
|
a722b5b187 | ||
|
|
56086b709a | ||
|
|
679be3df69 | ||
|
|
e35a44e9ba | ||
|
|
8d467a1c2f | ||
|
|
05084c7f81 | ||
|
|
bf8f98d726 | ||
|
|
8d5797b11c | ||
|
|
c2876fda6c | ||
|
|
e3e7a93b28 | ||
|
|
d9f3b36634 | ||
|
|
7a1d1113cf | ||
|
|
0ecc2df590 | ||
|
|
259d3caf70 | ||
|
|
2d2172541a | ||
|
|
07df13d4c9 | ||
|
|
5c746cb0a4 | ||
|
|
ce2c254f1d | ||
|
|
f42390dc79 | ||
|
|
baeddb68f5 | ||
|
|
39b32b4b61 | ||
|
|
3fe3995f14 | ||
|
|
7e9dc3fe5f | ||
|
|
6c094b4f50 | ||
|
|
b6edef640a | ||
|
|
6a99d3fc97 | ||
|
|
df01fbf0c1 | ||
|
|
c8e921681c | ||
|
|
a95f4e693d | ||
|
|
aebf289b9a | ||
|
|
b92d139d82 | ||
|
|
d5c93f1ea3 | ||
|
|
4a4cf2874b | ||
|
|
166db1ee9a | ||
|
|
83a8c0ec27 | ||
|
|
ba3121a63c | ||
|
|
e20104d07f | ||
|
|
044ac78c17 | ||
|
|
11a72f1726 | ||
|
|
1e4f19ab89 | ||
|
|
bb468d5b55 | ||
|
|
4cc750d6ec | ||
|
|
3e58a4cadf | ||
|
|
44bb25e81c | ||
|
|
d1907ae541 | ||
|
|
368b7f6b69 | ||
|
|
ffd0212a28 | ||
|
|
216a2b8f7a | ||
|
|
c394e00b89 | ||
|
|
c99ddb4fae | ||
|
|
fbe87a64ef | ||
|
|
cd17ccfbaa | ||
|
|
1eb178d0e4 | ||
|
|
0b4cb08ca0 | ||
|
|
d289f6332d | ||
|
|
e18d14f1e2 | ||
|
|
8e627fb9fa | ||
|
|
60f017c1a1 | ||
|
|
25cc98fed3 | ||
|
|
289dbd13ce | ||
|
|
0591733faf | ||
|
|
118f208fcc | ||
|
|
19198dc227 | ||
|
|
50b1f69213 | ||
|
|
90fa494866 | ||
|
|
ceef2c461f | ||
|
|
7f5953edf1 | ||
|
|
282c19dcf7 | ||
|
|
3b5a8bac25 | ||
|
|
5eae918ef6 | ||
|
|
d64262676a | ||
|
|
cc6551ca9a | ||
|
|
f5640147e8 | ||
|
|
1a4127875c | ||
|
|
73187162ac | ||
|
|
bd89b97aea | ||
|
|
6512979e0e | ||
|
|
0dad665b1f | ||
|
|
d94b9e0f9b | ||
|
|
9430758db6 | ||
|
|
ffaa742298 | ||
|
|
52f074cad9 | ||
|
|
276607285e | ||
|
|
e67c2450e8 | ||
|
|
3cefae912c | ||
|
|
e79d134050 | ||
|
|
ba9549f21f | ||
|
|
c32bf8b697 | ||
|
|
db7fecde30 | ||
|
|
de163b2ac9 | ||
|
|
3acbf75f48 | ||
|
|
a1a547df61 | ||
|
|
27f49b3fd1 | ||
|
|
beefc04d20 | ||
|
|
a4259bca80 | ||
|
|
75cb79fc6a | ||
|
|
77775b25f5 | ||
|
|
43536b0220 | ||
|
|
f0c4c12dcf | ||
|
|
bd9cda8aa1 | ||
|
|
f56f7287c3 | ||
|
|
d555637af4 | ||
|
|
4dcc03b32c | ||
|
|
bffd9307b5 | ||
|
|
bf50fa65eb | ||
|
|
8f59c6df5f | ||
|
|
90ca861673 | ||
|
|
51770c1a15 | ||
|
|
d6ffd7ab7c | ||
|
|
59b71410fe | ||
|
|
4c5a1109fc | ||
|
|
6e1a5b9aae | ||
|
|
2acd96c226 | ||
|
|
a3be49850d | ||
|
|
d6e65ed33c | ||
|
|
20309e44d1 | ||
|
|
65e9215d06 | ||
|
|
40185476f2 | ||
|
|
99ec5bf065 | ||
|
|
00b63cfa7a | ||
|
|
4def4dcbaf | ||
|
|
0e1c7eec3d | ||
|
|
65e32eec77 | ||
|
|
f4a3a9cc72 | ||
|
|
f2c41cce0a | ||
|
|
41b2297047 | ||
|
|
b7cb44a574 | ||
|
|
45f30169ea | ||
|
|
f59962d90a | ||
|
|
d10089c549 | ||
|
|
d5e7f0a0e5 | ||
|
|
b87874ae13 | ||
|
|
60005769a1 | ||
|
|
78511e0f06 | ||
|
|
82a6fc53f4 | ||
|
|
2e9866a2a6 | ||
|
|
02cca66a22 | ||
|
|
0256763f48 | ||
|
|
e8f9a7539a | ||
|
|
0ca29afda6 | ||
|
|
fd3f5d722e | ||
|
|
7df0d7a199 | ||
|
|
9326d8c148 | ||
|
|
ed2e5fa953 | ||
|
|
3cac39f0b9 | ||
|
|
2f581d9960 | ||
|
|
4ca88d7850 | ||
|
|
6a77fba237 | ||
|
|
b59eaf492b | ||
|
|
edf7077a47 | ||
|
|
7c0e3a1937 | ||
|
|
497bb61f99 | ||
|
|
b69348b8c7 | ||
|
|
735367451c | ||
|
|
784ba807b3 | ||
|
|
5ffb7af18a | ||
|
|
8040aa3ac1 | ||
|
|
dcab50c4ca | ||
|
|
5236e06228 | ||
|
|
6e40a1bdd6 | ||
|
|
232232ef5b | ||
|
|
aff3d8de3e | ||
|
|
3f87e26aad | ||
|
|
5f61cdfe4c | ||
|
|
ecaf3a4eab | ||
|
|
ac6bf92b64 | ||
|
|
f3a4684a9e | ||
|
|
c22196b7b8 | ||
|
|
b913c4ba10 | ||
|
|
39b4f80134 | ||
|
|
c6c888c4b6 | ||
|
|
7bb48bace0 | ||
|
|
5adcc5a355 | ||
|
|
e187c9a228 | ||
|
|
2e25211364 | ||
|
|
38f468661b | ||
|
|
1d880bede7 | ||
|
|
a7019f5ca3 | ||
|
|
430111e532 | ||
|
|
c59ee69504 | ||
|
|
a58780460a | ||
|
|
8ad27ce358 | ||
|
|
f6319c8b6e | ||
|
|
57e5e0f5e5 | ||
|
|
cea2024288 | ||
|
|
8b529e55f0 | ||
|
|
ab37ff328d | ||
|
|
3b94c7aa63 | ||
|
|
123e137581 | ||
|
|
c16fc13568 | ||
|
|
6ee7179cb9 | ||
|
|
bfafe19352 | ||
|
|
85ea2d8934 | ||
|
|
0183f41df5 | ||
|
|
430c89cc48 | ||
|
|
6f0631a41a | ||
|
|
39fd1fed03 | ||
|
|
3e8e3cc392 | ||
|
|
0a7815fcb7 | ||
|
|
d7a740ae10 | ||
|
|
4f90957090 | ||
|
|
785bc9fa98 | ||
|
|
d09c19c64f | ||
|
|
372a81fb6e | ||
|
|
b935a55356 | ||
|
|
a42decd00f | ||
|
|
5baa1b2560 | ||
|
|
50a1cb367b | ||
|
|
cb4c07f421 | ||
|
|
8e35eee3b1 | ||
|
|
4e4adf1a5c | ||
|
|
a3fff1531b | ||
|
|
99a9813d70 | ||
|
|
cbb1fdc152 | ||
|
|
0781285b62 | ||
|
|
3383783f3c | ||
|
|
6dfba91da8 | ||
|
|
f725c86519 | ||
|
|
2dc8c1979f | ||
|
|
004f4846ab | ||
|
|
434326012c | ||
|
|
bfece83217 | ||
|
|
92612bc4fe | ||
|
|
19f71cfd62 | ||
|
|
5fd859f4d0 | ||
|
|
ac88b05e6e | ||
|
|
8f701d722f | ||
|
|
d4dc1f247a | ||
|
|
fee4eb7072 | ||
|
|
0fced69244 | ||
|
|
fc719aed64 | ||
|
|
3b15fe9024 | ||
|
|
4f7266e4a5 | ||
|
|
0ae2628fb6 | ||
|
|
b58bc739bf | ||
|
|
346593e0c3 | ||
|
|
36cb5f3705 | ||
|
|
606278a9be | ||
|
|
74996163cb | ||
|
|
44e0ac4182 | ||
|
|
1e4a90f19a | ||
|
|
e933997d26 | ||
|
|
c05c83db14 | ||
|
|
9ee30f363a | ||
|
|
4afb471d33 | ||
|
|
4e074b1061 | ||
|
|
bc71f537ad | ||
|
|
9aed821057 | ||
|
|
c831af12cd | ||
|
|
b806741905 | ||
|
|
132fcd9a45 | ||
|
|
f711b074cf | ||
|
|
da7a535cc6 | ||
|
|
c6312abbad | ||
|
|
139e9a1a39 | ||
|
|
40f8c7601c | ||
|
|
2a04853948 | ||
|
|
585592faa9 | ||
|
|
5fc7b5999d | ||
|
|
ef17617085 | ||
|
|
f89bd19899 | ||
|
|
0dacfa0a50 | ||
|
|
ca4ed2a98f | ||
|
|
44beef4098 | ||
|
|
2da9552c5b | ||
|
|
3015bb67b8 | ||
|
|
ebbefa0e94 | ||
|
|
9782c2dfd5 | ||
|
|
2b074ad9e0 | ||
|
|
e8cabc94c7 | ||
|
|
f6ab753c70 | ||
|
|
12bf3cd5fb | ||
|
|
047c3bb0bc | ||
|
|
9351387787 | ||
|
|
a91eb405bc | ||
|
|
9b066470ba | ||
|
|
0de21f3c73 | ||
|
|
31c24a287f | ||
|
|
caeb1d266d | ||
|
|
aa528b6cb3 | ||
|
|
ef4fa6e124 | ||
|
|
5077b36f47 | ||
|
|
f510aa552b | ||
|
|
b061667558 | ||
|
|
9d22d446fd | ||
|
|
88318a69a1 | ||
|
|
39305c0228 | ||
|
|
e830f3dc42 | ||
|
|
89a106b5b5 | ||
|
|
644ffcd069 | ||
|
|
f96fd3e3c2 | ||
|
|
025bfc338a | ||
|
|
362e10751f | ||
|
|
324cd8b67e | ||
|
|
9063c7dfc5 | ||
|
|
3c638e5f0a | ||
|
|
9bd30a0ffb | ||
|
|
31d7e3941e | ||
|
|
0d06fed76b | ||
|
|
acb49acab8 | ||
|
|
0ae3e159c6 | ||
|
|
8e49998a47 | ||
|
|
726f21b1e2 | ||
|
|
8df5d7a790 | ||
|
|
0ad746dad4 | ||
|
|
feefa9fc47 | ||
|
|
a38d314ee0 | ||
|
|
1c88464f1d | ||
|
|
b992f1138e | ||
|
|
39d4b2614e | ||
|
|
76cea02b23 | ||
|
|
9a25400234 | ||
|
|
a0abeed224 | ||
|
|
59368bd67a | ||
|
|
eda6869873 | ||
|
|
8a40f5bdf7 | ||
|
|
f6a051b851 | ||
|
|
27773a6d5a | ||
|
|
25e9206efb | ||
|
|
91d997c6c6 | ||
|
|
66e37b4449 | ||
|
|
fa535cdf37 | ||
|
|
885d1930f2 | ||
|
|
06013ed4d0 | ||
|
|
afd468ca18 | ||
|
|
0ec6716a4c | ||
|
|
7c110e8234 | ||
|
|
eb77b0474e | ||
|
|
ab89083fae | ||
|
|
494a33c385 | ||
|
|
20cba0aa15 | ||
|
|
1a362242d4 | ||
|
|
b7f8d9d3ff | ||
|
|
f420959df5 | ||
|
|
56168e1745 | ||
|
|
251e14bdd5 | ||
|
|
ceb99119d0 | ||
|
|
c6f0c2e5a0 | ||
|
|
ff8d57a34a | ||
|
|
e9fe4351b4 | ||
|
|
96d0898ca1 | ||
|
|
2cb2ed9458 | ||
|
|
148afed858 | ||
|
|
f410bbad73 | ||
|
|
a6c49d9cec | ||
|
|
4a0645d92e | ||
|
|
e5caf7a418 | ||
|
|
c3d2817c56 | ||
|
|
402cf0fbf2 | ||
|
|
0beb4e746f | ||
|
|
d6600472b2 | ||
|
|
a77736cb63 | ||
|
|
da01569d99 | ||
|
|
5e6e430687 | ||
|
|
ec3fac99e0 | ||
|
|
960c65e044 | ||
|
|
11e6bcbd1c | ||
|
|
efa2d416b3 | ||
|
|
9443bb2562 | ||
|
|
06e522b684 | ||
|
|
af85273466 | ||
|
|
40931b30c1 | ||
|
|
abf7bc4ec5 | ||
|
|
f254aa2b85 | ||
|
|
1818a4ceb9 | ||
|
|
5951985e4c | ||
|
|
641a72b06c | ||
|
|
8bde78595b | ||
|
|
9bf9ddd199 | ||
|
|
c0ff65b8d4 | ||
|
|
6d96d1065b | ||
|
|
308f250d6c | ||
|
|
b9f02d6b30 | ||
|
|
c0e4b39938 | ||
|
|
058e7b9a35 | ||
|
|
28ad57896b | ||
|
|
521b98ea46 | ||
|
|
2134432750 | ||
|
|
7a7001b386 | ||
|
|
932246e501 | ||
|
|
254426fa0c | ||
|
|
8b36cdece5 | ||
|
|
b1ebbf51be | ||
|
|
98bb292872 | ||
|
|
f848a00a80 | ||
|
|
52a4dd541f | ||
|
|
dbd1d7ab02 | ||
|
|
936f66d887 | ||
|
|
92a194acf2 | ||
|
|
eb2991f40f | ||
|
|
16f984db1e | ||
|
|
83b6bfcd1e | ||
|
|
aed229e212 | ||
|
|
f67f2b4924 | ||
|
|
5b9bfd7205 | ||
|
|
83e572f362 | ||
|
|
83ebe6d25b | ||
|
|
b98d611989 | ||
|
|
629533e674 | ||
|
|
5a5aa39af2 | ||
|
|
a24ced1098 | ||
|
|
974640550e | ||
|
|
5fa415635f | ||
|
|
42ef2d1052 | ||
|
|
d23158ac4c | ||
|
|
74650ea7b9 | ||
|
|
4223c07a70 | ||
|
|
ffbb13313e | ||
|
|
0d4d8cebe0 | ||
|
|
70cce6b9a5 | ||
|
|
07c4ae8736 | ||
|
|
cfa939bb84 | ||
|
|
d37906ec14 | ||
|
|
ec1400171e | ||
|
|
6bc08d95e4 | ||
|
|
a6b072ab39 | ||
|
|
90cdaa2fa4 | ||
|
|
cb494c444d | ||
|
|
771f249a70 | ||
|
|
356d5e2c46 | ||
|
|
b20e9d7733 | ||
|
|
6932995fc3 | ||
|
|
f85b8f70cf | ||
|
|
0ae76b6abd | ||
|
|
1bc8e5360a | ||
|
|
1391dd6abd | ||
|
|
cfcadb3ce0 | ||
|
|
99e60a5fc4 | ||
|
|
4163cbbe3d | ||
|
|
dabcd3a097 | ||
|
|
462fa33cbf | ||
|
|
a84c87f748 | ||
|
|
314c7a5796 | ||
|
|
e7df5cd70a | ||
|
|
57f903fd3d | ||
|
|
f7a1f8d429 | ||
|
|
0d7b8ec41e | ||
|
|
46400d930f | ||
|
|
d42c8b42c5 | ||
|
|
bf40e9c080 | ||
|
|
d670a4523d | ||
|
|
f73130f5af | ||
|
|
634a8a856b | ||
|
|
26b5cad8d5 | ||
|
|
becd7ed77b | ||
|
|
0e8fa1b978 | ||
|
|
34c92ddece | ||
|
|
d6ca1b5a79 | ||
|
|
d8969652f9 | ||
|
|
c8d456add7 | ||
|
|
9e1c701a6f | ||
|
|
a260e3b352 | ||
|
|
ded6dc74a7 | ||
|
|
36bef95b2a | ||
|
|
c79717794c | ||
|
|
b8852fe66f | ||
|
|
6a407a2df8 | ||
|
|
af8355f609 | ||
|
|
74c19ecd08 | ||
|
|
2dce97d800 | ||
|
|
4555c12959 | ||
|
|
9bd91df86c | ||
|
|
d225437271 | ||
|
|
a61c1250fc | ||
|
|
bcfdbe4406 | ||
|
|
d7406a1b05 | ||
|
|
2311b90743 | ||
|
|
2de499095b | ||
|
|
903c2de7ca | ||
|
|
def8a6b8d3 | ||
|
|
c453c1dc95 | ||
|
|
87dbf1d35e | ||
|
|
41b3f7e893 | ||
|
|
901bde6630 | ||
|
|
49b3b46c7a | ||
|
|
67031a0b98 | ||
|
|
3bf65d8233 | ||
|
|
333efd4040 | ||
|
|
60642de76c | ||
|
|
09b66934b6 | ||
|
|
7a63614c2f | ||
|
|
f9c85c0893 | ||
|
|
69ec96413f | ||
|
|
f8d4024b1f | ||
|
|
00844f9860 | ||
|
|
15d680745b | ||
|
|
c8ccadd0bd | ||
|
|
a441808012 | ||
|
|
b15ff33bd2 | ||
|
|
813e0a45b2 | ||
|
|
abba859bdf | ||
|
|
0dd6c4a49a | ||
|
|
f3eb7df160 | ||
|
|
4b735daa41 | ||
|
|
6c60c9d13d | ||
|
|
8bb37684f8 | ||
|
|
aea538c8ee | ||
|
|
0a344840f2 | ||
|
|
7d1e63d7c5 | ||
|
|
b91fcb1436 | ||
|
|
24d5cbb2ce | ||
|
|
9c8755b009 | ||
|
|
bfa194e849 | ||
|
|
fa8b9a22e5 | ||
|
|
3ad61baf96 | ||
|
|
da63d97145 | ||
|
|
a3ab099147 | ||
|
|
d5c0039566 | ||
|
|
87ed3314bb | ||
|
|
4c8e2260bd | ||
|
|
6421a6c9e3 | ||
|
|
9be0feccd8 | ||
|
|
5a7bd4baf3 | ||
|
|
80d49fcd6d | ||
|
|
e44cc285e4 | ||
|
|
7a041a7f54 | ||
|
|
36c85cb0a3 | ||
|
|
986a4b0dd5 | ||
|
|
7aca52e1b8 | ||
|
|
8fb18f030b | ||
|
|
d1ae7f4b56 | ||
|
|
71d832384c | ||
|
|
2bcccd46df | ||
|
|
a37c34092f | ||
|
|
b84fb38a7d | ||
|
|
08b5d5af27 | ||
|
|
b03914bd1f | ||
|
|
701bfdbd24 | ||
|
|
ec21f0600d | ||
|
|
6fead9a79e | ||
|
|
57c3b0f307 | ||
|
|
23b9a0e72f | ||
|
|
e410bef045 | ||
|
|
9924f233e4 | ||
|
|
1bef762d74 | ||
|
|
5826b245e5 | ||
|
|
981c0f7d39 | ||
|
|
0d373be689 | ||
|
|
a26ab583f3 | ||
|
|
44bcaad9fe | ||
|
|
50adbfd804 | ||
|
|
f8660cdca6 | ||
|
|
b018318251 | ||
|
|
145db79661 | ||
|
|
ac66ae339f | ||
|
|
fbd969e36b | ||
|
|
c91beb4aa6 | ||
|
|
42249ebba4 | ||
|
|
d4acd75a95 | ||
|
|
0c04f03717 | ||
|
|
3435d5e8dd | ||
|
|
710a9b6ed2 | ||
|
|
55c3b3e45a | ||
|
|
106a153597 | ||
|
|
b485c8ed40 | ||
|
|
33ba035216 | ||
|
|
2ae9aa5f22 | ||
|
|
2cfd81b24b | ||
|
|
231f08bca5 | ||
|
|
e0dd7a0836 | ||
|
|
4fa3f7b53b | ||
|
|
3ce4deaafe | ||
|
|
d3b267e2d5 | ||
|
|
a1a32d1cb2 | ||
|
|
481a37a3f7 | ||
|
|
1d9935fb07 | ||
|
|
8b912ced17 | ||
|
|
5b28a17c55 | ||
|
|
1e8ed2d556 | ||
|
|
9a42fc1a78 | ||
|
|
10f8e0abdc | ||
|
|
926375077a | ||
|
|
49d82d7988 | ||
|
|
af07374ba9 | ||
|
|
b95dd6e069 | ||
|
|
f4661b5166 | ||
|
|
02766dfabb | ||
|
|
2c5dcdfe8e | ||
|
|
adf9ae21d1 | ||
|
|
0b5e4ce77e | ||
|
|
d8043ecdef | ||
|
|
f80c588789 | ||
|
|
411f62f987 | ||
|
|
ea6cfefb69 | ||
|
|
2cdec0ac64 | ||
|
|
96f105aae1 | ||
|
|
ac1fa995d3 | ||
|
|
9687c94c36 | ||
|
|
22e6c0116f | ||
|
|
685041c0ab | ||
|
|
cc2783a248 | ||
|
|
f34dce37b4 | ||
|
|
b8d9fac78f | ||
|
|
7b9b151319 | ||
|
|
4610c50ea0 | ||
|
|
d9272b95bf | ||
|
|
8460e76026 | ||
|
|
be58ec7870 | ||
|
|
1cf8a8e16d | ||
|
|
3477542818 | ||
|
|
5ba31b67ed | ||
|
|
15f6a6b0c2 | ||
|
|
063feac04d | ||
|
|
dde6e00910 | ||
|
|
b1403ed009 | ||
|
|
e02b01070c | ||
|
|
fbf5c299dc | ||
|
|
f1201eee7b | ||
|
|
8ef0287023 | ||
|
|
3710984679 | ||
|
|
4cc9dfabd4 | ||
|
|
94a5404867 | ||
|
|
a737e1f2f2 | ||
|
|
d258e900b1 | ||
|
|
36726b5e90 | ||
|
|
1d42dbd2f2 | ||
|
|
ff0bab3d65 | ||
|
|
bd9695e4e4 | ||
|
|
65d2a3f918 | ||
|
|
d510468bc9 | ||
|
|
8d0a0590b9 | ||
|
|
69162ac6c0 | ||
|
|
821607f031 | ||
|
|
5e363fb2ee | ||
|
|
7c524ec79f | ||
|
|
420b7d1bc1 | ||
|
|
43bbc4410d | ||
|
|
becafe67a6 | ||
|
|
a8e76255ae | ||
|
|
44a9308fc6 | ||
|
|
a41cb262f2 | ||
|
|
cf084acb84 | ||
|
|
1f53b62ee0 | ||
|
|
df257cde7b | ||
|
|
b69f9d3792 | ||
|
|
b6c3b0591f | ||
|
|
97b6e5d67c | ||
|
|
4350f85ccd | ||
|
|
2bed2a6b52 | ||
|
|
078a433e14 | ||
|
|
9a6bd2a33f | ||
|
|
f454beb902 | ||
|
|
ebcf1f192b | ||
|
|
c37e763b2e | ||
|
|
13b93fda04 | ||
|
|
6ecf6183f0 | ||
|
|
854ac29d29 | ||
|
|
e5ad5e1eaf | ||
|
|
d2e6e8269c | ||
|
|
d9e8ffadf2 | ||
|
|
1ca1f234ba | ||
|
|
a2bb888de8 | ||
|
|
0cbcdf8117 | ||
|
|
544d2c7379 | ||
|
|
0a8680eaa3 | ||
|
|
c230be6ec3 | ||
|
|
b18fe7c21b | ||
|
|
a3d87faa7f | ||
|
|
c6d3fd95bc | ||
|
|
79489a40df | ||
|
|
35dbd5cb6d | ||
|
|
4856050bb1 | ||
|
|
1e7606077d | ||
|
|
477719569d | ||
|
|
b9fa1eff03 | ||
|
|
9401f7547d | ||
|
|
21e33a8a9f | ||
|
|
3afe5fae7c | ||
|
|
7f69947f2c | ||
|
|
642d6f5353 | ||
|
|
480b1696eb | ||
|
|
7a354cdd15 | ||
|
|
4c01e3e30e | ||
|
|
40fe20dba4 | ||
|
|
752d1965f7 | ||
|
|
76e850f573 | ||
|
|
f7ae26f067 | ||
|
|
cec2561040 | ||
|
|
ef20046451 | ||
|
|
597b648bba | ||
|
|
179acb3540 | ||
|
|
027e11ad84 | ||
|
|
dc186506f8 | ||
|
|
5ec46a12a6 | ||
|
|
e8b18d03ab | ||
|
|
a47a0e17ae | ||
|
|
f12c17ee93 | ||
|
|
9f6d3f1f98 | ||
|
|
602a4dbc4c | ||
|
|
4cddeb6a2e | ||
|
|
1c5b4664d2 | ||
|
|
4dfab51739 | ||
|
|
df3176444a | ||
|
|
2f573e0437 | ||
|
|
67bc5f240f | ||
|
|
aaac172e1f | ||
|
|
7a0b77ec7f | ||
|
|
3536649aa6 | ||
|
|
8f33edb746 | ||
|
|
c034f01852 | ||
|
|
a54bb4b78d | ||
|
|
6709ca371e | ||
|
|
48d0af5713 | ||
|
|
cb6ce5bb65 | ||
|
|
74211d01d9 | ||
|
|
085ee14cc8 | ||
|
|
9de0ab9787 | ||
|
|
34a2c1f93f | ||
|
|
22d5dd4e09 | ||
|
|
70657c4035 | ||
|
|
e02dc66150 | ||
|
|
8f74ac1b19 | ||
|
|
f787f6630e | ||
|
|
97daa4e309 | ||
|
|
966d4f56ad | ||
|
|
5648fbefe9 | ||
|
|
bbd33153b8 | ||
|
|
9b12fe3895 | ||
|
|
53b75c31cc | ||
|
|
cde7863060 | ||
|
|
1d8b13de2f | ||
|
|
bf0057ba1d | ||
|
|
1ff82e59af | ||
|
|
f62b5e7c04 | ||
|
|
64f781ee1f | ||
|
|
1904d82b5a | ||
|
|
f71f586073 | ||
|
|
a91bae2e86 | ||
|
|
2bccab0e73 | ||
|
|
5e4f410f8b | ||
|
|
3db3584f9f | ||
|
|
21fffb5f8b | ||
|
|
fbad2de39b | ||
|
|
b940bbf53b | ||
|
|
f78526ad19 | ||
|
|
5ecf33eb0a | ||
|
|
6aa09557d2 | ||
|
|
7e4887b8a2 | ||
|
|
889f3dd7e3 | ||
|
|
c1b28c506c | ||
|
|
842009219d | ||
|
|
c5f798b1e5 | ||
|
|
ae19c229ae | ||
|
|
ed4cd48fc6 | ||
|
|
5eb9920392 | ||
|
|
a0114df488 | ||
|
|
4a8008761a | ||
|
|
0d122d3753 | ||
|
|
e4906b9a40 | ||
|
|
b176b94e07 | ||
|
|
47c3525b24 | ||
|
|
a4366ec93d | ||
|
|
0db014e560 | ||
|
|
be2e0dd05a | ||
|
|
f4df416ba4 | ||
|
|
a3188e3141 | ||
|
|
e8d56d5e05 | ||
|
|
ace874a810 | ||
|
|
df8f8b7ff4 | ||
|
|
f4c14f772c | ||
|
|
ed0bca03cd | ||
|
|
176d08b4fe | ||
|
|
05bfb906fb | ||
|
|
c7ec5ec63c | ||
|
|
4189bc8903 | ||
|
|
b63e4d847f | ||
|
|
13b721f49b | ||
|
|
0712353b53 | ||
|
|
12ae881b97 | ||
|
|
37ffe991f3 | ||
|
|
7efb2fc17f | ||
|
|
737b947238 | ||
|
|
345561af59 | ||
|
|
5d5e26f685 | ||
|
|
e3c4377b4c | ||
|
|
9cb3723ed1 | ||
|
|
d390c03fd5 | ||
|
|
1c8fcb863f | ||
|
|
24476792f1 | ||
|
|
13f2dfd0ce | ||
|
|
b6feed4796 | ||
|
|
b236b5024e | ||
|
|
c99fc7b2db | ||
|
|
dd03d8dca6 | ||
|
|
484230d226 | ||
|
|
99e1b8989b | ||
|
|
2ce2284297 | ||
|
|
faefc77726 | ||
|
|
aedd7b8b05 | ||
|
|
f4b0a022ba | ||
|
|
873b70f626 | ||
|
|
9e5224ada4 | ||
|
|
ade1b14516 | ||
|
|
57747b6beb | ||
|
|
e4baca1a00 | ||
|
|
3635d44f9c | ||
|
|
f47851cb75 | ||
|
|
f8bbf7a29b | ||
|
|
5483cd158d | ||
|
|
0baed1a634 | ||
|
|
0911a5b630 | ||
|
|
6d2d7e3b9c | ||
|
|
f8599bf334 | ||
|
|
a352f37831 | ||
|
|
a05fbd3a2c | ||
|
|
ba58156bbe | ||
|
|
5654601ea0 | ||
|
|
02e7582db6 | ||
|
|
e4327ecad5 | ||
|
|
2ca81a35d9 | ||
|
|
d9d11b2d33 | ||
|
|
d6d0c5090e | ||
|
|
e9fb55cb53 | ||
|
|
5fdc25506a | ||
|
|
502538d619 | ||
|
|
69da1315c5 | ||
|
|
2e361aaafe | ||
|
|
3d571d0438 | ||
|
|
60e919f950 | ||
|
|
6f492092b9 | ||
|
|
6f85779152 | ||
|
|
106fea02bd | ||
|
|
f52db9c18e | ||
|
|
dc0ff3f7e4 | ||
|
|
a52ec2bfe5 | ||
|
|
e36d4a24d0 | ||
|
|
48d51123a2 | ||
|
|
da6a9c8a11 | ||
|
|
b4a9c17ad3 | ||
|
|
e36583fc1f | ||
|
|
d2cb1cfada | ||
|
|
77f848b74e | ||
|
|
1eb6f3a59f | ||
|
|
9f760ef532 | ||
|
|
53ef00440c | ||
|
|
109e999de7 | ||
|
|
c83cec40db | ||
|
|
0a8ee997de | ||
|
|
8c5485da6f | ||
|
|
8146d58a84 | ||
|
|
dffa68bc39 | ||
|
|
caaa981caa | ||
|
|
7acdd0fce6 | ||
|
|
3f9684f872 | ||
|
|
6630fa1e14 | ||
|
|
f6b9ff2441 | ||
|
|
51643e4116 | ||
|
|
50b8705345 | ||
|
|
179cec67d0 | ||
|
|
14e373ca92 | ||
|
|
617180f391 | ||
|
|
5265526133 | ||
|
|
9ed9cc8169 | ||
|
|
a1e4865025 | ||
|
|
26444f6ea8 | ||
|
|
ff0bd65aa2 | ||
|
|
084d348a76 | ||
|
|
072968e102 | ||
|
|
22c933c082 | ||
|
|
eddd08f757 | ||
|
|
95ae4b98c1 | ||
|
|
b3ac7e9ecf | ||
|
|
a2e4d26401 | ||
|
|
35f37afd77 | ||
|
|
a9caa0eb2a | ||
|
|
e62d3e904b | ||
|
|
1e0e6266b9 | ||
|
|
dc2abbc96d | ||
|
|
aae785cad9 | ||
|
|
07c5933e78 | ||
|
|
2cf9868494 | ||
|
|
928bdd4dcf | ||
|
|
c5f164a094 | ||
|
|
68a8635f4b | ||
|
|
79e837cebf | ||
|
|
49554adb0b | ||
|
|
79143e60f2 | ||
|
|
86e54c0032 | ||
|
|
c2460b0369 | ||
|
|
344b452e7d | ||
|
|
2ef4dfa1a9 | ||
|
|
162974442f | ||
|
|
4cc08d8c9f | ||
|
|
4e0e9ef989 | ||
|
|
bb76b4b5c2 | ||
|
|
71f5e43498 | ||
|
|
746e08d550 | ||
|
|
7890e4a0e8 | ||
|
|
5fb70c3717 | ||
|
|
3301d91ad7 | ||
|
|
457abe9730 | ||
|
|
7cfd77c6d3 | ||
|
|
53417c6c82 | ||
|
|
fda54149ea | ||
|
|
af7e5aac89 | ||
|
|
8270c7318b | ||
|
|
660be09d51 | ||
|
|
b22ceafadb | ||
|
|
9798e969cd | ||
|
|
ee05821801 | ||
|
|
bed5d08755 | ||
|
|
f4c1b6ccb5 | ||
|
|
2093fea6a1 | ||
|
|
f3404ce41f | ||
|
|
3f79c3c526 | ||
|
|
acdfd6f1ff | ||
|
|
572659f3ab | ||
|
|
8c2db97dbb | ||
|
|
178630f82d | ||
|
|
9a3bba3453 | ||
|
|
6c80dbc4aa | ||
|
|
774de65ca5 | ||
|
|
51bb402376 | ||
|
|
7029aa2a4b | ||
|
|
4151608edb | ||
|
|
c987542e86 | ||
|
|
ae69e262c3 |
@@ -7,8 +7,9 @@
|
|||||||
!/app.js
|
!/app.js
|
||||||
!/server.js
|
!/server.js
|
||||||
!/package.json
|
!/package.json
|
||||||
!/package-lock.json
|
|
||||||
!/index.js
|
!/index.js
|
||||||
!/generateConfig.js
|
!/generateConfig.js
|
||||||
!/main.js
|
!/main.js
|
||||||
!/data
|
!/data
|
||||||
|
!/.env
|
||||||
|
!/pnpm-lock.yaml
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
### CORS资源共享设置
|
||||||
|
## 如果你需要配置, 请取消下面的注释并设置具体的域名
|
||||||
|
# CORS_ALLOW_ORIGIN = "*"
|
||||||
|
|
||||||
|
### 代理设置
|
||||||
|
## 启用反向代理
|
||||||
|
ENABLE_PROXY = false
|
||||||
|
## 代理配置
|
||||||
|
PROXY_URL = "https://your-proxy-url.com/?proxy="
|
||||||
|
|
||||||
|
### 随机IP设置
|
||||||
|
## 启用随机中国IP(默认关闭);启用后,所有请求默认使用随机中国IP,除非请求参数randomCNIP显式关闭
|
||||||
|
ENABLE_RANDOM_CN_IP = false
|
||||||
|
|
||||||
|
### UnblockNeteaseMusic 设置项
|
||||||
|
## 启用全局解灰, 无论是否调用参数都会使用解灰(不推荐开启)
|
||||||
|
ENABLE_GENERAL_UNBLOCK = false
|
||||||
|
## 歌曲启用无损音质
|
||||||
|
ENABLE_FLAC = true
|
||||||
|
## 启用无损音质时,是否选择音质最高的
|
||||||
|
SELECT_MAX_BR = false
|
||||||
|
## 严格按照配置音源设置顺序进行匹配
|
||||||
|
FOLLOW_SOURCE_ORDER = true
|
||||||
|
## Cookie设置项; 推荐在Vercel等平台进行环境变量的设置或者设置github secret
|
||||||
|
# 网易云 cookie
|
||||||
|
# 格式:MUSIC_U=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
# NETEASE_COOKIE = ""
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
|
|
||||||
ignorePatterns: ['public/'],
|
|
||||||
parserOptions: {
|
|
||||||
parser: 'babel-eslint',
|
|
||||||
ecmaVersion: 2018,
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
plugins: ['html'],
|
|
||||||
extends: ['plugin:prettier/recommended'],
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
rules: {
|
|
||||||
'prettier/prettier': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
endOfLine: 'auto',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
indent: ['error', 2, { SwitchCase: 1 }],
|
|
||||||
'space-infix-ops': ['error', { int32Hint: false }],
|
|
||||||
'key-spacing': [
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
beforeColon: false,
|
|
||||||
afterColon: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'no-octal': 2,
|
|
||||||
'no-redeclare': 2,
|
|
||||||
'comma-spacing': 2,
|
|
||||||
'no-new-object': 2,
|
|
||||||
'arrow-spacing': 2,
|
|
||||||
quotes: [
|
|
||||||
2,
|
|
||||||
'single',
|
|
||||||
{
|
|
||||||
avoidEscape: true,
|
|
||||||
allowTemplateLiterals: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ['**/*.ts'],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
extends: [
|
|
||||||
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
|
||||||
// 'prettier/@typescript-eslint',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
custom: ["https://rewards.focalors.ltd"]
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
name: 创建问题反馈
|
|
||||||
about: 问题反馈
|
|
||||||
title: "<不按照模版格式或者删除模版信息将得不到处理,确认看完了调用前须知再发 issues>"
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 环境
|
|
||||||
- 系统/平台: <你的系统和平台>
|
|
||||||
|
|
||||||
- nodejs 版本: <你的 NodeJS 版本号>
|
|
||||||
|
|
||||||
- API版本: <运行的云音乐 API 的版本号, 对应 package.json 里面的 version>
|
|
||||||
|
|
||||||
## 出现问题
|
|
||||||
<请清晰描述出现的问题,不要使用xxx不行之类的模糊不清的语句,我没有读心术>
|
|
||||||
|
|
||||||
## 重现步骤
|
|
||||||
<请清晰描述重现步骤,调用的接口链接和参数以及命令行错误信息,最好贴上截图(命令行错误信息和接口返回内容)>
|
|
||||||
|
|
||||||
## 期待效果
|
|
||||||
<请清晰描述现在的效果和期待的效果>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
>维护项目都是业余时间,精力有限,我只能挑容易解决的issues处理,为了节约双方时间,请尽可能提供足够的有用的信息,给的信息不够我只能根据精力和时间看情况处理,如果模板信息看都不看就删掉,我不会进行任何回复,并且一个月后close掉issue
|
|
||||||
|
|
||||||
>先看文档有没有相关说明,调用前须知必看,确认看完了调用前须知再发 issues
|
|
||||||
|
|
||||||
>先在 issues 搜一下是否有相似问题,没有再发,否则直接关闭
|
|
||||||
|
|
||||||
>不处理别人搭建的线上服务的问题,此项目不提供任何线上服务,请自行搭建
|
|
||||||
|
|
||||||
>重现步骤尽量详细,不能含糊不清,包含请求地址和对应参数以及操作过程描述,不是每个人都喜欢猜别人遇到了什么问题和找参数一个个试,也比较浪费时间
|
|
||||||
|
|
||||||
>如果不是提建议,提 issues 如果不照着模版来将不会优先处理或放着不管
|
|
||||||
|
|
||||||
>460 cheating 的问题把 `utils/request.js` 里面的 `headers['X-Real-IP']` 的注释取消掉就好
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
name: 功能需求
|
|
||||||
about: 希望添加新接口
|
|
||||||
title: ''
|
|
||||||
labels: 功能需求
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**功能描述**
|
|
||||||
- 请清晰描述你所希望添加的功能,最好贴上对应客户端界面的截图
|
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
name: 🐛 Bug报告
|
||||||
|
description: 报告API其中的错误或问题
|
||||||
|
title: "[Bug] "
|
||||||
|
labels: ["bug"]
|
||||||
|
assignees: []
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢您报告Bug!请填写以下信息帮助我们快速定位和修复问题。
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: 确认事项
|
||||||
|
description: |
|
||||||
|
在提交Bug报告前,请确认以下事项:
|
||||||
|
|
||||||
|
- 我已经搜索了现有的issues,确认这不是重复问题
|
||||||
|
- 我使用的是最新版本的API, 而且是官方发布的版本,而不是fork或修改版
|
||||||
|
- 不处理别人搭建的线上服务的问题,此项目提供任何线上服务不保证质量
|
||||||
|
- 如果不是提建议,提 issues 如果不照着模版来将不会优先处理或放着不管
|
||||||
|
- 维护项目都是业余时间,精力有限,我只能挑容易解决的issues处理,为了节约双方时间,请尽可能提供足够的有用的信息,给的信息不够我只能根据精力和时间看情况处理,如果模板信息看都不看就删掉,我不会进行任何回复,并且一个月后close掉issue
|
||||||
|
|
||||||
|
options:
|
||||||
|
- label: 我已确认以上事项
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: API版本
|
||||||
|
description: 请提供您使用的API版本以及nodejs版本
|
||||||
|
placeholder: "v4.2x.x, Node.js v22.x.x"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: deployment
|
||||||
|
attributes:
|
||||||
|
label: 部署方式
|
||||||
|
description: 此项目支持本地部署和云平台部署,您使用的是哪种方式?
|
||||||
|
options:
|
||||||
|
- 本机直接运行 (node / pm2)
|
||||||
|
- 本机 Docker 部署
|
||||||
|
- Vercel
|
||||||
|
- Heroku
|
||||||
|
- Railway
|
||||||
|
- 其他云平台 (请在描述中说明)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: bug-description
|
||||||
|
attributes:
|
||||||
|
label: Bug描述
|
||||||
|
description: 请详细描述您遇到的问题
|
||||||
|
placeholder: |
|
||||||
|
清晰简洁地描述bug的具体表现...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction-steps
|
||||||
|
attributes:
|
||||||
|
label: 重现步骤
|
||||||
|
description: 请提供详细的重现步骤
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
attributes:
|
||||||
|
label: 期望行为
|
||||||
|
description: 请描述您期望发生的行为
|
||||||
|
placeholder: 清晰简洁地描述您期望应该发生什么...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual-behavior
|
||||||
|
attributes:
|
||||||
|
label: 实际行为
|
||||||
|
description: 请描述实际发生的行为
|
||||||
|
placeholder: 清晰简洁地描述实际发生了什么...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: error-output
|
||||||
|
attributes:
|
||||||
|
label: 错误输出日志
|
||||||
|
description: 请提供完整的错误信息日志
|
||||||
|
render: shell
|
||||||
|
placeholder: |
|
||||||
|
错误信息输出...
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: 额外信息
|
||||||
|
description: 添加任何其他有助于理解问题的信息
|
||||||
|
placeholder: |
|
||||||
|
- 截图
|
||||||
|
- 相关配置文件
|
||||||
|
- 其他相关信息
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: help
|
||||||
|
attributes:
|
||||||
|
label: 愿意协助
|
||||||
|
description: 您是否愿意帮助修复这个bug?
|
||||||
|
options:
|
||||||
|
- label: 我愿意提交PR来修复这个bug
|
||||||
|
- label: 我可以提供更多测试用例
|
||||||
|
- label: 我可以帮助测试修复方案
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: ↑请尽量使用议题模板创建议题↑
|
||||||
|
url: https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced/issues/new/choose
|
||||||
|
about: 选择适合的议题模板可以帮助我们更快地定位和解决问题
|
||||||
|
- name: 提问的艺术
|
||||||
|
url: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md
|
||||||
|
about: 默认所有 Issues 发起者均已了解此处的内容
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
name: ✨ 接口添加请求
|
||||||
|
description: 希望添加新接口
|
||||||
|
title: "[Feature] "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
assignees: []
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢您提出新功能建议!请填写以下信息帮助我们理解您的需求。
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: 确认事项
|
||||||
|
description: 在提交功能请求前,请确认以下事项
|
||||||
|
options:
|
||||||
|
- label: 我已经搜索了现有的issues,确认这不是重复请求
|
||||||
|
required: true
|
||||||
|
- label: 我已经查看了项目文档
|
||||||
|
required: true
|
||||||
|
- label: 该项目符合该项目的目标和范围
|
||||||
|
required: true
|
||||||
|
- label: 我确认我提供的接口请求是合理且有意义的
|
||||||
|
required: true
|
||||||
|
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: proposed-solution
|
||||||
|
attributes:
|
||||||
|
label: 功能描述
|
||||||
|
description: 请详细描述您希望添加的功能,最好贴上对应客户端界面的截图
|
||||||
|
placeholder: |
|
||||||
|
请详细描述您期望的功能:
|
||||||
|
- 新的网易云音乐api 接口?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: priority
|
||||||
|
attributes:
|
||||||
|
label: 优先级
|
||||||
|
description: 您认为这个功能的优先级如何?
|
||||||
|
options:
|
||||||
|
- 低 (可有可无的改进)
|
||||||
|
- 中 (会显著改善使用体验)
|
||||||
|
- 高 (严重影响了我使用)
|
||||||
|
- 紧急 (阻塞了重要工作)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: compatibility
|
||||||
|
attributes:
|
||||||
|
label: 兼容性考虑
|
||||||
|
description: 这个功能是否会影响现有代码的兼容性?
|
||||||
|
placeholder: |
|
||||||
|
- 是否是破坏性变更?
|
||||||
|
- 是否需要语法变更?
|
||||||
|
- 对性能的影响?
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: implementation
|
||||||
|
attributes:
|
||||||
|
label: 实现意愿
|
||||||
|
description: 您是否愿意参与这个功能的实现?
|
||||||
|
options:
|
||||||
|
- label: 我愿意提交PR来实现这个功能
|
||||||
|
- label: 我可以协助设计API接口
|
||||||
|
- label: 我可以提供测试用例
|
||||||
|
- label: 我可以帮助编写文档
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# .github/dependabot.yml
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# 更新 npm (JavaScript) 依赖
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/" # package.json 所在的位置
|
||||||
|
schedule:
|
||||||
|
interval: "weekly" # 每周一检查
|
||||||
|
# 默认情况下,Dependabot 会自动获取最新版本。
|
||||||
|
# 你可以使用 `ignore` 来排除某些特定更新
|
||||||
|
ignore:
|
||||||
|
- dependency-name: "eslint"
|
||||||
|
# 对于 ESLint,忽略主版本 8 以上的所有更新
|
||||||
|
versions: ["> 8"]
|
||||||
|
# 自动合并小版本更新的 PR (可选)
|
||||||
|
# 可以设置针对 patch 或 minor 版本更新自动合并
|
||||||
|
commit-message:
|
||||||
|
prefix: "build(packages):" # PR 合并提交信息的前缀
|
||||||
|
|
||||||
|
# 更新 GitHub Actions 依赖
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/" # .github/workflows/ 目录所在的位置
|
||||||
|
schedule:
|
||||||
|
interval: "monthly"
|
||||||
|
|
||||||
|
# 更新 Python (pip) 依赖,如果你的项目有 requirements.txt
|
||||||
|
- package-ecosystem: "pip"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
|
||||||
|
# 如果你有多个目录(例如一个 monorepo),可以为每个目录单独配置
|
||||||
|
# - package-ecosystem: "npm"
|
||||||
|
# directory: "/packages/frontend"
|
||||||
|
# schedule:
|
||||||
|
# interval: "weekly"
|
||||||
|
# - package-ecosystem: "npm"
|
||||||
|
# directory: "/packages/backend"
|
||||||
|
# schedule:
|
||||||
|
# interval: "weekly"
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: Build Artifacts
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch: # 手动触发
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux
|
||||||
|
target: node18-linux-x64
|
||||||
|
output: precompiled/app
|
||||||
|
- os: windows-latest
|
||||||
|
platform: win
|
||||||
|
target: node18-win-x64
|
||||||
|
output: precompiled/app.exe
|
||||||
|
- os: macos-latest
|
||||||
|
platform: macos
|
||||||
|
target: node18-macos-x64
|
||||||
|
output: precompiled/app
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
npm install -g pnpm
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
- name: Build for ${{ matrix.platform }}
|
||||||
|
run: |
|
||||||
|
npm run pkg${{ matrix.platform }}
|
||||||
|
env:
|
||||||
|
PKG_TARGET: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: app-${{ matrix.platform }}
|
||||||
|
path: ${{ matrix.output }}
|
||||||
|
if-no-files-found: error
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
name: Issue Management
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened, labeled]
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
welcome-new-issues:
|
||||||
|
if: github.event.action == 'opened'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Welcome new issue
|
||||||
|
uses: peter-evans/create-or-update-comment@v5
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.issue.number }}
|
||||||
|
body: |
|
||||||
|
## 快速链接
|
||||||
|
|
||||||
|
在我们处理您的问题之前,您可以先查看以下资源:
|
||||||
|
|
||||||
|
- 📖 查看[项目文档](https://neteasecloudmusicapienhanced.js.org)
|
||||||
|
- 💬 加入[QQ交流群](https://qm.qq.com/q/TpeP9Uv2yk)
|
||||||
|
- 🔍 搜索[现有issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues) 看是否有类似问题
|
||||||
|
|
||||||
|
handle-help-wanted:
|
||||||
|
if: contains(github.event.issue.labels.*.name, 'question')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Add help resources
|
||||||
|
uses: peter-evans/create-or-update-comment@v5
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.issue.number }}
|
||||||
|
body: |
|
||||||
|
🆘 需要帮助?
|
||||||
|
|
||||||
|
这里有一些可能对您有用的资源:
|
||||||
|
|
||||||
|
📚 **文档资源**
|
||||||
|
- [项目文档](https://neteasecloudmusicapienhanced.js.org)
|
||||||
|
|
||||||
|
🔍 **常见问题**
|
||||||
|
- 搜索[已关闭的issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues?q=is%3Aissue+is%3Aclosed) 看看是否有类似的问题已经被解答。
|
||||||
|
|
||||||
|
💬 **即时帮助**
|
||||||
|
- 加入QQ群:https://qm.qq.com/q/TpeP9Uv2yk
|
||||||
|
|
||||||
|
如果以上资源无法解决您的问题,请提供更多详细信息,我们会尽快为您解答!
|
||||||
|
|
||||||
|
stale-issues:
|
||||||
|
if: github.event.action == 'opened'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Mark stale issues
|
||||||
|
uses: actions/stale@v10
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
stale-issue-message: |
|
||||||
|
这个issue已经30天没有活动了。
|
||||||
|
|
||||||
|
如果这个问题仍然存在,请回复此评论说明当前状态。
|
||||||
|
否则,这个issue将在7天后自动关闭。
|
||||||
|
|
||||||
|
您可以随时重新打开已关闭的issue。
|
||||||
|
stale-pr-message: |
|
||||||
|
这个PR已经30天没有活动了。
|
||||||
|
|
||||||
|
如果您仍在处理这个PR,请回复此评论。
|
||||||
|
否则,这个PR将在7天后自动关闭。
|
||||||
|
days-before-stale: 30
|
||||||
|
days-before-close: 7
|
||||||
|
stale-issue-label: "stale"
|
||||||
|
stale-pr-label: "stale"
|
||||||
|
exempt-issue-labels: "pinned,enhancement,priority:high,ongoing,fatal"
|
||||||
|
exempt-pr-labels: "pinned,priority:high,ongoing,fatal"
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
name: Create Release on Version Change
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- package.json
|
||||||
|
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: release-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
detect:
|
||||||
|
name: Detect Version Change
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.value }}
|
||||||
|
should_release: ${{ steps.check.outputs.should_release }}
|
||||||
|
tag_exists: ${{ steps.tagcheck.outputs.exists }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Extract version from package.json
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
echo "value=$(jq -r .version package.json)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Check if version changed
|
||||||
|
id: check
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
|
echo "should_release=true" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git diff "${{ github.event.before }}" "${{ github.sha }}" -- package.json | grep -q '"version"'; then
|
||||||
|
echo "should_release=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "should_release=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check if tag already exists
|
||||||
|
id: tagcheck
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.value }}"
|
||||||
|
|
||||||
|
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build ${{ matrix.platform }}
|
||||||
|
needs: detect
|
||||||
|
|
||||||
|
if: |
|
||||||
|
needs.detect.outputs.should_release == 'true' &&
|
||||||
|
needs.detect.outputs.tag_exists != 'true'
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux
|
||||||
|
target: node18-linux-x64
|
||||||
|
output: ncm-api-linux-x64
|
||||||
|
|
||||||
|
- os: windows-latest
|
||||||
|
platform: windows
|
||||||
|
target: node18-win-x64
|
||||||
|
output: ncm-api-win-x64.exe
|
||||||
|
|
||||||
|
- os: macos-latest
|
||||||
|
platform: macos
|
||||||
|
target: node18-macos-x64
|
||||||
|
output: ncm-api-macos-x64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build binary
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PKG_TARGET: ${{ matrix.target }}
|
||||||
|
run: |
|
||||||
|
mkdir -p release-artifacts
|
||||||
|
|
||||||
|
case "${{ matrix.platform }}" in
|
||||||
|
linux)
|
||||||
|
npm run pkglinux
|
||||||
|
mv precompiled/app "release-artifacts/${{ matrix.output }}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
windows)
|
||||||
|
npm run pkgwin
|
||||||
|
mv precompiled/app.exe "release-artifacts/${{ matrix.output }}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
macos)
|
||||||
|
npm run pkgmacos
|
||||||
|
mv precompiled/app "release-artifacts/${{ matrix.output }}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Upload build artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.platform }}-binary
|
||||||
|
path: release-artifacts/*
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create GitHub Release
|
||||||
|
|
||||||
|
needs:
|
||||||
|
- detect
|
||||||
|
- build
|
||||||
|
|
||||||
|
if: |
|
||||||
|
needs.detect.outputs.should_release == 'true' &&
|
||||||
|
needs.detect.outputs.tag_exists != 'true'
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Download build artifacts
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
path: release-artifacts
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: List flattened artifacts
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p final-artifacts
|
||||||
|
cp release-artifacts/* final-artifacts/
|
||||||
|
ls -lah final-artifacts
|
||||||
|
|
||||||
|
- name: Generate release notes
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
VERSION="${{ needs.detect.outputs.version }}"
|
||||||
|
|
||||||
|
PREV_TAG=$(git tag --sort=-v:refname | head -1)
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Release v${VERSION}"
|
||||||
|
echo ""
|
||||||
|
echo "## 更新内容 / Changelog"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ -n "$PREV_TAG" ]; then
|
||||||
|
echo "从 \`$PREV_TAG\` 到 \`v$VERSION\` 的提交记录:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
git log "$PREV_TAG..HEAD" \
|
||||||
|
--no-merges \
|
||||||
|
--pretty=format:"- %s (%h)"
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "首次发布,包含以下提交:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
git log \
|
||||||
|
--no-merges \
|
||||||
|
--pretty=format:"- %s (%h)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
echo "---"
|
||||||
|
echo "自动发布 via GitHub Actions"
|
||||||
|
|
||||||
|
} > release-notes.md
|
||||||
|
|
||||||
|
cat release-notes.md
|
||||||
|
|
||||||
|
- name: Create Git tag
|
||||||
|
run: |
|
||||||
|
git tag "v${{ needs.detect.outputs.version }}"
|
||||||
|
git push origin "v${{ needs.detect.outputs.version }}"
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: v${{ needs.detect.outputs.version }}
|
||||||
|
name: Release v${{ needs.detect.outputs.version }}
|
||||||
|
body_path: release-notes.md
|
||||||
|
files: final-artifacts/*
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
publish-docker:
|
||||||
|
name: Publish Docker Image
|
||||||
|
needs: detect
|
||||||
|
|
||||||
|
if: |
|
||||||
|
needs.detect.outputs.should_release == 'true' &&
|
||||||
|
needs.detect.outputs.tag_exists != 'true'
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Read package version
|
||||||
|
id: pkg
|
||||||
|
run: echo "VERSION=$(jq -r .version package.json)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
moefurina/ncm-api:latest
|
||||||
|
moefurina/ncm-api:${{ env.VERSION }}
|
||||||
|
ghcr.io/neteasecloudmusicapienhanced/ncm-api:latest
|
||||||
|
ghcr.io/neteasecloudmusicapienhanced/ncm-api:${{ env.VERSION }}
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
|
||||||
|
publish-npm:
|
||||||
|
name: Publish to npm
|
||||||
|
needs: detect
|
||||||
|
|
||||||
|
if: |
|
||||||
|
needs.detect.outputs.should_release == 'true' &&
|
||||||
|
needs.detect.outputs.tag_exists != 'true'
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- run: pnpm publish --access public --provenance
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
name: Upstream Sync
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 4 * * *" # At 12PM UTC+8
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync_latest_from_upstream:
|
||||||
|
name: Sync latest commits from upstream repo
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.repository.fork }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Step 1: run a standard checkout action
|
||||||
|
- name: Checkout target repo
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
# Step 2: run the sync action
|
||||||
|
- name: Sync upstream changes
|
||||||
|
id: sync
|
||||||
|
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.3
|
||||||
|
with:
|
||||||
|
upstream_sync_repo: NeteaseCloudMusicApiEnhanced/api-enhanced
|
||||||
|
upstream_sync_branch: main
|
||||||
|
target_sync_branch: main
|
||||||
|
target_repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Sync check
|
||||||
|
if: failure()
|
||||||
|
run: |
|
||||||
|
echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork."
|
||||||
|
exit 1
|
||||||
@@ -8,3 +8,5 @@ examples/moddef.json
|
|||||||
bin
|
bin
|
||||||
anonymous_token
|
anonymous_token
|
||||||
.vercel
|
.vercel
|
||||||
|
.env
|
||||||
|
IFLOW.md
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
|
|
||||||
node_js:
|
node_js:
|
||||||
- 12
|
- 22
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Agent Instructions for NeteaseCloudMusicApiEnhanced
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
- **Package Manager**: Use `pnpm` (not npm or yarn).
|
||||||
|
- **Node Version**: Requires Node.js 18 or later.
|
||||||
|
|
||||||
|
## Developer Commands
|
||||||
|
- **Install dependencies**: `pnpm i`
|
||||||
|
- **Start server**: `pnpm start` or `node app.js`
|
||||||
|
- **Start dev server**: `pnpm dev` (uses nodemon)
|
||||||
|
- **Run tests**: `pnpm test` (uses Mocha)
|
||||||
|
- **Linting**: `pnpm lint` (check) or `pnpm lint-fix` (auto-fix)
|
||||||
|
|
||||||
|
## Architecture & Entrypoints
|
||||||
|
- **Executable Server**: `app.js` is the main entrypoint for running the API server.
|
||||||
|
- **Module Exports**: `main.js` is the entrypoint when the project is imported as a Node.js dependency.
|
||||||
|
- **API Endpoints**: Located in the `module/` directory. Each file typically corresponds to an API route.
|
||||||
|
- **Core Utilities**: Request handling, encryption, and core utilities are found in the `util/` directory.
|
||||||
|
|
||||||
|
## Important Gotchas & Quirks
|
||||||
|
- **Environment Variables**: The server defaults to port 3000 but can be overridden with the `PORT` environment variable.
|
||||||
|
- **Proxy Variables**: Be very careful with proxy environment variables (`http_proxy`, `https_proxy`, `no_proxy`). The request library (like axios) will automatically pick these up. If they point to an unavailable proxy (especially common in Docker environments), requests will fail silently or throw connection errors.
|
||||||
|
- **Code Style**: The project uses ESLint and Prettier. Always run `pnpm lint-fix` before committing changes to ensure formatting consistency.
|
||||||
@@ -1,76 +1,90 @@
|
|||||||
# 网易云音乐 API
|
<p align="center">
|
||||||
|
<img src="./public/static/neteaselogo.png" alt="网易云音乐 API Enhanced Logo" />
|
||||||
网易云音乐 Node.js API service
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<a href="https://www.npmjs.com/package/NeteaseCloudMusicApi"><img src="https://img.shields.io/npm/v/NeteaseCloudMusicApi.svg" alt="Version"></a>
|
|
||||||
<a href="https://www.npmjs.com/package/NeteaseCloudMusicApi"><img src="https://img.shields.io/npm/l/NeteaseCloudMusicApi.svg" alt="License"></a>
|
|
||||||
<a href="https://www.npmjs.com/package/NeteaseCloudMusicApi"><img src="https://img.shields.io/david/dev/binaryify/NeteaseCloudMusicApi.svg" alt="devDependencies" ></a>
|
|
||||||
<a href="https://www.npmjs.com/package/NeteaseCloudMusicApi"><img src="https://img.shields.io/david/binaryify/NeteaseCloudMusicApi.svg" alt="devDependencies" ></a>
|
|
||||||
<a href="https://codeclimate.com/github/Binaryify/NeteaseCloudMusicApi"><img src="https://codeclimate.com/github/Binaryify/NeteaseCloudMusicApi/badges/gpa.svg" /></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## 灵感来自
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/package/@neteasecloudmusicapienhanced/api">
|
||||||
|
<img src="https://img.shields.io/npm/v/@neteasecloudmusicapienhanced/api?style=flat-square" alt="NPM Version" />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.npmjs.com/package/@neteasecloudmusicapienhanced/api">
|
||||||
|
<img src="https://img.shields.io/npm/dm/@neteasecloudmusicapienhanced/api?style=flat-square" alt="NPM Downloads" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced">
|
||||||
|
<img src="https://img.shields.io/github/license/NeteaseCloudMusicApiEnhanced/api-enhanced?style=flat-square" alt="License" />
|
||||||
|
</a>
|
||||||
|
<a href="https://nodejs.org/">
|
||||||
|
<img src="https://img.shields.io/node/v/@neteasecloudmusicapienhanced/api?style=flat-square" alt="Node Version" />
|
||||||
|
</a>
|
||||||
|
<a href="https://hub.docker.com/r/moefurina/ncm-api">
|
||||||
|
<img src="https://img.shields.io/docker/v/moefurina/ncm-api?style=flat-square" alt="Docker Version" />
|
||||||
|
</a>
|
||||||
|
<a href="https://hub.docker.com/r/moefurina/ncm-api">
|
||||||
|
<img src="https://img.shields.io/docker/pulls/moefurina/ncm-api?style=flat-square" alt="Docker Pulls" />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.typescriptlang.org/">
|
||||||
|
<img src="https://img.shields.io/badge/TypeScript-Ready-blue?style=flat-square&logo=typescript" alt="TypeScript" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
[disoul/electron-cloud-music](https://github.com/disoul/electron-cloud-music)
|
|
||||||
|
|
||||||
[darknessomi/musicbox](https://github.com/darknessomi/musicbox)
|
|
||||||
|
|
||||||
[sqaiyan/netmusic-node](https://github.com/sqaiyan/netmusic-node)
|
## 项目简介
|
||||||
|
|
||||||
[greats3an/pyncm](https://github.com/greats3an/pyncm)
|
网易云音乐第三方 Node.js API, 支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署
|
||||||
|
|
||||||
## 环境要求
|
> [!IMPORTANT]
|
||||||
|
>
|
||||||
|
> ## 注意
|
||||||
|
>
|
||||||
|
> - 本项目是自由项目, 不带任何担保, 在基于`MIT License`的许可下, 任何人都可以自由使用、修改和分发本项目的代码, 包括用于商业目的.
|
||||||
|
> - 该项目是个合作计划, 有很多人参与了此次贡献, 前往 [Contributors](https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced/graphs/contributors) 页面查看所有贡献者
|
||||||
|
> - 原作者项目 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 并非完全停止维护, 你可以在 [NeteaseCloudMusicApi 的 NPMJS 页面](https://www.npmjs.com/package/NeteaseCloudMusicApi) 查看最新版本信息。
|
||||||
|
> - 感谢尊重和理解!
|
||||||
|
|
||||||
需要 NodeJS 18+ 环境
|
## 快速开始
|
||||||
|
|
||||||
## 安装
|
### 环境要求
|
||||||
|
|
||||||
```shell
|
- Node.js 22 及以上
|
||||||
$ git clone git@gitlab.com:Binaryify/NeteaseCloudMusicApi.git
|
- 推荐使用 pnpm 进行依赖管理
|
||||||
$ cd NeteaseCloudMusicApi
|
|
||||||
$ npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
或者
|
### 安装
|
||||||
|
|
||||||
```shell
|
|
||||||
$ git clone https://gitlab.com/Binaryify/NeteaseCloudMusicApi.git
|
|
||||||
$ cd NeteaseCloudMusicApi
|
|
||||||
$ npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
## 运行
|
|
||||||
|
|
||||||
调用前务必阅读文档的`调用前须知`
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ node app.js
|
|
||||||
```
|
|
||||||
|
|
||||||
服务器启动默认端口为 3000,若不想使用 3000 端口,可使用以下命令: Mac/Linux
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ PORT=4000 node app.js
|
|
||||||
```
|
|
||||||
|
|
||||||
windows 下使用 git-bash 或者 cmder 等终端执行以下命令:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ set PORT=4000 && node app.js
|
|
||||||
```
|
|
||||||
|
|
||||||
## npx 方式运行
|
|
||||||
|
|
||||||
可在终端直接执行
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx NeteaseCloudMusicApi@latest
|
git clone https://github.com/neteasecloudmusicapienhanced/api-enhanced.git
|
||||||
|
cd api-enhanced
|
||||||
|
pnpm i
|
||||||
```
|
```
|
||||||
|
|
||||||
使用此命令,可直接启动服务,无需下载或者 clone 项目
|
### 启动服务
|
||||||
|
|
||||||
## Docker 容器运行
|
```bash
|
||||||
|
# 默认端口 3000
|
||||||
|
node app.js
|
||||||
|
|
||||||
|
# 指定端口(如 4000)
|
||||||
|
PORT=4000 node app.js # Mac/Linux
|
||||||
|
set PORT=4000 && node app.js # Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重要提示
|
||||||
|
|
||||||
|
- 调用前请务必阅读文档的 [调用前须知](https://neteasecloudmusicapienhanced.js.org/#/?id=%e8%b0%83%e7%94%a8%e5%89%8d%e9%a1%bb%e7%9f%a5) 部分。
|
||||||
|
- 推荐将敏感信息(如 cookie)通过部署平台的环境变量进行配置。
|
||||||
|
|
||||||
|
## 在线体验与文档
|
||||||
|
|
||||||
|
- [在线文档](https://neteasecloudmusicapienhanced.js.org/)
|
||||||
|
- [NPM 包地址](https://www.npmjs.com/package/@neteasecloudmusicapienhanced/api)
|
||||||
|
- [Dockerhub 主页](https://hub.docker.com/r/moefurina/ncm-api)
|
||||||
|
|
||||||
|
## 常见部署方式
|
||||||
|
|
||||||
|
- 推荐参考[在线文档](https://neteasecloudmusicapienhanced.js.org/)
|
||||||
|
|
||||||
|
## Docker 部署说明
|
||||||
|
|
||||||
|
本项目支持通过 Docker 一键部署,无需手动安装 Node.js 或依赖。
|
||||||
|
|
||||||
> 注意: 在 docker 中运行的时候, 由于使用了 request 来发请求, 所以会检查几个 proxy 相关的环境变量(如下所列), 这些环境变量 会影响到 request 的代理, 详情请参考[request 的文档](https://github.com/request/request#proxies), 如果这些环境变量 指向的代理不可用, 那么就会造成错误, 所以在使用 docker 的时候一定要注意这些环境变量. 不过, 要是你在 query 中加上了 proxy 参数, 那么环境变量会被覆盖, 就会用你通过 proxy 参数提供的代理了.
|
> 注意: 在 docker 中运行的时候, 由于使用了 request 来发请求, 所以会检查几个 proxy 相关的环境变量(如下所列), 这些环境变量 会影响到 request 的代理, 详情请参考[request 的文档](https://github.com/request/request#proxies), 如果这些环境变量 指向的代理不可用, 那么就会造成错误, 所以在使用 docker 的时候一定要注意这些环境变量. 不过, 要是你在 query 中加上了 proxy 参数, 那么环境变量会被覆盖, 就会用你通过 proxy 参数提供的代理了.
|
||||||
|
|
||||||
@@ -84,487 +98,156 @@ request 相关的环境变量
|
|||||||
6. NO_PROXY
|
6. NO_PROXY
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker pull binaryify/netease_cloud_music_api
|
docker pull moefurina/ncm-api:latest
|
||||||
|
|
||||||
docker run -d -p 3000:3000 --name netease_cloud_music_api binaryify/netease_cloud_music_api
|
docker run -d -p 3000:3000 --name ncm-api moefurina/ncm-api:latest
|
||||||
|
|
||||||
|
|
||||||
// 或者
|
// 或者
|
||||||
docker run -d -p 3000:3000 binaryify/netease_cloud_music_api
|
docker run -d -p 3000:3000 moefurina/ncm-api:latest
|
||||||
|
|
||||||
// 去掉或者设置相关的环境变量
|
// 去掉或者设置相关的环境变量
|
||||||
|
|
||||||
docker run -d -p 3000:3000 --name netease_cloud_music_api -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= binaryify/netease_cloud_music_api
|
docker run -d -p 3000:3000 --name ncm-api -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= moefurina/ncm-api:latest
|
||||||
|
|
||||||
// 或者
|
// 或者
|
||||||
docker run -d -p 3000:3000 -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= binaryify/netease_cloud_music_api
|
docker run -d -p 3000:3000 -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= moefurina/ncm-api:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
> 以下是自行 build docker 镜像方式
|
> 以下是自行 build docker 镜像方式
|
||||||
|
|
||||||
```
|
```shell
|
||||||
$ git clone https://gitlab.com/Binaryify/neteasecloudmusicapi.git && cd NeteaseCloudMusicApi
|
$ git clone https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced && cd api-enhanced
|
||||||
|
|
||||||
$ sudo docker build . -t netease-music-api
|
$ sudo docker build . -t ncm-api
|
||||||
|
|
||||||
$ sudo docker run -d -p 3000:3000 netease-music-api
|
$ sudo docker run -d -p 3000:3000 ncm-api
|
||||||
|
|
||||||
## Vercel 部署
|
|
||||||
|
|
||||||
v4.0.8 加入了 Vercel 配置文件,可以直接在 Vercel 下部署了,不需要自己的服务器
|
|
||||||
|
|
||||||
### 操作方法
|
|
||||||
|
|
||||||
1. fork 此项目
|
|
||||||
2. 在 Vercel 官网点击 `New Project`
|
|
||||||
3. 点击 `Import Git Repository` 并选择你 fork 的此项目并点击`import`
|
|
||||||
4. 点击 `PERSONAL ACCOUNT` 的 `select`
|
|
||||||
5. 直接点`Continue`
|
|
||||||
6. `PROJECT NAME`自己填,`FRAMEWORK PRESET` 选 `Other` 然后直接点 `Deploy` 接着等部署完成即可
|
|
||||||
|
|
||||||
## 腾讯云 serverless 部署
|
|
||||||
|
|
||||||
因 `Vercel` 在国内访问太慢,在此提供腾讯云 serverless 部署方法(注意:腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费)
|
|
||||||
|
|
||||||
### 操作方法
|
|
||||||
|
|
||||||
1. fork 此项目
|
|
||||||
2. 在腾讯云 serverless 应用管理页面( https://console.cloud.tencent.com/sls ),点击`新建应用`
|
|
||||||
3. 顶部`创建方式`选择 `Web 应用`
|
|
||||||
4. 选择 `Express框架`,点击底部`下一步按钮`
|
|
||||||
5. 输入`应用名`,上传方式选择`代码仓库`,进行 GitHub 授权(如已授权可跳过这一步),代码仓库选择刚刚 fork 的项目
|
|
||||||
6. 启动文件填入:
|
|
||||||
|
|
||||||
```
|
|
||||||
#!/bin/bash
|
|
||||||
export PORT=9000
|
|
||||||
/var/lang/node16/bin/node app.js
|
|
||||||
```
|
```
|
||||||
|
|
||||||
7. 点击`完成`,等待部署完成,点击`资源列表`的 `API网关` 里的 `URL`,正常情况会打开文档地址,点击文档`例子`可查看接口调用效果
|
## 3. 环境变量
|
||||||
|
|
||||||
## 可以在 Node.js 调用
|
| 变量名 | 默认值 | 说明 |
|
||||||
|
|----------------------------|--------------------------------------|----------------------------------------------------|
|
||||||
|
| **CORS_ALLOW_ORIGIN** | `*` | 允许跨域请求的域名。可填写单个源,或使用逗号分隔多个源(例如 `https://a.com,https://b.com`)。 |
|
||||||
|
| **ENABLE_PROXY** | `false` | 是否启用反向代理功能。 |
|
||||||
|
| **PROXY_URL** | `https://your-proxy-url.com/?proxy=` | 代理服务地址。仅当 `ENABLE_PROXY=true` 时生效。 |
|
||||||
|
| **ENABLE_RANDOM_CN_IP** | `false` | 是否默认启用随机中国IP。启用后,所有请求默认使用随机中国IP,除非请求参数 `randomCNIP` 显式关闭。 |
|
||||||
|
| **ENABLE_GENERAL_UNBLOCK** | `true` | 是否启用全局解灰(推荐开启)。开启后所有歌曲都尝试自动解锁。 |
|
||||||
|
| **ENABLE_FLAC** | `true` | 是否启用无损音质(FLAC)。 |
|
||||||
|
| **SELECT_MAX_BR** | `false` | 启用无损音质时,是否选择最高码率音质。 |
|
||||||
|
| **FOLLOW_SOURCE_ORDER** | `true` | 是否严格按照音源列表顺序进行匹配。 |
|
||||||
|
|
||||||
v3.31.0 后支持 Node.js 调用,导入的方法为`module`内的文件名,返回内容包含`status`和`body`,`status`为状态码,`body`为请求返回内容,参考`module_example` 文件夹下的 `test.js`
|
---
|
||||||
|
|
||||||
|
### 4. 更新镜像
|
||||||
|
|
||||||
|
更新到最新版本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull moefurina/ncm-api:latest
|
||||||
|
docker stop ncm-api && docker rm ncm-api
|
||||||
|
docker run -d -p 3000:3000 moefurina/ncm-api:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vercel 一键部署
|
||||||
|
|
||||||
|
1. fork 本项目
|
||||||
|
2. 在 Vercel 官网新建项目,导入 fork 的仓库
|
||||||
|
3. 直接 Deploy
|
||||||
|
|
||||||
|
## 腾讯云 Serverless 部署
|
||||||
|
|
||||||
|
1. fork 本项目
|
||||||
|
2. 在腾讯云 serverless 控制台新建 Web 应用,选择 Express 框架
|
||||||
|
3. 代码仓库选择 fork 的项目,启动文件填写:
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
export PORT=9000
|
||||||
|
/var/lang/node16/bin/node app.js
|
||||||
|
```
|
||||||
|
4. 完成部署后,访问 API 网关的 URL 即可
|
||||||
|
|
||||||
|
## Node.js 方式调用
|
||||||
|
|
||||||
|
支持直接在 Node.js 项目中引入和调用,返回 Promise:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const { login_cellphone, user_cloud } = require('NeteaseCloudMusicApi')
|
const {
|
||||||
|
login_cellphone,
|
||||||
|
user_cloud,
|
||||||
|
} = require('@neteasecloudmusicapienhanced/api')
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
const result = await login_cellphone({ phone: '手机号', password: '密码' })
|
||||||
const result = await login_cellphone({
|
console.log(result)
|
||||||
phone: '手机号',
|
const result2 = await user_cloud({ cookie: result.body.cookie })
|
||||||
password: '密码',
|
console.log(result2.body)
|
||||||
})
|
|
||||||
console.log(result)
|
|
||||||
const result2 = await user_cloud({
|
|
||||||
cookie: result.body.cookie, // 凭证
|
|
||||||
})
|
|
||||||
console.log(result2.body)
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
main()
|
main()
|
||||||
```
|
```
|
||||||
|
|
||||||
## 支持 TypeScript
|
## TypeScript 支持
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// test.ts
|
import { banner } from '@neteasecloudmusicapienhanced/api'
|
||||||
import { banner } from 'NeteaseCloudMusicApi'
|
banner({ type: 0 }).then((res) => console.log(res))
|
||||||
banner({ type: 0 }).then((res) => {
|
|
||||||
console.log(res)
|
|
||||||
})
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用文档
|
|
||||||
|
|
||||||
[文档地址](https://docs-neteasecloudmusicapi.vercel.app)
|
|
||||||
|
|
||||||
## 调试工具
|
|
||||||
|
|
||||||
- `eapi` 请求参数或返回内容可在 `/eapi_decrypt.html` 里解析
|
|
||||||
- 请求参数模式下, 解密结果可直接带到 `/api.html` 继续调试
|
|
||||||
- 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
1. 登录
|
|
||||||
2. 刷新登录
|
|
||||||
3. 发送验证码
|
|
||||||
4. 校验验证码
|
|
||||||
5. 注册(修改密码)
|
|
||||||
6. 获取用户信息 , 歌单,收藏,mv, dj 数量
|
|
||||||
7. 获取用户歌单
|
|
||||||
8. 获取用户电台
|
|
||||||
9. 获取用户关注列表
|
|
||||||
10. 获取用户粉丝列表
|
|
||||||
11. 获取用户动态
|
|
||||||
12. 获取用户播放记录
|
|
||||||
13. 获取精品歌单
|
|
||||||
14. 获取歌单详情
|
|
||||||
15. 搜索
|
|
||||||
16. 搜索建议
|
|
||||||
17. 获取歌词
|
|
||||||
18. 歌曲评论
|
|
||||||
19. 收藏单曲到歌单
|
|
||||||
20. 专辑评论
|
|
||||||
21. 歌单评论
|
|
||||||
22. mv 评论
|
|
||||||
23. 电台节目评论
|
|
||||||
24. banner
|
|
||||||
25. 获取歌曲详情
|
|
||||||
26. 获取专辑内容
|
|
||||||
27. 获取歌手单曲
|
|
||||||
28. 获取歌手 mv
|
|
||||||
29. 获取歌手专辑
|
|
||||||
30. 获取歌手描述
|
|
||||||
31. 获取相似歌手
|
|
||||||
32. 获取相似歌单
|
|
||||||
33. 相似 mv
|
|
||||||
34. 获取相似音乐
|
|
||||||
35. 获取最近 5 个听了这首歌的用户
|
|
||||||
36. 获取每日推荐歌单
|
|
||||||
37. 获取每日推荐歌曲
|
|
||||||
38. 私人 FM
|
|
||||||
39. 签到
|
|
||||||
40. 喜欢音乐
|
|
||||||
41. 垃圾桶
|
|
||||||
42. 歌单 ( 网友精选碟 )
|
|
||||||
43. 新碟上架
|
|
||||||
44. 热门歌手
|
|
||||||
45. 最新 mv
|
|
||||||
46. 推荐 mv
|
|
||||||
47. 推荐歌单
|
|
||||||
48. 推荐新音乐
|
|
||||||
49. 推荐电台
|
|
||||||
50. 推荐节目
|
|
||||||
51. 独家放送
|
|
||||||
52. mv 排行
|
|
||||||
53. 获取 mv 数据
|
|
||||||
54. 播放 mv/视频
|
|
||||||
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. 收藏 MV
|
|
||||||
87. 视频详情
|
|
||||||
88. 相关视频
|
|
||||||
89. 关注用户
|
|
||||||
90. 新歌速递
|
|
||||||
91. 喜欢音乐列表(无序)
|
|
||||||
92. 收藏的 MV 列表
|
|
||||||
93. 获取最新专辑
|
|
||||||
94. 听歌打卡
|
|
||||||
95. 获取视频标签/分类下的视频
|
|
||||||
96. 已收藏专辑列表
|
|
||||||
97. 获取动态评论
|
|
||||||
98. 歌单收藏者列表
|
|
||||||
99. 云盘歌曲删除
|
|
||||||
100. 热门话题
|
|
||||||
101. 电台 - 推荐类型
|
|
||||||
102. 电台 - 非热门类型
|
|
||||||
103. 电台 - 今日优选
|
|
||||||
104. 心动模式/智能播放
|
|
||||||
105. 转发动态
|
|
||||||
106. 删除动态
|
|
||||||
107. 分享歌曲、歌单、mv、电台、电台节目到动态
|
|
||||||
108. 通知-私信
|
|
||||||
109. 通知-评论
|
|
||||||
110. 通知-@我
|
|
||||||
111. 通知-通知
|
|
||||||
112. 设置
|
|
||||||
113. 云盘数据详情
|
|
||||||
114. 私信内容
|
|
||||||
115. 我的数字专辑
|
|
||||||
116. batch 批量请求接口
|
|
||||||
117. 获取视频标签列表
|
|
||||||
118. 全部 mv
|
|
||||||
119. 网易出品 mv
|
|
||||||
120. 收藏/取消收藏专辑
|
|
||||||
121. 专辑动态信息
|
|
||||||
122. 热搜列表(详细)
|
|
||||||
123. 更换绑定手机
|
|
||||||
124. 检测手机号码是否已注册
|
|
||||||
125. 初始化昵称
|
|
||||||
126. 更新歌单描述
|
|
||||||
127. 更新歌单名
|
|
||||||
128. 更新歌单标签
|
|
||||||
129. 默认搜索关键词
|
|
||||||
130. 删除歌单
|
|
||||||
131. 电台 banner
|
|
||||||
132. 用户电台
|
|
||||||
133. 热门电台
|
|
||||||
134. 电台 - 节目详情
|
|
||||||
135. 电台 - 节目榜
|
|
||||||
136. 电台 - 新晋电台榜/热门电台榜
|
|
||||||
137. 类别热门电台
|
|
||||||
138. 云村热评(官方下架,暂不能用)
|
|
||||||
139. 电台 24 小时节目榜
|
|
||||||
140. 电台 24 小时主播榜
|
|
||||||
141. 电台最热主播榜
|
|
||||||
142. 电台主播新人榜
|
|
||||||
143. 电台付费精品榜
|
|
||||||
144. 歌手热门 50 首歌曲
|
|
||||||
145. 购买数字专辑
|
|
||||||
146. 获取 mv 点赞转发评论数数据
|
|
||||||
147. 获取视频点赞转发评论数数据
|
|
||||||
148. 调整歌单顺序
|
|
||||||
149. 调整歌曲顺序
|
|
||||||
150. 独家放送列表
|
|
||||||
151. 获取推荐视频
|
|
||||||
152. 获取视频分类列表
|
|
||||||
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. 云贝 todo 任务
|
|
||||||
184. 云贝今日签到信息
|
|
||||||
185. 云贝完成任务
|
|
||||||
186. 云贝收入
|
|
||||||
187. 云贝支出
|
|
||||||
188. 云贝账户信息
|
|
||||||
189. 账号信息
|
|
||||||
190. 最近联系人
|
|
||||||
191. 私信音乐
|
|
||||||
192. 抱一抱评论
|
|
||||||
193. 评论抱一抱列表
|
|
||||||
194. 收藏的专栏
|
|
||||||
195. 关注歌手新歌
|
|
||||||
196. 关注歌手新 MV
|
|
||||||
197. 歌手详情
|
|
||||||
198. 云盘上传
|
|
||||||
199. 二维码登录
|
|
||||||
200. 话题详情
|
|
||||||
201. 话题详情热门动态
|
|
||||||
202. 歌单详情动态
|
|
||||||
203. 绑定手机
|
|
||||||
204. 一起听状态
|
|
||||||
205. 用户历史评论
|
|
||||||
206. 云盘歌曲信息匹配纠正
|
|
||||||
207. 云贝推歌
|
|
||||||
208. 云贝推歌历史记录
|
|
||||||
209. 已购单曲
|
|
||||||
210. 获取 mlog 播放地址
|
|
||||||
211. 将 mlog id 转为视频 id
|
|
||||||
212. vip 成长值
|
|
||||||
213. vip 成长值获取记录
|
|
||||||
214. vip 任务
|
|
||||||
215. 领取 vip 成长值
|
|
||||||
216. 歌手粉丝
|
|
||||||
217. 数字专辑详情
|
|
||||||
218. 数字专辑销量
|
|
||||||
219. 音乐人数据概况
|
|
||||||
220. 音乐人播放趋势
|
|
||||||
221. 音乐人任务
|
|
||||||
222. 账号云豆数
|
|
||||||
223. 领取云豆
|
|
||||||
224. 获取 VIP 信息
|
|
||||||
225. 音乐人签到
|
|
||||||
226. 获取客户端歌曲下载 url
|
|
||||||
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. 根据nickname获取userid接口
|
|
||||||
261. 播客声音列表
|
|
||||||
262. 专辑简要百科信息
|
|
||||||
263. 歌曲简要百科信息
|
|
||||||
264. 歌手简要百科信息
|
|
||||||
265. mv简要百科信息
|
|
||||||
266. 搜索歌手
|
|
||||||
267. 用户贡献内容
|
|
||||||
268. 用户贡献条目、积分、云贝数量
|
|
||||||
269. 年度听歌报告
|
|
||||||
270. 播客声音搜索
|
|
||||||
271. 播客声音排序
|
|
||||||
272. 播客列表详情
|
|
||||||
273. 本地歌曲文件匹配网易云歌曲信息
|
|
||||||
274. 歌曲音质详情
|
|
||||||
275. 歌曲红心数量
|
|
||||||
276. 私人 FM 模式选择
|
|
||||||
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. DIFM电台 - 分类
|
|
||||||
318. DIFM电台 - 收藏列表
|
|
||||||
319. DIFM电台 - 收藏频道
|
|
||||||
320. DIFM电台 - 取消收藏频道
|
|
||||||
321. DIFM电台 - 播放列表
|
|
||||||
322. 助眠解压 - 特定时间场景下的推荐资源
|
|
||||||
323. 助眠解压 - 标签列表
|
|
||||||
324. 助眠解压 - 获取标签下资源列表
|
|
||||||
325. 助眠解压 - 查看同类推荐
|
|
||||||
326. 助眠解压 - 收藏
|
|
||||||
327. 助眠解压 - 收藏列表
|
|
||||||
328. 跑步漫游
|
|
||||||
329. 歌曲创作者信息
|
|
||||||
330. 获取用户详情
|
|
||||||
331. 更新用户信息
|
|
||||||
332. 更新歌单
|
|
||||||
333. 歌手分类列表
|
|
||||||
334. 收藏/取消收藏歌手
|
|
||||||
335. 获取音乐 url
|
|
||||||
336. 获取音乐 url - 新版
|
|
||||||
337. 搜索多重匹配
|
|
||||||
338. 对歌单添加或删除歌曲
|
|
||||||
339. 获取逐字歌词
|
|
||||||
340. 资源点赞( MV,电台,视频)
|
|
||||||
341. 每日推荐歌曲-不感兴趣
|
|
||||||
342. mv 地址
|
|
||||||
343. 排行榜详情
|
|
||||||
344. 电台 - 付费精品
|
|
||||||
345. 电台 - 付费精选
|
|
||||||
346. 发送私信(带专辑)
|
|
||||||
347. 云贝
|
|
||||||
348. 一起听相关
|
|
||||||
349. 获取 VIP 信息(app端)
|
|
||||||
350. 公开隐私歌单
|
|
||||||
351. 云村星评馆 - 简要评论
|
|
||||||
352. 私人 DJ
|
|
||||||
353. 播客列表
|
|
||||||
354. 播客声音详情
|
|
||||||
355. 多级行政区划数据
|
|
||||||
356. 指定维度音乐排行榜详情
|
|
||||||
357. 指定维度音乐排行榜列表
|
|
||||||
|
|
||||||
## 单元测试
|
## 单元测试
|
||||||
|
|
||||||
```shell
|
```bash
|
||||||
$ npm test
|
pnpm test
|
||||||
```
|
```
|
||||||
|
|
||||||
## SDK
|
## 主要功能特性
|
||||||
|
|
||||||
| 语言 | 作者 | 地址 | 类型 |
|
- 登录/注册/验证码
|
||||||
| :----: | :-----------------------------------------: | :--------------------------------------------------------------------------------------------------------: | :----: |
|
- 用户信息、歌单、动态、播放记录
|
||||||
| Java | [JackuXL](https://github.com/JackuXL) | [https://github.com/JackuXL/NeteaseCloudMusicApi-SDK](https://github.com/JackuXL/NeteaseCloudMusicApi-SDK) | 第三方 |
|
- 歌曲、专辑、歌手、MV、歌词、评论、排行榜
|
||||||
| Java | [1015770492](https://github.com/1015770492) | https://github.com/1015770492/yumbo-music-utils | 第三方 |
|
- 搜索、推荐、私人 FM、签到、云盘
|
||||||
| Python | [盧瞳](https://github.com/2061360308) | [NeteaseCloudMusic_PythonSDK](https://github.com/2061360308/NeteaseCloudMusic_PythonSDK) | 第三方 |
|
- 歌曲解锁(解灰)
|
||||||
|
|
||||||
## 贡献者
|
## 贡献与社区
|
||||||
|
|
||||||

|
- 欢迎提交 PR、Issue 参与维护
|
||||||
|
- 要贡献新接口, 请参考[这篇文章](https://www.focalors.ltd/post/how-to-contribute-ncm-api)
|
||||||
|
|
||||||
|
### 致谢
|
||||||
|
|
||||||
|
原作者 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 项目为本项目基础 (该项目在`npmjs`网站上仍持续维护, 但 github 仓库已不再更新)
|
||||||
|
|
||||||
|
感谢大佬们为逆向eapi, weapi, xeapi等加密算法所做的贡献
|
||||||
|
|
||||||
|
项目参考:
|
||||||
|
|
||||||
|
[trazyn/ieaseMusic](https://github.com/trazyn/ieaseMusic)
|
||||||
|
|
||||||
|
[listen1/listen1_chrome_extension](https://github.com/listen1/listen1_chrome_extension)
|
||||||
|
|
||||||
|
[chaunsin/netease-cloud-music](https://github.com/chaunsin/netease-cloud-music)
|
||||||
|
|
||||||
|
[folltoshe/netease-report-listen-song](https://github.com/folltoshe/netease-report-listen-song)
|
||||||
|
|
||||||
|
### SDK 生态
|
||||||
|
|
||||||
|
| 语言 | 作者 | 地址 | 类型 |
|
||||||
|
|--------|---------------------------------------------|------------------------------------------------------------------------------------------|-----|
|
||||||
|
| Java | [JackuXL](https://github.com/JackuXL) | [NeteaseCloudMusicApi-SDK](https://github.com/JackuXL/NeteaseCloudMusicApi-SDK) | 第三方 |
|
||||||
|
| Java | [1015770492](https://github.com/1015770492) | [yumbo-music-utils](https://github.com/1015770492/yumbo-music-utils) | 第三方 |
|
||||||
|
| Python | [盧瞳](https://github.com/2061360308) | [NeteaseCloudMusic_PythonSDK](https://github.com/2061360308/NeteaseCloudMusic_PythonSDK) | 第三方 |
|
||||||
|
| Swift | [Lincb522](https://github.com/Lincb522) | [NeteaseCloudMusicApi-Swift](https://github.com/Lincb522/NeteaseCloudMusicAPI-Swift) | 第三方 |
|
||||||
|
| Rust | [SPlayer-Dev](https://github.com/splayer-dev) | [ncm-api-rs](https://github.com/SPlayer-Dev/ncm-api-rs) | 第三方 |
|
||||||
|
|
||||||
|
|
||||||
|
### 依赖此项目的优秀开源项目
|
||||||
|
|
||||||
|
- [qier2222/YesPlayMusic](https://github.com/qier222/YesPlayMusic)
|
||||||
|
|
||||||
|
- [MaigoLabs/amaoke.app](https://github.com/MaigoLabs/amaoke.app)
|
||||||
|
|
||||||
|
- [Yueby/music-together](https://github.com/Yueby/music-together)
|
||||||
|
|
||||||
|
- [chthollyphlie/folia-major](https://github.com/chthollyphile/folia-major)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[The MIT License (MIT)](https://gitlab.com/Binaryify/NeteaseCloudMusicApi/blob/main/LICENSE)
|
[MIT License](https://github.com/MoeFurina/NeteaseCloudMusicApiEnhanced/blob/main/LICENSE)
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
const { defineConfig, globalIgnores } = require('eslint/config')
|
||||||
|
|
||||||
|
const html = require('eslint-plugin-html')
|
||||||
|
const globals = require('globals')
|
||||||
|
const tsParser = require('@typescript-eslint/parser')
|
||||||
|
const js = require('@eslint/js')
|
||||||
|
|
||||||
|
const { FlatCompat } = require('@eslint/eslintrc')
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
allConfig: js.configs.all,
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = defineConfig([
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
parserOptions: {
|
||||||
|
parser: 'babel-eslint',
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: {
|
||||||
|
html,
|
||||||
|
},
|
||||||
|
|
||||||
|
extends: compat.extends('plugin:prettier/recommended'),
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
endOfLine: 'auto',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
indent: [
|
||||||
|
'error',
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
SwitchCase: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
'space-infix-ops': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
int32Hint: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
'key-spacing': [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
beforeColon: false,
|
||||||
|
afterColon: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
'no-octal': 2,
|
||||||
|
'no-redeclare': 2,
|
||||||
|
'comma-spacing': 2,
|
||||||
|
'no-new-object': 2,
|
||||||
|
'arrow-spacing': 2,
|
||||||
|
|
||||||
|
quotes: [
|
||||||
|
2,
|
||||||
|
'single',
|
||||||
|
{
|
||||||
|
avoidEscape: true,
|
||||||
|
allowTemplateLiterals: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
globalIgnores(['**/public/']),
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
},
|
||||||
|
extends: compat.extends('plugin:@typescript-eslint/recommended'),
|
||||||
|
},
|
||||||
|
])
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
const fsPromises = require('fs/promises')
|
const fsPromises = require('fs/promises')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const server = require('../server')
|
const server = require('../server')
|
||||||
|
const logger = require('../util/logger.js')
|
||||||
|
|
||||||
const exportFile = path.join(__dirname, 'moddef.json')
|
const exportFile = path.join(__dirname, 'moddef.json')
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ async function main() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
fsPromises.writeFile(exportFile, JSON.stringify(def, null, 4))
|
fsPromises.writeFile(exportFile, JSON.stringify(def, null, 4))
|
||||||
console.log(`👍 Get your own definition at: ${exportFile}`)
|
logger.info(`👍 Get your own definition at: ${exportFile}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const fs = require('fs')
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const { register_anonimous } = require('./main')
|
const { register_anonimous } = require('./main')
|
||||||
const { cookieToJson, generateRandomChineseIP } = require('./util/index')
|
const { cookieToJson, generateRandomChineseIP } = require('./util/index')
|
||||||
|
const { getXeapiPublicKey } = require('./util/xeapiKey')
|
||||||
const tmpPath = require('os').tmpdir()
|
const tmpPath = require('os').tmpdir()
|
||||||
|
|
||||||
async function generateConfig() {
|
async function generateConfig() {
|
||||||
@@ -20,5 +21,21 @@ async function generateConfig() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
let currentPublicKey = {}
|
||||||
|
try {
|
||||||
|
currentPublicKey = JSON.parse(
|
||||||
|
fs.readFileSync(path.resolve(tmpPath, 'xeapi_public_key'), 'utf-8'),
|
||||||
|
)
|
||||||
|
} catch (_) {}
|
||||||
|
const publicKey = await getXeapiPublicKey(currentPublicKey, global.deviceId)
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.resolve(tmpPath, 'xeapi_public_key'),
|
||||||
|
JSON.stringify(publicKey),
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = generateConfig
|
module.exports = generateConfig
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export interface RequestBaseConfig {
|
|||||||
cookie?: string
|
cookie?: string
|
||||||
realIP?: string // IPv4/IPv6 filled in X-Real-IP
|
realIP?: string // IPv4/IPv6 filled in X-Real-IP
|
||||||
proxy?: string // HTTP proxy
|
proxy?: string // HTTP proxy
|
||||||
|
randomCNIP?: boolean // Whether to use a random Chinese IP address filled in X-Real-IP
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MultiPageConfig {
|
export interface MultiPageConfig {
|
||||||
@@ -1165,6 +1166,13 @@ export function song_url_v1(
|
|||||||
params: { id: string | number; level: SoundQualityType } & RequestBaseConfig,
|
params: { id: string | number; level: SoundQualityType } & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_copyright_rcmd(
|
||||||
|
params: {
|
||||||
|
songid?: string | number
|
||||||
|
id?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function top_album(
|
export function top_album(
|
||||||
params: {
|
params: {
|
||||||
area?: AlbumListArea
|
area?: AlbumListArea
|
||||||
@@ -1466,6 +1474,20 @@ export function artist_new_song(
|
|||||||
} & RequestBaseConfig,
|
} & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function artist_new_song_playall(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function artist_new_song_mv_list_v2(
|
||||||
|
params: {
|
||||||
|
startTimestamp?: number | string
|
||||||
|
before?: number | string
|
||||||
|
sourceType?: number | string
|
||||||
|
limit?: number | string
|
||||||
|
firstRequest?: boolean | string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function artist_detail(
|
export function artist_detail(
|
||||||
params: {
|
params: {
|
||||||
id: number | string
|
id: number | string
|
||||||
@@ -1586,6 +1608,10 @@ export function vip_growthpoint_get(
|
|||||||
} & RequestBaseConfig,
|
} & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function vip_growthpoint_getall(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function artist_fans(
|
export function artist_fans(
|
||||||
params: { id: number | string } & MultiPageConfig & RequestBaseConfig,
|
params: { id: number | string } & MultiPageConfig & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
@@ -1714,6 +1740,8 @@ export function nickname_check(
|
|||||||
|
|
||||||
export function musician_tasks_new(params: RequestBaseConfig): Promise<Response>
|
export function musician_tasks_new(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function musician_vip_tasks(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
export function playlist_update_playcount(
|
export function playlist_update_playcount(
|
||||||
params: {
|
params: {
|
||||||
id?: number | string
|
id?: number | string
|
||||||
@@ -1831,3 +1859,731 @@ export function voice_lyric(
|
|||||||
id: number | string
|
id: number | string
|
||||||
} & RequestBaseConfig,
|
} & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function aidj_content_rcmd(
|
||||||
|
params: {
|
||||||
|
latitude?: string | number
|
||||||
|
longitude?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function album_privilege(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function artist_detail_dynamic(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function artist_follow_count(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function broadcast_category_region_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function broadcast_channel_collect_list(
|
||||||
|
params: MultiPageConfig & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function broadcast_channel_currentinfo(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function broadcast_channel_list(
|
||||||
|
params: {
|
||||||
|
categoryId?: string | number
|
||||||
|
regionId?: string | number
|
||||||
|
lastId?: string | number
|
||||||
|
score?: string | number
|
||||||
|
} & MultiPageConfig &
|
||||||
|
RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function broadcast_sub(
|
||||||
|
params: {
|
||||||
|
t: SubAction
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function chart_detail(
|
||||||
|
params: {
|
||||||
|
chartCode: string | number
|
||||||
|
targetId: string | number
|
||||||
|
targetType: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function chart_song_detail(
|
||||||
|
params: {
|
||||||
|
chartCode: string | number
|
||||||
|
targetId: string | number
|
||||||
|
targetType: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function cloud_import(
|
||||||
|
params: {
|
||||||
|
md5: string
|
||||||
|
id?: string | number
|
||||||
|
bitrate?: string | number
|
||||||
|
fileSize?: string | number
|
||||||
|
artist?: string
|
||||||
|
album?: string
|
||||||
|
song?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function cloud_lyric_get(
|
||||||
|
params: {
|
||||||
|
uid: string | number
|
||||||
|
sid: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function cloud_upload_complete(
|
||||||
|
params: {
|
||||||
|
songId: string | number
|
||||||
|
resourceId: string | number
|
||||||
|
md5: string
|
||||||
|
filename: string
|
||||||
|
song?: string
|
||||||
|
artist?: string
|
||||||
|
album?: string
|
||||||
|
bitrate?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function cloud_upload_token(
|
||||||
|
params: {
|
||||||
|
md5: string
|
||||||
|
fileSize: string | number
|
||||||
|
filename: string
|
||||||
|
bitrate?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function comment_info_list(
|
||||||
|
params: {
|
||||||
|
ids?: string
|
||||||
|
type?: CommentType
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function comment_report(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
cid: string | number
|
||||||
|
reason: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function creator_authinfo_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function dj_difm_all_style_channel(
|
||||||
|
params: {
|
||||||
|
sources?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function dj_difm_channel_subscribe(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function dj_difm_channel_unsubscribe(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function dj_difm_playing_tracks_list(
|
||||||
|
params: {
|
||||||
|
channelId: string | number
|
||||||
|
limit?: string | number
|
||||||
|
source?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function dj_difm_subscribe_channels_get(
|
||||||
|
params: {
|
||||||
|
sources?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function fanscenter_basicinfo_age_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function fanscenter_basicinfo_gender_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function fanscenter_basicinfo_province_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function fanscenter_overview_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function fanscenter_trend_list(
|
||||||
|
params: {
|
||||||
|
startTime?: string | number
|
||||||
|
endTime?: string | number
|
||||||
|
type?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function lbs_city_code(
|
||||||
|
params: {
|
||||||
|
bizCode?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listen_data_realtime_report(
|
||||||
|
params: {
|
||||||
|
type?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listen_data_report(
|
||||||
|
params: {
|
||||||
|
type?: string
|
||||||
|
endTime?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listen_data_song_play_rank(
|
||||||
|
params: {
|
||||||
|
type?: string
|
||||||
|
endTime?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listen_data_today_song(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listen_data_total(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function listen_data_year_report(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_accept(
|
||||||
|
params: {
|
||||||
|
roomId: string | number
|
||||||
|
inviterId: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_end(
|
||||||
|
params: {
|
||||||
|
roomId: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_heatbeat(
|
||||||
|
params: {
|
||||||
|
roomId: string | number
|
||||||
|
songId: string | number
|
||||||
|
playStatus: string | number
|
||||||
|
progress: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_play_command(
|
||||||
|
params: {
|
||||||
|
roomId: string | number
|
||||||
|
commandType: string | number
|
||||||
|
progress?: string | number
|
||||||
|
playStatus: string | number
|
||||||
|
formerSongId: string | number
|
||||||
|
targetSongId: string | number
|
||||||
|
clientSeq: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_room_check(
|
||||||
|
params: {
|
||||||
|
roomId: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_room_create(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_sync_list_command(
|
||||||
|
params: {
|
||||||
|
roomId: string | number
|
||||||
|
commandType: string | number
|
||||||
|
userId: string | number
|
||||||
|
version: string | number
|
||||||
|
randomList: string
|
||||||
|
displayList: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function listentogether_sync_playlist_get(
|
||||||
|
params: {
|
||||||
|
roomId: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function mlog_music_rcmd(
|
||||||
|
params: {
|
||||||
|
mvid?: string | number
|
||||||
|
songid?: string | number
|
||||||
|
limit?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function music_first_listen_info(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function personal_fm_mode(
|
||||||
|
params: {
|
||||||
|
mode: string
|
||||||
|
submode?: string
|
||||||
|
limit?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function playlist_category_list(
|
||||||
|
params: {
|
||||||
|
cat?: string
|
||||||
|
limit?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function playlist_detail_rcmd_get(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function playlist_import_name_task_create(
|
||||||
|
params: {
|
||||||
|
local?: string
|
||||||
|
importStarPlaylist?: boolean
|
||||||
|
['name']?: string
|
||||||
|
['description']?: string
|
||||||
|
['id']?: string | number
|
||||||
|
['url']?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function playlist_import_task_status(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function playlist_privacy(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function playmode_song_vector(
|
||||||
|
params: {
|
||||||
|
ids: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function radio_sport_get(
|
||||||
|
params: {
|
||||||
|
bpm?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function recent_listen_list(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function recommend_songs_dislike(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function register_anonimous(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function sati_resource_list(
|
||||||
|
params: {
|
||||||
|
tag: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function sati_resource_list_more(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function sati_resource_sub(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
cancel?: string | boolean
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function sati_resource_sub_list(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function sati_tag_list(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function sati_timescene_resources_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function search_match(
|
||||||
|
params: {
|
||||||
|
title?: string
|
||||||
|
album?: string
|
||||||
|
artist?: string
|
||||||
|
duration?: string | number
|
||||||
|
md5?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function search_suggest_pc(
|
||||||
|
params: {
|
||||||
|
keyword: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function send_album(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
msg?: string
|
||||||
|
user_ids: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function send_song(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
msg?: string
|
||||||
|
user_ids: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_chorus(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_creators(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_downlist(
|
||||||
|
params: MultiPageConfig & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_download_url_v1(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
level: SoundQualityType
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_dynamic_cover(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_like(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
like?: string | boolean
|
||||||
|
uid?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_like_check(
|
||||||
|
params: {
|
||||||
|
ids: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_lyrics_mark(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_lyrics_mark_add(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
markId?: string
|
||||||
|
data?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_lyrics_mark_del(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_lyrics_mark_user_page(
|
||||||
|
params: MultiPageConfig & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_monthdownlist(
|
||||||
|
params: MultiPageConfig & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_music_detail(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_red_count(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_singledownlist(
|
||||||
|
params: MultiPageConfig & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_url_match(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
source?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function song_url_ncmget(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function song_url_v1_302(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
level: SoundQualityType
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function starpick_comments_summary(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function summary_annual(
|
||||||
|
params: {
|
||||||
|
year: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function threshold_detail_get(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function toplist_detail_v2(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function ugc_album_get(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function ugc_artist_get(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function ugc_artist_search(
|
||||||
|
params: {
|
||||||
|
keyword: string
|
||||||
|
limit?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function ugc_detail(
|
||||||
|
params: {
|
||||||
|
auditStatus?: string
|
||||||
|
type?: string | number
|
||||||
|
sortBy?: string
|
||||||
|
order?: string
|
||||||
|
} & MultiPageConfig &
|
||||||
|
RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function ugc_mv_get(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function ugc_song_get(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function ugc_user_devote(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function user_detail_new(
|
||||||
|
params: {
|
||||||
|
uid: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_follow_mixed(
|
||||||
|
params: {
|
||||||
|
size?: string | number
|
||||||
|
cursor?: string | number
|
||||||
|
scene?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_medal(
|
||||||
|
params: {
|
||||||
|
uid: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_mutualfollow_get(
|
||||||
|
params: {
|
||||||
|
uid: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_playlist_collect(
|
||||||
|
params: {
|
||||||
|
uid: string | number
|
||||||
|
} & MultiPageConfig &
|
||||||
|
RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_playlist_create(
|
||||||
|
params: {
|
||||||
|
uid: string | number
|
||||||
|
} & MultiPageConfig &
|
||||||
|
RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_social_status(
|
||||||
|
params: {
|
||||||
|
uid: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_social_status_edit(
|
||||||
|
params: {
|
||||||
|
type: string | number
|
||||||
|
iconUrl?: string
|
||||||
|
content?: string
|
||||||
|
actionUrl?: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_social_status_rcmd(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function user_social_status_support(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function verify_getQr(
|
||||||
|
params: {
|
||||||
|
vid: string | number
|
||||||
|
type: string | number
|
||||||
|
token: string
|
||||||
|
evid: string
|
||||||
|
sign: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function verify_qrcodestatus(
|
||||||
|
params: {
|
||||||
|
qr: string
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function vip_sign(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function vip_sign_info(params: RequestBaseConfig): Promise<Response>
|
||||||
|
|
||||||
|
export function vip_tasks_v1(
|
||||||
|
params: {
|
||||||
|
id?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function voice_detail(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function voice_upload(
|
||||||
|
params: {
|
||||||
|
songFile: {
|
||||||
|
name: string
|
||||||
|
data: string | Buffer
|
||||||
|
}
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function voicelist_detail(
|
||||||
|
params: {
|
||||||
|
id: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function voicelist_list(
|
||||||
|
params: {
|
||||||
|
voiceListId: string | number
|
||||||
|
} & MultiPageConfig &
|
||||||
|
RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function voicelist_my_created(
|
||||||
|
params: {
|
||||||
|
limit?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function voicelist_search(
|
||||||
|
params: {
|
||||||
|
keyword?: string
|
||||||
|
limit?: string | number
|
||||||
|
offset?: string | number
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
|
export function voicelist_trans(
|
||||||
|
params: {
|
||||||
|
radioId?: string | number
|
||||||
|
programId?: string | number
|
||||||
|
position?: string | number
|
||||||
|
} & MultiPageConfig &
|
||||||
|
RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
## 环境
|
|
||||||
- 系统/平台: <你的系统和平台>
|
|
||||||
|
|
||||||
- nodejs 版本: <你的 NodeJS 版本号>
|
|
||||||
|
|
||||||
- API版本:<运行的云音乐 API 的版本号, 对应 package.json 里面的 version>
|
|
||||||
|
|
||||||
## 出现问题
|
|
||||||
<出现的问题>
|
|
||||||
|
|
||||||
## 重现步骤
|
|
||||||
<重现步骤>
|
|
||||||
|
|
||||||
## 期待效果
|
|
||||||
<现在的效果,期待的效果>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
>先看文档有没有相关说明,调用前须知必看
|
|
||||||
|
|
||||||
>先在 issues 搜一下是否有相似问题,没有再发,否则直接关闭
|
|
||||||
|
|
||||||
>不处理别人搭建的线上服务的问题,此项目不提供任何线上服务,请自行搭建
|
|
||||||
|
|
||||||
>重现步骤尽量详细,不能含糊不清,包含请求地址和对应参数以及操作过程描述,不是每个人都喜欢猜别人遇到了什么问题和找参数一个个试,也比较浪费时间
|
|
||||||
|
|
||||||
>如果不是提建议,提 issues 如果不照着模版来将不会优先处理或直接关闭
|
|
||||||
|
|
||||||
>460 cheating 的问题把 `util/request.js` 里面的 `headers['X-Real-IP']` 的注释取消掉就好
|
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
// 可按需修改此 API 的代码
|
// 可按需修改此 API 的代码
|
||||||
/* {"extInfo":"{\"lastRequestTimestamp\":1692358373509,\"lbsInfoList\":[{\"lat\":40.23076381,\"lon\":129.07545186,\"time\":1692358543},{\"lat\":40.23076381,\"lon\":129.07545186,\"time\":1692055283}],\"listenedTs\":false,\"noAidjToAidj\":true}","header":"{}"} */
|
/* {"extInfo":"{\"lastRequestTimestamp\":1692358373509,\"lbsInfoList\":[{\"lat\":40.23076381,\"lon\":129.07545186,\"time\":1692358543},{\"lat\":40.23076381,\"lon\":129.07545186,\"time\":1692055283}],\"listenedTs\":false,\"noAidjToAidj\":true}","header":"{}"} */
|
||||||
|
|
||||||
|
const logger = require('../util/logger.js')
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
var extInfo = {}
|
var extInfo = {}
|
||||||
@@ -22,6 +23,6 @@ module.exports = (query, request) => {
|
|||||||
const data = {
|
const data = {
|
||||||
extInfo: JSON.stringify(extInfo),
|
extInfo: JSON.stringify(extInfo),
|
||||||
}
|
}
|
||||||
// console.log(data)
|
// logger.info(data)
|
||||||
return request(`/api/aidj/content/rcmd/info`, data, createOption(query))
|
return request(`/api/aidj/content/rcmd/info`, data, createOption(query))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 获取关注歌手的新歌曲和 MV
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
startTimestamp: query.startTimestamp || query.before || Date.now(),
|
||||||
|
sourceType: query.sourceType || 1,
|
||||||
|
limit: query.limit || 10,
|
||||||
|
firstRequest: query.firstRequest ?? true,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/sub/artist/new/works/song-mv/list/v2`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// 获取所有关注歌手最近的 50 首新歌
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(
|
||||||
|
`/api/sub/artist/new/works/song/playall`,
|
||||||
|
{},
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,23 +1,26 @@
|
|||||||
const mm = require('music-metadata')
|
|
||||||
const uploadPlugin = require('../plugins/songUpload')
|
const uploadPlugin = require('../plugins/songUpload')
|
||||||
const md5 = require('md5')
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
|
const logger = require('../util/logger.js')
|
||||||
|
const {
|
||||||
|
isTempFile,
|
||||||
|
getFileSize,
|
||||||
|
getFileMd5,
|
||||||
|
cleanupTempFile,
|
||||||
|
getFileExtension,
|
||||||
|
sanitizeFilename,
|
||||||
|
} = require('../util/fileHelper')
|
||||||
|
|
||||||
|
let mm
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
let ext = 'mp3'
|
mm = require('music-metadata')
|
||||||
// if (query.songFile.name.indexOf('flac') > -1) {
|
|
||||||
// ext = 'flac'
|
|
||||||
// }
|
|
||||||
if (query.songFile.name.includes('.')) {
|
|
||||||
ext = query.songFile.name.split('.').pop()
|
|
||||||
}
|
|
||||||
query.songFile.name = Buffer.from(query.songFile.name, 'latin1').toString(
|
query.songFile.name = Buffer.from(query.songFile.name, 'latin1').toString(
|
||||||
'utf-8',
|
'utf-8',
|
||||||
)
|
)
|
||||||
const filename = query.songFile.name
|
const ext = getFileExtension(query.songFile.name)
|
||||||
.replace('.' + ext, '')
|
const filename = sanitizeFilename(query.songFile.name)
|
||||||
.replace(/\s/g, '')
|
|
||||||
.replace(/\./g, '_')
|
|
||||||
const bitrate = 999000
|
const bitrate = 999000
|
||||||
|
|
||||||
if (!query.songFile) {
|
if (!query.songFile) {
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
status: 500,
|
status: 500,
|
||||||
@@ -27,119 +30,135 @@ module.exports = async (query, request) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (!query.songFile.md5) {
|
|
||||||
// 命令行上传没有md5和size信息,需要填充
|
const useTemp = isTempFile(query.songFile)
|
||||||
query.songFile.md5 = md5(query.songFile.data)
|
let fileSize = await getFileSize(query.songFile)
|
||||||
query.songFile.size = query.songFile.data.byteLength
|
let fileMd5 = await getFileMd5(query.songFile)
|
||||||
}
|
|
||||||
const res = await request(
|
query.songFile.md5 = fileMd5
|
||||||
`/api/cloud/upload/check`,
|
query.songFile.size = fileSize
|
||||||
{
|
|
||||||
bitrate: String(bitrate),
|
|
||||||
ext: '',
|
|
||||||
length: query.songFile.size,
|
|
||||||
md5: query.songFile.md5,
|
|
||||||
songId: '0',
|
|
||||||
version: 1,
|
|
||||||
},
|
|
||||||
createOption(query),
|
|
||||||
)
|
|
||||||
let artist = ''
|
|
||||||
let album = ''
|
|
||||||
let songName = ''
|
|
||||||
try {
|
try {
|
||||||
const metadata = await mm.parseBuffer(
|
const res = await request(
|
||||||
query.songFile.data,
|
`/api/cloud/upload/check`,
|
||||||
query.songFile.mimetype,
|
{
|
||||||
|
bitrate: String(bitrate),
|
||||||
|
ext: '',
|
||||||
|
length: fileSize,
|
||||||
|
md5: fileMd5,
|
||||||
|
songId: '0',
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
createOption(query),
|
||||||
)
|
)
|
||||||
const info = metadata.common
|
|
||||||
|
|
||||||
if (info.title) {
|
let artist = ''
|
||||||
songName = info.title
|
let album = ''
|
||||||
}
|
let songName = ''
|
||||||
if (info.album) {
|
|
||||||
album = info.album
|
|
||||||
}
|
|
||||||
if (info.artist) {
|
|
||||||
artist = info.artist
|
|
||||||
}
|
|
||||||
// if (metadata.native.ID3v1) {
|
|
||||||
// metadata.native.ID3v1.forEach((item) => {
|
|
||||||
// // console.log(item.id, item.value)
|
|
||||||
// if (item.id === 'title') {
|
|
||||||
// songName = item.value
|
|
||||||
// }
|
|
||||||
// if (item.id === 'artist') {
|
|
||||||
// artist = item.value
|
|
||||||
// }
|
|
||||||
// if (item.id === 'album') {
|
|
||||||
// album = item.value
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// // console.log({
|
|
||||||
// // songName,
|
|
||||||
// // album,
|
|
||||||
// // songName,
|
|
||||||
// // })
|
|
||||||
// }
|
|
||||||
// console.log({
|
|
||||||
// songName,
|
|
||||||
// album,
|
|
||||||
// songName,
|
|
||||||
// })
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
const tokenRes = await request(
|
|
||||||
`/api/nos/token/alloc`,
|
|
||||||
{
|
|
||||||
bucket: '',
|
|
||||||
ext: ext,
|
|
||||||
filename: filename,
|
|
||||||
local: false,
|
|
||||||
nos_product: 3,
|
|
||||||
type: 'audio',
|
|
||||||
md5: query.songFile.md5,
|
|
||||||
},
|
|
||||||
createOption(query),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (res.body.needUpload) {
|
try {
|
||||||
const uploadInfo = await uploadPlugin(query, request)
|
let metadata
|
||||||
// console.log('uploadInfo', uploadInfo.body.result.resourceId)
|
if (useTemp) {
|
||||||
}
|
metadata = await mm.parseFile(query.songFile.tempFilePath)
|
||||||
// console.log(tokenRes.body.result)
|
} else {
|
||||||
const res2 = await request(
|
metadata = await mm.parseBuffer(
|
||||||
`/api/upload/cloud/info/v2`,
|
query.songFile.data,
|
||||||
{
|
query.songFile.mimetype,
|
||||||
md5: query.songFile.md5,
|
)
|
||||||
songid: res.body.songId,
|
}
|
||||||
filename: query.songFile.name,
|
const info = metadata.common
|
||||||
song: songName || filename,
|
if (info.title) songName = info.title
|
||||||
album: album || '未知专辑',
|
if (info.album) album = info.album
|
||||||
artist: artist || '未知艺术家',
|
if (info.artist) artist = info.artist
|
||||||
bitrate: String(bitrate),
|
} catch (error) {
|
||||||
resourceId: tokenRes.body.result.resourceId,
|
logger.info('元数据解析错误:', error.message)
|
||||||
},
|
}
|
||||||
createOption(query),
|
|
||||||
)
|
const tokenRes = await request(
|
||||||
// console.log({ res2, privateCloud: res2.body.privateCloud })
|
`/api/nos/token/alloc`,
|
||||||
// console.log(res.body.songId, 'songid')
|
{
|
||||||
const res3 = await request(
|
bucket: '',
|
||||||
`/api/cloud/pub/v2`,
|
ext: ext,
|
||||||
{
|
filename: filename,
|
||||||
songid: res2.body.songId,
|
local: false,
|
||||||
},
|
nos_product: 3,
|
||||||
createOption(query),
|
type: 'audio',
|
||||||
)
|
md5: fileMd5,
|
||||||
// console.log({ res3 })
|
},
|
||||||
return {
|
createOption(query),
|
||||||
status: 200,
|
)
|
||||||
body: {
|
|
||||||
...res.body,
|
if (!tokenRes.body.result || !tokenRes.body.result.resourceId) {
|
||||||
...res3.body,
|
logger.error('Token分配失败:', tokenRes.body)
|
||||||
// ...uploadInfo,
|
return Promise.reject({
|
||||||
},
|
status: 500,
|
||||||
cookie: res.cookie,
|
body: {
|
||||||
|
code: 500,
|
||||||
|
msg: '获取上传token失败',
|
||||||
|
detail: tokenRes.body,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.body.needUpload) {
|
||||||
|
logger.info('需要上传,开始上传流程...')
|
||||||
|
try {
|
||||||
|
const uploadInfo = await uploadPlugin(query, request)
|
||||||
|
logger.info('上传完成:', uploadInfo?.body?.result?.resourceId)
|
||||||
|
} catch (uploadError) {
|
||||||
|
logger.error('上传失败:', uploadError)
|
||||||
|
return Promise.reject(uploadError)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info('文件已存在,跳过上传')
|
||||||
|
}
|
||||||
|
|
||||||
|
const res2 = await request(
|
||||||
|
`/api/upload/cloud/info/v2`,
|
||||||
|
{
|
||||||
|
md5: fileMd5,
|
||||||
|
songid: res.body.songId,
|
||||||
|
filename: query.songFile.name,
|
||||||
|
song: songName || filename,
|
||||||
|
album: album || '未知专辑',
|
||||||
|
artist: artist || '未知艺术家',
|
||||||
|
bitrate: String(bitrate),
|
||||||
|
resourceId: tokenRes.body.result.resourceId,
|
||||||
|
},
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res2.body.code !== 200) {
|
||||||
|
logger.error('云盘信息上传失败:', res2.body)
|
||||||
|
return Promise.reject({
|
||||||
|
status: res2.status || 500,
|
||||||
|
body: {
|
||||||
|
code: res2.body.code || 500,
|
||||||
|
msg: res2.body.msg || '上传云盘信息失败',
|
||||||
|
detail: res2.body,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const res3 = await request(
|
||||||
|
`/api/cloud/pub/v2`,
|
||||||
|
{
|
||||||
|
songid: res2.body.songId,
|
||||||
|
},
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
...res.body,
|
||||||
|
...res3.body,
|
||||||
|
},
|
||||||
|
cookie: res.cookie,
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (useTemp) {
|
||||||
|
await cleanupTempFile(query.songFile.tempFilePath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 获取云盘歌词
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
userId: query.uid,
|
||||||
|
songId: query.sid,
|
||||||
|
lv: -1,
|
||||||
|
kv: -1,
|
||||||
|
}
|
||||||
|
return request(`/api/cloud/lyric/get`, data, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const {
|
||||||
|
songId,
|
||||||
|
resourceId,
|
||||||
|
md5,
|
||||||
|
filename,
|
||||||
|
song,
|
||||||
|
artist,
|
||||||
|
album,
|
||||||
|
bitrate = 999000,
|
||||||
|
} = query
|
||||||
|
|
||||||
|
if (!songId || !resourceId || !md5 || !filename) {
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
msg: '缺少必要参数: songId, resourceId, md5, filename',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const songName = song || filename.replace(/\.[^.]+$/, '')
|
||||||
|
|
||||||
|
const res2 = await request(
|
||||||
|
`/api/upload/cloud/info/v2`,
|
||||||
|
{
|
||||||
|
md5: md5,
|
||||||
|
songid: songId,
|
||||||
|
filename: filename,
|
||||||
|
song: songName,
|
||||||
|
album: album || '未知专辑',
|
||||||
|
artist: artist || '未知艺术家',
|
||||||
|
bitrate: String(bitrate),
|
||||||
|
resourceId: resourceId,
|
||||||
|
},
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res2.body.code !== 200) {
|
||||||
|
return Promise.reject({
|
||||||
|
status: res2.status || 500,
|
||||||
|
body: {
|
||||||
|
code: res2.body.code || 500,
|
||||||
|
msg: res2.body.msg || '上传云盘信息失败',
|
||||||
|
detail: res2.body,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const res3 = await request(
|
||||||
|
`/api/cloud/pub/v2`,
|
||||||
|
{
|
||||||
|
songid: res2.body.songId,
|
||||||
|
},
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
songId: res2.body.songId,
|
||||||
|
...res3.body,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cookie: res2.cookie,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
const { default: axios } = require('axios')
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const { md5, fileSize, filename, bitrate = 999000 } = query
|
||||||
|
|
||||||
|
if (!md5 || !fileSize || !filename) {
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
msg: '缺少必要参数: md5, fileSize, filename',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = filename.includes('.') ? filename.split('.').pop() : 'mp3'
|
||||||
|
|
||||||
|
const checkRes = await request(
|
||||||
|
`/api/cloud/upload/check`,
|
||||||
|
{
|
||||||
|
bitrate: String(bitrate),
|
||||||
|
ext: '',
|
||||||
|
length: fileSize,
|
||||||
|
md5: md5,
|
||||||
|
songId: '0',
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
|
||||||
|
const bucket = 'jd-musicrep-privatecloud-audio-public'
|
||||||
|
const tokenRes = await request(
|
||||||
|
`/api/nos/token/alloc`,
|
||||||
|
{
|
||||||
|
bucket: bucket,
|
||||||
|
ext: ext,
|
||||||
|
filename: filename
|
||||||
|
.replace(/\.[^.]+$/, '')
|
||||||
|
.replace(/\s/g, '')
|
||||||
|
.replace(/\./g, '_'),
|
||||||
|
local: false,
|
||||||
|
nos_product: 3,
|
||||||
|
type: 'audio',
|
||||||
|
md5: md5,
|
||||||
|
},
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!tokenRes.body.result || !tokenRes.body.result.objectKey) {
|
||||||
|
return Promise.reject({
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
code: 500,
|
||||||
|
msg: '获取上传token失败',
|
||||||
|
detail: tokenRes.body,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let lbs
|
||||||
|
try {
|
||||||
|
lbs = (
|
||||||
|
await axios({
|
||||||
|
method: 'get',
|
||||||
|
url: `https://wanproxy.127.net/lbs?version=1.0&bucketname=${bucket}`,
|
||||||
|
timeout: 10000,
|
||||||
|
})
|
||||||
|
).data
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject({
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
code: 500,
|
||||||
|
msg: '获取上传服务器地址失败',
|
||||||
|
detail: error.message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lbs || !lbs.upload || !lbs.upload[0]) {
|
||||||
|
return Promise.reject({
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
code: 500,
|
||||||
|
msg: '获取上传服务器地址无效',
|
||||||
|
detail: lbs,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
needUpload: checkRes.body.needUpload,
|
||||||
|
songId: checkRes.body.songId,
|
||||||
|
uploadToken: tokenRes.body.result.token,
|
||||||
|
objectKey: tokenRes.body.result.objectKey,
|
||||||
|
resourceId: tokenRes.body.result.resourceId,
|
||||||
|
uploadUrl: `${lbs.upload[0]}/${bucket}/${tokenRes.body.result.objectKey.replace(/\//g, '%2F')}?offset=0&complete=true&version=1.0`,
|
||||||
|
bucket: bucket,
|
||||||
|
md5: md5,
|
||||||
|
fileSize: fileSize,
|
||||||
|
filename: filename,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cookie: checkRes.cookie,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// 评论统计数据
|
||||||
|
// type: 0=歌曲 1=MV 2=歌单 3=专辑 4=电台节目 5=视频 6=动态 7=电台
|
||||||
|
// ids: 资源 ID 列表,多个用逗号分隔,如 "123,456"
|
||||||
|
const { resourceTypeMap } = require('../util/config.json')
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
// 从 resourceTypeMap 的前缀值中提取网易云内部资源类型编号
|
||||||
|
// 例如 "R_SO_4_" -> "4", "A_DR_14_" -> "14"
|
||||||
|
const resourceTypeIdMap = Object.fromEntries(
|
||||||
|
Object.entries(resourceTypeMap).map(([key, prefix]) => [
|
||||||
|
key,
|
||||||
|
prefix.replace(/_$/, '').split('_').pop(),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const ids = String(query.ids || query.id || '')
|
||||||
|
.split(',')
|
||||||
|
.map((id) => id.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
return request(
|
||||||
|
`/api/resource/commentInfo/list`,
|
||||||
|
{
|
||||||
|
resourceType: resourceTypeIdMap[String(query.type || 0)],
|
||||||
|
resourceIds: JSON.stringify(ids),
|
||||||
|
},
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 举报评论
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
threadId: 'R_SO_4_' + query.id,
|
||||||
|
commentId: query.cid,
|
||||||
|
reason: query.reason,
|
||||||
|
}
|
||||||
|
return request(`/api/report/reportcomment`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
const {
|
||||||
|
eapiResDecrypt,
|
||||||
|
eapiReqDecrypt,
|
||||||
|
aesDecrypt,
|
||||||
|
xeapiResDecrypt,
|
||||||
|
} = require('../util/crypto')
|
||||||
|
const CryptoJS = require('crypto-js')
|
||||||
|
|
||||||
|
const linuxapiKey = 'rFgB&h#%2?^eDg:Q'
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const crypto = query.crypto || 'eapi'
|
||||||
|
const data = query.data || query.hexString || ''
|
||||||
|
const isReq = query.isReq !== 'false'
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: { code: 400, message: 'data is required' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result
|
||||||
|
switch (crypto) {
|
||||||
|
case 'eapi': {
|
||||||
|
const pureHex = data.replace(/\s/g, '')
|
||||||
|
result = isReq ? eapiReqDecrypt(pureHex) : eapiResDecrypt(pureHex)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'weapi': {
|
||||||
|
if (isReq) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message:
|
||||||
|
'weapi 请求解密需要 RSA 私钥,暂不支持;仅支持 weapi 返回数据解密(e_r=true 时与 eapi 相同)',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pureHex = data.replace(/\s/g, '')
|
||||||
|
result = eapiResDecrypt(pureHex)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'linuxapi': {
|
||||||
|
if (isReq) {
|
||||||
|
const pureHex = data.replace(/\s/g, '')
|
||||||
|
const decrypted = aesDecrypt(pureHex, linuxapiKey, '', 'hex')
|
||||||
|
result = JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
|
||||||
|
} else {
|
||||||
|
result = typeof data === 'string' ? JSON.parse(data) : data
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'xeapi': {
|
||||||
|
if (isReq) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message:
|
||||||
|
'xeapi 请求解密涉及 X25519 ECDH 密钥交换,流程复杂,暂不支持;仅支持 xeapi 返回数据解密',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const buf = Buffer.from(data, 'base64')
|
||||||
|
result = xeapiResDecrypt(buf)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'api': {
|
||||||
|
result = typeof data === 'string' ? JSON.parse(data) : data
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: { code: 400, message: `未知加密方式: ${crypto}` },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { code: 200, data: result },
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: { code: 400, message: `解密失败: ${error.message}` },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// 听歌足迹 - 歌曲播放排行 (Top20)
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
return request(
|
||||||
|
`/api/content/activity/listen/data/song/play/rank`,
|
||||||
|
{
|
||||||
|
type: query.type || 'month', //周 week 月 month
|
||||||
|
endTime: query.endTime, // 不填就是本周/月的
|
||||||
|
},
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 获取音乐人任务
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
return request(
|
||||||
|
`/api/nmusician/workbench/special/right/vip/info`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
// 私信和通知接口
|
// 私信和通知接口
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {}
|
const data = {}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
|
const logger = require('../util/logger.js')
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
query.ids = query.ids || ''
|
query.ids = query.ids || ''
|
||||||
const data = {
|
const data = {
|
||||||
@@ -9,7 +10,7 @@ module.exports = async (query, request) => {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
console.log(data)
|
logger.info(data)
|
||||||
|
|
||||||
return request(`/api/playlist/track/add`, data, createOption(query, 'weapi'))
|
return request(`/api/playlist/track/add`, data, createOption(query, 'weapi'))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {}
|
const data = {
|
||||||
|
afresh: query.afresh,
|
||||||
|
}
|
||||||
return request(
|
return request(
|
||||||
`/api/v3/discovery/recommend/songs`,
|
`/api/v3/discovery/recommend/songs`,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
// 获取游客cookie
|
||||||
|
|
||||||
const CryptoJS = require('crypto-js')
|
const CryptoJS = require('crypto-js')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const ID_XOR_KEY_1 = '3go8&$8*3*3h0k(2)2'
|
const ID_XOR_KEY_1 = '3go8&$8*3*3h0k(2)2'
|
||||||
|
const logger = require('../util/logger.js')
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
const { generateDeviceId } = require('../util/index')
|
const { generateDeviceId } = require('../util/index')
|
||||||
@@ -23,7 +26,7 @@ function cloudmusic_dll_encode_id(some_id) {
|
|||||||
|
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
const deviceId = generateDeviceId()
|
const deviceId = generateDeviceId()
|
||||||
console.log(`[register_anonimous] deviceId: ${deviceId}`)
|
logger.info(`Successfully registered anonimous token, deviceId: ${deviceId}`)
|
||||||
global.deviceId = deviceId
|
global.deviceId = deviceId
|
||||||
const encodedId = CryptoJS.enc.Base64.stringify(
|
const encodedId = CryptoJS.enc.Base64.stringify(
|
||||||
CryptoJS.enc.Utf8.parse(
|
CryptoJS.enc.Utf8.parse(
|
||||||
@@ -36,7 +39,7 @@ module.exports = async (query, request) => {
|
|||||||
let result = await request(
|
let result = await request(
|
||||||
`/api/register/anonimous`,
|
`/api/register/anonimous`,
|
||||||
data,
|
data,
|
||||||
createOption(query, 'weapi'),
|
createOption(query, 'xeapi'),
|
||||||
)
|
)
|
||||||
if (result.body.code === 200) {
|
if (result.body.code === 200) {
|
||||||
result = {
|
result = {
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
const { default: axios } = require('axios')
|
||||||
|
const encrypt = require('../util/crypto')
|
||||||
|
const { APP_CONF } = require('../util/config.json')
|
||||||
|
|
||||||
|
const generateNonce = () => {
|
||||||
|
let nonce = ''
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
nonce += Math.floor(Math.random() * 10).toString()
|
||||||
|
}
|
||||||
|
return nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const nonce = generateNonce()
|
||||||
|
const timestamp = String(Date.now())
|
||||||
|
const deviceId = query.deviceId || global.deviceId || ''
|
||||||
|
const currentKeyVersion = query.currentKeyVersion || ''
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
appVersion: '9.1.65',
|
||||||
|
currentKeyVersion,
|
||||||
|
deviceId,
|
||||||
|
nonce,
|
||||||
|
os: 'android',
|
||||||
|
requestType: 'active',
|
||||||
|
signature: encrypt.xeapiSign(timestamp, nonce),
|
||||||
|
t1: '',
|
||||||
|
t2: '',
|
||||||
|
timestamp,
|
||||||
|
uid: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: APP_CONF.apiDomain + '/api/gorilla/anti/crawler/security/key/get',
|
||||||
|
headers: {
|
||||||
|
'User-Agent':
|
||||||
|
'NeteaseMusic/9.1.65.240927161425(9001065);Dalvik/2.1.0 (Linux; U; Android 14; 23013RK75C Build/UKQ1.230804.001)',
|
||||||
|
Cookie: deviceId ? `deviceId=${encodeURIComponent(deviceId)}` : '',
|
||||||
|
},
|
||||||
|
data: new URLSearchParams(data).toString(),
|
||||||
|
proxy: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (
|
||||||
|
!res.data ||
|
||||||
|
res.data.code !== 200 ||
|
||||||
|
!res.data.data ||
|
||||||
|
!res.data.data.encryptedData
|
||||||
|
) {
|
||||||
|
throw new Error('xeapi public key request failed')
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!res.data.data.signature ||
|
||||||
|
encrypt.xeapiSign(res.data.data.timestamp, nonce) !==
|
||||||
|
res.data.data.signature
|
||||||
|
) {
|
||||||
|
throw new Error('xeapi public key response signature mismatch')
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKey = encrypt.xeapiDecryptPublicKey(res.data.data.encryptedData)
|
||||||
|
if (!publicKey.sk) {
|
||||||
|
throw new Error('xeapi public key response missing sk')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
...publicKey,
|
||||||
|
deviceId,
|
||||||
|
},
|
||||||
|
cookie: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// 提交歌曲播放状态
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
const generateSessionId = () =>
|
||||||
|
Array.from(
|
||||||
|
{ length: 12 },
|
||||||
|
() =>
|
||||||
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'[Math.floor(Math.random() * 36)],
|
||||||
|
).join('')
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
sessionId,
|
||||||
|
progress = 0,
|
||||||
|
playMode = 'list_loop',
|
||||||
|
type = 'song',
|
||||||
|
} = query
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return Promise.reject({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
msg: '缺少必要参数:id',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const playStateSubmitReq = JSON.stringify({
|
||||||
|
resource: {
|
||||||
|
id: String(id),
|
||||||
|
type: type,
|
||||||
|
},
|
||||||
|
progress: Number(progress) || 0,
|
||||||
|
sessionId: sessionId || generateSessionId(),
|
||||||
|
playMode: playMode,
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
playStateSubmitReq: playStateSubmitReq,
|
||||||
|
}
|
||||||
|
|
||||||
|
return request(
|
||||||
|
'/api/relay/play/state/submit',
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,8 +1,42 @@
|
|||||||
// 听歌打卡
|
// 听歌打卡
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
const { APP_CONF } = require('../util/config.json')
|
||||||
const data = {
|
const DOMAIN = APP_CONF.clDomian
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
// 注入 os=osx 的 cookie
|
||||||
|
let cookie = query.cookie || ''
|
||||||
|
if (typeof cookie === 'object') {
|
||||||
|
cookie = Object.assign({ os: 'osx' }, cookie)
|
||||||
|
} else if (typeof cookie === 'string') {
|
||||||
|
if (cookie.indexOf('os=') > -1) {
|
||||||
|
cookie = cookie.replace(/os=[^;]+/g, 'os=osx')
|
||||||
|
} else {
|
||||||
|
cookie = cookie + '; os=osx'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cookie = 'os=osx'
|
||||||
|
}
|
||||||
|
query.cookie = cookie
|
||||||
|
|
||||||
|
// 1) startplay → 进「最近播放」
|
||||||
|
const startplayData = {
|
||||||
|
logs: JSON.stringify([
|
||||||
|
{
|
||||||
|
action: 'startplay',
|
||||||
|
json: {
|
||||||
|
id: query.id,
|
||||||
|
type: 'song',
|
||||||
|
mainsite: '1',
|
||||||
|
mainsiteWeb: '1',
|
||||||
|
content: `id=${query.sourceid}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) play → 涨「听歌排行」计数
|
||||||
|
const playData = {
|
||||||
logs: JSON.stringify([
|
logs: JSON.stringify([
|
||||||
{
|
{
|
||||||
action: 'play',
|
action: 'play',
|
||||||
@@ -15,12 +49,30 @@ module.exports = (query, request) => {
|
|||||||
type: 'song',
|
type: 'song',
|
||||||
wifi: 0,
|
wifi: 0,
|
||||||
source: 'list',
|
source: 'list',
|
||||||
mainsite: 1,
|
mainsite: '1',
|
||||||
content: '',
|
mainsiteWeb: '1',
|
||||||
|
content: `id=${query.sourceid}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
|
|
||||||
return request(`/api/feedback/weblog`, data, createOption(query, 'weapi'))
|
const option = createOption(query, 'eapi')
|
||||||
|
option.domain = DOMAIN
|
||||||
|
|
||||||
|
// 发送两次请求
|
||||||
|
const res1 = await request(`/api/feedback/weblog`, startplayData, option)
|
||||||
|
const res2 = await request(`/api/feedback/weblog`, playData, option)
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
data: 'success',
|
||||||
|
details: {
|
||||||
|
startplay: res1.body,
|
||||||
|
play: res2.body,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
// 听歌打卡 - NCBL 加密版 (仿桌面客户端 PLV/PLD 上报)
|
||||||
|
// 复制自 https://github.com/folltoshe/netease-report-listen-song 的 PC 端日志上报方式
|
||||||
|
//
|
||||||
|
// PLV 和 PLD 分两次独立上传
|
||||||
|
|
||||||
|
const {
|
||||||
|
buildPlv,
|
||||||
|
buildPld,
|
||||||
|
buildRecords,
|
||||||
|
extractContext,
|
||||||
|
parseCookie,
|
||||||
|
buildCookieStr,
|
||||||
|
buildMetaJson,
|
||||||
|
doUpload,
|
||||||
|
} = require('../util/ncbl')
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
// --- 参数校验 ---
|
||||||
|
const songId = Number(query.id)
|
||||||
|
if (!songId || isNaN(songId)) {
|
||||||
|
return { status: 400, body: { code: 400, msg: '缺少有效的 id (歌曲ID)' } }
|
||||||
|
}
|
||||||
|
const playTime = Number(query.time)
|
||||||
|
if (isNaN(playTime) || playTime <= 0) {
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
body: { code: 400, msg: '缺少有效的 time (播放时长)' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const totalTime = Number(query.total) || playTime
|
||||||
|
const sourceId = String(query.sourceid || query.sourceId || '')
|
||||||
|
const sourceName = query.source || 'list'
|
||||||
|
|
||||||
|
// --- 解析认证上下文 ---
|
||||||
|
const rawCookie = query.cookie || ''
|
||||||
|
const cookieObj = parseCookie(rawCookie)
|
||||||
|
cookieObj.os = 'pc'
|
||||||
|
const ctx = extractContext(cookieObj)
|
||||||
|
|
||||||
|
// 兜底取 token
|
||||||
|
if (!ctx.auth.token && rawCookie) {
|
||||||
|
const parsed =
|
||||||
|
typeof rawCookie === 'string' ? parseCookie(rawCookie) : rawCookie
|
||||||
|
ctx.auth.token = parsed.MUSIC_U || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.auth.token) {
|
||||||
|
return { status: 401, body: { code: 401, msg: '缺少 MUSIC_U 鉴权令牌' } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 构建歌曲和来源 ---
|
||||||
|
const song = {
|
||||||
|
id: songId,
|
||||||
|
name: query.name || '',
|
||||||
|
artist: query.artist || '',
|
||||||
|
bitrate: Number(query.bitrate) || 320,
|
||||||
|
level: query.level || 'exhigh',
|
||||||
|
vip: query.vip === 'true' || query.vip === true,
|
||||||
|
time: totalTime,
|
||||||
|
}
|
||||||
|
const source = {
|
||||||
|
id: sourceId || String(songId),
|
||||||
|
type: 'track',
|
||||||
|
name: sourceName,
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaJson = buildMetaJson(ctx)
|
||||||
|
const cookieStr = buildCookieStr(ctx)
|
||||||
|
|
||||||
|
const ts = Math.floor(Date.now() / 1000)
|
||||||
|
const played = Math.min(playTime, totalTime)
|
||||||
|
|
||||||
|
const plvBody = buildRecords([
|
||||||
|
{ time: ts, action: '_plv', data: buildPlv(ctx, song, source) },
|
||||||
|
])
|
||||||
|
const pldBody = buildRecords([
|
||||||
|
{ time: ts, action: '_pld', data: buildPld(ctx, song, source, played) },
|
||||||
|
])
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1) 上传 PLV
|
||||||
|
const plv = await doUpload(ctx, metaJson, plvBody, cookieStr, 'PLV')
|
||||||
|
if (!plv.success) {
|
||||||
|
const rateMsg =
|
||||||
|
plv.respBody?.data?.rate != null
|
||||||
|
? ` (rate=${plv.respBody.data.rate})`
|
||||||
|
: ''
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: plv.respBody?.code || -1,
|
||||||
|
msg: `PLV 上报失败${rateMsg}`,
|
||||||
|
details: plv.respBody,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 上传 PLD
|
||||||
|
const pld = await doUpload(ctx, metaJson, pldBody, cookieStr, 'PLD')
|
||||||
|
if (!pld.success) {
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: pld.respBody?.code || -1,
|
||||||
|
msg: 'PLV 成功但 PLD 失败',
|
||||||
|
details: { plv: plv.respBody, pld: pld.respBody },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
data: 'scrobble_v1 上报成功',
|
||||||
|
details: {
|
||||||
|
plv: { fileName: plv.fileName, payloadSize: plv.payload.length },
|
||||||
|
pld: { fileName: pld.fileName, payloadSize: pld.payload.length },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
status: 502,
|
||||||
|
body: { code: 502, msg: `请求异常: ${err.message || err}` },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 搜索建议pc端
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
keyword: query.keyword || '',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/search/pc/suggest/keyword/get`,
|
||||||
|
data,
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// 从云盘获取歌曲下载链接
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
songId: query.id,
|
||||||
|
}
|
||||||
|
return request(`/api/cloud/dowonload`, data, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// 灰色歌曲的其他版本推荐
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
songid: query.songid || query.id,
|
||||||
|
}
|
||||||
|
return request(`/api/song/copyright/rcmd`, data, createOption(query, 'eapi'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// 喜欢歌曲
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const like = query.like !== 'false'
|
||||||
|
const data = {
|
||||||
|
trackId: query.id,
|
||||||
|
userid: query.uid,
|
||||||
|
like: like,
|
||||||
|
}
|
||||||
|
return request(`/api/song/like`, data, createOption(query))
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// 网易云歌曲解灰(适配SPlayer的UNM-Server)
|
||||||
|
// 支持qq音乐、酷狗音乐、酷我音乐、咪咕音乐、第三方网易云API等等(来自GD音乐台)
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
const logger = require('../util/logger.js')
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
matchID,
|
||||||
|
} = require('@neteasecloudmusicapienhanced/unblockmusic-utils')
|
||||||
|
const result = await matchID(query.id, query.source)
|
||||||
|
const proxy = process.env.PROXY_URL
|
||||||
|
logger.info('开始解灰', query.id, result)
|
||||||
|
const useProxy = process.env.ENABLE_PROXY || 'false'
|
||||||
|
if (result.data.url && result.data.url.includes('kuwo')) {
|
||||||
|
result.proxyUrl =
|
||||||
|
useProxy === 'true' ? proxy + result.data.url : result.data.url
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
data: result.data.url,
|
||||||
|
proxyUrl: result.proxyUrl || '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
code: 500,
|
||||||
|
msg: e.message || 'unblock error',
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// 夹带私货的东西就不要放在这里了
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
return { status: 200, body: { code: 200, data: [] } }
|
||||||
|
}
|
||||||
@@ -1,16 +1,61 @@
|
|||||||
// 歌曲链接 - v1
|
// 歌曲链接 - v1
|
||||||
// 此版本不再采用 br 作为音质区分的标准
|
// 此版本不再采用 br 作为音质区分的标准
|
||||||
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
|
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
|
||||||
|
// 当unblock为true时, 会尝试使用unblockmusic-utils进行解锁, 同时音质设置不会生效, 但仍然为必须传入参数
|
||||||
|
|
||||||
|
const logger = require('../util/logger.js')
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
|
const {
|
||||||
|
matchID,
|
||||||
|
} = require('@neteasecloudmusicapienhanced/unblockmusic-utils')
|
||||||
|
require('dotenv').config()
|
||||||
const data = {
|
const data = {
|
||||||
ids: '[' + query.id + ']',
|
ids: '[' + query.id + ']',
|
||||||
level: query.level,
|
level: query.level,
|
||||||
encodeType: 'flac',
|
encodeType: 'flac',
|
||||||
}
|
}
|
||||||
|
if (query.unblock === 'true') {
|
||||||
|
try {
|
||||||
|
const result = await matchID(query.id, query.source)
|
||||||
|
logger.info('Starting unblock(uses modules unblock):', query.id, result)
|
||||||
|
const useProxy = process.env.ENABLE_PROXY || 'false'
|
||||||
|
let proxyUrl = ''
|
||||||
|
if (result.data.url && result.data.url.includes('kuwo')) {
|
||||||
|
proxyUrl =
|
||||||
|
useProxy === 'true' && process.env.PROXY_URL
|
||||||
|
? process.env.PROXY_URL + result.data.url
|
||||||
|
: result.data.url
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
msg: 'Warning: Customizing unblock sources is not supported on this endpoint. Please use `/song/url/match` instead.',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: Number(query.id),
|
||||||
|
url: result.data.url,
|
||||||
|
type: 'flac',
|
||||||
|
level: query.level,
|
||||||
|
freeTrialInfo: 'null',
|
||||||
|
fee: 0,
|
||||||
|
proxyUrl: proxyUrl || '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
cookie: [],
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error in unblocking music:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (data.level == 'sky') {
|
if (data.level == 'sky') {
|
||||||
data.immerseType = 'c51'
|
data.immerseType = 'c51'
|
||||||
}
|
}
|
||||||
return request(`/api/song/enhance/player/url/v1`, data, createOption(query))
|
return request(
|
||||||
|
`/api/song/enhance/player/url/v1`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// 获取客户端歌曲下载链接 - v1
|
||||||
|
// 此版本不再采用 br 作为音质区分的标准
|
||||||
|
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const data = {
|
||||||
|
id: query.id,
|
||||||
|
immerseType: 'c51',
|
||||||
|
level: query.level,
|
||||||
|
}
|
||||||
|
const response = await request(
|
||||||
|
`/api/song/enhance/download/url/v1`,
|
||||||
|
data,
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
let url = response?.body?.data?.[0]?.url
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
const fallbackData = {
|
||||||
|
ids: `[${query.id}]`,
|
||||||
|
level: query.level,
|
||||||
|
encodeType: 'flac',
|
||||||
|
}
|
||||||
|
if (query.level === 'sky') {
|
||||||
|
fallbackData.immerseType = 'c51'
|
||||||
|
}
|
||||||
|
const fallback = await request(
|
||||||
|
`/api/song/enhance/player/url/v1`,
|
||||||
|
fallbackData,
|
||||||
|
createOption(query),
|
||||||
|
)
|
||||||
|
url = fallback?.body?.data?.[0]?.url
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 302,
|
||||||
|
body: '',
|
||||||
|
cookie: fallback.cookie || [],
|
||||||
|
redirectUrl: url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 302,
|
||||||
|
body: '',
|
||||||
|
cookie: response.cookie || [],
|
||||||
|
redirectUrl: url,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ module.exports = (query, request) => {
|
|||||||
const data = {
|
const data = {
|
||||||
userId: query.uid,
|
userId: query.uid,
|
||||||
time: '0',
|
time: '0',
|
||||||
limit: query.limit || 30,
|
limit: query.limit || 20,
|
||||||
offset: query.offset || 0,
|
offset: query.offset || 0,
|
||||||
getcounts: 'true',
|
getcounts: 'true',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 一键领取所有会员成长值
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
return request(
|
||||||
|
`/api/vipnewcenter/app/level/task/reward/getall`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// 黑胶乐签打卡
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const results = {}
|
||||||
|
|
||||||
|
const taskSign = await request(
|
||||||
|
'/api/vip-center-bff/task/sign',
|
||||||
|
{},
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
results.taskSign = taskSign.body
|
||||||
|
|
||||||
|
const checkinDetail = await request(
|
||||||
|
'/api/vipnewcenter/app/level/user/checkin/history/detail',
|
||||||
|
{
|
||||||
|
signDayTime: Date.now(),
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
results.checkinDetail = checkinDetail.body
|
||||||
|
|
||||||
|
// 两个接口都返回 code=200 即视为打卡成功
|
||||||
|
const signed =
|
||||||
|
Number(results.taskSign?.code) === 200 &&
|
||||||
|
Number(results.checkinDetail?.code) === 200
|
||||||
|
|
||||||
|
return {
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
...results,
|
||||||
|
signed,
|
||||||
|
message: signed ? '黑胶乐签打卡成功' : '黑胶乐签打卡失败',
|
||||||
|
},
|
||||||
|
cookie: taskSign.cookie,
|
||||||
|
status: 200,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 黑胶乐签打卡详情
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
signDayTime: query.timestamp,
|
||||||
|
type: '1',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/vipnewcenter/app/level/user/checkin/history/detail`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 黑胶乐签打卡历史 / 状态查询
|
||||||
|
// 支持传入 type=0(用户信息栏)或 type=1(黑胶乐签)
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
type: query.type || '0',
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/vipnewcenter/app/minidesk/music/sign/pc`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'eapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 黑胶乐签未来签到信息
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {}
|
||||||
|
return request(
|
||||||
|
`/api/vipnewcenter/app/user/sign/info`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 会员任务 - 新版
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
taskType: 'app_vip_task_center',
|
||||||
|
userId: query.id,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/middle/vip/mission/user/progress/list`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'xeapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,25 +1,26 @@
|
|||||||
const { default: axios } = require('axios')
|
const { default: axios } = require('axios')
|
||||||
|
const fs = require('fs')
|
||||||
var xml2js = require('xml2js')
|
var xml2js = require('xml2js')
|
||||||
|
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
var parser = new xml2js.Parser(/* options */)
|
const { getFileExtension, readFileChunk } = require('../util/fileHelper')
|
||||||
|
|
||||||
|
var parser = new xml2js.Parser()
|
||||||
|
|
||||||
function createDupkey() {
|
function createDupkey() {
|
||||||
// 格式:3b443c7c-a87f-468d-ba38-46d407aaf23a
|
|
||||||
var s = []
|
var s = []
|
||||||
var hexDigits = '0123456789abcdef'
|
var hexDigits = '0123456789abcdef'
|
||||||
for (var i = 0; i < 36; i++) {
|
for (var i = 0; i < 36; i++) {
|
||||||
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1)
|
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1)
|
||||||
}
|
}
|
||||||
s[14] = '4' // bits 12-15 of the time_hi_and_version field to 0010
|
s[14] = '4'
|
||||||
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) // bits 6-7 of the clock_seq_hi_and_reserved to 01
|
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1)
|
||||||
s[8] = s[13] = s[18] = s[23] = '-'
|
s[8] = s[13] = s[18] = s[23] = '-'
|
||||||
return s.join('')
|
return s.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
let ext = 'mp3'
|
const ext = getFileExtension(query.songFile.name)
|
||||||
if (query.songFile.name.indexOf('flac') > -1) {
|
|
||||||
ext = 'flac'
|
|
||||||
}
|
|
||||||
const filename =
|
const filename =
|
||||||
query.songName ||
|
query.songName ||
|
||||||
query.songFile.name
|
query.songFile.name
|
||||||
@@ -50,43 +51,58 @@ module.exports = async (query, request) => {
|
|||||||
createOption(query, 'weapi'),
|
createOption(query, 'weapi'),
|
||||||
)
|
)
|
||||||
|
|
||||||
const objectKey = tokenRes.body.result.objectKey.replace('/', '%2F')
|
const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F')
|
||||||
const docId = tokenRes.body.result.docId
|
const docId = tokenRes.body.result.docId
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploads`,
|
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploads`,
|
||||||
headers: {
|
headers: {
|
||||||
'x-nos-token': tokenRes.body.result.token,
|
'x-nos-token': tokenRes.body.result.token,
|
||||||
'X-Nos-Meta-Content-Type': 'audio/mpeg',
|
'X-Nos-Meta-Content-Type': query.songFile.mimetype || 'audio/mpeg',
|
||||||
},
|
},
|
||||||
data: null,
|
data: null,
|
||||||
})
|
})
|
||||||
// return xml
|
|
||||||
const res2 = await parser.parseStringPromise(res.data)
|
const res2 = await parser.parseStringPromise(res.data)
|
||||||
|
|
||||||
const fileSize = query.songFile.data.length
|
const useTempFile = !!query.songFile.tempFilePath
|
||||||
const blockSize = 10 * 1024 * 1024 // 10MB
|
let fileSize = query.songFile.size
|
||||||
|
|
||||||
|
if (useTempFile) {
|
||||||
|
const stats = await fs.promises.stat(query.songFile.tempFilePath)
|
||||||
|
fileSize = stats.size
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockSize = 10 * 1024 * 1024
|
||||||
let offset = 0
|
let offset = 0
|
||||||
let blockIndex = 1
|
let blockIndex = 1
|
||||||
|
|
||||||
let etags = []
|
let etags = []
|
||||||
|
|
||||||
while (offset < fileSize) {
|
while (offset < fileSize) {
|
||||||
const chunk = query.songFile.data.slice(
|
let chunk
|
||||||
offset,
|
if (useTempFile) {
|
||||||
Math.min(offset + blockSize, fileSize),
|
chunk = await readFileChunk(
|
||||||
)
|
query.songFile.tempFilePath,
|
||||||
|
offset,
|
||||||
|
Math.min(blockSize, fileSize - offset),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
chunk = query.songFile.data.slice(
|
||||||
|
offset,
|
||||||
|
Math.min(offset + blockSize, fileSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const res3 = await axios({
|
const res3 = await axios({
|
||||||
method: 'put',
|
method: 'put',
|
||||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?partNumber=${blockIndex}&uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?partNumber=${blockIndex}&uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
||||||
headers: {
|
headers: {
|
||||||
'x-nos-token': tokenRes.body.result.token,
|
'x-nos-token': tokenRes.body.result.token,
|
||||||
'Content-Type': 'audio/mpeg',
|
'Content-Type': query.songFile.mimetype || 'audio/mpeg',
|
||||||
},
|
},
|
||||||
data: chunk,
|
data: chunk,
|
||||||
})
|
})
|
||||||
// get etag
|
|
||||||
const etag = res3.headers.etag
|
const etag = res3.headers.etag
|
||||||
etags.push(etag)
|
etags.push(etag)
|
||||||
offset += blockSize
|
offset += blockSize
|
||||||
@@ -101,19 +117,17 @@ module.exports = async (query, request) => {
|
|||||||
}
|
}
|
||||||
completeStr += '</CompleteMultipartUpload>'
|
completeStr += '</CompleteMultipartUpload>'
|
||||||
|
|
||||||
// 文件处理
|
|
||||||
await axios({
|
await axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/plain;charset=UTF-8',
|
'Content-Type': 'text/plain;charset=UTF-8',
|
||||||
'X-Nos-Meta-Content-Type': 'audio/mpeg',
|
'X-Nos-Meta-Content-Type': query.songFile.mimetype || 'audio/mpeg',
|
||||||
'x-nos-token': tokenRes.body.result.token,
|
'x-nos-token': tokenRes.body.result.token,
|
||||||
},
|
},
|
||||||
data: completeStr,
|
data: completeStr,
|
||||||
})
|
})
|
||||||
|
|
||||||
// preCheck
|
|
||||||
await request(
|
await request(
|
||||||
`/api/voice/workbench/voice/batch/upload/preCheck`,
|
`/api/voice/workbench/voice/batch/upload/preCheck`,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 我创建的播客声音
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const data = {
|
||||||
|
limit: query.limit || 20,
|
||||||
|
}
|
||||||
|
return request(
|
||||||
|
`/api/social/my/created/voicelist/v1`,
|
||||||
|
data,
|
||||||
|
createOption(query, 'weapi'),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {
|
const data = {
|
||||||
fee: '-1',
|
keyword: query.keyword || '',
|
||||||
limit: query.limit || '200',
|
scene: 'normal',
|
||||||
offset: query.offset || '0',
|
limit: query.limit || '10',
|
||||||
podcastName: query.podcastName || '',
|
offset: query.offset || '30',
|
||||||
|
e_r: true,
|
||||||
}
|
}
|
||||||
return request(
|
return request(`/api/search/voicelist/get`, data, createOption(query))
|
||||||
`/api/voice/workbench/voicelist/search`,
|
|
||||||
data,
|
|
||||||
createOption(query),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const { eapiResDecrypt, eapiReqDecrypt } = require('../util/crypto')
|
const { eapiResDecrypt, eapiReqDecrypt } = require('../util/crypto')
|
||||||
|
const logger = require('../util/logger.js')
|
||||||
|
|
||||||
let reqParamsHex =
|
let reqParamsHex =
|
||||||
'6E0B1C712DCB3648589D7C950C296204072A88C3E080C4CFFD0A71A553EC2533BA88E11B1E1C6DF3BE8EFA26177FCB6FCA34EB3FAFAB4671B2BBAFA9781AFDA2BF53A3DC423722493837B9BC6E80CED5BBD2DDC2856920E4D4E3E7F3EB77ECF265217A66AE677BE36F2D6FB203F721CA250E1453EA61A34904E33D5FCB9D483601D744BE0AE979AC911A00F25828538844F4B1C24F6C34880A4AB257F530C7FB10A81FED32B18D09D70C0B1B9D34A2E58A3C3FAD382C6F958077059C4F801AD7B3B248FDB9D7A59B6A9EEFF8C781A84315B33A7AFD48BE9FCFCBE1902CCC27949ACF2BDE3FA34D116E230C3597E8320B8C42BBBF371A00C03EC428E0440EB94C1540F3FD4173D29E310AFE43AB0EF449852904103EF305FC435AD43B7D8673642F74C89CCB2F1A6A79B3BE14F1235D3843C3B241D12C05DBDDF37B68CA8B5D0230AF1FCF2A9705886F4D126B33FFF6948DE1E4046DB6423D687E96C5B65122464D2E71AEC7722935FF2C3796FAE253A16AA3B102FBE7296AB0DB9EA5C46AD12B'.replaceAll(
|
'6E0B1C712DCB3648589D7C950C296204072A88C3E080C4CFFD0A71A553EC2533BA88E11B1E1C6DF3BE8EFA26177FCB6FCA34EB3FAFAB4671B2BBAFA9781AFDA2BF53A3DC423722493837B9BC6E80CED5BBD2DDC2856920E4D4E3E7F3EB77ECF265217A66AE677BE36F2D6FB203F721CA250E1453EA61A34904E33D5FCB9D483601D744BE0AE979AC911A00F25828538844F4B1C24F6C34880A4AB257F530C7FB10A81FED32B18D09D70C0B1B9D34A2E58A3C3FAD382C6F958077059C4F801AD7B3B248FDB9D7A59B6A9EEFF8C781A84315B33A7AFD48BE9FCFCBE1902CCC27949ACF2BDE3FA34D116E230C3597E8320B8C42BBBF371A00C03EC428E0440EB94C1540F3FD4173D29E310AFE43AB0EF449852904103EF305FC435AD43B7D8673642F74C89CCB2F1A6A79B3BE14F1235D3843C3B241D12C05DBDDF37B68CA8B5D0230AF1FCF2A9705886F4D126B33FFF6948DE1E4046DB6423D687E96C5B65122464D2E71AEC7722935FF2C3796FAE253A16AA3B102FBE7296AB0DB9EA5C46AD12B'.replaceAll(
|
||||||
@@ -10,6 +11,6 @@ const resHex =
|
|||||||
' ',
|
' ',
|
||||||
'',
|
'',
|
||||||
)
|
)
|
||||||
console.log(reqParamsHex)
|
logger.info(reqParamsHex)
|
||||||
console.log(eapiReqDecrypt(reqParamsHex))
|
logger.info(eapiReqDecrypt(reqParamsHex))
|
||||||
console.log(eapiResDecrypt(resHex))
|
logger.info(eapiResDecrypt(resHex))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const { cloud, login_cellphone } = require('../main')
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const yargs = require('yargs')
|
const yargs = require('yargs')
|
||||||
|
const logger = require('../util/logger.js')
|
||||||
|
|
||||||
const MUSIC_FILE_EXTENSIONS = new Set(['.mp3', '.flac'])
|
const MUSIC_FILE_EXTENSIONS = new Set(['.mp3', '.flac'])
|
||||||
|
|
||||||
@@ -37,11 +38,11 @@ async function uploadArrayOfFile(token, arrayOfFiles) {
|
|||||||
cookie: token.body.cookie,
|
cookie: token.body.cookie,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
logger.info(error)
|
||||||
failed += 1
|
failed += 1
|
||||||
failedFiles.push(file)
|
failedFiles.push(file)
|
||||||
}
|
}
|
||||||
console.log(`Uploaded ${k + 1}/${fileCount} songs`)
|
logger.info(`Uploaded ${k + 1}/${fileCount} songs`)
|
||||||
}
|
}
|
||||||
return { failedFiles, failed }
|
return { failedFiles, failed }
|
||||||
}
|
}
|
||||||
@@ -86,19 +87,19 @@ async function main() {
|
|||||||
const files = args.file ? [args.file] : getAllMusicFiles(args.dir)
|
const files = args.file ? [args.file] : getAllMusicFiles(args.dir)
|
||||||
const fileCount = files.length
|
const fileCount = files.length
|
||||||
|
|
||||||
console.log(`Found ${fileCount} files, uploading...`)
|
logger.info(`Found ${fileCount} files, uploading...`)
|
||||||
let res = await uploadArrayOfFile(token, files)
|
let res = await uploadArrayOfFile(token, files)
|
||||||
if (res.failed) {
|
if (res.failed) {
|
||||||
console.log(`Failed to upload ${res.failed} songs, retrying...`)
|
logger.info(`Failed to upload ${res.failed} songs, retrying...`)
|
||||||
res = await uploadArrayOfFile(token, res.failedFiles)
|
res = await uploadArrayOfFile(token, res.failedFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Uploaded ${fileCount - res.failed} songs`)
|
logger.info(`Uploaded ${fileCount - res.failed} songs`)
|
||||||
console.log(
|
logger.info(
|
||||||
`Failed to upload ${res.failed} songs, you can reupload the files below`,
|
`Failed to upload ${res.failed} songs, you can reupload the files below`,
|
||||||
)
|
)
|
||||||
for (let k in res.failedFiles) {
|
for (let k in res.failedFiles) {
|
||||||
console.log(res.failedFiles[k])
|
logger.info(res.failedFiles[k])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const { cloud, login_cellphone } = require('../main')
|
const { cloud, login_cellphone } = require('../main')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
const logger = require('../util/logger.js')
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const result = await login_cellphone({
|
const result = await login_cellphone({
|
||||||
@@ -17,7 +18,7 @@ async function main() {
|
|||||||
cookie: result.body.cookie,
|
cookie: result.body.cookie,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error, 'error')
|
logger.info(error, 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const logger = require('../util/logger.js')
|
||||||
const {
|
const {
|
||||||
login_cellphone,
|
login_cellphone,
|
||||||
user_cloud,
|
user_cloud,
|
||||||
@@ -10,22 +11,22 @@ async function test() {
|
|||||||
phone: '手机号',
|
phone: '手机号',
|
||||||
password: '密码',
|
password: '密码',
|
||||||
})
|
})
|
||||||
console.log(result)
|
logger.info(result)
|
||||||
const result2 = await user_cloud({
|
const result2 = await user_cloud({
|
||||||
cookie: result.body.cookie,
|
cookie: result.body.cookie,
|
||||||
})
|
})
|
||||||
console.log(result2.body)
|
logger.info(result2.body)
|
||||||
const result3 = await album_sublist({
|
const result3 = await album_sublist({
|
||||||
cookie: result.body.cookie,
|
cookie: result.body.cookie,
|
||||||
})
|
})
|
||||||
console.log(result3.body)
|
logger.info(result3.body)
|
||||||
const result4 = await song_url({
|
const result4 = await song_url({
|
||||||
cookie: result.body.cookie,
|
cookie: result.body.cookie,
|
||||||
id: 33894312,
|
id: 33894312,
|
||||||
})
|
})
|
||||||
console.log(result4.body)
|
logger.info(result4.body)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
logger.info(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
test()
|
test()
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { banner, lyric } from 'NeteaseCloudMusicApi'
|
import { banner, lyric } from '@neteasecloudmusicapienhanced/api'
|
||||||
|
import { logger } from '../util/logger.js'
|
||||||
banner({ type: 0 }).then((res) => {
|
banner({ type: 0 }).then((res) => {
|
||||||
console.log(res)
|
logger.info(res)
|
||||||
})
|
})
|
||||||
lyric({
|
lyric({
|
||||||
id: '33894312',
|
id: '33894312',
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
console.log(res)
|
logger.info(res)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "NeteaseCloudMusicApi",
|
"name": "@neteasecloudmusicapienhanced/api",
|
||||||
"version": "4.32.0",
|
"version": "4.36.2",
|
||||||
"description": "网易云音乐 NodeJS 版 API",
|
"description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"dev": "nodemon app.js",
|
||||||
"start": "node app.js",
|
"start": "node app.js",
|
||||||
"test": "mocha -r intelli-espower-loader -t 60000 server.test.js main.test.js --exit",
|
"test": "mocha -r intelli-espower-loader -t 60000 server.test.js main.test.js --exit",
|
||||||
"lint": "eslint \"**/*.{js,ts}\"",
|
"lint": "eslint \"**/*.{js,ts}\"",
|
||||||
"lint-fix": "eslint --fix \"**/*.{js,ts}\"",
|
"lint-fix": "eslint --fix \"**/*.{js,ts}\"",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"pkgwin": "pkg . -t node18-win-x64 -C GZip -o bin/app --no-bytecode",
|
"docs:format": "node scripts/format-docs.js",
|
||||||
"pkglinux": "pkg . -t node18-linux-x64 -C GZip -o bin/app --no-bytecode",
|
"docs:check": "node scripts/format-docs.js --check",
|
||||||
"pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o bin/app --no-bytecode"
|
"pkgwin": "pkg . -t node18-win-x64 -C GZip -o precompiled/app",
|
||||||
|
"pkglinux": "pkg . -t node18-linux-x64 -C GZip -o precompiled/app",
|
||||||
|
"pkgmacos": "pkg . -t node18-macos-x64 -C GZip -o precompiled/app"
|
||||||
},
|
},
|
||||||
"bin": "./app.js",
|
"bin": "./app.js",
|
||||||
"pkg": {
|
"pkg": {
|
||||||
@@ -19,7 +22,6 @@
|
|||||||
"node_modules/axios",
|
"node_modules/axios",
|
||||||
"node_modules/express",
|
"node_modules/express",
|
||||||
"node_modules/express-fileupload",
|
"node_modules/express-fileupload",
|
||||||
"node_modules/md5",
|
|
||||||
"node_modules/music-metadata",
|
"node_modules/music-metadata",
|
||||||
"node_modules/pac-proxy-agent",
|
"node_modules/pac-proxy-agent",
|
||||||
"node_modules/qrcode",
|
"node_modules/qrcode",
|
||||||
@@ -41,6 +43,14 @@
|
|||||||
],
|
],
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"types": "./interface.d.ts",
|
"types": "./interface.d.ts",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://neteasecloudmusicapienhanced.js.org/",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -50,7 +60,7 @@
|
|||||||
"git add"
|
"git add"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"author": "binaryify",
|
"author": "MoeFurina",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"files": [
|
"files": [
|
||||||
"module",
|
"module",
|
||||||
@@ -65,38 +75,48 @@
|
|||||||
"data"
|
"data"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.6",
|
"@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.3.4",
|
||||||
|
"axios": "^1.18.1",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"express": "^4.22.1",
|
"dotenv": "^17.4.2",
|
||||||
|
"express": "^5.2.1",
|
||||||
"express-fileupload": "^1.5.2",
|
"express-fileupload": "^1.5.2",
|
||||||
"md5": "^2.3.0",
|
"gzip": "^0.1.0",
|
||||||
"music-metadata": "^7.14.0",
|
"music-metadata": "^11.13.0",
|
||||||
"node-forge": "^1.3.3",
|
"node-forge": "^1.4.0",
|
||||||
"pac-proxy-agent": "^7.2.0",
|
"pac-proxy-agent": "^7.2.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"safe-decode-uri-component": "^1.2.1",
|
"safe-decode-uri-component": "^1.2.1",
|
||||||
"tunnel": "^0.0.6",
|
"tunnel": "^0.0.6",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^18.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.25",
|
"@eslint/eslintrc": "^3.3.5",
|
||||||
|
"@eslint/js": "^9.39.4",
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
"@types/express-fileupload": "^1.5.1",
|
"@types/express-fileupload": "^1.5.1",
|
||||||
"@types/mocha": "^9.1.1",
|
"@types/mocha": "^10.0.10",
|
||||||
"@types/node": "16.11.19",
|
"@types/node": "25.9.2",
|
||||||
"@typescript-eslint/eslint-plugin": "5.0.0",
|
"@typescript-eslint/eslint-plugin": "^8.62.1",
|
||||||
"@typescript-eslint/parser": "5.0.0",
|
"@typescript-eslint/parser": "^8.62.1",
|
||||||
"eslint": "8.7.0",
|
"eslint": "^9.39.4",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-html": "7.1.0",
|
"eslint-plugin-html": "^8.1.4",
|
||||||
"eslint-plugin-prettier": "4.0.0",
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
"husky": "7.0.4",
|
"globals": "^17.7.0",
|
||||||
"intelli-espower-loader": "1.1.0",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "12.1.7",
|
"intelli-espower-loader": "^1.1.0",
|
||||||
"mocha": "10.0.0",
|
"lint-staged": "^16.4.0",
|
||||||
|
"mocha": "^11.7.6",
|
||||||
|
"nodemon": "^3.1.14",
|
||||||
"pkg": "^5.8.1",
|
"pkg": "^5.8.1",
|
||||||
"power-assert": "1.6.1",
|
"power-assert": "^1.6.1",
|
||||||
"prettier": "2.7.1",
|
"prettier": "^3.9.4",
|
||||||
"typescript": "4.5.2"
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
const { default: axios } = require('axios')
|
const { default: axios } = require('axios')
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
|
const logger = require('../util/logger.js')
|
||||||
|
const {
|
||||||
|
getUploadData,
|
||||||
|
getFileExtension,
|
||||||
|
sanitizeFilename,
|
||||||
|
} = require('../util/fileHelper')
|
||||||
|
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
let ext = 'mp3'
|
const ext = getFileExtension(query.songFile.name)
|
||||||
// if (query.songFile.name.indexOf('flac') > -1) {
|
const filename = sanitizeFilename(query.songFile.name)
|
||||||
// ext = 'flac'
|
|
||||||
// }
|
|
||||||
if (query.songFile.name.includes('.')) {
|
|
||||||
ext = query.songFile.name.split('.').pop()
|
|
||||||
}
|
|
||||||
const filename = query.songFile.name
|
|
||||||
.replace('.' + ext, '')
|
|
||||||
.replace(/\s/g, '')
|
|
||||||
.replace(/\./g, '_')
|
|
||||||
const bucket = 'jd-musicrep-privatecloud-audio-public'
|
const bucket = 'jd-musicrep-privatecloud-audio-public'
|
||||||
// 获取key和token
|
|
||||||
const tokenRes = await request(
|
const tokenRes = await request(
|
||||||
`/api/nos/token/alloc`,
|
`/api/nos/token/alloc`,
|
||||||
{
|
{
|
||||||
@@ -28,31 +26,82 @@ module.exports = async (query, request) => {
|
|||||||
createOption(query, 'weapi'),
|
createOption(query, 'weapi'),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 上传
|
if (!tokenRes.body.result || !tokenRes.body.result.objectKey) {
|
||||||
const objectKey = tokenRes.body.result.objectKey.replace('/', '%2F')
|
logger.error('Token分配失败:', tokenRes.body)
|
||||||
|
throw {
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
code: 500,
|
||||||
|
msg: '获取上传token失败',
|
||||||
|
detail: tokenRes.body,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F')
|
||||||
|
let lbs
|
||||||
try {
|
try {
|
||||||
const lbs = (
|
lbs = (
|
||||||
await axios({
|
await axios({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: `https://wanproxy.127.net/lbs?version=1.0&bucketname=${bucket}`,
|
url: `https://wanproxy.127.net/lbs?version=1.0&bucketname=${bucket}`,
|
||||||
|
timeout: 10000,
|
||||||
})
|
})
|
||||||
).data
|
).data
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('LBS获取失败:', error.message)
|
||||||
|
throw {
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
code: 500,
|
||||||
|
msg: '获取上传服务器地址失败',
|
||||||
|
detail: error.message,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lbs || !lbs.upload || !lbs.upload[0]) {
|
||||||
|
logger.error('无效的LBS响应:', lbs)
|
||||||
|
throw {
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
code: 500,
|
||||||
|
msg: '获取上传服务器地址无效',
|
||||||
|
detail: lbs,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await axios({
|
await axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `${lbs.upload[0]}/${bucket}/${objectKey}?offset=0&complete=true&version=1.0`,
|
url: `${lbs.upload[0]}/${bucket}/${objectKey}?offset=0&complete=true&version=1.0`,
|
||||||
headers: {
|
headers: {
|
||||||
'x-nos-token': tokenRes.body.result.token,
|
'x-nos-token': tokenRes.body.result.token,
|
||||||
'Content-MD5': query.songFile.md5,
|
'Content-MD5': query.songFile.md5,
|
||||||
'Content-Type': 'audio/mpeg',
|
'Content-Type': query.songFile.mimetype || 'audio/mpeg',
|
||||||
'Content-Length': String(query.songFile.size),
|
'Content-Length': String(query.songFile.size),
|
||||||
},
|
},
|
||||||
data: query.songFile.data,
|
data: getUploadData(query.songFile),
|
||||||
maxContentLength: Infinity,
|
maxContentLength: Infinity,
|
||||||
maxBodyLength: Infinity,
|
maxBodyLength: Infinity,
|
||||||
|
timeout: 300000,
|
||||||
})
|
})
|
||||||
|
logger.info('上传成功:', filename)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error', error.response)
|
logger.error('上传失败:', {
|
||||||
throw error.response
|
status: error.response?.status,
|
||||||
|
data: error.response?.data,
|
||||||
|
message: error.message,
|
||||||
|
})
|
||||||
|
throw {
|
||||||
|
status: error.response?.status || 500,
|
||||||
|
body: {
|
||||||
|
code: error.response?.status || 500,
|
||||||
|
msg: '文件上传失败',
|
||||||
|
detail: error.response?.data || error.message,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...tokenRes,
|
...tokenRes,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
const { default: axios } = require('axios')
|
const { default: axios } = require('axios')
|
||||||
const createOption = require('../util/option.js')
|
const createOption = require('../util/option.js')
|
||||||
|
const { getUploadData } = require('../util/fileHelper')
|
||||||
|
|
||||||
module.exports = async (query, request) => {
|
module.exports = async (query, request) => {
|
||||||
const data = {
|
const data = {
|
||||||
bucket: 'yyimgs',
|
bucket: 'yyimgs',
|
||||||
@@ -10,27 +12,23 @@ module.exports = async (query, request) => {
|
|||||||
return_body: `{"code":200,"size":"$(ObjectSize)"}`,
|
return_body: `{"code":200,"size":"$(ObjectSize)"}`,
|
||||||
type: 'other',
|
type: 'other',
|
||||||
}
|
}
|
||||||
// 获取key和token
|
|
||||||
const res = await request(
|
const res = await request(
|
||||||
`/api/nos/token/alloc`,
|
`/api/nos/token/alloc`,
|
||||||
data,
|
data,
|
||||||
createOption(query, 'weapi'),
|
createOption(query, 'weapi'),
|
||||||
)
|
)
|
||||||
// 上传图片
|
|
||||||
const res2 = await axios({
|
const res2 = await axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `https://nosup-hz1.127.net/yyimgs/${res.body.result.objectKey}?offset=0&complete=true&version=1.0`,
|
url: `https://nosup-hz1.127.net/yyimgs/${res.body.result.objectKey}?offset=0&complete=true&version=1.0`,
|
||||||
headers: {
|
headers: {
|
||||||
'x-nos-token': res.body.result.token,
|
'x-nos-token': res.body.result.token,
|
||||||
'Content-Type': 'image/jpeg',
|
'Content-Type': query.imgFile.mimetype || 'image/jpeg',
|
||||||
},
|
},
|
||||||
data: query.imgFile.data,
|
data: getUploadData(query.imgFile),
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// ...res.body.result,
|
|
||||||
// ...res2.data,
|
|
||||||
// ...res3.body,
|
|
||||||
url_pre: 'https://p1.music.126.net/' + res.body.result.objectKey,
|
url_pre: 'https://p1.music.126.net/' + res.body.result.objectKey,
|
||||||
imgId: res.body.result.docId,
|
imgId: res.body.result.docId,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,83 +5,358 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>API 调试界面</title>
|
<title>API 调试界面</title>
|
||||||
<style>
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
margin: 20px;
|
min-height: 100vh;
|
||||||
display: flex;
|
background: #f5f5f5;
|
||||||
flex-direction: column;
|
padding: 20px;
|
||||||
min-height: 100vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
max-width: 1200px;
|
||||||
flex-direction: column;
|
margin: 0 auto;
|
||||||
flex-grow: 1;
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
align-items: center;
|
gap: 16px;
|
||||||
gap: 10px;
|
margin-bottom: 24px;
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
input, button {
|
|
||||||
padding: 10px;
|
.form-row {
|
||||||
box-sizing: border-box;
|
display: flex;
|
||||||
flex: 1;
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
flex: 1;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, select:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background-color: #4CAF50;
|
background: #333;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
padding: 10px 24px;
|
||||||
cursor: pointer;
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
.data-result {
|
.data-result {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
gap: 16px;
|
||||||
flex-grow: 1;
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-result > div {
|
.data-result > div {
|
||||||
display: flex;
|
flex: 1;
|
||||||
flex-direction: column;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-direction: column;
|
||||||
padding: 10px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-result label {
|
.data-result label {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 8px;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
#data, #result {
|
|
||||||
height: 100%;
|
textarea {
|
||||||
box-sizing: border-box;
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 350px;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
#data {
|
|
||||||
border-right: 1px solid #ccc;
|
textarea:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 自定义选项区域 ===== */
|
||||||
|
.section-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-container + .section-container {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title .count-badge {
|
||||||
|
font-size: 11px;
|
||||||
|
background: #ddd;
|
||||||
|
color: #666;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-row .key-input {
|
||||||
|
flex: 0 0 200px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-row .value-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-row .key-input:focus,
|
||||||
|
.dynamic-row .value-input:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
background: #fff;
|
||||||
|
color: #999;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn:hover {
|
||||||
|
background: #fee;
|
||||||
|
color: #e33;
|
||||||
|
border-color: #fcc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
background: #fff;
|
||||||
|
color: #333;
|
||||||
|
border: 1px dashed #ccc;
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn:hover {
|
||||||
|
border-color: #999;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 参数编辑区 Tab ===== */
|
||||||
|
.param-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-header-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
padding: 4px 14px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #888;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:hover {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
background: #fff;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-panel textarea {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 350px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-panel textarea:focus {
|
||||||
|
border-color: #333;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<h1>API 调试界面</h1>
|
||||||
<form onsubmit="event.preventDefault(); sendRequest();">
|
<form onsubmit="event.preventDefault(); sendRequest();">
|
||||||
<label for="uri">uri</label>
|
<div class="form-row">
|
||||||
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
|
<label for="uri">URI</label>
|
||||||
<label for="crypto">crypto</label>
|
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
|
||||||
<select id="crypto" name="crypto">
|
</div>
|
||||||
<option value="weapi">weapi</option>
|
<div class="form-row">
|
||||||
<option value="eapi">eapi</option>
|
<label for="method">请求方法</label>
|
||||||
<option value="api">api</option>
|
<select id="method" name="method">
|
||||||
<option value="linuxapi">linuxapi</option>
|
<option value="POST" selected>POST</option>
|
||||||
<option value="" selected>(默认)</option>
|
<option value="GET">GET</option>
|
||||||
</select>
|
<option value="PUT">PUT</option>
|
||||||
<button type="submit">发送</button>
|
<option value="DELETE">DELETE</option>
|
||||||
|
<option value="PATCH">PATCH</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="crypto">加密方式</label>
|
||||||
|
<select id="crypto" name="crypto">
|
||||||
|
<option value="weapi">weapi</option>
|
||||||
|
<option value="eapi">eapi</option>
|
||||||
|
<option value="api">api</option>
|
||||||
|
<option value="linuxapi">linuxapi</option>
|
||||||
|
<option value="xeapi">xeapi</option>
|
||||||
|
<option value="" selected>(默认)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="timeout">超时(ms)</label>
|
||||||
|
<input type="number" id="timeout" name="timeout" value="30000" min="0" step="1000" placeholder="0 表示无超时">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label></label>
|
||||||
|
<button type="submit">发送请求</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- ===== 自定义请求头 ===== -->
|
||||||
|
<div class="section-container" id="headersSection">
|
||||||
|
<div class="section-title">
|
||||||
|
请求头 (Headers)
|
||||||
|
<span class="count-badge" id="headerCount">4</span>
|
||||||
|
</div>
|
||||||
|
<div id="headersList"></div>
|
||||||
|
<button class="add-btn" onclick="addHeaderRow('', '')">+ 添加请求头</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="data-result">
|
<div class="data-result">
|
||||||
<div>
|
<div>
|
||||||
<label for="result">result</label>
|
<label for="result">响应结果</label>
|
||||||
<textarea id="result" name="result"></textarea>
|
<textarea id="result" name="result" readonly></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="param-container">
|
||||||
<label for="data">data</label>
|
<div class="param-header">
|
||||||
<textarea id="data" name="data">
|
<span class="param-header-title">请求参数</span>
|
||||||
|
<div class="param-tabs">
|
||||||
|
<button class="tab-btn active" data-mode="json" onclick="switchParamTab('json')">JSON</button>
|
||||||
|
<button class="tab-btn" data-mode="kv" onclick="switchParamTab('kv')">键值对</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="param-panel" id="jsonPanel">
|
||||||
|
<textarea id="data" name="data">
|
||||||
{
|
{
|
||||||
"cp": false,
|
"cp": false,
|
||||||
"id": "2058263032",
|
"id": "2058263032",
|
||||||
@@ -93,61 +368,303 @@
|
|||||||
"ytv": 0,
|
"ytv": 0,
|
||||||
"yv": 0
|
"yv": 0
|
||||||
}</textarea>
|
}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="param-panel" id="kvPanel" style="display:none">
|
||||||
|
<div id="kvList"></div>
|
||||||
|
<button class="add-btn" onclick="addKvRow('', '')">+ 添加参数</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
async function sendRequest() {
|
// ===== 动态行管理 =====
|
||||||
document.getElementById('result').value = ""
|
function escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
const uri = document.getElementById('uri').value;
|
function addHeaderRow(key, value) {
|
||||||
const crypto = document.getElementById('crypto').value;
|
const list = document.getElementById('headersList');
|
||||||
const data = document.getElementById('data').value;
|
const row = document.createElement('div');
|
||||||
|
row.className = 'dynamic-row';
|
||||||
|
row.innerHTML =
|
||||||
|
'<input type="text" class="key-input" placeholder="Header 名称" value="' +
|
||||||
|
escapeHtml(key) +
|
||||||
|
'">' +
|
||||||
|
'<input type="text" class="value-input" placeholder="值" value="' +
|
||||||
|
escapeHtml(value) +
|
||||||
|
'">' +
|
||||||
|
'<button class="remove-btn" onclick="removeRow(this)" title="删除">✕</button>';
|
||||||
|
list.appendChild(row);
|
||||||
|
updateCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addKvRow(key, value) {
|
||||||
|
const list = document.getElementById('kvList');
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'dynamic-row';
|
||||||
|
row.innerHTML =
|
||||||
|
'<input type="text" class="key-input" placeholder="参数名" value="' +
|
||||||
|
escapeHtml(key) +
|
||||||
|
'">' +
|
||||||
|
'<input type="text" class="value-input" placeholder="值" value="' +
|
||||||
|
escapeHtml(value) +
|
||||||
|
'">' +
|
||||||
|
'<button class="remove-btn" onclick="removeRow(this)" title="删除">✕</button>';
|
||||||
|
list.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRow(btn) {
|
||||||
|
btn.parentElement.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectHeaders() {
|
||||||
|
const headers = {};
|
||||||
|
document.querySelectorAll('#headersList .dynamic-row').forEach(function (row) {
|
||||||
|
var key = row.querySelector('.key-input').value.trim();
|
||||||
|
var value = row.querySelector('.value-input').value;
|
||||||
|
if (key) headers[key] = value;
|
||||||
|
});
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectKvData() {
|
||||||
|
var obj = {};
|
||||||
|
document.querySelectorAll('#kvList .dynamic-row').forEach(function (row) {
|
||||||
|
var key = row.querySelector('.key-input').value.trim();
|
||||||
|
var val = row.querySelector('.value-input').value;
|
||||||
|
if (!key) return;
|
||||||
|
// 自动类型转换
|
||||||
|
if (val === 'true') obj[key] = true;
|
||||||
|
else if (val === 'false') obj[key] = false;
|
||||||
|
else if (val === 'null') obj[key] = null;
|
||||||
|
else if (/^-?\d+(\.\d+)?$/.test(val) && !isNaN(Number(val))) obj[key] = Number(val);
|
||||||
|
else obj[key] = val;
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Tab 切换 =====
|
||||||
|
function syncJsonToKv() {
|
||||||
try {
|
try {
|
||||||
const res = await axios({
|
var data = JSON.parse(document.getElementById('data').value);
|
||||||
url: `/api?timestamp=${Date.now()}`,
|
var list = document.getElementById('kvList');
|
||||||
method: 'post',
|
list.innerHTML = '';
|
||||||
data: {
|
Object.keys(data).forEach(function (k) {
|
||||||
uri: uri,
|
addKvRow(k, String(data[k]));
|
||||||
data: JSON.parse(data),
|
|
||||||
crypto: crypto,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
document.getElementById('result').value = JSON.stringify(res.data, null, 2);
|
} catch (e) {
|
||||||
} catch (error) {
|
return false;
|
||||||
document.getElementById('result').value = 'Request failed: ' + error.message;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(function fillFromQuery() {
|
function syncKvToJson() {
|
||||||
const params = new URLSearchParams(window.location.search);
|
var obj = collectKvData();
|
||||||
if (!params.toString()) return;
|
document.getElementById('data').value = JSON.stringify(obj, null, 2);
|
||||||
const uri = params.get('uri');
|
}
|
||||||
const crypto = params.get('crypto');
|
|
||||||
const dataParam = params.get('data');
|
function switchParamTab(mode) {
|
||||||
if (uri) {
|
var jsonPanel = document.getElementById('jsonPanel');
|
||||||
document.getElementById('uri').value = uri;
|
var kvPanel = document.getElementById('kvPanel');
|
||||||
|
|
||||||
|
if (mode === 'kv') {
|
||||||
|
if (!syncJsonToKv()) {
|
||||||
|
alert('JSON 格式有误,无法切换到键值对模式,请先修正 JSON');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
jsonPanel.style.display = 'none';
|
||||||
|
kvPanel.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
syncKvToJson();
|
||||||
|
kvPanel.style.display = 'none';
|
||||||
|
jsonPanel.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(function (btn) {
|
||||||
|
btn.classList.toggle('active', btn.dataset.mode === mode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 默认请求头(直接套用后端预设值)=====
|
||||||
|
var defaultHeaders = [
|
||||||
|
{ key: 'Cookie', value: '' },
|
||||||
|
{
|
||||||
|
key: 'User-Agent',
|
||||||
|
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0',
|
||||||
|
},
|
||||||
|
{ key: 'Referer', value: 'https://music.163.com' },
|
||||||
|
{ key: 'Content-Type', value: 'application/json' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ===== 发送请求 =====
|
||||||
|
async function sendRequest() {
|
||||||
|
document.getElementById('result').value = '';
|
||||||
|
|
||||||
|
var uri = document.getElementById('uri').value;
|
||||||
|
var method = document.getElementById('method').value;
|
||||||
|
var crypto = document.getElementById('crypto').value;
|
||||||
|
var timeout = parseInt(document.getElementById('timeout').value, 10) || 0;
|
||||||
|
var headers = collectHeaders();
|
||||||
|
|
||||||
|
// 根据当前激活的 tab 读取数据
|
||||||
|
var parsedData;
|
||||||
|
var activeTab = document.querySelector('.tab-btn.active');
|
||||||
|
if (activeTab && activeTab.dataset.mode === 'kv') {
|
||||||
|
parsedData = collectKvData();
|
||||||
|
} else {
|
||||||
|
var rawData = document.getElementById('data').value;
|
||||||
|
try {
|
||||||
|
parsedData = rawData.trim() ? JSON.parse(rawData) : {};
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('result').value =
|
||||||
|
'请求数据格式错误:' + e.message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var axiosConfig = {
|
||||||
|
url: '/api?timestamp=' + Date.now(),
|
||||||
|
method: method,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (timeout > 0) {
|
||||||
|
axiosConfig.timeout = timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'GET') {
|
||||||
|
axiosConfig.params = {
|
||||||
|
uri: uri,
|
||||||
|
crypto: crypto,
|
||||||
|
data: JSON.stringify(parsedData),
|
||||||
|
headers: Object.keys(headers).length ? JSON.stringify(headers) : undefined,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
axiosConfig.data = {
|
||||||
|
uri: uri,
|
||||||
|
crypto: crypto,
|
||||||
|
data: parsedData,
|
||||||
|
headers: Object.keys(headers).length ? headers : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var res = await axios(axiosConfig);
|
||||||
|
document.getElementById('result').value = JSON.stringify(
|
||||||
|
res.data,
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
document.getElementById('result').value =
|
||||||
|
'Request failed: ' + error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 从 URL 查询参数恢复 =====
|
||||||
|
(function fillFromQuery() {
|
||||||
|
var params = new URLSearchParams(window.location.search);
|
||||||
|
if (!params.toString()) {
|
||||||
|
// 没有查询参数,用默认 header
|
||||||
|
defaultHeaders.forEach(function (h) {
|
||||||
|
addHeaderRow(h.key, h.value);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var uri = params.get('uri');
|
||||||
|
var crypto = params.get('crypto');
|
||||||
|
var dataParam = params.get('data');
|
||||||
|
var method = params.get('method');
|
||||||
|
var timeout = params.get('timeout');
|
||||||
|
var headersParam = params.get('headers');
|
||||||
|
var kvParams = params.get('kvParams');
|
||||||
|
var paramMode = params.get('paramMode');
|
||||||
|
|
||||||
|
if (uri) document.getElementById('uri').value = uri;
|
||||||
|
if (method) {
|
||||||
|
var methodSelect = document.getElementById('method');
|
||||||
|
if (
|
||||||
|
[].slice.call(methodSelect.options).some(function (opt) {
|
||||||
|
return opt.value === method;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
methodSelect.value = method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (timeout) document.getElementById('timeout').value = timeout;
|
||||||
|
|
||||||
if (crypto) {
|
if (crypto) {
|
||||||
const cryptoSelect = document.getElementById('crypto');
|
var cryptoSelect = document.getElementById('crypto');
|
||||||
if ([...cryptoSelect.options].some((opt) => opt.value === crypto)) {
|
if (
|
||||||
|
[].slice.call(cryptoSelect.options).some(function (opt) {
|
||||||
|
return opt.value === crypto;
|
||||||
|
})
|
||||||
|
) {
|
||||||
cryptoSelect.value = crypto;
|
cryptoSelect.value = crypto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataParam) {
|
if (dataParam) {
|
||||||
const decoded = dataParam;
|
|
||||||
try {
|
try {
|
||||||
document.getElementById('data').value = JSON.stringify(
|
document.getElementById('data').value = JSON.stringify(
|
||||||
JSON.parse(decoded),
|
JSON.parse(dataParam),
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (_) {
|
||||||
document.getElementById('data').value = decoded;
|
document.getElementById('data').value = dataParam;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headersParam) {
|
||||||
|
try {
|
||||||
|
var parsed = JSON.parse(headersParam);
|
||||||
|
Object.keys(parsed).forEach(function (k) {
|
||||||
|
addHeaderRow(k, parsed[k]);
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
// fallback: 用默认 header
|
||||||
|
defaultHeaders.forEach(function (h) {
|
||||||
|
addHeaderRow(h.key, h.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
defaultHeaders.forEach(function (h) {
|
||||||
|
addHeaderRow(h.key, h.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kvParams) {
|
||||||
|
try {
|
||||||
|
var parsed = JSON.parse(kvParams);
|
||||||
|
// 先填充 kv 列表,再切换到 kv 模式
|
||||||
|
Object.keys(parsed).forEach(function (k) {
|
||||||
|
addKvRow(k, String(parsed[k]));
|
||||||
|
});
|
||||||
|
if (paramMode === 'kv') {
|
||||||
|
switchParamTab('kv');
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
} else if (paramMode === 'kv' && dataParam) {
|
||||||
|
// 从 data 参数同步到键值对模式
|
||||||
|
try {
|
||||||
|
var data = JSON.parse(dataParam);
|
||||||
|
Object.keys(data).forEach(function (k) {
|
||||||
|
addKvRow(k, String(data[k]));
|
||||||
|
});
|
||||||
|
switchParamTab('kv');
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -0,0 +1,695 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>API 参数和返回内容解析</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 200px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item input[type="radio"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item label {
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button + button {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section label {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decode-result {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: auto;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decode-result .json-key { color: #881391; }
|
||||||
|
.decode-result .json-string { color: #1a7f37; }
|
||||||
|
.decode-result .json-number { color: #0550ae; }
|
||||||
|
.decode-result .json-boolean { color: #cf222e; }
|
||||||
|
.decode-result .json-null { color: #656d76; }
|
||||||
|
|
||||||
|
.example-section {
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-section h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-section img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* new elements for multi-crypto support */
|
||||||
|
.crypto-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crypto-bar .bar-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crypto-btn {
|
||||||
|
padding: 5px 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crypto-btn:hover {
|
||||||
|
border-color: #333;
|
||||||
|
color: #333;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crypto-btn.active {
|
||||||
|
border-color: #333;
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crypto-btn .badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 10px;
|
||||||
|
background: rgba(0,0,0,0.08);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 5px;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
.crypto-btn.active .badge {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-row .mode-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-row .radio-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-hint.warn {
|
||||||
|
border-left-color: #e67e22;
|
||||||
|
background: #fef9f0;
|
||||||
|
color: #d35400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row button + button {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.btn-secondary {
|
||||||
|
background: #e8e8e8;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
button.btn-secondary:hover {
|
||||||
|
background: #d5d5d5;
|
||||||
|
}
|
||||||
|
button.btn-secondary:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
button.btn-success {
|
||||||
|
background: #4caf50;
|
||||||
|
}
|
||||||
|
button.btn-success:hover {
|
||||||
|
background: #43a047;
|
||||||
|
}
|
||||||
|
button.btn-success:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section .section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section .section-header label {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 3px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.copy-btn:hover {
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card {
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card pre {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
background: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card .tag {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-eapi { background: #e3f2fd; color: #1565c0; }
|
||||||
|
.tag-weapi { background: #fce4ec; color: #c62828; }
|
||||||
|
.tag-linuxapi { background: #e8f5e9; color: #2e7d32; }
|
||||||
|
.tag-xeapi { background: #f3e5f5; color: #7b1fa2; }
|
||||||
|
.tag-api { background: #fff3e0; color: #e65100; }
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.example-grid { grid-template-columns: 1fr; }
|
||||||
|
.crypto-bar { gap: 4px; }
|
||||||
|
.crypto-btn { padding: 4px 10px; font-size: 12px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app" class="container">
|
||||||
|
<h1>API 参数和返回内容解析</h1>
|
||||||
|
|
||||||
|
<!-- 加密方式选择 -->
|
||||||
|
<div class="crypto-bar">
|
||||||
|
<span class="bar-label">加密方式</span>
|
||||||
|
<button
|
||||||
|
v-for="c in cryptoList"
|
||||||
|
:key="c.value"
|
||||||
|
class="crypto-btn"
|
||||||
|
:class="{ active: crypto === c.value }"
|
||||||
|
@click="selectCrypto(c.value)"
|
||||||
|
>
|
||||||
|
{{ c.label }}
|
||||||
|
<span class="badge">{{ c.badge }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mode-row">
|
||||||
|
<span class="mode-label">数据类型</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<div class="radio-item">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="mode-req"
|
||||||
|
:value="true"
|
||||||
|
v-model="isReq"
|
||||||
|
:disabled="cryptoInfo.reqDisabled"
|
||||||
|
/>
|
||||||
|
<label for="mode-req">请求数据 request</label>
|
||||||
|
</div>
|
||||||
|
<div class="radio-item">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="mode-resp"
|
||||||
|
:value="false"
|
||||||
|
v-model="isReq"
|
||||||
|
:disabled="cryptoInfo.respDisabled"
|
||||||
|
/>
|
||||||
|
<label for="mode-resp">返回数据 response</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dataInput">{{ cryptoInfo.inputLabel }}</label>
|
||||||
|
<textarea
|
||||||
|
id="dataInput"
|
||||||
|
v-model="encryptedData"
|
||||||
|
:rows="crypto === 'xeapi' ? 8 : 10"
|
||||||
|
:placeholder="cryptoInfo.placeholder"
|
||||||
|
></textarea>
|
||||||
|
<div class="info-hint" :class="{ warn: cryptoInfo.warning }">
|
||||||
|
{{ cryptoInfo.hint }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-row">
|
||||||
|
<button @click="decrypt">解密</button>
|
||||||
|
<button class="btn-success" @click="sendToApi" :disabled="!canSend">
|
||||||
|
填入 API 调试
|
||||||
|
</button>
|
||||||
|
<button class="btn-secondary" @click="loadExample">加载示例</button>
|
||||||
|
<button class="btn-secondary" @click="clearAll">清空</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<label>解密结果:</label>
|
||||||
|
<button
|
||||||
|
v-if="result && result !== '{}' && result !== 'null'"
|
||||||
|
class="copy-btn"
|
||||||
|
@click="copyResult"
|
||||||
|
>
|
||||||
|
复制
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre class="decode-result" v-html="highlightedResult"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="example-section">
|
||||||
|
<h2>使用示例</h2>
|
||||||
|
<img src="/static/eapi_params.png" alt="请求示例" />
|
||||||
|
<img src="/static/eapi_response.png" alt="响应示例" />
|
||||||
|
|
||||||
|
<div class="example-grid">
|
||||||
|
<div class="example-card">
|
||||||
|
<span class="tag tag-eapi">eapi</span>
|
||||||
|
<h3>请求参数解密</h3>
|
||||||
|
<pre>输入 hex 字符串,解密后得到请求 URL 和参数</pre>
|
||||||
|
<button class="copy-btn" @click="loadExampleData('eapi_req')">加载示例</button>
|
||||||
|
</div>
|
||||||
|
<div class="example-card">
|
||||||
|
<span class="tag tag-linuxapi">linuxapi</span>
|
||||||
|
<h3>请求 eparams 解密</h3>
|
||||||
|
<pre>AES-ECB 解密 linuxapi 的 eparams 参数(hex)</pre>
|
||||||
|
<button class="copy-btn" @click="loadExampleData('linuxapi_req')">加载示例</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/axios"></script>
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const cryptoInfos = {
|
||||||
|
eapi: {
|
||||||
|
label: 'eapi',
|
||||||
|
badge: 'AES-ECB',
|
||||||
|
inputLabel: '十六进制字符串(Hex)',
|
||||||
|
placeholder: '粘贴 eapi 加密后的 Hex 字符串...',
|
||||||
|
hint: 'eapi 使用 AES-ECB 加密,密钥 e82ckenh8dichen8。请求格式: url-36cd479b6b5-data-36cd479b6b5-md5',
|
||||||
|
warning: false,
|
||||||
|
reqDisabled: false,
|
||||||
|
respDisabled: false,
|
||||||
|
},
|
||||||
|
weapi: {
|
||||||
|
label: 'weapi',
|
||||||
|
badge: 'AES-CBC+RSA',
|
||||||
|
inputLabel: '十六进制字符串(Hex)',
|
||||||
|
placeholder: '粘贴 weapi 返回数据的 Hex 字符串(e_r=true 时)...',
|
||||||
|
hint: 'weapi 请求解密需要 RSA 私钥,暂不支持。仅支持返回数据解密(同 eapi 的 AES-ECB)',
|
||||||
|
warning: true,
|
||||||
|
reqDisabled: true,
|
||||||
|
respDisabled: false,
|
||||||
|
},
|
||||||
|
linuxapi: {
|
||||||
|
label: 'linuxapi',
|
||||||
|
badge: 'AES-ECB',
|
||||||
|
inputLabel: '十六进制字符串(Hex)',
|
||||||
|
placeholder: '粘贴 linuxapi 的 eparams (Hex 格式)...',
|
||||||
|
hint: 'linuxapi 请求 eparams 使用 AES-ECB 加密,密钥 rFgB&h#%2?^eDg:Q。返回为明文 JSON',
|
||||||
|
warning: false,
|
||||||
|
reqDisabled: false,
|
||||||
|
respDisabled: false,
|
||||||
|
},
|
||||||
|
xeapi: {
|
||||||
|
label: 'xeapi',
|
||||||
|
badge: 'X25519+AES',
|
||||||
|
inputLabel: 'Base64 编码字符串',
|
||||||
|
placeholder: '粘贴 xeapi 返回数据的 Base64 原始二进制...',
|
||||||
|
hint: 'xeapi 请求涉及 X25519 ECDH 密钥交换,暂不支持。返回数据使用 AES-ECB + eapiKey 加密,可能带 gzip',
|
||||||
|
warning: true,
|
||||||
|
reqDisabled: true,
|
||||||
|
respDisabled: false,
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
label: 'api',
|
||||||
|
badge: '明文',
|
||||||
|
inputLabel: 'JSON 文本',
|
||||||
|
placeholder: '粘贴 api 的请求或返回 JSON...',
|
||||||
|
hint: 'api(明文)不经加密,直接传递 JSON 数据',
|
||||||
|
warning: false,
|
||||||
|
reqDisabled: false,
|
||||||
|
respDisabled: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const examples = {
|
||||||
|
eapi_req:
|
||||||
|
'AD96DDB984491E79B6F429DD650C6E2AE524627AC223AC9A123C66BB0997965950FED137544A93DFC718E16F57C8C121AF537086F395570A5602A3922366D11964DAFACD7830AACABF62E5650E67F457E79C1D2E13502391FC3487216CC5BF8681843FCB8E05559487EB18AAC1BE0EFEA4F7B6A050478366153A9426C238B8869600B275704555A9EB94C92E4F3FDABE9E0BCE07645410D0AA7B675698A4CAE6CD3620633ABF0B849A4244CC8DFC5DB2646D5EA9B3954E62BFEF19AFEAFDDC34E55C3E9A1DD3167CF53D443617108141',
|
||||||
|
linuxapi_req:
|
||||||
|
'A0D9583F4C5FF68DE851D2893A49DE98CC059A8845B664AA2459CEA7271A2C0E5BCA8A188E1BE398DB4C9A3FC117E19C9BAC491F454D17D403C2389476AB0FF4296B00294AD1EBDA141C188DF918F6B9599DAA5739928FD52B4AE580D8657903CE3C6633D2E46AD242408AE219B8191E',
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = Vue.createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
crypto: 'eapi',
|
||||||
|
encryptedData: '',
|
||||||
|
result: '{}',
|
||||||
|
isReq: true,
|
||||||
|
cryptoList: [
|
||||||
|
{ value: 'eapi', label: 'eapi', badge: 'AES-ECB' },
|
||||||
|
{ value: 'weapi', label: 'weapi', badge: 'AES-CBC+RSA' },
|
||||||
|
{ value: 'linuxapi', label: 'linuxapi', badge: 'AES-ECB' },
|
||||||
|
{ value: 'xeapi', label: 'xeapi', badge: 'X25519+AES' },
|
||||||
|
{ value: 'api', label: 'api', badge: '明文' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadExample()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
cryptoInfo() {
|
||||||
|
return cryptoInfos[this.crypto] || cryptoInfos.eapi
|
||||||
|
},
|
||||||
|
isRequestMode() {
|
||||||
|
return this.isReq === true || this.isReq === 'true'
|
||||||
|
},
|
||||||
|
canSend() {
|
||||||
|
if (!this.isRequestMode) return false
|
||||||
|
if (!this.result || this.result === '{}' || this.result === 'null') return false
|
||||||
|
try {
|
||||||
|
JSON.parse(this.result)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
highlightedResult() {
|
||||||
|
if (!this.result || this.result === '') return ''
|
||||||
|
try {
|
||||||
|
const parsed = typeof this.result === 'string' ? JSON.parse(this.result) : this.result
|
||||||
|
const json = JSON.stringify(parsed, null, 2)
|
||||||
|
return this.syntaxHighlight(json)
|
||||||
|
} catch (error) {
|
||||||
|
return this.escapeHtml(String(this.result))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
crypto(val) {
|
||||||
|
const info = cryptoInfos[val]
|
||||||
|
if (this.isReq && info.reqDisabled) {
|
||||||
|
this.isReq = false
|
||||||
|
} else if (!this.isReq && info.respDisabled) {
|
||||||
|
this.isReq = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectCrypto(val) {
|
||||||
|
this.crypto = val
|
||||||
|
},
|
||||||
|
syntaxHighlight(json) {
|
||||||
|
const escaped = this.escapeHtml(json)
|
||||||
|
return escaped
|
||||||
|
.replace(/(?:"(?:\\.|[^"\\])*")\s*:/g, '<span class="json-key">$&</span>')
|
||||||
|
.replace(/:(\s*)(?:"(?:\\.|[^"\\])*")/g, ':<span class="json-string">$1$2</span>')
|
||||||
|
.replace(/:\s*(\d+(?:\.\d+)?)/g, ': <span class="json-number">$1</span>')
|
||||||
|
.replace(/:\s*(true|false)/g, ': <span class="json-boolean">$1</span>')
|
||||||
|
.replace(/:\s*(null)/g, ': <span class="json-null">$1</span>')
|
||||||
|
},
|
||||||
|
escapeHtml(text) {
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.textContent = text
|
||||||
|
return div.innerHTML
|
||||||
|
},
|
||||||
|
formatResult(value) {
|
||||||
|
if (value == null || value === '') return ''
|
||||||
|
try {
|
||||||
|
const parsed = typeof value === 'string' ? JSON.parse(value) : value
|
||||||
|
return JSON.stringify(parsed, null, 2)
|
||||||
|
} catch (error) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async decrypt() {
|
||||||
|
if (!this.encryptedData || !this.encryptedData.trim()) {
|
||||||
|
alert('请先输入要解密的数据')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await axios({
|
||||||
|
url: `/decrypt?crypto=${this.crypto}&isReq=${this.isReq}×tamp=${Date.now()}`,
|
||||||
|
method: 'post',
|
||||||
|
data: { data: this.encryptedData },
|
||||||
|
})
|
||||||
|
this.result = JSON.stringify(res.data.data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
const msg = error?.response?.data?.message || error?.message || '解密失败,数据格式错误'
|
||||||
|
alert(msg)
|
||||||
|
this.result = '{}'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sendToApi() {
|
||||||
|
if (!this.canSend) return
|
||||||
|
const payload = JSON.parse(this.result)
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
const uri = payload.uri || payload.url || payload.path || ''
|
||||||
|
params.set('uri', uri)
|
||||||
|
params.set('crypto', this.crypto)
|
||||||
|
const data = payload.params || payload.data || payload.body || payload.payload || payload.request || {}
|
||||||
|
params.set('data', JSON.stringify(data))
|
||||||
|
window.open(`/api.html?${params.toString()}`, '_blank')
|
||||||
|
},
|
||||||
|
loadExample() {
|
||||||
|
switch (this.crypto) {
|
||||||
|
case 'eapi':
|
||||||
|
this.encryptedData = examples.eapi_req
|
||||||
|
break
|
||||||
|
case 'linuxapi':
|
||||||
|
this.isReq = true
|
||||||
|
this.encryptedData = examples.linuxapi_req
|
||||||
|
break
|
||||||
|
case 'xeapi':
|
||||||
|
this.isReq = false
|
||||||
|
this.encryptedData = ''
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
this.encryptedData = ''
|
||||||
|
}
|
||||||
|
if (this.encryptedData) this.decrypt()
|
||||||
|
},
|
||||||
|
loadExampleData(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'eapi_req':
|
||||||
|
this.crypto = 'eapi'
|
||||||
|
this.isReq = true
|
||||||
|
this.encryptedData = examples.eapi_req
|
||||||
|
break
|
||||||
|
case 'linuxapi_req':
|
||||||
|
this.crypto = 'linuxapi'
|
||||||
|
this.isReq = true
|
||||||
|
this.encryptedData = examples.linuxapi_req
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (this.encryptedData) this.decrypt()
|
||||||
|
},
|
||||||
|
clearAll() {
|
||||||
|
this.encryptedData = ''
|
||||||
|
this.result = '{}'
|
||||||
|
},
|
||||||
|
copyResult() {
|
||||||
|
const text = typeof this.result === 'string' ? this.result : JSON.stringify(this.result, null, 2)
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
alert('已复制到剪贴板')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
app.mount('#app')
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1611,9 +1611,9 @@ function instantiateRuntime(){
|
|||||||
|
|
||||||
function GenerateFP(floatArray) {
|
function GenerateFP(floatArray) {
|
||||||
let PCMBuffer = Float32Array.from(floatArray)
|
let PCMBuffer = Float32Array.from(floatArray)
|
||||||
console.log('[afp] input samples n=', PCMBuffer.length)
|
console.info('[afp] input samples n=', PCMBuffer.length)
|
||||||
return instantiateRuntime().then((fpRuntime) => {
|
return instantiateRuntime().then((fpRuntime) => {
|
||||||
console.log('[afp] begin fingerprinting')
|
console.info('[afp] begin fingerprinting')
|
||||||
let fp_vector = fpRuntime.ExtractQueryFP(PCMBuffer.buffer)
|
let fp_vector = fpRuntime.ExtractQueryFP(PCMBuffer.buffer)
|
||||||
let result_buf = new Uint8Array(fp_vector.size());
|
let result_buf = new Uint8Array(fp_vector.size());
|
||||||
for (let t = 0; t < fp_vector.size(); t++)
|
for (let t = 0; t < fp_vector.size(); t++)
|
||||||
|
|||||||
@@ -1,27 +1,142 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>听歌识曲 Demo</title>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
font-family: sans-serif;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
body {
|
||||||
font-family: monospace;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #555;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-family: sans-serif;
|
color: #333;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background: #999;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="file"] {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group input[type="checkbox"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group label {
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
audio {
|
audio {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0;
|
height: 0;
|
||||||
transition: all linear 0.1s;
|
transition: all linear 0.1s;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-active {
|
.canvas-active {
|
||||||
@@ -29,39 +144,80 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
overflow: scroll;
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fef3c7;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #92400e;
|
||||||
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1>听歌识曲 Demo (Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a>)</h1>
|
<div class="container">
|
||||||
<hr>
|
<h1>听歌识曲 Demo</h1>
|
||||||
<p><b>DISCLAIMER: </b></p>
|
<p class="subtitle">Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a></p>
|
||||||
<p>This site uses the offical NetEase audio matcher APIs (reverse engineered from <a
|
|
||||||
href="https://fn.music.163.com/g/chrome-extension-home-page-beta/">https://fn.music.163.com/g/chrome-extension-home-page-beta/</a>)
|
|
||||||
</p>
|
|
||||||
<p>And DOES NOT condone copyright infringment nor intellectual property theft.</p>
|
|
||||||
<hr>
|
|
||||||
<p><b>NOTE:</b></p>
|
|
||||||
<p>Before start using the site, you may want to access this link first:</p>
|
|
||||||
<a href="https://cors-anywhere.herokuapp.com/corsdemo">https://cors-anywhere.herokuapp.com/corsdemo</a>
|
|
||||||
<p>Since Netease APIs do not have CORS headers, this is required to alleviate this restriction.</p>
|
|
||||||
<hr>
|
|
||||||
<p>Usage:</p>
|
|
||||||
<li>Select your audio file through "Choose File" picker</li>
|
|
||||||
<li>Hit the "Clip" button and wait for the results!</li>
|
|
||||||
|
|
||||||
<audio id="audio" controls autoplay></audio>
|
<hr>
|
||||||
<canvas id="canvas"></canvas>
|
|
||||||
<button id="invoke">Clip</button>
|
<div class="warning">
|
||||||
<input type="file" name="picker" accept="*" id="file">
|
<strong>免责声明:</strong>本站点使用网易云音乐官方音频识别API(逆向自 <a href="https://fn.music.163.com/g/chrome-extension-home-page-beta/" target="_blank">Chrome 扩展页面</a>),不鼓励版权侵犯或知识产权盗窃。
|
||||||
<hr>
|
</div>
|
||||||
<label for="use-mic">Mix in Microphone input</label>
|
|
||||||
<input type="checkbox" name="use-mic" id="usemic">
|
<div class="section">
|
||||||
<hr>
|
<h3>使用说明</h3>
|
||||||
<pre id="logs"></pre>
|
<p>在使用本站点之前,您可能需要先访问以下链接:</p>
|
||||||
|
<p><a href="https://cors-anywhere.herokuapp.com/corsdemo" target="_blank">https://cors-anywhere.herokuapp.com/corsdemo</a></p>
|
||||||
|
<p>由于网易云音乐API没有CORS头,这是解决此限制的必要步骤。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>使用方法</h3>
|
||||||
|
<ul style="padding-left: 20px; font-size: 14px; color: #555;">
|
||||||
|
<li>通过"选择文件"选择您的音频文件</li>
|
||||||
|
<li>点击"识别"按钮并等待结果</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<audio id="audio" controls autoplay></audio>
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<button id="invoke">识别</button>
|
||||||
|
<input type="file" name="picker" accept="*" id="file">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<input type="checkbox" name="use-mic" id="usemic">
|
||||||
|
<label for="usemic">混合麦克风输入</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 12px;">日志</h3>
|
||||||
|
<pre id="logs"></pre>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<script src="./afp.wasm.js"></script>
|
<script src="./afp.wasm.js"></script>
|
||||||
<script src="./afp.js"></script>
|
<script src="./afp.js"></script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
@@ -76,13 +232,17 @@
|
|||||||
let canvas = document.getElementById('canvas')
|
let canvas = document.getElementById('canvas')
|
||||||
let canvasCtx = canvas.getContext('2d')
|
let canvasCtx = canvas.getContext('2d')
|
||||||
let logs = document.getElementById('logs')
|
let logs = document.getElementById('logs')
|
||||||
logs.write = line => logs.innerHTML += line + '\n'
|
logs.write = line => {
|
||||||
|
// Append log lines as text to avoid interpreting content as HTML
|
||||||
|
logs.appendChild(document.createTextNode(line));
|
||||||
|
logs.appendChild(document.createElement('br'));
|
||||||
|
}
|
||||||
|
|
||||||
function RecorderCallback(channelL) {
|
function RecorderCallback(channelL) {
|
||||||
let sampleBuffer = new Float32Array(channelL.subarray(0, duration * 8000))
|
let sampleBuffer = new Float32Array(channelL.subarray(0, duration * 8000))
|
||||||
GenerateFP(sampleBuffer).then(FP => {
|
GenerateFP(sampleBuffer).then(FP => {
|
||||||
logs.write(`[index] Generated FP ${FP}`)
|
logs.write(`[index] 生成指纹 ${FP}`)
|
||||||
logs.write('[index] Now querying, please wait...')
|
logs.write('[index] 正在查询,请稍候...')
|
||||||
fetch(
|
fetch(
|
||||||
'/audio/match?' +
|
'/audio/match?' +
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
@@ -91,9 +251,9 @@
|
|||||||
method: 'POST'
|
method: 'POST'
|
||||||
}).then(resp => resp.json()).then(resp => {
|
}).then(resp => resp.json()).then(resp => {
|
||||||
if (!resp.data.result) {
|
if (!resp.data.result) {
|
||||||
return logs.write('[index] Query failed with no results.')
|
return logs.write('[index] 查询失败,无结果')
|
||||||
}
|
}
|
||||||
logs.write(`[index] Query complete. Results=${resp.data.result.length}`)
|
logs.write(`[index] 查询完成。结果数量=${resp.data.result.length}`)
|
||||||
for (var song of resp.data.result) {
|
for (var song of resp.data.result) {
|
||||||
logs.write(
|
logs.write(
|
||||||
`[result] <a target="_blank" href="https://music.163.com/song?id=${song.song.id}">${song.song.name} - ${song.song.album.name} (${song.startTime / 1000}s)</a>`
|
`[result] <a target="_blank" href="https://music.163.com/song?id=${song.song.id}">${song.song.name} - ${song.song.album.name} (${song.startTime / 1000}s)</a>`
|
||||||
@@ -104,20 +264,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function InitAudioCtx() {
|
function InitAudioCtx() {
|
||||||
// AFP.wasm can't do it with anything other than 8KHz
|
|
||||||
audioCtx = new AudioContext({ 'sampleRate': 8000 })
|
audioCtx = new AudioContext({ 'sampleRate': 8000 })
|
||||||
if (audioCtx.state == 'suspended')
|
if (audioCtx.state == 'suspended')
|
||||||
return false
|
return false
|
||||||
let audioNode = audioCtx.createMediaElementSource(audio)
|
let audioNode = audioCtx.createMediaElementSource(audio)
|
||||||
audioCtx.audioWorklet.addModule('rec.js').then(() => {
|
audioCtx.audioWorklet.addModule('rec.js').then(() => {
|
||||||
recorderNode = new AudioWorkletNode(audioCtx, 'timed-recorder')
|
recorderNode = new AudioWorkletNode(audioCtx, 'timed-recorder')
|
||||||
audioNode.connect(recorderNode) // recorderNode doesn't output anything
|
audioNode.connect(recorderNode)
|
||||||
audioNode.connect(audioCtx.destination)
|
audioNode.connect(audioCtx.destination)
|
||||||
recorderNode.port.onmessage = event => {
|
recorderNode.port.onmessage = event => {
|
||||||
switch (event.data.message) {
|
switch (event.data.message) {
|
||||||
case 'finished':
|
case 'finished':
|
||||||
RecorderCallback(event.data.recording)
|
RecorderCallback(event.data.recording)
|
||||||
clip.innerHTML = 'Clip'
|
clip.innerHTML = '识别'
|
||||||
clip.disabled = false
|
clip.disabled = false
|
||||||
canvas.classList.remove('canvas-active')
|
canvas.classList.remove('canvas-active')
|
||||||
break
|
break
|
||||||
@@ -130,7 +289,6 @@
|
|||||||
logs.write(event.data.message)
|
logs.write(event.data.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Attempt to get user's microphone and connect it to the AudioContext.
|
|
||||||
navigator.mediaDevices.getUserMedia({
|
navigator.mediaDevices.getUserMedia({
|
||||||
audio: {
|
audio: {
|
||||||
echoCancellation: false,
|
echoCancellation: false,
|
||||||
@@ -142,7 +300,7 @@
|
|||||||
micSourceNode = audioCtx.createMediaStreamSource(micStream);
|
micSourceNode = audioCtx.createMediaStreamSource(micStream);
|
||||||
micSourceNode.connect(recorderNode)
|
micSourceNode.connect(recorderNode)
|
||||||
usemic.checked = true
|
usemic.checked = true
|
||||||
logs.write('[rec.js] Microphone attached.')
|
logs.write('[rec.js] 麦克风已连接')
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return true
|
return true
|
||||||
@@ -161,10 +319,20 @@
|
|||||||
else
|
else
|
||||||
micSourceNode.connect(recorderNode)
|
micSourceNode.connect(recorderNode)
|
||||||
})
|
})
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.replace(/\//g, '/');
|
||||||
|
}
|
||||||
file.addEventListener('change', event => {
|
file.addEventListener('change', event => {
|
||||||
file.files[0].arrayBuffer().then(
|
file.files[0].arrayBuffer().then(
|
||||||
async buffer => {
|
async buffer => {
|
||||||
logs.write(`[index] File ${file.files[0].name} loaded.`)
|
const safeName = escapeHtml(file.files[0].name)
|
||||||
|
logs.write(`[index] 文件 ${safeName} 已加载`)
|
||||||
audio.src = window.URL.createObjectURL(new Blob([buffer]))
|
audio.src = window.URL.createObjectURL(new Blob([buffer]))
|
||||||
clip.disabled = false
|
clip.disabled = false
|
||||||
})
|
})
|
||||||
@@ -188,12 +356,13 @@
|
|||||||
UpdateCanvas()
|
UpdateCanvas()
|
||||||
let requestCtx = setInterval(() => {
|
let requestCtx = setInterval(() => {
|
||||||
try {
|
try {
|
||||||
if (InitAudioCtx()) { // Put this here so we don't have to deal with the 'user did not interact' thing
|
if (InitAudioCtx()) {
|
||||||
clearInterval(requestCtx)
|
clearInterval(requestCtx)
|
||||||
logs.write('[rec.js] Audio Context started.')
|
logs.write('[rec.js] 音频上下文已启动')
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fail silently
|
// Fail silently
|
||||||
}
|
}
|
||||||
}, 100)
|
}, 100)
|
||||||
</script>
|
</script>
|
||||||
|
</html>
|
||||||
@@ -5,67 +5,320 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>更新头像</title>
|
<title>更新头像</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
margin: 0 auto 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrapper.loading .avatar {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrapper.loading .loading-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid #e0e0e0;
|
||||||
|
border-top-color: #333;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
padding: 12px 28px;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn input[type="file"] {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link {
|
||||||
|
display: block;
|
||||||
|
margin-top: 24px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link:hover {
|
||||||
|
color: #333;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(100px);
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 1000;
|
||||||
|
max-width: 90%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.show {
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success {
|
||||||
|
background: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.info {
|
||||||
|
background: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div>
|
<div class="container">
|
||||||
<a href="/qrlogin-nocookie.html">
|
<h1>更新头像</h1>
|
||||||
如果没登录,请先登录
|
<p class="subtitle">选择一张图片作为您的新头像</p>
|
||||||
|
|
||||||
|
<div class="avatar-wrapper" id="avatarWrapper">
|
||||||
|
<img id="avatar" class="avatar" src="" alt="头像" />
|
||||||
|
<div class="loading-overlay">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="upload-btn">
|
||||||
|
选择图片
|
||||||
|
<input id="file" type="file" accept="image/*" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<a href="/qrlogin-nocookie.html" class="login-link">
|
||||||
|
还没有登录?点击登录
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<p class="hint">支持 JPG、PNG 格式,建议尺寸 200x200</p>
|
||||||
</div>
|
</div>
|
||||||
<input id="file" type="file" />
|
|
||||||
<img id="avatar" style="height: 200px; width: 200px; border-radius: 50%" />
|
<div id="toast" class="toast"></div>
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js
|
|
||||||
"></script>
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
main()
|
main()
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
document.querySelector('input[type="file"]').addEventListener(
|
const fileInput = document.querySelector('input[type="file"]');
|
||||||
'change',
|
const avatarWrapper = document.getElementById('avatarWrapper');
|
||||||
function (e) {
|
|
||||||
var file = this.files[0]
|
fileInput.addEventListener('change', function(e) {
|
||||||
upload(file)
|
const file = this.files[0];
|
||||||
},
|
if (file) {
|
||||||
false,
|
upload(file);
|
||||||
)
|
}
|
||||||
const res = await axios({
|
}, false);
|
||||||
url: `/user/detail?uid=32953014×tamp=${Date.now()}`,
|
|
||||||
withCredentials: true, //跨域的话必须设置
|
try {
|
||||||
})
|
showToast('正在加载头像...', 'info');
|
||||||
document.querySelector('#avatar').src = res.data.profile.avatarUrl
|
avatarWrapper.classList.add('loading');
|
||||||
|
const res = await axios({
|
||||||
|
url: `/user/detail?uid=32953014×tamp=${Date.now()}`,
|
||||||
|
withCredentials: true
|
||||||
|
});
|
||||||
|
document.querySelector('#avatar').src = res.data.profile.avatarUrl;
|
||||||
|
hideToast();
|
||||||
|
} catch (error) {
|
||||||
|
hideToast();
|
||||||
|
showToast('加载头像失败,请刷新页面重试', 'error');
|
||||||
|
console.error('加载头像失败:', error);
|
||||||
|
} finally {
|
||||||
|
avatarWrapper.classList.remove('loading');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upload(file) {
|
async function upload(file) {
|
||||||
var formData = new FormData()
|
const avatarWrapper = document.getElementById('avatarWrapper');
|
||||||
formData.append('imgFile', file)
|
|
||||||
const imgSize = await getImgSize(file)
|
if (!file.type.startsWith('image/')) {
|
||||||
const res = await axios({
|
showToast('请选择图片文件', 'error');
|
||||||
method: 'post',
|
return;
|
||||||
url: `/avatar/upload?cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width
|
}
|
||||||
}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
|
||||||
headers: {
|
try {
|
||||||
'Content-Type': 'multipart/form-data',
|
showToast('正在上传头像...', 'info');
|
||||||
},
|
avatarWrapper.classList.add('loading');
|
||||||
data: formData,
|
|
||||||
})
|
var formData = new FormData();
|
||||||
document.querySelector('#avatar').src = res.data.data.url
|
formData.append('imgFile', file);
|
||||||
|
const imgSize = await getImgSize(file);
|
||||||
|
|
||||||
|
const res = await axios({
|
||||||
|
method: 'post',
|
||||||
|
url: `/avatar/upload?cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
data: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('#avatar').src = res.data.data.url;
|
||||||
|
showToast('头像更新成功!', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传失败:', error);
|
||||||
|
const errorMsg = error.response?.data?.message || error.message || '上传失败,请重试';
|
||||||
|
showToast(errorMsg, 'error');
|
||||||
|
} finally {
|
||||||
|
avatarWrapper.classList.remove('loading');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getImgSize(file) {
|
function getImgSize(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let reader = new FileReader()
|
let reader = new FileReader();
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file);
|
||||||
reader.onload = function (theFile) {
|
reader.onload = function(theFile) {
|
||||||
let image = new Image()
|
let image = new Image();
|
||||||
image.src = theFile.target.result
|
image.src = theFile.target.result;
|
||||||
image.onload = function () {
|
image.onload = function() {
|
||||||
resolve({
|
resolve({
|
||||||
width: this.width,
|
width: this.width,
|
||||||
height: this.height,
|
height: this.height,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
}
|
image.onerror = function() {
|
||||||
})
|
reject(new Error('图片加载失败'));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
reader.onerror = function() {
|
||||||
|
reject(new Error('文件读取失败'));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(message, type = 'info') {
|
||||||
|
const toast = document.getElementById('toast');
|
||||||
|
toast.textContent = message;
|
||||||
|
toast.className = `toast ${type}`;
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
toast.classList.add('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('show');
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideToast() {
|
||||||
|
const toast = document.getElementById('toast');
|
||||||
|
toast.classList.remove('show');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,99 +1,578 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh">
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
<head>
|
<meta charset="UTF-8" />
|
||||||
<meta charset="UTF-8" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<title>云盘上传</title>
|
||||||
<title>云盘上传</title>
|
<style>
|
||||||
</head>
|
* {
|
||||||
|
margin: 0;
|
||||||
<body>
|
padding: 0;
|
||||||
<div>
|
box-sizing: border-box;
|
||||||
<a href="/qrlogin-nocookie.html">
|
|
||||||
如果没登录,请先登录
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<input id="file" type="file" multiple />
|
|
||||||
<div id="app">
|
|
||||||
<ul>
|
|
||||||
<li v-for="(item,index) in songs" :key="index">
|
|
||||||
{{item.songName}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/vue"></script>
|
|
||||||
<script>
|
|
||||||
const app = Vue.createApp({
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
songs: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.getData()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getData() {
|
|
||||||
console.log('getdata');
|
|
||||||
const _this = this
|
|
||||||
axios({
|
|
||||||
url: `/user/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
|
|
||||||
}).then(res => {
|
|
||||||
console.log(res.data)
|
|
||||||
_this.songs = res.data.data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).mount('#app')
|
|
||||||
const fileUpdateTime = {}
|
|
||||||
let fileLength = 0
|
|
||||||
|
|
||||||
function main() {
|
body {
|
||||||
document
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
.querySelector('input[type="file"]')
|
min-height: 100vh;
|
||||||
.addEventListener('change', function (e) {
|
background: #f5f5f5;
|
||||||
console.log(this.files)
|
padding: 20px;
|
||||||
let currentIndx = 0
|
}
|
||||||
fileLength = this.files.length
|
|
||||||
for (const item of this.files) {
|
.container {
|
||||||
currentIndx += 1
|
max-width: 800px;
|
||||||
upload(item, currentIndx)
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link:hover {
|
||||||
|
color: #333;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-section label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-option input[type="radio"] {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-option-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-option-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-option-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-option input[type="radio"]:checked + .mode-option-text .mode-option-title {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 28px;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn input[type="file"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn.disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songs-list {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-item {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-section.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item .name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item .status {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 6px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar .fill {
|
||||||
|
height: 100%;
|
||||||
|
background: #333;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
width: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item.success .fill {
|
||||||
|
background: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item.error .fill {
|
||||||
|
background: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item.error .status {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #e65100;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>云盘上传</h1>
|
||||||
|
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
|
||||||
|
|
||||||
|
<div class="mode-section">
|
||||||
|
<label>上传模式</label>
|
||||||
|
<div class="mode-options">
|
||||||
|
<label class="mode-option">
|
||||||
|
<input type="radio" name="uploadMode" value="direct" checked />
|
||||||
|
<span class="mode-option-text">
|
||||||
|
<span class="mode-option-title">客户端直传</span>
|
||||||
|
<span class="mode-option-desc">文件直接上传到云存储,支持大文件,适合 Vercel 等平台</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="mode-option">
|
||||||
|
<input type="radio" name="uploadMode" value="proxy" />
|
||||||
|
<span class="mode-option-text">
|
||||||
|
<span class="mode-option-title">后端代理</span>
|
||||||
|
<span class="mode-option-desc">文件通过服务器转发,更简洁,需要服务器支持大文件</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="upload-section">
|
||||||
|
<label class="upload-btn" id="uploadBtn">
|
||||||
|
选择文件(支持多选)
|
||||||
|
<input id="file" type="file" multiple accept="audio/*" />
|
||||||
|
</label>
|
||||||
|
<p class="info-text" id="modeInfo">支持大文件上传,文件将直接传输到云存储服务器</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="progressSection" class="progress-section"></div>
|
||||||
|
|
||||||
|
<div id="app">
|
||||||
|
<div v-if="loading" class="loading">加载中...</div>
|
||||||
|
<ul v-else-if="songs.length > 0" class="songs-list">
|
||||||
|
<li v-for="(item, index) in songs" :key="index" class="song-item">
|
||||||
|
{{ item.songName }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-else class="empty-state">暂无云盘歌曲</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsmediatags/3.9.5/jsmediatags.min.js"></script>
|
||||||
|
<script>
|
||||||
|
const app = Vue.createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
songs: [],
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getData() {
|
||||||
|
this.loading = true
|
||||||
|
axios({
|
||||||
|
url: `/user/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
this.songs = res.data.data || []
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('获取云盘数据失败:', err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).mount('#app')
|
||||||
|
|
||||||
|
let isUploading = false
|
||||||
|
let uploadMode = 'direct'
|
||||||
|
const progressSection = document.getElementById('progressSection')
|
||||||
|
const uploadBtn = document.getElementById('uploadBtn')
|
||||||
|
const fileInput = document.querySelector('input[type="file"]')
|
||||||
|
const modeInfo = document.getElementById('modeInfo')
|
||||||
|
|
||||||
|
document.querySelectorAll('input[name="uploadMode"]').forEach(radio => {
|
||||||
|
radio.addEventListener('change', function() {
|
||||||
|
uploadMode = this.value
|
||||||
|
if (uploadMode === 'direct') {
|
||||||
|
modeInfo.textContent = '支持大文件上传,文件将直接传输到云存储服务器'
|
||||||
|
modeInfo.className = 'info-text'
|
||||||
|
} else {
|
||||||
|
modeInfo.textContent = '文件将通过服务器转发,服务器需支持大文件上传(Vercel 限制 4.5MB)'
|
||||||
|
modeInfo.className = 'warning-text'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
main()
|
|
||||||
|
|
||||||
function upload(file, currentIndx) {
|
|
||||||
var formData = new FormData()
|
|
||||||
formData.append('songFile', file)
|
|
||||||
axios({
|
|
||||||
method: 'post',
|
|
||||||
url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
data: formData,
|
|
||||||
}).then(res => {
|
|
||||||
console.log(`${file.name} 上传成功`)
|
|
||||||
if (currentIndx >= fileLength) { console.log('上传完毕') }
|
|
||||||
app.getData()
|
|
||||||
}).catch(async err => {
|
|
||||||
console.log(err)
|
|
||||||
console.log(fileUpdateTime)
|
|
||||||
fileUpdateTime[file.name] ? fileUpdateTime[file.name] += 1 : fileUpdateTime[file.name] = 1
|
|
||||||
if (fileUpdateTime[file.name] >= 4) {
|
|
||||||
console.error(`丢,这首歌怎么都传不上:${file.name}`)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
console.error(`${file.name} 失败 ${fileUpdateTime[file.name]} 次`)
|
|
||||||
}
|
|
||||||
// await login()
|
|
||||||
upload(file, currentIndx)
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
fileInput.addEventListener('change', function (e) {
|
||||||
|
const files = this.files
|
||||||
|
if (files.length === 0) return
|
||||||
|
if (isUploading) return
|
||||||
|
|
||||||
|
uploadFilesSequentially(Array.from(files))
|
||||||
|
this.value = ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
main()
|
||||||
|
|
||||||
|
async function uploadFilesSequentially(files) {
|
||||||
|
isUploading = true
|
||||||
|
uploadBtn.classList.add('disabled')
|
||||||
|
progressSection.classList.add('active')
|
||||||
|
progressSection.innerHTML = ''
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
if (uploadMode === 'direct') {
|
||||||
|
await uploadFileDirect(files[i], i + 1, files.length)
|
||||||
|
} else {
|
||||||
|
await uploadFileProxy(files[i], i + 1, files.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isUploading = false
|
||||||
|
uploadBtn.classList.remove('disabled')
|
||||||
|
app.getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProgressItem(file, index, total) {
|
||||||
|
const item = document.createElement('div')
|
||||||
|
item.className = 'progress-item'
|
||||||
|
item.id = `progress-${index}`
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="name">${file.name} (${formatSize(file.size)})</div>
|
||||||
|
<div class="status">准备中...</div>
|
||||||
|
<div class="progress-bar"><div class="fill"></div></div>
|
||||||
|
`
|
||||||
|
progressSection.appendChild(item)
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress(index, status, percent, isError = false) {
|
||||||
|
const item = document.getElementById(`progress-${index}`)
|
||||||
|
if (!item) return
|
||||||
|
item.querySelector('.status').textContent = status
|
||||||
|
item.querySelector('.fill').style.width = `${percent}%`
|
||||||
|
if (isError) {
|
||||||
|
item.classList.add('error')
|
||||||
|
} else if (percent >= 100) {
|
||||||
|
item.classList.add('success')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSize(bytes) {
|
||||||
|
if (bytes < 1024) return bytes + ' B'
|
||||||
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
||||||
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFileProxy(file, index, total) {
|
||||||
|
createProgressItem(file, index, total)
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateProgress(index, '上传中...', 10)
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('songFile', file)
|
||||||
|
|
||||||
|
await axios({
|
||||||
|
method: 'post',
|
||||||
|
url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
data: formData,
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 90) + 10
|
||||||
|
updateProgress(index, `上传中... ${Math.round(progressEvent.loaded / progressEvent.total * 100)}%`, Math.min(percent, 100))
|
||||||
|
},
|
||||||
|
timeout: 600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
updateProgress(index, '上传完成!', 100)
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`${file.name} 上传失败:`, err)
|
||||||
|
const errorMsg = err.response?.data?.msg || err.message || '未知错误'
|
||||||
|
if (err.response?.status === 413 || errorMsg.includes('PAYLOAD_TOO_LARGE')) {
|
||||||
|
updateProgress(index, '文件过大,请切换到客户端直传模式', 0, true)
|
||||||
|
} else {
|
||||||
|
updateProgress(index, `上传失败: ${errorMsg}`, 0, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function calculateMD5(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const chunkSize = 2 * 1024 * 1024
|
||||||
|
const chunks = Math.ceil(file.size / chunkSize)
|
||||||
|
let currentChunk = 0
|
||||||
|
const spark = new SparkMD5.ArrayBuffer()
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
reader.onload = (e) => {
|
||||||
|
spark.append(e.target.result)
|
||||||
|
currentChunk++
|
||||||
|
if (currentChunk < chunks) {
|
||||||
|
loadNext()
|
||||||
|
} else {
|
||||||
|
resolve(spark.end())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.onerror = () => reject(reader.error)
|
||||||
|
|
||||||
|
function loadNext() {
|
||||||
|
const start = currentChunk * chunkSize
|
||||||
|
const end = Math.min(start + chunkSize, file.size)
|
||||||
|
reader.readAsArrayBuffer(file.slice(start, end))
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNext()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parseMediaTags(file) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
jsmediatags.read(file, {
|
||||||
|
onSuccess: function(tag) {
|
||||||
|
resolve({
|
||||||
|
title: tag.tags.title || null,
|
||||||
|
artist: tag.tags.artist || null,
|
||||||
|
album: tag.tags.album || null,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: function() {
|
||||||
|
resolve({ title: null, artist: null, album: null })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFileDirect(file, index, total) {
|
||||||
|
createProgressItem(file, index, total)
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateProgress(index, '计算文件MD5...', 5)
|
||||||
|
|
||||||
|
const md5 = await calculateMD5(file)
|
||||||
|
const fileSize = file.size
|
||||||
|
const filename = file.name
|
||||||
|
|
||||||
|
updateProgress(index, '解析音频元数据...', 8)
|
||||||
|
|
||||||
|
const mediaTags = await parseMediaTags(file)
|
||||||
|
|
||||||
|
updateProgress(index, '获取上传凭证...', 10)
|
||||||
|
|
||||||
|
const tokenRes = await axios({
|
||||||
|
method: 'post',
|
||||||
|
url: `/cloud/upload/token?time=${Date.now()}`,
|
||||||
|
data: {
|
||||||
|
cookie: localStorage.getItem('cookie'),
|
||||||
|
md5: md5,
|
||||||
|
fileSize: fileSize,
|
||||||
|
filename: filename,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (tokenRes.data.code !== 200) {
|
||||||
|
throw new Error(tokenRes.data.msg || '获取上传凭证失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenData = tokenRes.data.data
|
||||||
|
|
||||||
|
if (!tokenData.needUpload) {
|
||||||
|
updateProgress(index, '文件已存在,直接导入云盘...', 80)
|
||||||
|
await completeUpload(tokenData, file, mediaTags)
|
||||||
|
updateProgress(index, '上传完成!', 100)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(index, '开始上传到云存储...', 15)
|
||||||
|
|
||||||
|
await axios({
|
||||||
|
method: 'post',
|
||||||
|
url: tokenData.uploadUrl,
|
||||||
|
headers: {
|
||||||
|
'x-nos-token': tokenData.uploadToken,
|
||||||
|
'Content-MD5': md5,
|
||||||
|
'Content-Type': 'audio/mpeg',
|
||||||
|
'Content-Length': String(fileSize),
|
||||||
|
},
|
||||||
|
data: file,
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 70) + 15
|
||||||
|
updateProgress(index, `上传中... ${Math.round(progressEvent.loaded / progressEvent.total * 100)}%`, Math.min(percent, 85))
|
||||||
|
},
|
||||||
|
maxContentLength: Infinity,
|
||||||
|
maxBodyLength: Infinity,
|
||||||
|
timeout: 600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
updateProgress(index, '上传完成,正在导入云盘...', 90)
|
||||||
|
|
||||||
|
await completeUpload(tokenData, file, mediaTags)
|
||||||
|
|
||||||
|
updateProgress(index, '上传完成!', 100)
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`${file.name} 上传失败:`, err)
|
||||||
|
const errorMsg = err.response?.data?.msg || err.message || '未知错误'
|
||||||
|
updateProgress(index, `上传失败: ${errorMsg}`, 0, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeUpload(tokenData, file, mediaTags = {}) {
|
||||||
|
const songName = mediaTags.title || file.name.replace(/\.[^.]+$/, '')
|
||||||
|
const artist = mediaTags.artist || '未知艺术家'
|
||||||
|
const album = mediaTags.album || '未知专辑'
|
||||||
|
|
||||||
|
const completeRes = await axios({
|
||||||
|
method: 'post',
|
||||||
|
url: `/cloud/upload/complete?time=${Date.now()}`,
|
||||||
|
data: {
|
||||||
|
cookie: localStorage.getItem('cookie'),
|
||||||
|
songId: tokenData.songId,
|
||||||
|
resourceId: tokenData.resourceId,
|
||||||
|
md5: tokenData.md5,
|
||||||
|
filename: file.name,
|
||||||
|
song: songName,
|
||||||
|
artist: artist,
|
||||||
|
album: album,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (completeRes.data.code !== 200) {
|
||||||
|
throw new Error(completeRes.data.msg || '导入云盘失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
return completeRes.data
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
# 网易云音乐 API
|
# NeteaseCloudMusicAPI Enhanced
|
||||||
|
|
||||||
> 网易云音乐 NodeJS 版 API
|
> 🎉 全网收集最全的网易云音乐api接口 基于[NeteaseCloudMusicAPI](https://github.com/binaryify/NeteaseCloudMusicApi)的复刻版本
|
||||||
|
|
||||||
- 全部接口已升级到最新
|
- ⚡ 四种加密模式 · 后端代理
|
||||||
- 具备登录接口,多达200多个接口
|
- 🪛 具备多达200多个接口
|
||||||
- 更完善的文档
|
- 📄 更完善的文档
|
||||||
|
|
||||||
|
|
||||||
[GitLab](https://gitlab.com/Binaryify/NeteaseCloudMusicApi)
|
[Github](https://github.com/neteasecloudmusicapienhanced/api-enhanced)
|
||||||
[Get Started](#neteasecloudmusicapi)
|
[前往本家](https://github.com/binaryify/NeteaseCloudMusicApi)
|
||||||
|
[快速开始](#neteasecloudmusicapienhanced)
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@@ -3,13 +3,56 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="KEYWords" contect="网易云音乐,网易云音乐 api,网易云音乐 nodejs,网易云音乐 node.js">
|
<meta name="KEYWords" contect="网易云音乐,网易云音乐 api,网易云音乐 nodejs,网易云音乐 node.js">
|
||||||
<meta name="description" content="网易云音乐 NodeJS 版 API">
|
<meta name="description" content="网易云音乐 NodeJS API Enhanced">
|
||||||
<title>网易云音乐 NodeJS 版 API</title>
|
<title>网易云音乐 NodeJS API Enhanced</title>
|
||||||
<link rel="icon" href="favicon.ico">
|
<link rel="icon" href="netease.png">
|
||||||
<meta name="description" content="Description">
|
<meta name="description" content="Description">
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||||
<meta name="referrer" content="never">
|
<meta name="referrer" content="never">
|
||||||
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
|
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css" id="theme-css">
|
||||||
|
<style>
|
||||||
|
.theme-toggle {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
z-index: 999;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
background: rgba(30,30,46,0.75);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4,0,0.2,1);
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.theme-toggle:hover {
|
||||||
|
transform: scale(1.12) rotate(12deg);
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
|
.theme-toggle:active {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
.theme-toggle.light {
|
||||||
|
background: rgba(255,255,255,0.85);
|
||||||
|
color: #333;
|
||||||
|
border-color: rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
html, body, .sidebar, .content, .markdown-section,
|
||||||
|
.sidebar-nav, .search, .docsify-pagination-container {
|
||||||
|
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
|
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
|
||||||
<script>
|
<script>
|
||||||
(adsbygoogle = window.adsbygoogle || []).push({
|
(adsbygoogle = window.adsbygoogle || []).push({
|
||||||
@@ -20,11 +63,50 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme">🌙</button>
|
||||||
</body>
|
</body>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var STORAGE_KEY = 'docsify-theme';
|
||||||
|
var link = document.getElementById('theme-css');
|
||||||
|
var btn = document.getElementById('themeToggle');
|
||||||
|
var LIGHT_URL = '//unpkg.com/docsify/lib/themes/vue.css';
|
||||||
|
var DARK_URL = '//unpkg.com/docsify/lib/themes/dark.css';
|
||||||
|
|
||||||
|
function setTheme(theme) {
|
||||||
|
link.href = theme === 'dark' ? DARK_URL : LIGHT_URL;
|
||||||
|
btn.textContent = theme === 'dark' ? '☀️' : '🌙';
|
||||||
|
btn.className = 'theme-toggle' + (theme === 'dark' ? '' : ' light');
|
||||||
|
localStorage.setItem(STORAGE_KEY, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
var isDark = link.href.includes('dark');
|
||||||
|
setTheme(isDark ? 'light' : 'dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
var saved = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (saved) {
|
||||||
|
setTheme(saved);
|
||||||
|
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
setTheme('dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.addEventListener('click', toggleTheme);
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
|
||||||
|
if (!localStorage.getItem(STORAGE_KEY)) {
|
||||||
|
setTheme(e.matches ? 'dark' : 'light');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch(e) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<script>
|
<script>
|
||||||
window.$docsify = {
|
window.$docsify = {
|
||||||
name: '网易云音乐 API',
|
name: '网易云音乐 API Enhanced',
|
||||||
repo: 'https://gitlab.com/Binaryify/NeteaseCloudMusicApi',
|
repo: 'https://github.com/neteasecloudmusicapienhanced/api-enhanced',
|
||||||
coverpage: true,
|
coverpage: true,
|
||||||
homepage: 'home.md',
|
homepage: 'home.md',
|
||||||
}
|
}
|
||||||
@@ -36,12 +118,12 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-139996012-1"></script>
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-BPRGR23JEG"></script>
|
||||||
<script>
|
<script>
|
||||||
window.dataLayer = window.dataLayer || [];
|
window.dataLayer = window.dataLayer || [];
|
||||||
function gtag(){dataLayer.push(arguments);}
|
function gtag() { dataLayer.push(arguments); }
|
||||||
gtag('js', new Date());
|
gtag('js', new Date());
|
||||||
|
|
||||||
gtag('config', 'UA-139996012-1');
|
gtag('config', 'G-BPRGR23JEG');
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g>
|
||||||
|
<path fill="none" d="M0 0h24v24H0z"/>
|
||||||
|
<path d="M10.421 11.375c-.294 1.028.012 2.064.784 2.653 1.061.81 2.565.3 2.874-.995.08-.337.103-.722.027-1.056-.23-1.001-.52-1.988-.792-2.996-1.33.154-2.543 1.172-2.893 2.394zm5.548-.287c.273 1.012.285 2.017-.127 3-1.128 2.69-4.721 3.14-6.573.826-1.302-1.627-1.28-3.961.06-5.734.78-1.032 1.804-1.707 3.048-2.054l.379-.104c-.084-.415-.188-.816-.243-1.224-.176-1.317.512-2.503 1.744-3.04 1.226-.535 2.708-.216 3.53.76.406.479.395 1.08-.025 1.464-.412.377-.996.346-1.435-.09-.247-.246-.51-.44-.877-.436-.525.006-.987.418-.945.937.037.468.173.93.3 1.386.022.078.216.135.338.153 1.334.197 2.504.731 3.472 1.676 2.558 2.493 2.861 6.531.672 9.44-1.529 2.032-3.61 3.168-6.127 3.409-4.621.44-8.664-2.53-9.7-7.058C2.515 10.255 4.84 5.831 8.795 4.25c.586-.234 1.143-.031 1.371.498.232.537-.019 1.086-.61 1.35-2.368 1.06-3.817 2.855-4.215 5.424-.533 3.433 1.656 6.776 5 7.72 2.723.77 5.658-.166 7.308-2.33 1.586-2.08 1.4-5.099-.427-6.873a3.979 3.979 0 0 0-1.823-1.013c.198.716.389 1.388.57 2.062z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
@@ -1,132 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>eapi 参数和返回内容解析</title>
|
|
||||||
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.decode-result {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-top: 10px;
|
|
||||||
height: 300px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app" class="p-5 flex flex-col">
|
|
||||||
<h1 class="text-2xl font-bold mb-5">eapi 参数和返回内容解析</h1>
|
|
||||||
<textarea class="border border-gray-300 p-3 mb-5" v-model="hexString" rows="10"></textarea>
|
|
||||||
<div class="flex gap-3 mb-4">
|
|
||||||
<button @click="decrypt" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
|
||||||
解密
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click="sendToApi"
|
|
||||||
:disabled="!canSend"
|
|
||||||
:class="[
|
|
||||||
'bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded',
|
|
||||||
{ 'opacity-50 cursor-not-allowed pointer-events-none': !canSend },
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
填入 API 调试
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3">
|
|
||||||
<input type="radio" id="format" name="format" v-model="isReq" value="true">
|
|
||||||
<label for="format" class="ml-2">请求数据request params(针对请求数据的 params)</label>
|
|
||||||
<input type="radio" id="noFormat" name="format" v-model="isReq" value="false" class="ml-5">
|
|
||||||
<label for="noFormat" class="ml-2">返回数据 response 二进制数据(针对返回内容解析)</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>解密结果:
|
|
||||||
<pre class="decode-result">{{ JSON.stringify(JSON.parse(result), null, 2) }}</pre>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p>使用例子:</p>
|
|
||||||
<img src="/static/eapi_params.png" />
|
|
||||||
<img src="/static/eapi_response.png" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios"></script>
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const app = Vue.createApp({
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hexString: 'AD96DDB984491E79B6F429DD650C6E2AE524627AC223AC9A123C66BB0997965950FED137544A93DFC718E16F57C8C121AF537086F395570A5602A3922366D11964DAFACD7830AACABF62E5650E67F457E79C1D2E13502391FC3487216CC5BF8681843FCB8E05559487EB18AAC1BE0EFEA4F7B6A050478366153A9426C238B8869600B275704555A9EB94C92E4F3FDABE9E0BCE07645410D0AA7B675698A4CAE6CD3620633ABF0B849A4244CC8DFC5DB2646D5EA9B3954E62BFEF19AFEAFDDC34E55C3E9A1DD3167CF53D443617108141',
|
|
||||||
result: '{}',
|
|
||||||
isReq: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.decrypt()
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isRequestMode() {
|
|
||||||
return this.isReq === true || this.isReq === 'true'
|
|
||||||
},
|
|
||||||
canSend() {
|
|
||||||
if (!this.isRequestMode) return false
|
|
||||||
if (!this.result || this.result === '{}' || this.result === 'null') return false
|
|
||||||
try {
|
|
||||||
JSON.parse(this.result)
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async decrypt() {
|
|
||||||
try {
|
|
||||||
const res = await axios({
|
|
||||||
url: `/eapi/decrypt?isReq=${this.isReq}×tamp=${Date.now()}`,
|
|
||||||
method: 'post',
|
|
||||||
data: {
|
|
||||||
hexString: this.hexString
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.result = JSON.stringify(res.data.data)
|
|
||||||
console.log(res.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
alert(error?.response?.data?.message || '解密失败,数据格式错误')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sendToApi() {
|
|
||||||
if (!this.canSend) return
|
|
||||||
const payload = JSON.parse(this.result)
|
|
||||||
const params = new URLSearchParams()
|
|
||||||
params.set('uri', payload.uri || payload.url || payload.path || '')
|
|
||||||
params.set('crypto', 'eapi')
|
|
||||||
const data =
|
|
||||||
payload.params ||
|
|
||||||
payload.data ||
|
|
||||||
payload.body ||
|
|
||||||
payload.payload ||
|
|
||||||
payload.request ||
|
|
||||||
{}
|
|
||||||
params.set('data', JSON.stringify(data))
|
|
||||||
window.open(`/api.html?${params.toString()}`, '_blank')
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
app.mount('#app')
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,44 +1,40 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh">
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>home</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
<head>
|
<body>
|
||||||
<meta charset="UTF-8" />
|
<div>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<a href="/qrlogin-nocookie.html"> 如果没登录,请先登录 </a>
|
||||||
<title>home</title>
|
</div>
|
||||||
</head>
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
|
<script>
|
||||||
<body>
|
async function main() {
|
||||||
<div>
|
const res = await axios({
|
||||||
<a href="/qrlogin-nocookie.html">
|
url: `/homepage/block/page`,
|
||||||
如果没登录,请先登录
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
|
||||||
<script>
|
|
||||||
async function main() {
|
|
||||||
const res = await axios({
|
|
||||||
url: `/homepage/block/page`,
|
|
||||||
data: {
|
|
||||||
cookie: localStorage.getItem('cookie'),
|
|
||||||
},
|
|
||||||
method: 'post',
|
|
||||||
})
|
|
||||||
let cursor = ''
|
|
||||||
console.log(res.data.data)
|
|
||||||
if (res.data.data.hasMore) {
|
|
||||||
cursor = res.data.data.cursor
|
|
||||||
const res2 = await axios({
|
|
||||||
url: `/homepage/block/page?time=${Date.now()}`,
|
|
||||||
data: {
|
data: {
|
||||||
cookie: localStorage.getItem('cookie'),
|
cookie: localStorage.getItem('cookie'),
|
||||||
cursor: cursor,
|
|
||||||
},
|
},
|
||||||
method: 'post',
|
method: 'post',
|
||||||
})
|
})
|
||||||
|
let cursor = ''
|
||||||
|
console.info(res.data.data)
|
||||||
|
if (res.data.data.hasMore) {
|
||||||
|
cursor = res.data.data.cursor
|
||||||
|
const res2 = await axios({
|
||||||
|
url: `/homepage/block/page?time=${Date.now()}`,
|
||||||
|
data: {
|
||||||
|
cookie: localStorage.getItem('cookie'),
|
||||||
|
cursor: cursor,
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
main()
|
||||||
main()
|
</script>
|
||||||
</script>
|
</body>
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,138 +1,126 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<title>网易云音乐 API</title>
|
<link rel="icon" href="docs/netease.png">
|
||||||
|
<title>网易云音乐 API Enhanced</title>
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
:root {
|
||||||
height: 100vh;
|
--fg: #333;
|
||||||
width: 100vw;
|
--muted: #666;
|
||||||
margin: 0;
|
--border: #ddd;
|
||||||
padding: 0;
|
--bg: #f5f5f5;
|
||||||
background: #f9f9f9;
|
--panel: #ffffff;
|
||||||
font-family: Arial, sans-serif;
|
--accent: #333;
|
||||||
color: #333;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
h1 {
|
html, body { height: 100%; }
|
||||||
font-size: 2.5em;
|
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; }
|
||||||
margin-bottom: 1em;
|
.container { max-width: 960px; margin: 40px auto; padding: 0 20px; }
|
||||||
color: #42b983;
|
@media (max-width: 480px) {
|
||||||
|
.container { margin: 20px auto; padding: 0 16px; }
|
||||||
|
header.site-header h1 { font-size: 22px; }
|
||||||
|
.block { padding: 16px; }
|
||||||
}
|
}
|
||||||
|
header.site-header { margin-bottom: 24px; }
|
||||||
p {
|
header.site-header h1 { font-size: 28px; font-weight: 600; margin: 0; }
|
||||||
font-size: 1.2em;
|
.badge { display: inline-block; margin-left: 8px; padding: 4px 10px; border: 1px solid var(--border); border-radius: 12px; font-size: 12px; color: var(--muted); }
|
||||||
margin-bottom: 2em;
|
.sub { margin-top: 8px; color: var(--muted); font-size: 14px; }
|
||||||
color: #666;
|
.block { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); }
|
||||||
|
.block h2 { margin: 0 0 12px; font-size: 18px; font-weight: 600; }
|
||||||
|
.kvs { display: grid; grid-template-columns: 100px 1fr; gap: 8px 12px; align-items: start; }
|
||||||
|
.kvs div:first-child { color: var(--muted); flex-shrink: 0; }
|
||||||
|
.kvs div:last-child { word-break: break-all; overflow-wrap: anywhere; min-width: 0; overflow: hidden; }
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.kvs { grid-template-columns: 1fr; gap: 4px 12px; }
|
||||||
|
.kvs div:first-child { font-weight: 500; }
|
||||||
}
|
}
|
||||||
|
ul.links { list-style: none; padding: 0; margin: 0; }
|
||||||
a {
|
ul.links li { margin: 8px 0; }
|
||||||
color: #42b983;
|
ul.links a { color: var(--fg); text-decoration: none; border-bottom: 1px dotted var(--border); transition: all 0.2s ease; }
|
||||||
text-decoration: none;
|
ul.links a:hover { color: var(--accent); border-bottom-color: var(--accent); }
|
||||||
transition: color 0.3s ease;
|
pre { margin: 0; background: #f9f9f9; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; }
|
||||||
}
|
code { font-family: 'Courier New', monospace; font-size: 13px; }
|
||||||
|
@media (max-width: 480px) {
|
||||||
a:hover {
|
code { font-size: 12px; }
|
||||||
color: #329d6e;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
text-align: left;
|
|
||||||
max-width: 600px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin: 0.5em 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.number {
|
|
||||||
margin-right: 10px;
|
|
||||||
color: #999;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background: #fff;
|
|
||||||
padding: 2em;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
max-height: 80vh; /* 设置最大高度 */
|
|
||||||
overflow-y: auto; /* 启用垂直滚动条 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.version {
|
|
||||||
margin-top: 0.5em;
|
|
||||||
font-size: 1.0em;
|
|
||||||
color: #666;
|
|
||||||
}
|
}
|
||||||
|
footer.site-footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); text-align: center; }
|
||||||
|
footer.site-footer a { color: var(--fg); text-decoration: none; transition: color 0.2s ease; }
|
||||||
|
footer.site-footer a:hover { color: var(--accent); }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<main class="container">
|
||||||
<h1>网易云音乐 API 二改<span id="api-version"></span></h1>
|
<header class="site-header">
|
||||||
<p>当你看到这个页面时,这个服务已经成功跑起来了~</p>
|
<h1>网易云音乐 API Enhanced <span id="api-version" class="badge"></span></h1>
|
||||||
<p>本项目基于原版API二改, 添加了解灰接口</p>
|
<p class="sub">🔍 A revival project for NeteaseCloudMusicApi Node.js Api Services || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护</p>
|
||||||
<p>本项目更新是及时的</p>
|
</header>
|
||||||
<p class="current-url"><span id="current-url"></span></p>
|
|
||||||
<a href="/docs">查看文档</a>
|
<section class="block">
|
||||||
<h2>例子:</h2>
|
<h2>状态</h2>
|
||||||
<ul id="example-list">
|
<div class="kvs">
|
||||||
<li><a href="./search?keywords=海阔天空">搜索</a></li>
|
<div>Base URL</div><div id="base-url">—</div>
|
||||||
<li><a href="./comment/music?id=186016&limit=1">歌曲评论</a></li>
|
<div>当前页</div><div id="current-url">—</div>
|
||||||
<li><a href="./dj/program?rid=336355127">电台节目</a></li>
|
</div>
|
||||||
<li><a href="./qrlogin.html">二维码登录</a></li>
|
</section>
|
||||||
<li><a href="./audio_match_demo/index.html">听歌识曲</a></li>
|
|
||||||
<li><a href="./cloud.html">云盘上传</a></li>
|
<section class="block">
|
||||||
<li><a href="./eapi_decrypt.html">eapi 参数/返回值解析</a></li>
|
<h2>文档</h2>
|
||||||
<li><a href="./api.html">API 调试界面</a></li>
|
<p><a href="/docs" target="_blank">查看在线文档</a></p>
|
||||||
<li><a href="./playlist_import.html">歌单导入工具</a></li>
|
</section>
|
||||||
</ul>
|
|
||||||
</div>
|
<section class="block">
|
||||||
|
<h2>常用接口</h2>
|
||||||
|
<ul class="links">
|
||||||
|
<li><a href="/search?keywords=妖精小姐的魔法邀约">搜索音乐: <code>GET /search</code></a></li>
|
||||||
|
<li><a href="/song/detail?ids=2756058128">获取音乐详情: <code>GET /song/detail</code></a></li>
|
||||||
|
<li><a href="/comment/music?id=2756058128&limit=1">获取音乐评论: <code>GET /comment/music</code></a></li>
|
||||||
|
<li><a href="/song/url/v1?id=2756058128&level=exhigh">获取音乐播放链接: <code>GET /song/url/v1</code></a></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="block">
|
||||||
|
<h2>调试部分</h2>
|
||||||
|
<pre><code>curl -s {origin}/inner/version
|
||||||
|
curl -s {origin}/search?keywords=网易云</code></pre>
|
||||||
|
<div style="margin-top:10px; line-height:2;">
|
||||||
|
<a href="/api.html">交互式调试</a> ·
|
||||||
|
<a href="/qrlogin.html">二维码登录示例</a> ·
|
||||||
|
<a href="/unblock_test.html">解灰测试</a> ·
|
||||||
|
<a href="/audio_match_demo/index.html">听歌识曲 Demo</a> ·
|
||||||
|
<a href="/cloud.html">云盘上传</a> ·
|
||||||
|
<a href="/playlist_import.html">歌单导入</a> ·
|
||||||
|
<a href="/api_decrypt.html">API 解密</a> ·
|
||||||
|
<a href="/listen_together_host.html">一起听示例</a> ·
|
||||||
|
<a href="/playlist_cover_update.html">更新歌单封面示例</a> ·
|
||||||
|
<a href="/avatar_update.html">头像更新示例</a> ·
|
||||||
|
<a href="/scrobble.html">听歌打卡示例</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<a href="https://github.com/neteasecloudmusicapienhanced/api-enhanced" target="_blank">GitHub</a>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const currentUrlElement = document.getElementById('current-url');
|
var origin = window.location.origin;
|
||||||
currentUrlElement.textContent = window.location.href;
|
document.getElementById('base-url').textContent = origin;
|
||||||
|
document.getElementById('current-url').textContent = window.location.href;
|
||||||
|
|
||||||
const exampleList = document.getElementById('example-list');
|
fetch('/inner/version', { method: 'POST' })
|
||||||
const items = exampleList.getElementsByTagName('li');
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (data) {
|
||||||
for (let i = 0; i < items.length; i++) {
|
var v = data && data.data && data.data.version;
|
||||||
const number = document.createElement('span');
|
if (v) document.getElementById('api-version').textContent = 'v' + v;
|
||||||
number.className = 'number';
|
var pre = document.querySelector('pre code');
|
||||||
number.textContent = (i + 1) + '. ';
|
if (pre) pre.textContent = pre.textContent.replace(/\{origin\}/g, origin);
|
||||||
items[i].prepend(number);
|
})
|
||||||
}
|
.catch(function () {});
|
||||||
|
|
||||||
// 获取API版本号
|
|
||||||
axios({
|
|
||||||
url: '/inner/version',
|
|
||||||
method: 'post',
|
|
||||||
data: {},
|
|
||||||
}).then((res) => {
|
|
||||||
const body = res.data;
|
|
||||||
const version = body.data.version;
|
|
||||||
const apiVersionElement = document.getElementById('api-version');
|
|
||||||
apiVersionElement.textContent = ` v${version}`;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,159 +1,481 @@
|
|||||||
<!-- eslint-disable prettier/prettier -->
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh">
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
<head>
|
<meta charset="UTF-8" />
|
||||||
<meta charset="UTF-8" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<title>一起听 - 主机模式</title>
|
||||||
<title>一起听</title>
|
<script src="https://unpkg.com/petite-vue"></script>
|
||||||
<script src="https://unpkg.com/petite-vue"></script>
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
<style>
|
||||||
<link rel="stylesheet" href="https://unpkg.com/mdui@1.0.2/dist/css/mdui.min.css" />
|
* {
|
||||||
<script src="https://unpkg.com/mdui@1.0.2/dist/js/mdui.min.js"></script>
|
margin: 0;
|
||||||
</head>
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
<body class="mdui-container">
|
|
||||||
<div>
|
|
||||||
<a href="/qrlogin.html">
|
|
||||||
如果没登录,请先登录
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<h1>一起听 - 主机模式</h1>
|
|
||||||
<div>消息: {{message}}</div>
|
|
||||||
<audio id="player" autoplay controls>
|
|
||||||
</audio>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<button v-if="!account.login" @click="login">获取登录状态</button>
|
|
||||||
<div>您的当前登录账号为: {{account.nickname}}</div>
|
|
||||||
<br />
|
|
||||||
<div v-if="account.login">
|
|
||||||
<button v-if="!roomInfo.roomId" @click="createRoom">创建房间</button>
|
|
||||||
<details>
|
|
||||||
<summary>加入房间</summary>
|
|
||||||
<div><span>房间ID: </span><input v-model="roomInfo.roomId" /></div>
|
|
||||||
<div><span>邀请者 ID: </span><input v-model="roomInfo.inviterId" /></div>
|
|
||||||
<button @click="joinRoom">点击加入</button>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<div v-if="roomInfo.roomId">
|
|
||||||
<div>分享链接为:
|
|
||||||
https://st.music.163.com/listen-together/share/?songId=1372188635&roomId={{roomInfo.roomId}}&inviterId={{roomInfo.inviterId}}
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<button @click="refreshRoom">刷新房间状态</button>
|
|
||||||
<div>在线用户: </div>
|
|
||||||
<ul class="mdui-list">
|
|
||||||
<li v-for="user in roomInfo.roomUsers" class="mdui-list-item mdui-ripple">
|
|
||||||
<div class="mdui-list-item-avatar">
|
|
||||||
<img :src="user.avatarUrl" />
|
|
||||||
</div>
|
|
||||||
<div class="mdui-list-item-content">{{user.nickname}}</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<button v-if="roomInfo.roomId" @click="closeRoom">关闭房间</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button @click="playTrack">播放</button>
|
|
||||||
<button @click="pauseTrack">暂停</button>
|
|
||||||
<button @click="seekTrack">同步进度</button>
|
|
||||||
<details>
|
|
||||||
<summary>播放列表</summary>
|
|
||||||
<br />
|
|
||||||
<div><span>歌单ID: </span><input v-model="playlistInfo.playlistId" /></div>
|
|
||||||
<button @click="loadPlaylist">加载歌单到播放列表</button>
|
|
||||||
<span>{{playlistInfo.playlistName}}</span>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<div>歌单内容: </div>
|
|
||||||
<ul class="mdui-list">
|
|
||||||
<li @click="gotoTrack(track.id)" v-for="track in playlistInfo.playlistTracks" class="mdui-list-item mdui-ripple">
|
|
||||||
<div class="mdui-list-item-avatar">
|
|
||||||
<img :src="track.al.picUrl" />
|
|
||||||
</div>
|
|
||||||
<div class="mdui-list-item-content">{{track.name}}</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
<script>
|
|
||||||
PetiteVue.createApp({
|
|
||||||
message: '请点击获取登录状态',
|
|
||||||
account: {
|
|
||||||
login: false,
|
|
||||||
userId: 0,
|
|
||||||
nickname: '未登录',
|
|
||||||
},
|
|
||||||
roomInfo: {
|
|
||||||
roomId: null,
|
|
||||||
inviterId: 0,
|
|
||||||
roomUsers: [],
|
|
||||||
},
|
|
||||||
playlistInfo: {
|
|
||||||
playlistId: 0,
|
|
||||||
playlistName: '未获取',
|
|
||||||
playlistTrackIds: [],
|
|
||||||
playlistTracks: [],
|
|
||||||
},
|
|
||||||
playingInfo: {
|
|
||||||
trackId: 0,
|
|
||||||
status: 'PLAY',
|
|
||||||
progress: 1,
|
|
||||||
},
|
|
||||||
clientSeq: 1,
|
|
||||||
login: async function () {
|
|
||||||
const res = await axios({
|
|
||||||
url: `/login/status?timestamp=${new Date().getTime()}`,
|
|
||||||
method: 'get',
|
|
||||||
data: {
|
|
||||||
cookie: localStorage.getItem('cookie'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (res.data.data.code != 200) {
|
|
||||||
alert('请先使用登录 API 登录到网易云音乐')
|
|
||||||
} else {
|
|
||||||
this.account.userId = res.data.data.profile.userId
|
|
||||||
this.account.nickname = res.data.data.profile.nickname
|
|
||||||
this.account.login = true
|
|
||||||
this.message = '成功登录, 请创建房间'
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
joinRoom: async function () {
|
|
||||||
const res = await axios({
|
|
||||||
url: 'listentogether/accept',
|
|
||||||
method: 'post',
|
|
||||||
data: {
|
|
||||||
roomId: this.roomInfo.roomId,
|
|
||||||
inviterId: this.roomInfo.inviterId,
|
|
||||||
cookie: localStorage.getItem('cookie'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
console.log(res)
|
|
||||||
if (res.data.code != 200) {
|
|
||||||
this.message = '加入房间出现问题: ' + res.data.message
|
|
||||||
} else {
|
|
||||||
this.message = '加入房间成功: ' + this.roomInfo.roomId
|
|
||||||
const res2 = await axios({
|
|
||||||
url: 'listentogether/room/check',
|
|
||||||
method: 'post',
|
|
||||||
data: {
|
|
||||||
roomId: this.roomInfo.roomId,
|
|
||||||
cookie: localStorage.getItem('cookie'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
console.log(res2)
|
|
||||||
const res3 = await axios({
|
|
||||||
url: 'listentogether/sync/playlist/get',
|
|
||||||
method: 'post',
|
|
||||||
data: {
|
|
||||||
roomId: this.roomInfo.roomId,
|
|
||||||
cookie: localStorage.getItem('cookie'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
this.playlistInfo.playlistName = "其他人的歌单"
|
body {
|
||||||
this.playlistInfo.playlistTrackIds = res3.data.data.playlist.displayList.result
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link:hover {
|
||||||
|
color: #333;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-player {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"], input[type="number"] {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #555;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-link {
|
||||||
|
padding: 12px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
word-break: break-all;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-list {
|
||||||
|
list-style: none;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-list {
|
||||||
|
list-style: none;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-item:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-cover {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
details {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
details summary {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
details summary:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] summary {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="container">
|
||||||
|
<a href="/qrlogin.html" class="login-link">还没登录?点击登录</a>
|
||||||
|
|
||||||
|
<h1>一起听 - 主机模式</h1>
|
||||||
|
|
||||||
|
<div class="message">消息: {{message}}</div>
|
||||||
|
|
||||||
|
<audio id="player" class="audio-player" autoplay controls></audio>
|
||||||
|
|
||||||
|
<div v-if="!account.login">
|
||||||
|
<button class="btn" @click="login">获取登录状态</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>账号信息</h3>
|
||||||
|
<div>当前登录账号: {{account.nickname}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="account.login" class="section">
|
||||||
|
<h3>房间管理</h3>
|
||||||
|
<div class="control-buttons">
|
||||||
|
<button v-if="!roomInfo.roomId" class="btn" @click="createRoom">创建房间</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>加入房间</summary>
|
||||||
|
<div class="input-group">
|
||||||
|
<label>房间ID:</label>
|
||||||
|
<input v-model="roomInfo.roomId" type="text" />
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label>邀请者ID:</label>
|
||||||
|
<input v-model="roomInfo.inviterId" type="text" />
|
||||||
|
</div>
|
||||||
|
<button class="btn" @click="joinRoom">加入房间</button>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div v-if="roomInfo.roomId" style="margin-top: 16px;">
|
||||||
|
<h4>分享链接</h4>
|
||||||
|
<div class="share-link">
|
||||||
|
https://st.music.163.com/listen-together/share/?songId=1372188635&roomId={{roomInfo.roomId}}&inviterId={{roomInfo.inviterId}}
|
||||||
|
</div>
|
||||||
|
<button class="btn" @click="refreshRoom">刷新房间状态</button>
|
||||||
|
<button class="btn" @click="closeRoom">关闭房间</button>
|
||||||
|
|
||||||
|
<h4 style="margin-top: 16px;">在线用户</h4>
|
||||||
|
<ul class="user-list">
|
||||||
|
<li v-for="user in roomInfo.roomUsers" :key="user.userId" class="user-item">
|
||||||
|
<img :src="user.avatarUrl" class="user-avatar" alt="avatar" />
|
||||||
|
<span class="user-name">{{user.nickname}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>播放控制</h3>
|
||||||
|
<div class="control-buttons">
|
||||||
|
<button class="btn" @click="playTrack">播放</button>
|
||||||
|
<button class="btn" @click="pauseTrack">暂停</button>
|
||||||
|
<button class="btn" @click="seekTrack">同步进度</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>播放列表</summary>
|
||||||
|
<div class="section">
|
||||||
|
<div class="input-group">
|
||||||
|
<label>歌单ID:</label>
|
||||||
|
<input v-model="playlistInfo.playlistId" type="text" />
|
||||||
|
</div>
|
||||||
|
<button class="btn" @click="loadPlaylist">加载歌单</button>
|
||||||
|
<div style="margin-top: 12px; font-size: 14px; color: #555;">
|
||||||
|
歌单名称: {{playlistInfo.playlistName}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 style="margin-top: 16px;">歌单内容</h4>
|
||||||
|
<ul class="track-list">
|
||||||
|
<li
|
||||||
|
@click="gotoTrack(track.id)"
|
||||||
|
v-for="track in playlistInfo.playlistTracks"
|
||||||
|
:key="track.id"
|
||||||
|
class="track-item"
|
||||||
|
>
|
||||||
|
<img :src="track.al.picUrl" class="track-cover" alt="cover" />
|
||||||
|
<span class="track-name">{{track.name}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
PetiteVue.createApp({
|
||||||
|
message: '请点击获取登录状态',
|
||||||
|
account: {
|
||||||
|
login: false,
|
||||||
|
userId: 0,
|
||||||
|
nickname: '未登录',
|
||||||
|
},
|
||||||
|
roomInfo: {
|
||||||
|
roomId: null,
|
||||||
|
inviterId: 0,
|
||||||
|
roomUsers: [],
|
||||||
|
},
|
||||||
|
playlistInfo: {
|
||||||
|
playlistId: 0,
|
||||||
|
playlistName: '未获取',
|
||||||
|
playlistTrackIds: [],
|
||||||
|
playlistTracks: [],
|
||||||
|
},
|
||||||
|
playingInfo: {
|
||||||
|
trackId: 0,
|
||||||
|
status: 'PLAY',
|
||||||
|
progress: 1,
|
||||||
|
},
|
||||||
|
clientSeq: 1,
|
||||||
|
login: async function () {
|
||||||
|
const res = await axios({
|
||||||
|
url: `/login/status?timestamp=${new Date().getTime()}`,
|
||||||
|
method: 'get',
|
||||||
|
data: {
|
||||||
|
cookie: localStorage.getItem('cookie'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (res.data.data.code != 200) {
|
||||||
|
alert('请先使用登录 API 登录到网易云音乐')
|
||||||
|
} else {
|
||||||
|
this.account.userId = res.data.data.profile.userId
|
||||||
|
this.account.nickname = res.data.data.profile.nickname
|
||||||
|
this.account.login = true
|
||||||
|
this.message = '成功登录,请创建房间'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
joinRoom: async function () {
|
||||||
|
const res = await axios({
|
||||||
|
url: 'listentogether/accept',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
roomId: this.roomInfo.roomId,
|
||||||
|
inviterId: this.roomInfo.inviterId,
|
||||||
|
cookie: localStorage.getItem('cookie'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
console.info(res)
|
||||||
|
if (res.data.code != 200) {
|
||||||
|
this.message = '加入房间出现问题: ' + res.data.message
|
||||||
|
} else {
|
||||||
|
this.message = '加入房间成功: ' + this.roomInfo.roomId
|
||||||
|
const res2 = await axios({
|
||||||
|
url: 'listentogether/room/check',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
roomId: this.roomInfo.roomId,
|
||||||
|
cookie: localStorage.getItem('cookie'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
console.info(res2)
|
||||||
|
const res3 = await axios({
|
||||||
|
url: 'listentogether/sync/playlist/get',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
roomId: this.roomInfo.roomId,
|
||||||
|
cookie: localStorage.getItem('cookie'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
this.playlistInfo.playlistName = '其他人的歌单'
|
||||||
|
this.playlistInfo.playlistTrackIds =
|
||||||
|
res3.data.data.playlist.displayList.result.join(',')
|
||||||
|
const resa = await axios({
|
||||||
|
url: '/song/detail',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
ids: this.playlistInfo.playlistTrackIds,
|
||||||
|
cookie: localStorage.getItem('cookie'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
console.info(resa)
|
||||||
|
this.playlistInfo.playlistTracks = resa.data.songs
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createRoom: async function () {
|
||||||
|
const res = await axios({
|
||||||
|
url: 'listentogether/room/create',
|
||||||
|
method: 'get',
|
||||||
|
data: {
|
||||||
|
cookie: localStorage.getItem('cookie'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
console.info(res)
|
||||||
|
if (res.data.code != 200) {
|
||||||
|
this.message = '创建房间出现问题: ' + res.data.message
|
||||||
|
} else {
|
||||||
|
this.message = '创建房间成功: ' + res.data.data.roomInfo.roomId
|
||||||
|
this.roomInfo.inviterId = this.account.userId
|
||||||
|
this.roomInfo.roomId = res.data.data.roomInfo.roomId
|
||||||
|
const res2 = await axios({
|
||||||
|
url: 'listentogether/room/check',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
roomId: this.roomInfo.roomId,
|
||||||
|
cookie: localStorage.getItem('cookie'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
console.info(res2)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refreshRoom: async function () {
|
||||||
|
const res = await axios({
|
||||||
|
url: '/listentogether/status',
|
||||||
|
data: {
|
||||||
|
cookie: localStorage.getItem('cookie'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
console.info(res)
|
||||||
|
if (res.data.code != 200 || !res.data.data.inRoom) {
|
||||||
|
this.message = '房间状态获取失败,可能退出了房间'
|
||||||
|
} else {
|
||||||
|
this.roomInfo.roomUsers = res.data.data.roomInfo.roomUsers
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeRoom: async function () {
|
||||||
|
const res = await axios({
|
||||||
|
url: '/listentogether/end',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
roomId: this.roomInfo.roomId,
|
||||||
|
cookie: localStorage.getItem('cookie'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
console.info(res)
|
||||||
|
if (res.data.code != 200 || !res.data.data.success) {
|
||||||
|
this.message = '房间关闭失败'
|
||||||
|
} else {
|
||||||
|
this.message = '房间关闭成功'
|
||||||
|
this.roomInfo.roomId = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadPlaylist: async function () {
|
||||||
|
const res = await axios({
|
||||||
|
url: '/playlist/detail',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
id: this.playlistInfo.playlistId,
|
||||||
|
cookie: localStorage.getItem('cookie'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
console.info(res)
|
||||||
|
this.playlistInfo.playlistName = res.data.playlist.name
|
||||||
|
this.playlistInfo.playlistTrackIds = res.data.playlist.trackIds
|
||||||
|
.map((track) => track.id)
|
||||||
.join(',')
|
.join(',')
|
||||||
const resa = await axios({
|
const resa = await axios({
|
||||||
url: '/song/detail',
|
url: '/song/detail',
|
||||||
@@ -163,156 +485,68 @@
|
|||||||
cookie: localStorage.getItem('cookie'),
|
cookie: localStorage.getItem('cookie'),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
console.log(resa)
|
console.info(resa)
|
||||||
this.playlistInfo.playlistTracks = resa.data.songs
|
this.playlistInfo.playlistTracks = resa.data.songs
|
||||||
|
if (this.roomInfo.roomId) {
|
||||||
|
const resb = await axios({
|
||||||
}
|
url: 'listentogether/sync/list/command',
|
||||||
|
method: 'post',
|
||||||
},
|
data: {
|
||||||
createRoom: async function () {
|
roomId: this.roomInfo.roomId,
|
||||||
const res = await axios({
|
commandType: 'REPLACE',
|
||||||
url: 'listentogether/room/create',
|
userId: this.account.userId,
|
||||||
method: 'get',
|
version: this.clientSeq++,
|
||||||
data: {
|
playMode: 'ORDER_LOOP',
|
||||||
cookie: localStorage.getItem('cookie'),
|
displayList: this.playlistInfo.playlistTrackIds,
|
||||||
},
|
randomList: this.playlistInfo.playlistTrackIds,
|
||||||
})
|
cookie: localStorage.getItem('cookie'),
|
||||||
console.log(res)
|
},
|
||||||
if (res.data.code != 200) {
|
})
|
||||||
this.message = '创建房间出现问题: ' + res.data.message
|
console.info(resb)
|
||||||
} else {
|
}
|
||||||
this.message = '创建房间成功: ' + res.data.data.roomInfo.roomId
|
},
|
||||||
this.roomInfo.inviterId = this.account.userId
|
gotoTrack: async function (trackId) {
|
||||||
this.roomInfo.roomId = res.data.data.roomInfo.roomId
|
this.playingInfo.trackId = trackId
|
||||||
const res2 = await axios({
|
if (this.roomInfo.roomId) {
|
||||||
url: 'listentogether/room/check',
|
await this.playCommand('GOTO')
|
||||||
|
}
|
||||||
|
document.getElementById('player').src =
|
||||||
|
'https://music.163.com/song/media/outer/url?id=' + trackId + '.mp3'
|
||||||
|
},
|
||||||
|
playTrack: async function () {
|
||||||
|
this.playingInfo.status = 'PLAY'
|
||||||
|
await this.playCommand('PLAY')
|
||||||
|
document.getElementById('player').play()
|
||||||
|
},
|
||||||
|
pauseTrack: async function () {
|
||||||
|
this.playingInfo.status = 'PAUSE'
|
||||||
|
await this.playCommand('PAUSE')
|
||||||
|
document.getElementById('player').pause()
|
||||||
|
},
|
||||||
|
seekTrack: async function () {
|
||||||
|
this.playingInfo.status = 'PLAY'
|
||||||
|
await this.playCommand('seek')
|
||||||
|
document.getElementById('player').play()
|
||||||
|
},
|
||||||
|
playCommand: async function (action) {
|
||||||
|
const res = await axios({
|
||||||
|
url: 'listentogether/play/command',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: {
|
data: {
|
||||||
roomId: this.roomInfo.roomId,
|
roomId: this.roomInfo.roomId,
|
||||||
|
progress: Math.floor(
|
||||||
|
document.getElementById('player').currentTime * 1000,
|
||||||
|
),
|
||||||
|
commandType: action,
|
||||||
|
formerSongId: '-1',
|
||||||
|
targetSongId: this.playingInfo.trackId,
|
||||||
|
clientSeq: this.clientSeq++,
|
||||||
|
playStatus: this.playingInfo.status,
|
||||||
cookie: localStorage.getItem('cookie'),
|
cookie: localStorage.getItem('cookie'),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
console.log(res2)
|
console.info(res)
|
||||||
}
|
},
|
||||||
},
|
}).mount()
|
||||||
refreshRoom: async function () {
|
</script>
|
||||||
const res = await axios({
|
|
||||||
url: '/listentogether/status',
|
|
||||||
data: {
|
|
||||||
cookie: localStorage.getItem('cookie'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
console.log(res)
|
|
||||||
if (res.data.code != 200 || !res.data.data.inRoom) {
|
|
||||||
this.message = '房间状态获取失败, 可能退出了房间'
|
|
||||||
} else {
|
|
||||||
this.roomInfo.roomUsers = res.data.data.roomInfo.roomUsers
|
|
||||||
}
|
|
||||||
},
|
|
||||||
closeRoom: async function () {
|
|
||||||
const res = await axios({
|
|
||||||
url: '/listentogether/end',
|
|
||||||
method: 'post',
|
|
||||||
data: {
|
|
||||||
roomId: this.roomInfo.roomId,
|
|
||||||
cookie: localStorage.getItem('cookie'),
|
|
||||||
|
|
||||||
},
|
|
||||||
})
|
|
||||||
console.log(res)
|
|
||||||
if (res.data.code != 200 || !res.data.data.success) {
|
|
||||||
this.message = '房间关闭失败'
|
|
||||||
} else {
|
|
||||||
this.message = '房间关闭成功'
|
|
||||||
this.roomInfo.roomId = null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
loadPlaylist: async function () {
|
|
||||||
const res = await axios({
|
|
||||||
url: '/playlist/detail',
|
|
||||||
method: 'post',
|
|
||||||
data: {
|
|
||||||
id: this.playlistInfo.playlistId,
|
|
||||||
cookie: localStorage.getItem('cookie'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
console.log(res)
|
|
||||||
this.playlistInfo.playlistName = res.data.playlist.name
|
|
||||||
this.playlistInfo.playlistTrackIds = res.data.playlist.trackIds
|
|
||||||
.map((track) => track.id)
|
|
||||||
.join(',')
|
|
||||||
const resa = await axios({
|
|
||||||
url: '/song/detail',
|
|
||||||
method: 'post',
|
|
||||||
data: {
|
|
||||||
ids: this.playlistInfo.playlistTrackIds,
|
|
||||||
cookie: localStorage.getItem('cookie'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
console.log(resa)
|
|
||||||
this.playlistInfo.playlistTracks = resa.data.songs
|
|
||||||
if (this.roomInfo.roomId) {
|
|
||||||
const resb = await axios({
|
|
||||||
url: 'listentogether/sync/list/command',
|
|
||||||
method: 'post',
|
|
||||||
data: {
|
|
||||||
roomId: this.roomInfo.roomId,
|
|
||||||
commandType: 'REPLACE',
|
|
||||||
userId: this.account.userId,
|
|
||||||
version: this.clientSeq++,
|
|
||||||
playMode: 'ORDER_LOOP',
|
|
||||||
displayList: this.playlistInfo.playlistTrackIds,
|
|
||||||
randomList: this.playlistInfo.playlistTrackIds,
|
|
||||||
cookie: localStorage.getItem('cookie'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
console.log(resb)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
gotoTrack: async function (trackId) {
|
|
||||||
this.playingInfo.trackId = trackId
|
|
||||||
if (this.roomInfo.roomId) {
|
|
||||||
await this.playCommand('GOTO')
|
|
||||||
}
|
|
||||||
document.getElementById('player').src =
|
|
||||||
'https://music.163.com/song/media/outer/url?id=' + trackId + '.mp3'
|
|
||||||
},
|
|
||||||
playTrack: async function () {
|
|
||||||
this.playingInfo.status = 'PLAY'
|
|
||||||
await this.playCommand('PLAY')
|
|
||||||
document.getElementById('player').play()
|
|
||||||
},
|
|
||||||
pauseTrack: async function () {
|
|
||||||
this.playingInfo.status = 'PAUSE'
|
|
||||||
await this.playCommand('PAUSE')
|
|
||||||
document.getElementById('player').pause()
|
|
||||||
},
|
|
||||||
seekTrack: async function () {
|
|
||||||
this.playingInfo.status = 'PLAY'
|
|
||||||
await this.playCommand('seek')
|
|
||||||
document.getElementById('player').play()
|
|
||||||
},
|
|
||||||
playCommand: async function (action) {
|
|
||||||
const res = await axios({
|
|
||||||
url: 'listentogether/play/command',
|
|
||||||
method: 'post',
|
|
||||||
data: {
|
|
||||||
roomId: this.roomInfo.roomId,
|
|
||||||
progress: Math.floor(
|
|
||||||
document.getElementById('player').currentTime * 1000,
|
|
||||||
),
|
|
||||||
commandType: action,
|
|
||||||
formerSongId: '-1',
|
|
||||||
targetSongId: this.playingInfo.trackId,
|
|
||||||
clientSeq: this.clientSeq++,
|
|
||||||
playStatus: this.playingInfo.status,
|
|
||||||
cookie: localStorage.getItem('cookie'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
console.log(res)
|
|
||||||
},
|
|
||||||
}).mount()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,21 +5,143 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>登录</title>
|
<title>登录</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 15px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
background: #999;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #ef4444;
|
||||||
|
background: #fef2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: #10b981;
|
||||||
|
background: #f0fdf4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>登录</h1>
|
||||||
|
<p class="subtitle">使用手机号和密码登录网易云音乐</p>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="phone">手机号</label>
|
||||||
|
<input type="tel" id="phone" placeholder="请输入手机号" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">密码</label>
|
||||||
|
<input type="password" id="password" placeholder="请输入密码" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="loginBtn" class="btn" onclick="handleLogin()">登录</button>
|
||||||
|
|
||||||
|
<div id="result" class="result" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const phone = '' // 这里填手机号
|
|
||||||
const password = '' // 这里填密码
|
|
||||||
const fileUpdateTime = {}
|
const fileUpdateTime = {}
|
||||||
if (!phone || !password) {
|
|
||||||
const msg = '请设置你的手机号码和密码'
|
|
||||||
alert(msg)
|
|
||||||
throw new Error(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function login() {
|
async function login(phone, password) {
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
url: `/login/cellphone`,
|
url: `/login/cellphone`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
@@ -30,17 +152,67 @@
|
|||||||
})
|
})
|
||||||
return res.data.cookie
|
return res.data.cookie
|
||||||
}
|
}
|
||||||
async function main() {
|
|
||||||
const cookieToken = await login()
|
async function handleLogin() {
|
||||||
const res = await axios({
|
const phoneInput = document.getElementById('phone')
|
||||||
url: `/login/status`,
|
const passwordInput = document.getElementById('password')
|
||||||
method: 'post',
|
const loginBtn = document.getElementById('loginBtn')
|
||||||
data: {
|
const resultDiv = document.getElementById('result')
|
||||||
cookie: cookieToken,
|
|
||||||
},
|
const phone = phoneInput.value.trim()
|
||||||
})
|
const password = passwordInput.value
|
||||||
|
|
||||||
|
if (!phone || !password) {
|
||||||
|
showResult('请输入手机号和密码', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loginBtn.disabled = true
|
||||||
|
loginBtn.textContent = '登录中...'
|
||||||
|
showResult('正在登录...', 'info')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cookieToken = await login(phone, password)
|
||||||
|
localStorage.setItem('cookie', cookieToken)
|
||||||
|
|
||||||
|
const res = await axios({
|
||||||
|
url: `/login/status`,
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
cookie: cookieToken,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
showResult(`登录成功!\n${JSON.stringify(res.data, null, 2)}`, 'success')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
const errorMsg = error.response?.data?.message || error.message || '登录失败,请重试'
|
||||||
|
showResult(`登录失败:${errorMsg}`, 'error')
|
||||||
|
} finally {
|
||||||
|
loginBtn.disabled = false
|
||||||
|
loginBtn.textContent = '登录'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
main()
|
|
||||||
|
function showResult(message, type = 'info') {
|
||||||
|
const resultDiv = document.getElementById('result')
|
||||||
|
resultDiv.style.display = 'block'
|
||||||
|
resultDiv.textContent = message
|
||||||
|
resultDiv.className = 'result ' + type
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支持回车登录
|
||||||
|
document.getElementById('password').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleLogin()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.getElementById('phone').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
document.getElementById('password').focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -5,56 +5,297 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>歌单封面上传</title>
|
<title>歌单封面上传</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link:hover {
|
||||||
|
color: #333;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
margin: 0 auto 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 4px solid #fff;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 28px;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn input[type="file"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 4px solid #e0e0e0;
|
||||||
|
border-top-color: #333;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result.success {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result.error {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div>
|
<div class="container">
|
||||||
<a href="/qrlogin-nocookie.html">
|
<h1>歌单封面上传</h1>
|
||||||
如果没登录,请先登录
|
<p class="subtitle">上传自定义歌单封面图片</p>
|
||||||
</a>
|
|
||||||
|
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="playlistId">歌单 ID</label>
|
||||||
|
<input type="text" id="playlistId" placeholder="请输入歌单ID" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cover-wrapper">
|
||||||
|
<img id="playlist_cover" class="cover" src="" alt="歌单封面" />
|
||||||
|
<div class="loading" id="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="upload-btn">
|
||||||
|
选择封面图片
|
||||||
|
<input id="file" type="file" name="filename" accept="image/*" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div id="result" class="result" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
<input id="file" type="file" name="filename" />
|
|
||||||
<img id="playlist_cover" style="height: 200px; width: 200px; border-radius: 50%" />
|
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const playlist_id = ''
|
const loadingOverlay = document.getElementById('loading')
|
||||||
if (!playlist_id) {
|
const playlistIdInput = document.getElementById('playlistId')
|
||||||
const msg = '请设置你的歌单id'
|
const resultDiv = document.getElementById('result')
|
||||||
alert(msg)
|
|
||||||
throw new Error(msg)
|
function showLoading() {
|
||||||
|
loadingOverlay.classList.add('active')
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
function hideLoading() {
|
||||||
async function main() {
|
loadingOverlay.classList.remove('active')
|
||||||
document.querySelector('input[type="file"]').addEventListener(
|
}
|
||||||
'change',
|
|
||||||
function (e) {
|
function showResult(message, type) {
|
||||||
var file = this.files[0]
|
resultDiv.textContent = message
|
||||||
upload(file)
|
resultDiv.className = 'result ' + type
|
||||||
},
|
resultDiv.style.display = 'block'
|
||||||
false,
|
}
|
||||||
)
|
|
||||||
const res = await axios({
|
function hideResult() {
|
||||||
url: `/playlist/detail?id=${playlist_id}×tamp=${Date.now()}`,
|
resultDiv.style.display = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPlaylistCover() {
|
||||||
|
const playlistId = playlistIdInput.value.trim()
|
||||||
|
if (!playlistId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading()
|
||||||
|
hideResult()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await axios({
|
||||||
|
url: `/playlist/detail?id=${playlistId}×tamp=${Date.now()}`,
|
||||||
|
})
|
||||||
|
document.querySelector('#playlist_cover').src = res.data.playlist.coverImgUrl
|
||||||
|
hideResult()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载封面失败:', error)
|
||||||
|
showResult('加载封面失败,请检查歌单ID', 'error')
|
||||||
|
} finally {
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听歌单ID输入变化
|
||||||
|
playlistIdInput.addEventListener('input', function() {
|
||||||
|
loadPlaylistCover()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听文件选择
|
||||||
|
document
|
||||||
|
.querySelector('input[type="file"]')
|
||||||
|
.addEventListener('change', async function (e) {
|
||||||
|
const file = this.files[0]
|
||||||
|
const playlistId = playlistIdInput.value.trim()
|
||||||
|
|
||||||
|
if (!playlistId) {
|
||||||
|
showResult('请先输入歌单ID', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading()
|
||||||
|
hideResult()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await upload(file, playlistId)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传失败:', error)
|
||||||
|
showResult('上传失败,请重试', 'error')
|
||||||
|
} finally {
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
document.querySelector('#playlist_cover').src = res.data.playlist.coverImgUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
async function upload(file) {
|
async function upload(file, playlistId) {
|
||||||
var formData = new FormData()
|
var formData = new FormData()
|
||||||
formData.append('imgFile', file)
|
formData.append('imgFile', file)
|
||||||
const imgSize = await getImgSize(file)
|
const imgSize = await getImgSize(file)
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `/playlist/cover/update?id=${playlist_id}&cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width
|
url: `/playlist/cover/update?id=${playlistId}&cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
||||||
}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
data: formData,
|
data: formData,
|
||||||
})
|
})
|
||||||
document.querySelector('#playlist_cover').src = res.data.data.url
|
document.querySelector('#playlist_cover').src = res.data.data.url
|
||||||
|
showResult('封面上传成功!', 'success')
|
||||||
}
|
}
|
||||||
|
|
||||||
function getImgSize(file) {
|
function getImgSize(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let reader = new FileReader()
|
let reader = new FileReader()
|
||||||
@@ -68,6 +309,12 @@
|
|||||||
height: this.height,
|
height: this.height,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
image.onerror = function() {
|
||||||
|
reject(new Error('图片加载失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.onerror = function() {
|
||||||
|
reject(new Error('文件读取失败'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,261 +2,416 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>歌单导入工具</title>
|
<title>歌单导入工具</title>
|
||||||
<!-- 引入Bootstrap CSS -->
|
<style>
|
||||||
<link href="https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
* {
|
||||||
<!-- 引入Bootstrap JS -->
|
margin: 0;
|
||||||
<script src="https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
padding: 0;
|
||||||
<!-- 引入axios用于发送异步请求 -->
|
box-sizing: border-box;
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
color: #333;
|
||||||
|
border-bottom-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"], textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus, textarea:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 120px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th, table td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group input[type="checkbox"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group label {
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:disabled {
|
||||||
|
background: #f5f5f5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container mt-5">
|
<div class="container">
|
||||||
<h1 class="mb-4">歌单导入工具</h1>
|
<h1>歌单导入工具</h1>
|
||||||
<p>请选择一种导入方式并填写相关信息:</p>
|
<p class="subtitle">请选择一种导入方式并填写相关信息</p>
|
||||||
|
|
||||||
<!-- 表单开始 -->
|
<ul class="tabs" id="importTabs" role="tablist">
|
||||||
<form id="importForm" novalidate>
|
<li role="presentation">
|
||||||
<!-- 选项卡导航 -->
|
<button class="tab-btn active" id="metadata-tab" data-bs-toggle="tab" data-bs-target="#metadata" type="button" role="tab" aria-controls="metadata" aria-selected="true">元数据导入</button>
|
||||||
<ul class="nav nav-tabs mb-3" id="importTabs" role="tablist">
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li role="presentation">
|
||||||
<button class="nav-link active" id="metadata-tab" data-bs-toggle="tab" data-bs-target="#metadata" type="button" role="tab" aria-controls="metadata" aria-selected="true">元数据导入</button>
|
<button class="tab-btn" id="text-tab" data-bs-toggle="tab" data-bs-target="#text" type="button" role="tab" aria-controls="text" aria-selected="false">文字导入</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li role="presentation">
|
||||||
<button class="nav-link" id="text-tab" data-bs-toggle="tab" data-bs-target="#text" type="button" role="tab" aria-controls="text" aria-selected="false">文字导入</button>
|
<button class="tab-btn" id="link-tab" data-bs-toggle="tab" data-bs-target="#link" type="button" role="tab" aria-controls="link" aria-selected="false">链接导入</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
</ul>
|
||||||
<button class="nav-link" id="link-tab" data-bs-toggle="tab" data-bs-target="#link" type="button" role="tab" aria-controls="link" aria-selected="false">链接导入</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<!-- 选项卡面板 -->
|
<div class="tab-content active" id="importTabContent">
|
||||||
<div class="tab-content" id="importTabContent">
|
<!-- 元数据导入 -->
|
||||||
<!-- 元数据导入 -->
|
<div class="tab-content active" id="metadata" role="tabpanel" aria-labelledby="metadata-tab">
|
||||||
<div class="tab-pane fade show active" id="metadata" role="tabpanel" aria-labelledby="metadata-tab">
|
<table>
|
||||||
<table class="table table-bordered mb-3">
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th style="width: 33%">歌曲名称</th>
|
||||||
<th scope="col">歌曲名称</th>
|
<th style="width: 33%">艺术家</th>
|
||||||
<th scope="col">艺术家</th>
|
<th style="width: 33%">专辑</th>
|
||||||
<th scope="col">专辑</th>
|
</tr>
|
||||||
</tr>
|
</thead>
|
||||||
</thead>
|
<tbody id="metadataTableBody">
|
||||||
<tbody id="metadataTableBody">
|
<tr>
|
||||||
<!-- 默认添加一行 -->
|
<td><input type="text" name="name[]" placeholder="歌曲名称"></td>
|
||||||
<tr>
|
<td><input type="text" name="artist[]" placeholder="艺术家"></td>
|
||||||
<td><input type="text" class="form-control" name="name[]" placeholder="歌曲名称"></td>
|
<td><input type="text" name="album[]" placeholder="专辑"></td>
|
||||||
<td><input type="text" class="form-control" name="artist[]" placeholder="艺术家"></td>
|
</tr>
|
||||||
<td><input type="text" class="form-control" name="album[]" placeholder="专辑"></td>
|
</tbody>
|
||||||
</tr>
|
</table>
|
||||||
</tbody>
|
<button type="button" class="btn btn-secondary" id="addMetadataRow">增加歌曲</button>
|
||||||
</table>
|
</div>
|
||||||
<button type="button" class="btn btn-secondary mb-3" id="addMetadataRow">增加歌曲</button>
|
<!-- 文字导入 -->
|
||||||
|
<div class="tab-content" id="text" role="tabpanel" aria-labelledby="text-tab">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="textInput">文字内容</label>
|
||||||
|
<textarea id="textInput" name="text" rows="5" placeholder="请输入歌曲信息,每行一首歌曲"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<!-- 文字导入 -->
|
<div class="form-group">
|
||||||
<div class="tab-pane fade" id="text" role="tabpanel" aria-labelledby="text-tab">
|
<label for="playlistNameInput">歌单名称</label>
|
||||||
<div class="mb-3">
|
<input type="text" id="playlistNameInput" name="playlistName" placeholder="请输入歌单名">
|
||||||
<label for="textInput" class="form-label">文字:</label>
|
|
||||||
<textarea class="form-control" id="textInput" name="text" rows="5"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="playlistNameInput" class="form-label">歌单名:</label>
|
|
||||||
<input type="text" class="form-control" id="playlistNameInput" name="playlistName" placeholder="请输入歌单名">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 链接导入 -->
|
|
||||||
<div class="tab-pane fade" id="link" role="tabpanel" aria-labelledby="link-tab">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="linkInputs" class="form-label">链接:</label>
|
|
||||||
<div id="linkInputsContainer">
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<input type="text" class="form-control" id="linkInput0" name="linkInput0" placeholder="请输入链接">
|
|
||||||
<button type="button" class="btn btn-secondary removeLinkButton" data-index="0">×</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn btn-secondary mb-3" id="addLinkButton">增加链接</button>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="playlistNameLinkInput" class="form-label">歌单名:</label>
|
|
||||||
<input type="text" class="form-control" id="playlistNameLinkInput" name="playlistName" placeholder="请输入歌单名">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 链接导入 -->
|
||||||
<!-- 是否导入我喜欢的音乐 -->
|
<div class="tab-content" id="link" role="tabpanel" aria-labelledby="link-tab">
|
||||||
<div class="form-check">
|
<div class="form-group">
|
||||||
<input class="form-check-input" type="checkbox" value="" id="importStarCheckbox">
|
<label>链接列表</label>
|
||||||
<label class="form-check-label" for="importStarCheckbox">
|
<div id="linkInputsContainer">
|
||||||
导入“我喜欢的音乐”
|
<div class="input-group">
|
||||||
</label>
|
<input type="text" id="linkInput0" name="linkInput0" placeholder="请输入链接">
|
||||||
|
<button type="button" class="btn btn-secondary removeLinkButton" data-index="0">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-secondary" id="addLinkButton" style="margin-top: 8px;">增加链接</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-top: 20px;">
|
||||||
|
<label for="playlistNameLinkInput">歌单名称</label>
|
||||||
|
<input type="text" id="playlistNameLinkInput" name="playlistName" placeholder="请输入歌单名">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 提交按钮 -->
|
<div class="checkbox-group">
|
||||||
<button type="submit" class="btn btn-primary mt-3">导入歌曲</button>
|
<input type="checkbox" value="" id="importStarCheckbox">
|
||||||
</form>
|
<label for="importStarCheckbox">
|
||||||
<!-- 表单结束 -->
|
导入"我喜欢的音乐"
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<button type="submit" class="btn" id="submitBtn">导入歌曲</button>
|
||||||
// 动态增加链接输入框
|
|
||||||
document.getElementById('addLinkButton').addEventListener('click', function() {
|
|
||||||
var container = document.getElementById('linkInputsContainer');
|
|
||||||
var newIndex = container.childElementCount - 1; // 减去非输入框元素的数量
|
|
||||||
var newInput = document.createElement('input');
|
|
||||||
newInput.type = 'text';
|
|
||||||
newInput.className = 'form-control';
|
|
||||||
newInput.id = `linkInput${newIndex}`;
|
|
||||||
newInput.name = `linkInput${newIndex}`;
|
|
||||||
newInput.placeholder = '请输入链接';
|
|
||||||
|
|
||||||
var removeButton = document.createElement('button');
|
|
||||||
removeButton.type = 'button';
|
|
||||||
removeButton.className = 'btn btn-secondary removeLinkButton';
|
|
||||||
removeButton.textContent = '×';
|
|
||||||
removeButton.dataset.index = newIndex.toString();
|
|
||||||
removeButton.addEventListener('click', function() {
|
|
||||||
var group = this.closest('.input-group');
|
|
||||||
container.removeChild(group);
|
|
||||||
});
|
|
||||||
|
|
||||||
var inputGroup = document.createElement('div');
|
|
||||||
inputGroup.className = 'input-group mb-3';
|
|
||||||
inputGroup.appendChild(newInput);
|
|
||||||
inputGroup.appendChild(removeButton);
|
|
||||||
|
|
||||||
container.appendChild(inputGroup);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 动态增加元数据行
|
|
||||||
document.getElementById('addMetadataRow').addEventListener('click', function() {
|
|
||||||
var container = document.getElementById('metadataTableBody');
|
|
||||||
var newRow = document.createElement('tr');
|
|
||||||
|
|
||||||
var nameInput = document.createElement('input');
|
|
||||||
nameInput.type = 'text';
|
|
||||||
nameInput.className = 'form-control';
|
|
||||||
nameInput.name = 'name[]';
|
|
||||||
nameInput.placeholder = '歌曲名称';
|
|
||||||
|
|
||||||
var artistInput = document.createElement('input');
|
|
||||||
artistInput.type = 'text';
|
|
||||||
artistInput.className = 'form-control';
|
|
||||||
artistInput.name = 'artist[]';
|
|
||||||
artistInput.placeholder = '艺术家';
|
|
||||||
|
|
||||||
var albumInput = document.createElement('input');
|
|
||||||
albumInput.type = 'text';
|
|
||||||
albumInput.className = 'form-control';
|
|
||||||
albumInput.name = 'album[]';
|
|
||||||
albumInput.placeholder = '专辑';
|
|
||||||
|
|
||||||
newRow.innerHTML = `
|
|
||||||
<td>${nameInput.outerHTML}</td>
|
|
||||||
<td>${artistInput.outerHTML}</td>
|
|
||||||
<td>${albumInput.outerHTML}</td>
|
|
||||||
`;
|
|
||||||
|
|
||||||
container.appendChild(newRow);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('importForm').addEventListener('submit', async function(event) {
|
|
||||||
// 阻止默认行为
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
// 获取表单值
|
|
||||||
let text = document.getElementById('textInput').value;
|
|
||||||
let links = [];
|
|
||||||
let local = [];
|
|
||||||
let playlistName = '';
|
|
||||||
|
|
||||||
// 获取所有链接输入框的值
|
|
||||||
let linkInputs = document.querySelectorAll('#linkInputsContainer .input-group .form-control');
|
|
||||||
linkInputs.forEach(function(input) {
|
|
||||||
if (input.value.trim() !== '') {
|
|
||||||
links.push(input.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取元数据
|
|
||||||
let metadataRows = document.querySelectorAll('#metadataTableBody tr');
|
|
||||||
metadataRows.forEach(function(row) {
|
|
||||||
let name = row.querySelector('input[name="name[]"]').value;
|
|
||||||
let artist = row.querySelector('input[name="artist[]"]').value;
|
|
||||||
let album = row.querySelector('input[name="album[]"]').value;
|
|
||||||
if (name && artist && album) {
|
|
||||||
local.push({ name, artist, album });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查是否有且只有一个输入字段被填写
|
|
||||||
let filledCount = (text ? 1 : 0) + (links.length > 0 ? 1 : 0) + (local.length > 0 ? 1 : 0);
|
|
||||||
if (filledCount !== 1) {
|
|
||||||
alert("请确保仅填写了一个输入字段!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取歌单名
|
|
||||||
if (document.getElementById('importStarCheckbox').checked) {
|
|
||||||
playlistName = '我喜欢的音乐';
|
|
||||||
} else {
|
|
||||||
playlistName = document.getElementById('playlistNameInput').value ||
|
|
||||||
document.getElementById('playlistNameLinkInput').value ||
|
|
||||||
'导入音乐 ' + new Date().toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建请求参数
|
|
||||||
let data = {};
|
|
||||||
if (text) {
|
|
||||||
data.text = text;
|
|
||||||
data.playlistName = playlistName;
|
|
||||||
} else if (links.length > 0) {
|
|
||||||
data.link = JSON.stringify(links);
|
|
||||||
data.playlistName = playlistName;
|
|
||||||
} else if (local.length > 0) {
|
|
||||||
data.local = JSON.stringify(local);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加额外参数
|
|
||||||
if (document.getElementById('importStarCheckbox').checked) {
|
|
||||||
data.importStarPlaylist = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await axios({
|
|
||||||
url: `/playlist/import/name/task/create?timestamp=${Date.now()}`,
|
|
||||||
method: 'post',
|
|
||||||
data: data,
|
|
||||||
});
|
|
||||||
|
|
||||||
let taskId = res.data?.data?.taskId
|
|
||||||
if (taskId) {
|
|
||||||
alert(`任务创建成功! 正在导入, 请稍等; 任务id:${taskId}`)
|
|
||||||
// const res2 = await axios({
|
|
||||||
// url: `/playlist/import/task/status?timestamp=${Date.now()}`,
|
|
||||||
// method: 'post',
|
|
||||||
// data: {
|
|
||||||
// id: taskId
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// alert(JSON.stringify(res2.data, null, 2));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error:', error);
|
|
||||||
alert('导入失败,请检查您的输入或稍后再试。');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听复选框状态变化
|
|
||||||
document.getElementById('importStarCheckbox').addEventListener('change', function() {
|
|
||||||
let isChecked = this.checked;
|
|
||||||
let playlistNameInputs = document.querySelectorAll('[name="playlistName"]');
|
|
||||||
playlistNameInputs.forEach(function(input) {
|
|
||||||
input.disabled = isChecked;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化时设置歌单名输入框的状态
|
|
||||||
document.getElementById('importStarCheckbox').dispatchEvent(new Event('change'));
|
|
||||||
</script>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// 选项卡切换
|
||||||
|
const tabBtns = document.querySelectorAll('.tab-btn');
|
||||||
|
const tabContents = document.querySelectorAll('.tab-content[id]');
|
||||||
|
|
||||||
|
tabBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
tabBtns.forEach(b => b.classList.remove('active'));
|
||||||
|
tabContents.forEach(c => c.classList.remove('active'));
|
||||||
|
|
||||||
|
btn.classList.add('active');
|
||||||
|
const targetId = btn.getAttribute('data-bs-target');
|
||||||
|
document.getElementById(targetId).classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 动态增加链接输入框
|
||||||
|
document.getElementById('addLinkButton').addEventListener('click', function() {
|
||||||
|
var container = document.getElementById('linkInputsContainer');
|
||||||
|
var newIndex = container.children.length;
|
||||||
|
var newInput = document.createElement('input');
|
||||||
|
newInput.type = 'text';
|
||||||
|
newInput.className = '';
|
||||||
|
newInput.id = `linkInput${newIndex}`;
|
||||||
|
newInput.name = `linkInput${newIndex}`;
|
||||||
|
newInput.placeholder = '请输入链接';
|
||||||
|
newInput.style.cssText = 'flex: 1; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;';
|
||||||
|
|
||||||
|
var removeButton = document.createElement('button');
|
||||||
|
removeButton.type = 'button';
|
||||||
|
removeButton.className = 'btn btn-secondary';
|
||||||
|
removeButton.textContent = '×';
|
||||||
|
removeButton.dataset.index = newIndex.toString();
|
||||||
|
removeButton.addEventListener('click', function() {
|
||||||
|
var group = this.closest('.input-group');
|
||||||
|
container.removeChild(group);
|
||||||
|
});
|
||||||
|
|
||||||
|
var inputGroup = document.createElement('div');
|
||||||
|
inputGroup.className = 'input-group';
|
||||||
|
inputGroup.appendChild(newInput);
|
||||||
|
inputGroup.appendChild(removeButton);
|
||||||
|
|
||||||
|
container.appendChild(inputGroup);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 动态增加元数据行
|
||||||
|
document.getElementById('addMetadataRow').addEventListener('click', function() {
|
||||||
|
var container = document.getElementById('metadataTableBody');
|
||||||
|
var newRow = document.createElement('tr');
|
||||||
|
|
||||||
|
newRow.innerHTML = `
|
||||||
|
<td><input type="text" name="name[]" placeholder="歌曲名称" style="width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;"></td>
|
||||||
|
<td><input type="text" name="artist[]" placeholder="艺术家" style="width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;"></td>
|
||||||
|
<td><input type="text" name="album[]" placeholder="专辑" style="width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;"></td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.appendChild(newRow);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('submitBtn').addEventListener('click', async function() {
|
||||||
|
// 获取表单值
|
||||||
|
let text = document.getElementById('textInput').value;
|
||||||
|
let links = [];
|
||||||
|
let local = [];
|
||||||
|
let playlistName = '';
|
||||||
|
|
||||||
|
// 获取所有链接输入框的值
|
||||||
|
let linkInputs = document.querySelectorAll('#linkInputsContainer .input-group input[type="text"]');
|
||||||
|
linkInputs.forEach(function(input) {
|
||||||
|
if (input.value.trim() !== '') {
|
||||||
|
links.push(input.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取元数据
|
||||||
|
let metadataRows = document.querySelectorAll('#metadataTableBody tr');
|
||||||
|
metadataRows.forEach(function(row) {
|
||||||
|
let name = row.querySelector('input[name="name[]"]').value;
|
||||||
|
let artist = row.querySelector('input[name="artist[]"]').value;
|
||||||
|
let album = row.querySelector('input[name="album[]"]').value;
|
||||||
|
if (name && artist && album) {
|
||||||
|
local.push({ name, artist, album });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查是否有且只有一个输入字段被填写
|
||||||
|
let filledCount = (text ? 1 : 0) + (links.length > 0 ? 1 : 0) + (local.length > 0 ? 1 : 0);
|
||||||
|
if (filledCount !== 1) {
|
||||||
|
alert("请确保仅填写了一个输入字段!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取歌单名
|
||||||
|
if (document.getElementById('importStarCheckbox').checked) {
|
||||||
|
playlistName = '我喜欢的音乐';
|
||||||
|
} else {
|
||||||
|
playlistName = document.getElementById('playlistNameInput').value ||
|
||||||
|
document.getElementById('playlistNameLinkInput').value ||
|
||||||
|
'导入音乐 ' + new Date().toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建请求参数
|
||||||
|
let data = {};
|
||||||
|
if (text) {
|
||||||
|
data.text = text;
|
||||||
|
data.playlistName = playlistName;
|
||||||
|
} else if (links.length > 0) {
|
||||||
|
data.link = JSON.stringify(links);
|
||||||
|
data.playlistName = playlistName;
|
||||||
|
} else if (local.length > 0) {
|
||||||
|
data.local = JSON.stringify(local);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加额外参数
|
||||||
|
if (document.getElementById('importStarCheckbox').checked) {
|
||||||
|
data.importStarPlaylist = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await axios({
|
||||||
|
url: `/playlist/import/name/task/create?timestamp=${Date.now()}`,
|
||||||
|
method: 'post',
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
|
||||||
|
let taskId = res.data?.data?.taskId
|
||||||
|
if (taskId) {
|
||||||
|
alert(`任务创建成功!正在导入,请稍等;任务id:${taskId}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('导入失败,请检查您的输入或稍后再试。');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听复选框状态变化
|
||||||
|
document.getElementById('importStarCheckbox').addEventListener('change', function() {
|
||||||
|
let isChecked = this.checked;
|
||||||
|
let playlistNameInputs = document.querySelectorAll('[name="playlistName"]');
|
||||||
|
playlistNameInputs.forEach(function(input) {
|
||||||
|
input.disabled = isChecked;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化时设置歌单名输入框的状态
|
||||||
|
document.getElementById('importStarCheckbox').dispatchEvent(new Event('change'));
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -5,45 +5,164 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>二维码登录</title>
|
<title>二维码登录</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 48px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 450px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#qrImg {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
text-align: left;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.waiting {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.success {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<img id="qrImg" />
|
<div class="container">
|
||||||
<div id="info" class="info"></div>
|
<h1>二维码登录</h1>
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js
|
<p class="subtitle">使用网易云音乐App扫描二维码登录</p>
|
||||||
"></script>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
|
<div class="qr-wrapper">
|
||||||
|
<img id="qrImg" src="" alt="二维码加载中..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status" class="status waiting">等待扫描...</div>
|
||||||
|
|
||||||
|
<div id="info" class="info"></div>
|
||||||
|
|
||||||
|
<p class="hint">请打开网易云音乐App,扫描上方二维码完成登录</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
|
<script>
|
||||||
async function login() {
|
async function login() {
|
||||||
let timer
|
let timer
|
||||||
let timestamp = Date.now()
|
const statusDiv = document.getElementById('status')
|
||||||
const cookie = localStorage.getItem('cookie')
|
const cookie = localStorage.getItem('cookie')
|
||||||
getLoginStatus(cookie)
|
|
||||||
const res = await axios({
|
|
||||||
url: `/login/qr/key?timestamp=${Date.now()}`,
|
|
||||||
})
|
|
||||||
const key = res.data.data.unikey
|
|
||||||
const res2 = await axios({
|
|
||||||
url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}`,
|
|
||||||
})
|
|
||||||
document.querySelector('#qrImg').src = res2.data.data.qrimg
|
|
||||||
|
|
||||||
timer = setInterval(async () => {
|
updateStatus('加载二维码...', 'waiting')
|
||||||
const statusRes = await checkStatus(key)
|
getLoginStatus(cookie)
|
||||||
if (statusRes.code === 800) {
|
|
||||||
alert('二维码已过期,请重新获取')
|
try {
|
||||||
clearInterval(timer)
|
const res = await axios({
|
||||||
}
|
url: `/login/qr/key?timestamp=${Date.now()}`,
|
||||||
if (statusRes.code === 803) {
|
})
|
||||||
// 这一步会返回cookie
|
const key = res.data.data.unikey
|
||||||
clearInterval(timer)
|
|
||||||
alert('授权登录成功')
|
const res2 = await axios({
|
||||||
await getLoginStatus(statusRes.cookie)
|
url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}`,
|
||||||
localStorage.setItem('cookie', statusRes.cookie)
|
})
|
||||||
}
|
document.querySelector('#qrImg').src = res2.data.data.qrimg
|
||||||
}, 3000)
|
updateStatus('请扫描二维码', 'waiting')
|
||||||
|
|
||||||
|
timer = setInterval(async () => {
|
||||||
|
const statusRes = await checkStatus(key)
|
||||||
|
if (statusRes.code === 800) {
|
||||||
|
updateStatus('二维码已过期,请刷新页面', 'error')
|
||||||
|
clearInterval(timer)
|
||||||
|
} else if (statusRes.code === 801) {
|
||||||
|
updateStatus('等待手机扫码...', 'waiting')
|
||||||
|
} else if (statusRes.code === 802) {
|
||||||
|
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
|
||||||
|
} else if (statusRes.code === 803) {
|
||||||
|
clearInterval(timer)
|
||||||
|
updateStatus('授权登录成功,正在保存信息...', 'success')
|
||||||
|
await getLoginStatus(statusRes.cookie)
|
||||||
|
localStorage.setItem('cookie', statusRes.cookie)
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
updateStatus('二维码加载失败,请刷新页面重试', 'error')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
login()
|
|
||||||
|
|
||||||
async function checkStatus(key) {
|
async function checkStatus(key) {
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
@@ -51,22 +170,30 @@
|
|||||||
})
|
})
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLoginStatus(cookie = '') {
|
async function getLoginStatus(cookie = '') {
|
||||||
const res = await axios({
|
try {
|
||||||
url: `/login/status?timestamp=${Date.now()}`,
|
const res = await axios({
|
||||||
method: 'post',
|
url: `/login/status?timestamp=${Date.now()}`,
|
||||||
data: {
|
method: 'post',
|
||||||
cookie,
|
data: {
|
||||||
},
|
cookie,
|
||||||
})
|
},
|
||||||
document.querySelector('#info').innerText = JSON.stringify(res.data, null, 2)
|
})
|
||||||
|
document.querySelector('#info').textContent = JSON.stringify(res.data, null, 2)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取登录状态失败:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateStatus(message, type) {
|
||||||
|
const statusDiv = document.getElementById('status')
|
||||||
|
statusDiv.textContent = message
|
||||||
|
statusDiv.className = 'status ' + type
|
||||||
|
}
|
||||||
|
|
||||||
|
login()
|
||||||
</script>
|
</script>
|
||||||
<style>
|
|
||||||
.info {
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,67 +5,195 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>二维码登录</title>
|
<title>二维码登录</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 48px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 450px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#qrImg {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
text-align: left;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.waiting {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.success {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<img id="qrImg" />
|
<div class="container">
|
||||||
<div id="info" class="info"></div>
|
<h1>二维码登录</h1>
|
||||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js
|
<p class="subtitle">使用网易云音乐App扫描二维码登录</p>
|
||||||
"></script>
|
|
||||||
|
<div class="qr-wrapper">
|
||||||
|
<img id="qrImg" src="" alt="二维码加载中..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status" class="status waiting">等待扫描...</div>
|
||||||
|
|
||||||
|
<div id="info" class="info"></div>
|
||||||
|
|
||||||
|
<p class="hint">请打开网易云音乐App,扫描上方二维码完成登录</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
async function login() {
|
async function login() {
|
||||||
let timer
|
let timer
|
||||||
let timestamp = Date.now()
|
const statusDiv = document.getElementById('status')
|
||||||
const cookie = localStorage.getItem('cookie')
|
const cookie = localStorage.getItem('cookie')
|
||||||
getLoginStatus(cookie)
|
|
||||||
const res = await axios({
|
|
||||||
url: `/login/qr/key?timestamp=${Date.now()}`,
|
|
||||||
})
|
|
||||||
const key = res.data.data.unikey
|
|
||||||
const res2 = await axios({
|
|
||||||
url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}`,
|
|
||||||
})
|
|
||||||
document.querySelector('#qrImg').src = res2.data.data.qrimg
|
|
||||||
|
|
||||||
timer = setInterval(async () => {
|
updateStatus('加载二维码...', 'waiting')
|
||||||
const statusRes = await checkStatus(key)
|
getLoginStatus(cookie)
|
||||||
if (statusRes.code === 800) {
|
|
||||||
alert('二维码已过期,请重新获取')
|
try {
|
||||||
clearInterval(timer)
|
const res = await axios({
|
||||||
}
|
url: `/login/qr/key?timestamp=${Date.now()}`,
|
||||||
if (statusRes.code === 803) {
|
})
|
||||||
// 这一步会返回cookie
|
const key = res.data.data.unikey
|
||||||
clearInterval(timer)
|
|
||||||
alert('授权登录成功')
|
const res2 = await axios({
|
||||||
await getLoginStatus(statusRes.cookie)
|
url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}&ua=pc`,
|
||||||
localStorage.setItem('cookie', statusRes.cookie)
|
})
|
||||||
}
|
document.querySelector('#qrImg').src = res2.data.data.qrimg
|
||||||
}, 3000)
|
updateStatus('请扫描二维码', 'waiting')
|
||||||
|
|
||||||
|
timer = setInterval(async () => {
|
||||||
|
const statusRes = await checkStatus(key)
|
||||||
|
if (statusRes.code === 800) {
|
||||||
|
updateStatus('二维码已过期,请刷新页面', 'error')
|
||||||
|
clearInterval(timer)
|
||||||
|
} else if (statusRes.code === 801) {
|
||||||
|
updateStatus('等待手机扫码...', 'waiting')
|
||||||
|
} else if (statusRes.code === 802) {
|
||||||
|
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
|
||||||
|
} else if (statusRes.code === 803) {
|
||||||
|
clearInterval(timer)
|
||||||
|
updateStatus('授权登录成功,正在保存信息...', 'success')
|
||||||
|
await getLoginStatus(statusRes.cookie)
|
||||||
|
localStorage.setItem('cookie', statusRes.cookie)
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
updateStatus('二维码加载失败,请刷新页面重试', 'error')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
login()
|
|
||||||
|
|
||||||
async function checkStatus(key) {
|
async function checkStatus(key) {
|
||||||
const res = await axios({
|
const res = await axios({
|
||||||
url: `/login/qr/check?key=${key}×tamp=${Date.now()}`,
|
url: `/login/qr/check?key=${key}×tamp=${Date.now()}&ua=pc`,
|
||||||
})
|
})
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLoginStatus(cookie = '') {
|
async function getLoginStatus(cookie = '') {
|
||||||
const res = await axios({
|
try {
|
||||||
url: `/login/status?timestamp=${Date.now()}`,
|
const res = await axios({
|
||||||
method: 'post',
|
url: `/login/status?timestamp=${Date.now()}&ua=pc`,
|
||||||
data: {
|
method: 'post',
|
||||||
cookie,
|
data: {
|
||||||
},
|
cookie,
|
||||||
})
|
},
|
||||||
document.querySelector('#info').innerText = JSON.stringify(res.data, null, 2)
|
})
|
||||||
|
document.querySelector('#info').textContent = JSON.stringify(res.data, null, 2)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取登录状态失败:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateStatus(message, type) {
|
||||||
|
const statusDiv = document.getElementById('status')
|
||||||
|
statusDiv.textContent = message
|
||||||
|
statusDiv.className = 'status ' + type
|
||||||
|
}
|
||||||
|
|
||||||
|
login()
|
||||||
</script>
|
</script>
|
||||||
<style>
|
|
||||||
.info {
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,480 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>听歌打卡 - 网易云音乐 API Enhanced</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 520px;
|
||||||
|
margin: 40px auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link:hover {
|
||||||
|
color: #333;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Tabs ---- */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border-bottom: 2px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:hover {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
color: #333;
|
||||||
|
border-bottom-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Form ---- */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label .tag {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label .tag.r {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
label .tag.o {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"],
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus,
|
||||||
|
input[type="number"]:focus,
|
||||||
|
textarea:focus {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .form-group {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-fill {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-fill span {
|
||||||
|
color: #0066cc;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-fill span:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result.success {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result.error {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result.info {
|
||||||
|
background: #e0f2fe;
|
||||||
|
color: #0369a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link a {
|
||||||
|
color: #666;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>听歌打卡</h1>
|
||||||
|
<p class="subtitle">同步听歌记录至网易云音乐,增加听歌排行计数</p>
|
||||||
|
|
||||||
|
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
|
||||||
|
|
||||||
|
<!-- Cookie (共享) -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cookie">Cookie <span class="tag o">可选</span></label>
|
||||||
|
<textarea id="cookie" placeholder="留空则默认读取本地存储的登录态" rows="2"></textarea>
|
||||||
|
<div class="quick-fill">
|
||||||
|
<span onclick="loadLocalCookie()">读取本地 Cookie</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn active" data-tab="v0" onclick="switchTab('v0')">原版</button>
|
||||||
|
<button class="tab-btn" data-tab="v1" onclick="switchTab('v1')">V2 (NCBL 加密版)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ Tab: v0 (原版 /scrobble) ============ -->
|
||||||
|
<div class="tab-content active" id="tab-v0">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>歌曲 ID <span class="tag r">必填</span></label>
|
||||||
|
<input type="text" id="v0-songId" placeholder="例如: 2756058128" value="2756058128" />
|
||||||
|
<div class="quick-fill">
|
||||||
|
示例:
|
||||||
|
<span onclick="fillV0('2756058128', '288651229')">妖精小姐的魔法邀约</span>
|
||||||
|
<span onclick="fillV0('2637402867', '251025018')">Echoes of Memoria</span>
|
||||||
|
<span onclick="fillV0('36307815', '3394198')">Lose Control</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>来源 ID (歌单或专辑 ID) <span class="tag r">必填</span></label>
|
||||||
|
<input type="text" id="v0-sourceId" placeholder="例如: 2756058128" value="2756058128" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>播放时间 (秒) <span class="tag o">可选</span></label>
|
||||||
|
<input type="number" id="v0-time" placeholder="300" value="300" />
|
||||||
|
</div>
|
||||||
|
<button class="btn" onclick="submitV0()">立即打卡 (原版)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ Tab: v1 (NCBL 加密版 /scrobble/v1) ============ -->
|
||||||
|
<div class="tab-content" id="tab-v1">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>歌曲 ID <span class="tag r">必填</span></label>
|
||||||
|
<input type="text" id="v1-songId" placeholder="例如: 518066366" value="518066366" />
|
||||||
|
<div class="quick-fill">
|
||||||
|
示例:
|
||||||
|
<span onclick="fillV1('518066366', '36780169', '291')">默认示例</span>
|
||||||
|
<span onclick="fillV1('2756058128', '288651229', '300')">妖精小姐的魔法邀约</span>
|
||||||
|
<span onclick="fillV1('2637402867', '251025018', '300')">Echoes of Memoria</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>播放时间 (秒) <span class="tag r">必填</span></label>
|
||||||
|
<input type="number" id="v1-time" placeholder="例如: 291" value="291" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>来源 ID <span class="tag o">可选</span></label>
|
||||||
|
<input type="text" id="v1-sourceId" placeholder="歌单或专辑 ID" value="36780169" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>歌曲名 <span class="tag o">可选</span></label>
|
||||||
|
<input type="text" id="v1-name" placeholder="歌曲名称" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>艺术家 <span class="tag o">可选</span></label>
|
||||||
|
<input type="text" id="v1-artist" placeholder="歌手名" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>码率 <span class="tag o">可选</span></label>
|
||||||
|
<input type="number" id="v1-bitrate" placeholder="默认 320" value="320" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>音质等级 <span class="tag o">可选</span></label>
|
||||||
|
<input type="text" id="v1-level" placeholder="默认 exhigh" value="exhigh" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>歌曲总时长 (秒) <span class="tag o">可选</span></label>
|
||||||
|
<input type="number" id="v1-total" placeholder="留空则与播放时间相同" />
|
||||||
|
</div>
|
||||||
|
<button class="btn" onclick="submitV1()">立即打卡 (NCBL 加密版)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Result -->
|
||||||
|
<div id="result" class="result" style="display: none;"></div>
|
||||||
|
|
||||||
|
<div class="footer-link">
|
||||||
|
<a href="/">返回首页</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||||
|
<script>
|
||||||
|
/* ---- DOM refs ---- */
|
||||||
|
const cookieInput = document.getElementById('cookie')
|
||||||
|
const resultDiv = document.getElementById('result')
|
||||||
|
|
||||||
|
/* ---- init ---- */
|
||||||
|
if (localStorage.getItem('cookie')) {
|
||||||
|
cookieInput.value = localStorage.getItem('cookie')
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Tab switching ---- */
|
||||||
|
function switchTab(name) {
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === name))
|
||||||
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.toggle('active', c.id === 'tab-' + name))
|
||||||
|
hideResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Cookie ---- */
|
||||||
|
function loadLocalCookie() {
|
||||||
|
const local = localStorage.getItem('cookie')
|
||||||
|
if (local) {
|
||||||
|
cookieInput.value = local
|
||||||
|
showResult('已成功读取本地 Cookie', 'success')
|
||||||
|
} else {
|
||||||
|
showResult('未在本地发现登录 Cookie,请先登录或手动粘贴', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Quick fill ---- */
|
||||||
|
function fillV0(id, sourceId) {
|
||||||
|
document.getElementById('v0-songId').value = id
|
||||||
|
document.getElementById('v0-sourceId').value = sourceId
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillV1(id, sourceId, time) {
|
||||||
|
document.getElementById('v1-songId').value = id
|
||||||
|
document.getElementById('v1-sourceId').value = sourceId
|
||||||
|
document.getElementById('v1-time').value = time
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Result ---- */
|
||||||
|
function hideResult() {
|
||||||
|
resultDiv.style.display = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResult(message, type) {
|
||||||
|
resultDiv.textContent = message
|
||||||
|
resultDiv.className = 'result ' + type
|
||||||
|
resultDiv.style.display = 'block'
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableAll(disabled) {
|
||||||
|
document.querySelectorAll('.btn').forEach(b => b.disabled = disabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Submit: 原版 /scrobble ---- */
|
||||||
|
async function submitV0() {
|
||||||
|
const id = document.getElementById('v0-songId').value.trim()
|
||||||
|
const sourceid = document.getElementById('v0-sourceId').value.trim()
|
||||||
|
const time = document.getElementById('v0-time').value.trim() || '300'
|
||||||
|
|
||||||
|
if (!id) { showResult('请输入歌曲 ID !', 'error'); return }
|
||||||
|
if (!sourceid) { showResult('请输入来源 ID !', 'error'); return }
|
||||||
|
|
||||||
|
disableAll(true)
|
||||||
|
showResult('打卡中,请稍候...', 'info')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = { id, sourceid: sourceid || id, time, timestamp: Date.now() }
|
||||||
|
const cookie = cookieInput.value.trim()
|
||||||
|
if (cookie) params.cookie = cookie
|
||||||
|
|
||||||
|
const res = await axios({ method: 'get', url: '/scrobble', params })
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
showResult('✅ 打卡成功! \n\n' + JSON.stringify(res.data, null, 2), 'success')
|
||||||
|
} else {
|
||||||
|
showResult('请求已发送,返回:\n\n' + JSON.stringify(res.data, null, 2), 'info')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errData = error.response ? error.response.data : { error: error.message }
|
||||||
|
showResult('❌ 打卡失败\n\n' + JSON.stringify(errData, null, 2), 'error')
|
||||||
|
} finally {
|
||||||
|
disableAll(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Submit: NCBL 加密版 /scrobble/v1 ---- */
|
||||||
|
async function submitV1() {
|
||||||
|
const id = document.getElementById('v1-songId').value.trim()
|
||||||
|
const time = document.getElementById('v1-time').value.trim()
|
||||||
|
|
||||||
|
if (!id) { showResult('请输入歌曲 ID !', 'error'); return }
|
||||||
|
if (!time || isNaN(+time) || +time <= 0) { showResult('请输入有效的播放时长(秒)!', 'error'); return }
|
||||||
|
|
||||||
|
disableAll(true)
|
||||||
|
showResult('打卡中(NCBL 加密),请稍候...', 'info')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
id,
|
||||||
|
time,
|
||||||
|
sourceid: document.getElementById('v1-sourceId').value.trim() || undefined,
|
||||||
|
name: document.getElementById('v1-name').value.trim() || undefined,
|
||||||
|
artist: document.getElementById('v1-artist').value.trim() || undefined,
|
||||||
|
bitrate: document.getElementById('v1-bitrate').value.trim() || undefined,
|
||||||
|
level: document.getElementById('v1-level').value.trim() || undefined,
|
||||||
|
total: document.getElementById('v1-total').value.trim() || undefined,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}
|
||||||
|
const cookie = cookieInput.value.trim()
|
||||||
|
if (cookie) params.cookie = cookie
|
||||||
|
|
||||||
|
// 清理 undefined 参数
|
||||||
|
Object.keys(params).forEach(k => params[k] === undefined && delete params[k])
|
||||||
|
|
||||||
|
const res = await axios({ method: 'get', url: '/scrobble/v1', params })
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
showResult('✅ NCBL 加密打卡成功! \n\n' + JSON.stringify(res.data, null, 2), 'success')
|
||||||
|
} else {
|
||||||
|
showResult('请求已发送,返回:\n\n' + JSON.stringify(res.data, null, 2), 'info')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errData = error.response ? error.response.data : { error: error.message }
|
||||||
|
showResult('❌ 打卡失败\n\n' + JSON.stringify(errData, null, 2), 'error')
|
||||||
|
} finally {
|
||||||
|
disableAll(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 1.4 MiB |