├── 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 | --------------------------------------------------------------------------------