Skip to content

Commit 9265c40

Browse files
committed
fix. QQ音乐无法播放VIP歌曲(切换为OGG)
1 parent 5dcfebb commit 9265c40

File tree

6 files changed

+92
-107
lines changed

6 files changed

+92
-107
lines changed

src/main/java/top/gregtao/concerto/command/AuditCommand.java

Lines changed: 0 additions & 87 deletions
This file was deleted.

src/main/java/top/gregtao/concerto/http/qq/QQMusicApiClient.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ public String getMP3Filename(String mid, String mediaMid) {
5353
return "M800" + mid + mediaMid + ".mp3";
5454
}
5555

56+
public String getOggFilename(String mid, String mediaMid) {
57+
return "O400" + mid + mediaMid + ".ogg";
58+
}
59+
5660
public String getQQUin() throws IOException, URISyntaxException {
5761
String uin = this.getCookie("https://u.y.qq.com", "wxuin");
5862
if (!uin.isEmpty()) return uin;
@@ -89,7 +93,7 @@ public String getQQLoginGTK() throws IOException, URISyntaxException {
8993
public String getMusicLink(String mid, String mediaMid) {
9094
try {
9195
String uin = this.getQQUin(), guid = this.generateGuid();
92-
String url = "https://u.y.qq.com/cgi-bin/musicu.fcg?-=getplaysongvkey&format=json&loginUin=" + uin + "&hostUin=0&inCharset=utf-8&needNewCode=0&outCharset=utf-8&platform=yqq.json&data=%7B%22req_0%22%3A%7B%22module%22%3A%22vkey.GetVkeyServer%22%2C%22method%22%3A%22CgiGetVkey%22%2C%22param%22%3A%7B%22filename%22%3A%5B%22" + this.getMP3Filename(mid, mediaMid) + "%22%5D%2C%22guid%22%3A%22" + guid + "%22%2C%22songmid%22%3A%5B%22" + mid + "%22%5D%2C%22songtype%22%3A%5B0%5D%2C%22uin%22%3A%22" + uin + "%22%2C%22loginflag%22%3A1%2C%22platform%22%3A%2220%22%7D%7D%2C%22comm%22%3A%7B%22uin%22%3A" + uin + "%2C%22format%22%3A%22json%22%2C%22ct%22%3A24%2C%22cv%22%3A0%7D%7D";
96+
String url = "https://u.y.qq.com/cgi-bin/musicu.fcg?-=getplaysongvkey&format=json&loginUin=" + uin + "&hostUin=0&inCharset=utf-8&needNewCode=0&outCharset=utf-8&platform=yqq.json&data=%7B%22req_0%22%3A%7B%22module%22%3A%22vkey.GetVkeyServer%22%2C%22method%22%3A%22CgiGetVkey%22%2C%22param%22%3A%7B%22filename%22%3A%5B%22" + this.getOggFilename(mid, mediaMid) + "%22%5D%2C%22guid%22%3A%22" + guid + "%22%2C%22songmid%22%3A%5B%22" + mid + "%22%5D%2C%22songtype%22%3A%5B0%5D%2C%22uin%22%3A%22" + uin + "%22%2C%22loginflag%22%3A1%2C%22platform%22%3A%2220%22%7D%7D%2C%22comm%22%3A%7B%22uin%22%3A" + uin + "%2C%22format%22%3A%22json%22%2C%22ct%22%3A24%2C%22cv%22%3A0%7D%7D";
9397
JsonObject object = parseJson(this.openUApi().url(url).get());
9498
if (object == null) return "";
9599
JsonObject data = object.getAsJsonObject("req_0").getAsJsonObject("data");
@@ -278,9 +282,9 @@ public Pair<ArrayList<Music>, PlaylistMetaData> parsePlaylistJson(JsonObject obj
278282
}
279283

280284
public JsonObject requestSignedApi(String module, String method, String params) throws IOException, URISyntaxException {
281-
String data = "{\"comm\":{\"cv\":4747474,\"ct\":24,\"format\":\"json\",\"inCharset\":\"utf-8\",\"outCharset\":\"utf-8\",\"notice\":0,\"platform\":\"yqq.json\",\"uin\":\"" + this.getQQUin() + "\",\"g_tk_new_20200303\":" + this.getQQLoginGTK() + ",\"g_tk\":" + this.getQQLoginGTK() + ",\"mesh_devops\":\"DevopsBase\"},\"req_1\":{\"module\":\"" + module + "\",\"method\":\"" + method + "\",\"param\":{" + params + "}}}";
285+
String data = "{\"comm\":{\"cv\":4747474,\"ct\":24,\"format\":\"json\",\"needNewCode\":1,\"inCharset\":\"utf-8\",\"outCharset\":\"utf-8\",\"notice\":0,\"platform\":\"yqq.json\",\"uin\":\"" + this.getQQUin() + "\",\"g_tk_new_20200303\":" + this.getQQLoginGTK() + ",\"g_tk\":" + this.getQQLoginGTK() + ",\"mesh_devops\":\"DevopsBase\"},\"req_1\":{\"module\":\"" + module + "\",\"method\":\"" + method + "\",\"param\":{" + params + "}}}";
282286
String url = "https://u.y.qq.com/cgi-bin/musics.fcg?_=" + TextUtil.getCurrentTime() + "&sign=" + QQMusicApiEncrypt.Sign.getSign(data);
283-
JsonObject object = parseJson(this.openUApi().setFixedReferer("https://y.qq.com/").url(url).post(
287+
JsonObject object = parseJson(this.openUApi().setFixedReferer("https://y.qq.com/").setHeader("Origin", "https://y.qq.com").url(url).post(
284288
HttpResponse.BodyHandlers.ofString(),
285289
HttpRequestBuilder.ContentType.FORM,
286290
data
@@ -289,7 +293,8 @@ public JsonObject requestSignedApi(String module, String method, String params)
289293
}
290294

291295
public JsonObject requestSignedApi(String module, String method, Map<?, ?> params) throws IOException, URISyntaxException {
292-
return this.requestSignedApi(module, method, HttpRequestBuilder.ContentType.toJson(params));
296+
String param = HttpRequestBuilder.ContentType.toJson(params);
297+
return this.requestSignedApi(module, method, param.substring(1, param.length() - 1));
293298
}
294299

295300
public Pair<ArrayList<Music>, PlaylistMetaData> getPlayList(String id) throws IOException, URISyntaxException {

src/main/java/top/gregtao/concerto/http/qq/QQMusicApiEncrypt.java

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,72 @@
22

33
import top.gregtao.concerto.util.HashUtil;
44

5+
import java.nio.charset.StandardCharsets;
56
import java.util.*;
67

78
public class QQMusicApiEncrypt {
89

9-
// // Original source by: https://blog.csdn.net/qq_23594799/article/details/111477320, https://blog.csdn.net/qq_37438485/article/details/124420854
10-
//
11-
// private static final String ENCRYPT_STATIC = "CJBPACrRuNy7";
12-
// private static final String PREFIX = "zzb";
13-
//
14-
// /**
15-
// * @param body 需要加密的参数,这是一段请求体数据,为json字符串格式,例如下面的格式,可以抓包获取
16-
// * {"comm":{"ct":24,"cv":0},"vip":{"module":"userInfo…baseinfo_v2","param":{"vec_uin":["3011429848"]}}}
17-
// * @return 加密的方式为固定字串 zza + 10-16位的随机字符串 + (CJBPACrRuNy7 + 请求数据)的MD5值
18-
// */
19-
// public static String getSign(String body){
20-
// return PREFIX + UUID.randomUUID().toString().replaceAll("-", "") + HashUtil.md5(ENCRYPT_STATIC + body);
21-
// }
10+
// Code by: https://github.com/jixunmoe/qmweb-sign, MIT license
11+
public static class ZzcSign {
12+
private static final int[] PART_1_INDEXES = {23, 14, 6, 36, 16, 40, 7, 19};
13+
private static final int[] PART_2_INDEXES = {16, 1, 32, 12, 19, 27, 8, 5};
14+
private static final int[] SCRAMBLE_VALUES = {89, 39, 179, 150, 218, 82, 58, 252, 177, 52, 186, 123, 120, 64, 242, 133, 143, 161, 121, 179};
15+
16+
public static String getSign(String payload) {
17+
String hash = HashUtil.sha1(payload);
18+
19+
StringBuilder part1 = new StringBuilder();
20+
for (int index : PART_1_INDEXES) {
21+
if (index < hash.length()) {
22+
part1.append(hash.charAt(index));
23+
}
24+
}
25+
26+
StringBuilder part2 = new StringBuilder();
27+
for (int index : PART_2_INDEXES) {
28+
if (index < hash.length()) {
29+
part2.append(hash.charAt(index));
30+
}
31+
}
32+
33+
StringBuilder part3 = new StringBuilder();
34+
35+
for (int i = 0; i < SCRAMBLE_VALUES.length; i++) {
36+
int scramble = SCRAMBLE_VALUES[i];
37+
int value = Integer.parseInt(hash.substring(i * 2, i * 2 + 2), 16);
38+
part3.append((char) (scramble ^ value));
39+
}
40+
41+
String b64Part = Base64.getEncoder().encodeToString(part3.toString().getBytes(StandardCharsets.UTF_8));
42+
b64Part = b64Part.replaceAll("[/+=]", "");
43+
44+
// byte[] part3 = new byte[20];
45+
// for (int i = 0; i < SCRAMBLE_VALUES.length; i++) {
46+
// int value = SCRAMBLE_VALUES[i] ^ Integer.parseInt(hash.substring(i * 2, i * 2 + 2), 16);
47+
// part3[i] = (byte) value;
48+
// }
49+
//
50+
// String b64Part = Base64.getEncoder().encodeToString(part3)
51+
// .replaceAll("[/+=]", "")
52+
// .toLowerCase();
53+
54+
return "zzc" + (part1 + b64Part + part2).toLowerCase();
55+
}
56+
}
57+
58+
/**
59+
* Original source by: https://blog.csdn.net/qq_23594799/article/details/111477320, https://blog.csdn.net/qq_37438485/article/details/124420854
60+
* private static final String ENCRYPT_STATIC = "CJBPACrRuNy7";
61+
* private static final String PREFIX = "zzb";
62+
* <p>
63+
* * @param body 需要加密的参数,这是一段请求体数据,为json字符串格式,例如下面的格式,可以抓包获取
64+
* * {"comm":{"ct":24,"cv":0},"vip":{"module":"userInfo…baseinfo_v2","param":{"vec_uin":["3011429848"]}}}
65+
* * @return 加密的方式为固定字串 zza + 10-16位的随机字符串 + (CJBPACrRuNy7 + 请求数据)的MD5值
66+
* <p>
67+
* public static String getSign(String body){
68+
* return PREFIX + UUID.randomUUID().toString().replaceAll("-", "") + HashUtil.md5(ENCRYPT_STATIC + body);
69+
* }
70+
*/
2271
public static class Sign {
2372

2473
private static void test(List<Integer> resNum, int a, int b, int c) {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package top.gregtao.concerto.network;
2+
3+
public class ServerMusicAgent {
4+
5+
6+
}

src/main/java/top/gregtao/concerto/screen/MusicAuditionScreen.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ protected void init() {
4545
ClientPlayerEntity player = MinecraftClient.getInstance().player;
4646
ConcertoListWidget<Pair<Music, UUID>>.Entry entry = this.widget.getSelectedOrNull();
4747
if (player != null && entry != null) {
48-
player.networkHandler.sendChatCommand("audit " + entry.item.getSecond());
48+
player.networkHandler.sendChatCommand("concerto-server audit " + entry.item.getSecond());
4949
this.widget.removeEntryWithoutScrolling(entry);
5050
}
5151
}).position(20, this.height - 30).size(60, 20).build());
@@ -54,15 +54,15 @@ protected void init() {
5454
ClientPlayerEntity player = MinecraftClient.getInstance().player;
5555
ConcertoListWidget<Pair<Music, UUID>>.Entry entry = this.widget.getSelectedOrNull();
5656
if (player != null && entry != null) {
57-
player.networkHandler.sendChatCommand("audit reject " + entry.item.getSecond());
57+
player.networkHandler.sendChatCommand("concerto-server audit reject " + entry.item.getSecond());
5858
this.widget.removeEntryWithoutScrolling(entry);
5959
}
6060
}).position(85, this.height - 30).size(60, 20).build());
6161

6262
this.addDrawableChild(ButtonWidget.builder(Text.translatable("concerto.reject.all"), button -> {
6363
ClientPlayerEntity player = MinecraftClient.getInstance().player;
6464
if (player != null) {
65-
player.networkHandler.sendChatCommand("audit reject all");
65+
player.networkHandler.sendChatCommand("concerto-server audit reject all");
6666
this.widget.clear();
6767
}
6868
}).position(150, this.height - 30).size(60, 20).build());

src/main/java/top/gregtao/concerto/util/HashUtil.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,16 @@ public static String md5(String text) {
1818
throw new RuntimeException("No such md5 algorithm");
1919
}
2020
}
21+
22+
public static String sha1(String text) {
23+
try {
24+
MessageDigest digest = MessageDigest.getInstance("SHA-1");
25+
digest.update(text.getBytes(StandardCharsets.UTF_8));
26+
String sha1 = new BigInteger(1, digest.digest()).toString(16);
27+
if (sha1.length() < 40) sha1 = "0".repeat(40 - sha1.length()) + sha1;
28+
return sha1;
29+
} catch (NoSuchAlgorithmException e) {
30+
throw new RuntimeException("No such sha1 algorithm");
31+
}
32+
}
2133
}

0 commit comments

Comments
 (0)