Skip to content

Commit acdb78f

Browse files
committed
1、完成网易云音乐转mp3格式
1 parent fcfc9fd commit acdb78f

4 files changed

Lines changed: 342 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ xJavaFxTool是使用javaFx开发的实用小工具集,利用业余时间把工
9999
48. RandomGeneratorTool:随机数生成工具
100100
49. ClipboardHistoryTool:剪贴板历史工具
101101
50. FileSearchTool:文件搜索工具
102-
51. Mp3ConvertTool:Mp3转换工具(目前支持.ncm、.qmc转换为mp3格式)
102+
51. Mp3ConvertTool:Mp3转换工具(目前支持网易云音乐.ncm、QQ音乐.qmc转换为mp3格式)
103103

104104
传输工具目前支持功能如下:
105105

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,13 @@
254254
<version>8.2.0</version>
255255
</dependency>
256256

257+
<!--音频信息提取-->
258+
<dependency>
259+
<groupId>net.jthink</groupId>
260+
<artifactId>jaudiotagger</artifactId>
261+
<version>2.2.3</version>
262+
</dependency>
263+
257264
</dependencies>
258265
<build>
259266
<plugins>

src/main/java/com/xwintop/xJavaFxTool/services/littleTools/Mp3ConvertToolService.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.xwintop.xJavaFxTool.services.littleTools;
22

33
import com.xwintop.xJavaFxTool.controller.littleTools.Mp3ConvertToolController;
4+
import com.xwintop.xJavaFxTool.utils.NcmDump;
45
import com.xwintop.xcore.util.FileUtil;
56
import lombok.Getter;
67
import lombok.Setter;
@@ -47,11 +48,18 @@ public void convertAction() {
4748
} else {
4849
tableDatum.put("convertStatus", "转换失败");
4950
}
51+
} else if (StringUtils.endsWithIgnoreCase(absolutePath, ".ncm")) {
52+
if (convertNcm(absolutePath)) {
53+
tableDatum.put("convertStatus", "转换成功");
54+
} else {
55+
tableDatum.put("convertStatus", "转换失败");
56+
}
5057
}
5158
}
5259
mp3ConvertToolController.getTableViewMain().refresh();
5360
}
5461

62+
//QQ音乐格式转换
5563
private boolean convertQmc(String absolutePath) {
5664
try {
5765
byte[] buffer = FileUtils.readFileToByteArray(new File(absolutePath));
@@ -71,6 +79,15 @@ private boolean convertQmc(String absolutePath) {
7179
return false;
7280
}
7381
}
82+
83+
//网易云音乐格式转换
84+
private boolean convertNcm(String absolutePath) {
85+
if (StringUtils.isEmpty(mp3ConvertToolController.getOutputFolderTextField().getText())) {
86+
return NcmDump.dump(new File(absolutePath), new File(absolutePath).getParentFile());
87+
} else {
88+
return NcmDump.dump(new File(absolutePath), new File(mp3ConvertToolController.getOutputFolderTextField().getText()));
89+
}
90+
}
7491
}
7592

7693
class QmcDecode {
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
package com.xwintop.xJavaFxTool.utils;
2+
3+
import com.google.gson.Gson;
4+
import org.jaudiotagger.audio.AudioFile;
5+
import org.jaudiotagger.audio.AudioFileIO;
6+
import org.jaudiotagger.audio.flac.metadatablock.MetadataBlockDataPicture;
7+
import org.jaudiotagger.tag.FieldKey;
8+
import org.jaudiotagger.tag.Tag;
9+
import org.jaudiotagger.tag.images.Artwork;
10+
import org.jaudiotagger.tag.images.ArtworkFactory;
11+
12+
import javax.crypto.Cipher;
13+
import javax.crypto.spec.SecretKeySpec;
14+
import javax.imageio.ImageIO;
15+
import java.awt.image.BufferedImage;
16+
import java.io.ByteArrayInputStream;
17+
import java.io.File;
18+
import java.io.FileInputStream;
19+
import java.io.FileOutputStream;
20+
import java.util.Base64;
21+
22+
/**
23+
* @ClassName: NcmDump
24+
* @Description: 网易云音乐转换工具
25+
* @author: xufeng
26+
* @date: 2019/8/10 0010 0:06
27+
*/
28+
29+
public class NcmDump {
30+
private static final byte[] CORE_KEY = {0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D, 0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E, 0x62, 0x61, 0x78, 0x57};
31+
private static final byte[] MODIFY_KEY = {0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 0x28};
32+
33+
/**
34+
* dump ncm file to mp3
35+
*
36+
* @param file input file (*.ncm)
37+
* @param outPath output path (folder/directory)
38+
* @return true = success or false = fail
39+
*/
40+
public static boolean dump(File file, File outPath) {
41+
NcmFile ncm = new NcmFile(file, outPath);
42+
if (dumpData(ncm)) {
43+
fixID3(ncm);
44+
return true;
45+
}
46+
return false;
47+
}
48+
49+
public static void fixID3(NcmFile ncm) {
50+
try {
51+
AudioFile f = AudioFileIO.read(ncm.outFile());
52+
Tag tag = f.getTag();
53+
tag.setField(FieldKey.ALBUM, ncm.id3.album);
54+
tag.setField(FieldKey.TITLE, ncm.id3.musicName);
55+
tag.setField(FieldKey.ARTIST, ncm.id3.artist());
56+
if (ncm.albumImage != null) {
57+
BufferedImage image = ImageIO.read(new ByteArrayInputStream(ncm.albumImage));
58+
MetadataBlockDataPicture coverArt = new MetadataBlockDataPicture(ncm.albumImage, 0, //
59+
ncm.albumImageMimeType(), //
60+
"", //
61+
image.getWidth(), //
62+
image.getHeight(), //
63+
image.getColorModel().hasAlpha() ? 32 : 24, //
64+
0);
65+
Artwork artwork = ArtworkFactory.createArtworkFromMetadataBlockDataPicture(coverArt);
66+
tag.setField(tag.createField(artwork));
67+
}
68+
AudioFileIO.write(f);
69+
} catch (Exception e) {
70+
e.printStackTrace();
71+
}
72+
}
73+
74+
private static boolean dumpData(NcmFile ncm) {
75+
FileInputStream fncm = null;
76+
FileOutputStream fout = null;
77+
try {
78+
fncm = new FileInputStream(ncm.fncm);
79+
byte[] b = new byte[1024];
80+
fncm.read(b, 0, 8);
81+
if (!"CTENFDAM".equals(new String(b, 0, 8))) {
82+
return false;
83+
}
84+
fncm.skip(2);
85+
//
86+
fncm.read(b, 0, 4);
87+
int len = b2i(b);
88+
if (len <= 0) {
89+
return false;// broken file
90+
}
91+
byte[] keyData = new byte[len];
92+
fncm.read(keyData);
93+
for (int i = 0; i < keyData.length; i++) {
94+
keyData[i] ^= 0x64;
95+
}
96+
// ID3 -------------------------------------------
97+
fncm.read(b, 0, 4);
98+
ID3Data id3 = readID3(fncm, b2i(b));
99+
ncm.setID3(id3);
100+
// skip crc32(4b) & unused chars(5b) ------------------
101+
fncm.skip(9);
102+
// albumImage -------------------------------------------
103+
fncm.read(b, 0, 4);
104+
int imgSize = b2i(b);
105+
if (imgSize > 0) {
106+
byte[] img_data = new byte[imgSize];
107+
fncm.read(img_data);
108+
ncm.setAlbumImage(img_data);
109+
}
110+
// mp3 data -------------------------------------------
111+
byte[] rawKeyData = aes128EcbDecrypt(keyData, CORE_KEY);
112+
int[] box = buildKeyBox(rawKeyData);
113+
// print(box);
114+
byte[] buffer = new byte[0x4000];
115+
File ftmp = ncm.tmpFile();
116+
fout = new FileOutputStream(ftmp);
117+
boolean first = true;
118+
while (true) {
119+
int n = fncm.read(buffer);
120+
if (n < 0) {
121+
break;
122+
}
123+
for (int i = 0; i < n; i++) {
124+
int j = (i + 1) & 0xff;
125+
int k = (box[j] + j) & 0xff;
126+
k = (box[j] + box[k]) & 0xff;
127+
byte key = (byte) (box[k] & 0xff);
128+
buffer[i] ^= key;
129+
}
130+
if (first) {
131+
ncm.setFormat(getFormat(ncm, buffer));
132+
first = false;
133+
}
134+
fout.write(buffer, 0, n);
135+
}
136+
fout.flush();
137+
fout.close();
138+
fout = null;
139+
fncm.close();
140+
fncm = null;
141+
return ftmp.renameTo(ncm.outFile());
142+
} catch (Exception e) {
143+
e.printStackTrace();
144+
return false;
145+
} finally {
146+
if (fncm != null) {
147+
try {
148+
fncm.close();
149+
} catch (Exception e2) {
150+
}
151+
}
152+
if (fout != null) {
153+
try {
154+
fout.close();
155+
} catch (Exception e2) {
156+
}
157+
}
158+
}
159+
}
160+
161+
private static ID3Data readID3(FileInputStream fncm, int n) {
162+
if (n > 0) {
163+
try {
164+
byte[] modifyData = new byte[n];
165+
fncm.read(modifyData);
166+
for (int i = 0; i < modifyData.length; i++) {
167+
modifyData[i] ^= 0x63;
168+
}
169+
// offset header
170+
byte[] tmp = new byte[modifyData.length - 22];
171+
System.arraycopy(modifyData, 22, tmp, 0, tmp.length);
172+
// escape `163 key(Don't modify):`
173+
byte[] data = Base64.getDecoder().decode(tmp);
174+
byte[] dedata = aes128EcbDecrypt(data, MODIFY_KEY);
175+
// escape `music:`
176+
String json = new String(dedata, 6, dedata.length - 6).trim();
177+
return new Gson().fromJson(json, ID3Data.class);
178+
} catch (Exception e) {
179+
e.printStackTrace();
180+
}
181+
}
182+
return null;
183+
}
184+
185+
private static String getFormat(NcmFile ncm, byte[] b) {
186+
if (b[0] == 0x49 && b[1] == 0x44 && b[2] == 0x33) {
187+
return "mp3";
188+
}
189+
return "flac";
190+
}
191+
192+
private static int b2i(byte[] b) {
193+
int i = 0;
194+
i |= b[0] & 0xff;
195+
i |= (b[1] & 0xff) << 8;
196+
i |= (b[2] & 0xff) << 16;
197+
i |= (b[3] & 0xff) << 24;
198+
return i;
199+
}
200+
201+
private static byte[] aes128EcbDecrypt(byte[] src, byte[] key) throws Exception {
202+
int l = src.length;
203+
int x = l % 16;
204+
byte[] content = src;
205+
if (x != 0) {
206+
content = new byte[l + 16 - x];
207+
System.arraycopy(src, 0, content, 0, l);
208+
}
209+
SecretKeySpec sks = new SecretKeySpec(key, "AES");// 转换为AES专用密钥
210+
Cipher cipher = Cipher.getInstance("AES_128/ECB/NoPadding");// 实例化
211+
cipher.init(Cipher.DECRYPT_MODE, sks);// 使用密钥初始化,设置为解密模式
212+
return cipher.doFinal(content);// 执行操作
213+
}
214+
215+
private static int[] buildKeyBox(byte[] key) {
216+
int key_len = key.length - 17 - key[key.length - 1];
217+
byte[] tmp = new byte[key.length - 17];
218+
System.arraycopy(key, 17, tmp, 0, tmp.length);
219+
key = tmp;
220+
int[] box = new int[256];
221+
for (int i = 0; i < 256; ++i) {
222+
box[i] = (byte) i;
223+
}
224+
225+
int last_byte = 0;
226+
int key_offset = 0;
227+
228+
for (int i = 0; i < 256; ++i) {
229+
int swap = box[i];
230+
int c = (swap + last_byte + key[key_offset++]) & 0xff;
231+
if (key_offset >= key_len) {
232+
key_offset = 0;
233+
}
234+
box[i] = box[c];
235+
box[c] = swap;
236+
last_byte = c;
237+
}
238+
return box;
239+
}
240+
}
241+
242+
class ID3Data {
243+
public String album;
244+
public String musicName;
245+
public String[][] artist;
246+
public String format;
247+
248+
public boolean isMP3() {
249+
return "mp3".equals(format);
250+
}
251+
252+
public String artist() {
253+
StringBuilder sb = new StringBuilder();
254+
for (int i = 0; i < artist.length; i++) {
255+
if (sb.length() > 0) {
256+
sb.append(", ");
257+
}
258+
sb.append(artist[i][0]);
259+
}
260+
return sb.toString();
261+
}
262+
}
263+
264+
class NcmFile {
265+
public final File fncm;
266+
private final File outPath;
267+
private final String fileNameWithoutExt;
268+
private String format;
269+
public ID3Data id3;
270+
public byte[] albumImage;
271+
272+
public NcmFile(File fncm, File outPath) {
273+
this.fncm = fncm;
274+
this.outPath = outPath;
275+
String fname = fncm.getName();
276+
int i = fname.lastIndexOf('.');
277+
fileNameWithoutExt = fname.substring(0, i);
278+
}
279+
280+
public void setID3(ID3Data id3) {
281+
this.id3 = id3;
282+
}
283+
284+
public void setAlbumImage(byte[] albumImage) {
285+
this.albumImage = albumImage;
286+
}
287+
288+
public String albumImageMimeType() {
289+
final byte[] mPNG = {(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};// PNG file header
290+
int l = mPNG.length;
291+
if (albumImage.length > l) {
292+
for (int i = 0; i < l; i++) {
293+
if (albumImage[i] != mPNG[i]) {
294+
return "image/jpg";
295+
}
296+
}
297+
}
298+
return "image/png";
299+
}
300+
301+
public void setFormat(String format) {
302+
this.format = format;
303+
}
304+
305+
public File tmpFile() {
306+
return new File(outPath, fileNameWithoutExt + ".tmp");
307+
}
308+
309+
public File outFile() {
310+
if (id3 != null && id3.format != null) {
311+
return new File(outPath, fileNameWithoutExt + "." + id3.format);
312+
}
313+
return new File(outPath, fileNameWithoutExt + "." + format);
314+
}
315+
316+
}
317+

0 commit comments

Comments
 (0)