├── README.md
├── NCM文件转换.exe
├── ncmdump-gui
├── DirectUI.dll
├── DesktopTool.exe
├── taglib-sharp.dll
├── policy.2.0.taglib-sharp.dll
├── DesktopTool.exe.config
└── policy.2.0.taglib-sharp.config
└── ncmdump
├── go.mod
├── README.md
└── main.go
/README.md:
--------------------------------------------------------------------------------
1 | # ncmdump
2 | 保存Github上的ncmdump。
3 |
--------------------------------------------------------------------------------
/NCM文件转换.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmarchive/ncmdump/HEAD/NCM文件转换.exe
--------------------------------------------------------------------------------
/ncmdump-gui/DirectUI.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmarchive/ncmdump/HEAD/ncmdump-gui/DirectUI.dll
--------------------------------------------------------------------------------
/ncmdump-gui/DesktopTool.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmarchive/ncmdump/HEAD/ncmdump-gui/DesktopTool.exe
--------------------------------------------------------------------------------
/ncmdump-gui/taglib-sharp.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmarchive/ncmdump/HEAD/ncmdump-gui/taglib-sharp.dll
--------------------------------------------------------------------------------
/ncmdump-gui/policy.2.0.taglib-sharp.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmarchive/ncmdump/HEAD/ncmdump-gui/policy.2.0.taglib-sharp.dll
--------------------------------------------------------------------------------
/ncmdump-gui/DesktopTool.exe.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ncmdump/go.mod:
--------------------------------------------------------------------------------
1 | module ncmdump
2 |
3 | require (
4 | github.com/bogem/id3v2 v1.1.1
5 | github.com/go-flac/flacpicture v0.2.0
6 | github.com/go-flac/flacvorbis v0.1.0
7 | github.com/go-flac/go-flac v0.2.0
8 | golang.org/x/text v0.3.0 // indirect
9 | )
10 |
--------------------------------------------------------------------------------
/ncmdump-gui/policy.2.0.taglib-sharp.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ncmdump/README.md:
--------------------------------------------------------------------------------
1 | # ncmdump
2 |
3 | ## 简介
4 |
5 | 本项目完全参考[anonymous5l/ncmdump](https://github.com/anonymous5l/ncmdump)实现,起初是为了能在windows下快速编译和运行
6 |
7 |
8 | ## 如何使用?
9 |
10 | - 下载可执行程序[ncmdump](https://github.com/yoki123/ncmdump/releases),将ncm文件或文件夹拖到执行程序`ncmdump`上即可
11 | - 也可以使用命令
12 |
13 | ncmdump [files/dirs]
14 |
15 | ## 感谢
16 |
17 | - [@anonymous5l](https://github.com/anonymous5l)提供的原版ncmdump
18 | - [@eternal-flame-AD](https://github.com/eternal-flame-AD)提供的flac封面写入和目录自动寻找ncm文件
19 |
--------------------------------------------------------------------------------
/ncmdump/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "crypto/aes"
6 | "encoding/base64"
7 | "encoding/binary"
8 | "encoding/json"
9 | "io"
10 | "io/ioutil"
11 | "log"
12 | "net/http"
13 | "os"
14 | "path/filepath"
15 | "strings"
16 | "time"
17 |
18 | "github.com/go-flac/flacpicture"
19 | "github.com/go-flac/flacvorbis"
20 | "github.com/go-flac/go-flac"
21 |
22 | "github.com/bogem/id3v2"
23 | )
24 |
25 | //
26 | var (
27 | aesCoreKey = []byte{0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D, 0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E, 0x62, 0x61, 0x78, 0x57}
28 | aesModifyKey = []byte{0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 0x28}
29 | )
30 |
31 | type MetaInfo struct {
32 | MusicID int `json:"musicId"`
33 | MusicName string `json:"musicName"`
34 | Artist [][]interface{} `json:"artist"` // [[string,int],]
35 | AlbumID int `json:"albumId"`
36 | Album string `json:"album"`
37 | AlbumPicDocID interface{} `json:"albumPicDocId"` // string or int
38 | AlbumPic string `json:"albumPic"`
39 | BitRate int `json:"bitrate"`
40 | Mp3DocID string `json:"mp3DocId"`
41 | Duration int `json:"duration"`
42 | MvID int `json:"mvId"`
43 | Alias []string `json:"alias"`
44 | TransNames []interface{} `json:"transNames"`
45 | Format string `json:"format"`
46 | }
47 |
48 | func buildKeyBox(key []byte) []byte {
49 | box := make([]byte, 256)
50 | for i := 0; i < 256; i++ {
51 | box[i] = byte(i)
52 | }
53 | keyLen := byte(len(key))
54 | var c, lastByte, keyOffset byte
55 | for i := 0; i < 256; i++ {
56 | c = (box[i] + lastByte + key[keyOffset]) & 0xff
57 | keyOffset++
58 | if keyOffset >= keyLen {
59 | keyOffset = 0
60 | }
61 | box[i], box[c] = box[c], box[i]
62 | lastByte = c
63 | }
64 | return box
65 | }
66 |
67 | func fixBlockSize(src []byte) []byte {
68 | return src[:len(src)/aes.BlockSize*aes.BlockSize]
69 | }
70 |
71 | func containPNGHeader(data []byte) bool {
72 | if len(data) < 8 {
73 | return false
74 | }
75 | return string(data[:8]) == string([]byte{137, 80, 78, 71, 13, 10, 26, 10})
76 | }
77 |
78 | func PKCS7UnPadding(src []byte) []byte {
79 | length := len(src)
80 | unpadding := int(src[length-1])
81 | return src[:(length - unpadding)]
82 | }
83 |
84 | func decryptAes128Ecb(key, data []byte) ([]byte, error) {
85 | block, err := aes.NewCipher([]byte(key))
86 | if err != nil {
87 | return nil, err
88 | }
89 | dataLen := len(data)
90 | decrypted := make([]byte, dataLen)
91 | bs := block.BlockSize()
92 | for i := 0; i <= dataLen-bs; i += bs {
93 | block.Decrypt(decrypted[i:i+bs], data[i:i+bs])
94 | }
95 | return PKCS7UnPadding(decrypted), nil
96 | }
97 |
98 | func readUint32(rBuf []byte, fp *os.File) uint32 {
99 | _, err := fp.Read(rBuf)
100 | checkError(err)
101 | return binary.LittleEndian.Uint32(rBuf)
102 | }
103 |
104 | func checkError(err error) {
105 | if err != nil {
106 | log.Panic(err)
107 | }
108 | }
109 |
110 | func processFile(name string) {
111 | fp, err := os.Open(name)
112 | if err != nil {
113 | log.Println(err)
114 | return
115 | }
116 | defer fp.Close()
117 |
118 | var rBuf = make([]byte, 4)
119 | uLen := readUint32(rBuf, fp)
120 |
121 | if uLen != 0x4e455443 {
122 | log.Println("isn't netease cloud music copyright file!")
123 | return
124 | }
125 |
126 | uLen = readUint32(rBuf, fp)
127 | if uLen != 0x4d414446 {
128 | log.Println("isn't netease cloud music copyright file!")
129 | return
130 | }
131 |
132 | fp.Seek(2, 1)
133 | uLen = readUint32(rBuf, fp)
134 |
135 | var keyData = make([]byte, uLen)
136 | _, err = fp.Read(keyData)
137 | checkError(err)
138 |
139 | for i := range keyData {
140 | keyData[i] ^= 0x64
141 | }
142 |
143 | deKeyData, err := decryptAes128Ecb(aesCoreKey, fixBlockSize(keyData))
144 | checkError(err)
145 |
146 | // 17 = len("neteasecloudmusic")
147 | deKeyData = deKeyData[17:]
148 |
149 | uLen = readUint32(rBuf, fp)
150 | var modifyData = make([]byte, uLen)
151 | _, err = fp.Read(modifyData)
152 | checkError(err)
153 |
154 | for i := range modifyData {
155 | modifyData[i] ^= 0x63
156 | }
157 | deModifyData := make([]byte, base64.StdEncoding.DecodedLen(len(modifyData)-22))
158 | _, err = base64.StdEncoding.Decode(deModifyData, modifyData[22:])
159 | checkError(err)
160 |
161 | deData, err := decryptAes128Ecb(aesModifyKey, fixBlockSize(deModifyData))
162 | checkError(err)
163 |
164 | // 6 = len("music:")
165 | deData = deData[6:]
166 |
167 | var meta MetaInfo
168 | err = json.Unmarshal(deData, &meta)
169 | checkError(err)
170 |
171 | // crc32 check
172 | fp.Seek(4, 1)
173 | fp.Seek(5, 1)
174 |
175 | imgLen := readUint32(rBuf, fp)
176 |
177 | imgData := func() []byte {
178 | if imgLen > 0 {
179 | data := make([]byte, imgLen)
180 | _, err = fp.Read(data)
181 | checkError(err)
182 | return data
183 | }
184 | return nil
185 | }()
186 |
187 | box := buildKeyBox(deKeyData)
188 | n := 0x8000
189 |
190 | outputName := strings.Replace(name, ".ncm", "."+meta.Format, -1)
191 |
192 | fpOut, err := os.OpenFile(outputName, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
193 | checkError(err)
194 |
195 | var tb = make([]byte, n)
196 | for {
197 | _, err := fp.Read(tb)
198 | if err == io.EOF { // read EOF
199 | break
200 | } else if err != nil {
201 | log.Println(err)
202 | }
203 | for i := 0; i < n; i++ {
204 | j := byte((i + 1) & 0xff)
205 | tb[i] ^= box[(box[j]+box[(box[j]+j)&0xff])&0xff]
206 | }
207 | _, err = fpOut.Write(tb)
208 | if err != nil {
209 | log.Println(err)
210 | }
211 | }
212 | fpOut.Close()
213 |
214 | log.Println(outputName)
215 | switch meta.Format {
216 | case "mp3":
217 | addMP3Tag(outputName, imgData, &meta)
218 | case "flac":
219 | addFLACTag(outputName, imgData, &meta)
220 | }
221 | }
222 |
223 | func fetchUrl(url string) []byte {
224 | req, err := http.NewRequest("GET", url, bytes.NewBuffer([]byte{}))
225 | if err != nil {
226 | log.Println(err)
227 | return nil
228 | }
229 | client := http.Client{
230 | Timeout: 30 * time.Second,
231 | }
232 | res, err := client.Do(req)
233 | if err != nil {
234 | log.Println(err)
235 | return nil
236 | }
237 | if res.StatusCode != http.StatusOK {
238 | log.Printf("Failed to download album pic: remote returned %d\n", res.StatusCode)
239 | return nil
240 | }
241 | defer res.Body.Close()
242 | data, err := ioutil.ReadAll(res.Body)
243 | if err != nil {
244 | log.Println(err)
245 | return nil
246 | }
247 | return data
248 | }
249 |
250 | func addFLACTag(fileName string, imgData []byte, meta *MetaInfo) {
251 | f, err := flac.ParseFile(fileName)
252 | if err != nil {
253 | log.Println(err)
254 | return
255 | }
256 |
257 | if imgData == nil && meta.AlbumPic != "" {
258 | imgData = fetchUrl(meta.AlbumPic)
259 | }
260 |
261 | if imgData != nil {
262 | picMIME := "image/jpeg"
263 | if containPNGHeader(imgData) {
264 | picMIME = "image/png"
265 | }
266 | picture, err := flacpicture.NewFromImageData(flacpicture.PictureTypeFrontCover, "Front cover", imgData, picMIME)
267 | if err == nil {
268 | picturemeta := picture.Marshal()
269 | f.Meta = append(f.Meta, &picturemeta)
270 | }
271 | } else if meta.AlbumPic != "" {
272 | picture := &flacpicture.MetadataBlockPicture{
273 | PictureType: flacpicture.PictureTypeFrontCover,
274 | MIME: "-->",
275 | Description: "Front cover",
276 | ImageData: []byte(meta.AlbumPic),
277 | }
278 | picturemeta := picture.Marshal()
279 | f.Meta = append(f.Meta, &picturemeta)
280 | }
281 |
282 | var cmtmeta *flac.MetaDataBlock
283 | for _, m := range f.Meta {
284 | if m.Type == flac.VorbisComment {
285 | cmtmeta = m
286 | break
287 | }
288 | }
289 | var cmts *flacvorbis.MetaDataBlockVorbisComment
290 | if cmtmeta != nil {
291 | cmts, err = flacvorbis.ParseFromMetaDataBlock(*cmtmeta)
292 | if err != nil {
293 | log.Println(err)
294 | return
295 | }
296 | } else {
297 | cmts = flacvorbis.New()
298 | }
299 |
300 | if titles, err := cmts.Get(flacvorbis.FIELD_TITLE); err != nil {
301 | log.Println(err)
302 | return
303 | } else if len(titles) == 0 {
304 | if meta.MusicName != "" {
305 | log.Println("Adding music name")
306 | cmts.Add(flacvorbis.FIELD_TITLE, meta.MusicName)
307 | }
308 | }
309 |
310 | if albums, err := cmts.Get(flacvorbis.FIELD_ALBUM); err != nil {
311 | log.Println(err)
312 | return
313 | } else if len(albums) == 0 {
314 | if meta.Album != "" {
315 | log.Println("Adding album name")
316 | cmts.Add(flacvorbis.FIELD_ALBUM, meta.Album)
317 | }
318 | }
319 |
320 | if artists, err := cmts.Get(flacvorbis.FIELD_ARTIST); err != nil {
321 | log.Println(err)
322 | return
323 | } else if len(artists) == 0 {
324 | artist := func() []string {
325 | res := make([]string, 0)
326 | if len(meta.Artist) < 1 {
327 | return nil
328 | }
329 | for _, artist := range meta.Artist {
330 | res = append(res, artist[0].(string))
331 | }
332 | return res
333 | }()
334 | if artist != nil {
335 | log.Println("Adding artist")
336 | for _, name := range artist {
337 | cmts.Add(flacvorbis.FIELD_ARTIST, name)
338 | }
339 | }
340 | }
341 | res := cmts.Marshal()
342 | if cmtmeta != nil {
343 | *cmtmeta = res
344 | } else {
345 | f.Meta = append(f.Meta, &res)
346 | }
347 |
348 | f.Save(fileName)
349 | }
350 |
351 | func addMP3Tag(fileName string, imgData []byte, meta *MetaInfo) {
352 | tag, err := id3v2.Open(fileName, id3v2.Options{Parse: true})
353 | if err != nil {
354 | log.Println(err)
355 | return
356 | }
357 | defer tag.Close()
358 |
359 | if imgData == nil && meta.AlbumPic != "" {
360 | imgData = fetchUrl(meta.AlbumPic)
361 | }
362 |
363 | if imgData != nil {
364 | picMIME := "image/jpeg"
365 | if containPNGHeader(imgData) {
366 | picMIME = "image/png"
367 | }
368 | pic := id3v2.PictureFrame{
369 | Encoding: id3v2.EncodingISO,
370 | MimeType: picMIME,
371 | PictureType: id3v2.PTFrontCover,
372 | Description: "Front cover",
373 | Picture: imgData,
374 | }
375 | tag.AddAttachedPicture(pic)
376 | } else if meta.AlbumPic != "" {
377 | pic := id3v2.PictureFrame{
378 | Encoding: id3v2.EncodingISO,
379 | MimeType: "-->",
380 | PictureType: id3v2.PTFrontCover,
381 | Description: "Front cover",
382 | Picture: []byte(meta.AlbumPic),
383 | }
384 | tag.AddAttachedPicture(pic)
385 | }
386 |
387 | if tag.GetTextFrame("TIT2").Text == "" {
388 | if meta.MusicName != "" {
389 | log.Println("Adding music name")
390 | tag.AddTextFrame("TIT2", id3v2.EncodingUTF8, meta.MusicName)
391 | }
392 | }
393 |
394 | if tag.GetTextFrame("TALB").Text == "" {
395 | if meta.Album != "" {
396 | log.Println("Adding album name")
397 | tag.AddTextFrame("TALB", id3v2.EncodingUTF8, meta.Album)
398 | }
399 | }
400 |
401 | if tag.GetTextFrame("TPE1").Text == "" {
402 | artist := func() []string {
403 | res := make([]string, 0)
404 | if len(meta.Artist) < 1 {
405 | return nil
406 | }
407 | for _, artist := range meta.Artist {
408 | res = append(res, artist[0].(string))
409 | }
410 | return res
411 | }()
412 | if artist != nil {
413 | log.Println("Adding artist")
414 | for _, name := range artist {
415 | tag.AddTextFrame("TPE1", id3v2.EncodingUTF8, name)
416 | }
417 | }
418 | }
419 |
420 | if err = tag.Save(); err != nil {
421 | log.Println(err)
422 | }
423 | }
424 |
425 | func main() {
426 | argc := len(os.Args)
427 | if argc <= 1 {
428 | log.Println("please input file path!")
429 | return
430 | }
431 | files := make([]string, 0)
432 |
433 | for i := 0; i < argc-1; i++ {
434 | path := os.Args[i+1]
435 | if info, err := os.Stat(path); err != nil {
436 | log.Fatalf("Path %s does not exist.", info)
437 | } else if info.IsDir() {
438 | filelist, err := ioutil.ReadDir(path)
439 | if err != nil {
440 | log.Fatalf("Error while reading %s: %s", path, err.Error())
441 | }
442 | for _, f := range filelist {
443 | files = append(files, filepath.Join(path, "./", f.Name()))
444 | }
445 | } else {
446 | files = append(files, path)
447 | }
448 | }
449 |
450 | for _, filename := range files {
451 | if filepath.Ext(filename) == ".ncm" {
452 | processFile(filename)
453 | } else {
454 | log.Printf("Skipping %s: not ncm file\n", filename)
455 | }
456 | }
457 |
458 | }
459 |
--------------------------------------------------------------------------------