From 98a645a45cc54aa9930a94c6258953f5164ae1b2 Mon Sep 17 00:00:00 2001 From: Emmm Monster <58943012+emmmx@users.noreply.github.com> Date: Fri, 25 Dec 2020 22:41:04 +0800 Subject: [PATCH] Add NCM Decoder --- algo/ncm/meta.go | 111 ++++++++++++++++++++ algo/ncm/ncm.go | 257 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 368 insertions(+) create mode 100644 algo/ncm/meta.go create mode 100644 algo/ncm/ncm.go diff --git a/algo/ncm/meta.go b/algo/ncm/meta.go new file mode 100644 index 0000000..f4a6400 --- /dev/null +++ b/algo/ncm/meta.go @@ -0,0 +1,111 @@ +package ncm + +import ( + "github.com/umlock-music/cli/algo/common" + "strings" +) + +type RawMeta interface { + common.Meta + GetFormat() string + GetAlbumImageURL() string +} +type RawMetaMusic struct { + Format string `json:"format"` + MusicID int `json:"musicId"` + MusicName string `json:"musicName"` + Artist [][]interface{} `json:"artist"` + Album string `json:"album"` + AlbumID int `json:"albumId"` + AlbumPicDocID interface{} `json:"albumPicDocId"` + AlbumPic string `json:"albumPic"` + MvID int `json:"mvId"` + Flag int `json:"flag"` + Bitrate int `json:"bitrate"` + Duration int `json:"duration"` + Alias []interface{} `json:"alias"` + TransNames []interface{} `json:"transNames"` +} + +func (m RawMetaMusic) GetAlbumImageURL() string { + return m.AlbumPic +} +func (m RawMetaMusic) GetArtists() (artists []string) { + for _, artist := range m.Artist { + for _, item := range artist { + name, ok := item.(string) + if ok { + artists = append(artists, name) + } + } + } + return +} + +func (m RawMetaMusic) GetTitle() string { + return m.MusicName +} + +func (m RawMetaMusic) GetAlbum() string { + return m.Album +} +func (m RawMetaMusic) GetFormat() string { + return m.Format +} + +type RawMetaDJ struct { + ProgramID int `json:"programId"` + ProgramName string `json:"programName"` + MainMusic RawMetaMusic `json:"mainMusic"` + DjID int `json:"djId"` + DjName string `json:"djName"` + DjAvatarURL string `json:"djAvatarUrl"` + CreateTime int64 `json:"createTime"` + Brand string `json:"brand"` + Serial int `json:"serial"` + ProgramDesc string `json:"programDesc"` + ProgramFeeType int `json:"programFeeType"` + ProgramBuyed bool `json:"programBuyed"` + RadioID int `json:"radioId"` + RadioName string `json:"radioName"` + RadioCategory string `json:"radioCategory"` + RadioCategoryID int `json:"radioCategoryId"` + RadioDesc string `json:"radioDesc"` + RadioFeeType int `json:"radioFeeType"` + RadioFeeScope int `json:"radioFeeScope"` + RadioBuyed bool `json:"radioBuyed"` + RadioPrice int `json:"radioPrice"` + RadioPurchaseCount int `json:"radioPurchaseCount"` +} + +func (m RawMetaDJ) GetArtists() []string { + if m.DjName != "" { + return []string{m.DjName} + } + return m.MainMusic.GetArtists() +} + +func (m RawMetaDJ) GetTitle() string { + if m.ProgramName != "" { + return m.ProgramName + } + return m.MainMusic.GetTitle() +} + +func (m RawMetaDJ) GetAlbum() string { + if m.Brand != "" { + return m.Brand + } + return m.MainMusic.GetAlbum() +} + +func (m RawMetaDJ) GetFormat() string { + return m.MainMusic.GetFormat() +} + +func (m RawMetaDJ) GetAlbumImageURL() string { + if strings.HasPrefix(m.MainMusic.GetAlbumImageURL(), "http") { + return m.MainMusic.GetAlbumImageURL() + } + return m.DjAvatarURL +} diff --git a/algo/ncm/ncm.go b/algo/ncm/ncm.go new file mode 100644 index 0000000..4d2f8ba --- /dev/null +++ b/algo/ncm/ncm.go @@ -0,0 +1,257 @@ +package ncm + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "encoding/json" + "errors" + "github.com/umlock-music/cli/algo/common" + "github.com/umlock-music/cli/internal/logging" + "github.com/umlock-music/cli/internal/utils" + "go.uber.org/zap" + "io/ioutil" + "net/http" + "strings" +) + +var ( + magicHeader = []byte{ + 0x43, 0x54, 0x45, 0x4E, 0x46, 0x44, 0x41, 0x4D} + keyCore = []byte{ + 0x68, 0x7a, 0x48, 0x52, 0x41, 0x6d, 0x73, 0x6f, + 0x35, 0x6b, 0x49, 0x6e, 0x62, 0x61, 0x78, 0x57} + keyMeta = []byte{ + 0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, + 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 0x28} +) + +func NewDecoder(data []byte) *Decoder { + return &Decoder{ + data: data, + fileLength: uint32(len(data)), + } +} + +type Decoder struct { + data []byte + fileLength uint32 + + key []byte + box []byte + + metaRaw []byte + metaType string + Meta RawMeta + + Cover []byte + Audio []byte + + offsetKey uint32 + offsetMeta uint32 + offsetCover uint32 + offsetAudio uint32 +} + +func (f *Decoder) Validate() bool { + if !bytes.Equal(magicHeader, f.data[:len(magicHeader)]) { + return false + } + + /*if status.IsDebug { + logging.Log().Info("the unknown field of the header is: \n" + spew.Sdump(f.data[8:10])) + }*/ + f.offsetKey = 8 + 2 + return true +} + +//todo: 读取前进行检查长度,防止越界 +func (f *Decoder) readKeyData() error { + if f.offsetKey == 0 || f.offsetKey+4 > f.fileLength { + return errors.New("invalid cover data offset") + } + bKeyLen := f.data[f.offsetKey : f.offsetKey+4] + iKeyLen := binary.LittleEndian.Uint32(bKeyLen) + f.offsetMeta = f.offsetKey + 4 + iKeyLen + + bKeyRaw := make([]byte, iKeyLen) + for i := uint32(0); i < iKeyLen; i++ { + bKeyRaw[i] = f.data[i+4+f.offsetKey] ^ 0x64 + } + + f.key = utils.PKCS7UnPadding(utils.DecryptAes128Ecb(bKeyRaw, keyCore))[17:] + return nil +} + +func (f *Decoder) readMetaData() error { + if f.offsetMeta == 0 || f.offsetMeta+4 > f.fileLength { + return errors.New("invalid meta data offset") + } + bMetaLen := f.data[f.offsetMeta : f.offsetMeta+4] + iMetaLen := binary.LittleEndian.Uint32(bMetaLen) + f.offsetCover = f.offsetMeta + 4 + iMetaLen + if iMetaLen == 0 { + return errors.New("no any meta data found") + } + + // Why sub 22: Remove "163 key(Don't modify):" + bKeyRaw := make([]byte, iMetaLen-22) + for i := uint32(0); i < iMetaLen-22; i++ { + bKeyRaw[i] = f.data[f.offsetMeta+4+22+i] ^ 0x63 + } + + cipherText, err := base64.StdEncoding.DecodeString(string(bKeyRaw)) + if err != nil { + return errors.New("decode ncm meta failed: " + err.Error()) + } + metaRaw := utils.PKCS7UnPadding(utils.DecryptAes128Ecb(cipherText, keyMeta)) + sepIdx := bytes.IndexRune(metaRaw, ':') + if sepIdx == -1 { + return errors.New("invalid ncm meta data") + } + + f.metaType = string(metaRaw[:sepIdx]) + f.metaRaw = metaRaw[sepIdx+1:] + return nil +} + +func (f *Decoder) buildKeyBox() { + box := make([]byte, 256) + for i := 0; i < 256; i++ { + box[i] = byte(i) + } + + keyLen := len(f.key) + var j byte + for i := 0; i < 256; i++ { + j = box[i] + j + f.key[i%keyLen] + box[i], box[j] = box[j], box[i] + } + + f.box = make([]byte, 256) + var _i byte + for i := 0; i < 256; i++ { + _i = byte(i + 1) + si := box[_i] + sj := box[_i+si] + f.box[i] = box[si+sj] + } +} + +func (f *Decoder) parseMeta() error { + switch f.metaType { + case "music": + f.Meta = new(RawMetaMusic) + return json.Unmarshal(f.metaRaw, f.Meta) + case "dj": + f.Meta = new(RawMetaDJ) + default: + return errors.New("unknown ncm meta type: " + f.metaType) + } + return nil +} + +func (f *Decoder) readCoverData() error { + if f.offsetCover == 0 || f.offsetCover+13 > f.fileLength { + return errors.New("invalid cover data offset") + } + + coverLenStart := f.offsetCover + 5 + 4 + bCoverLen := f.data[coverLenStart : coverLenStart+4] + + /*if status.IsDebug { + logging.Log().Info("the unknown field of the cover is: \n" + + spew.Sdump(f.data[f.offsetCover:f.offsetCover+5])) + coverLen2 := f.data[f.offsetCover+5 : f.offsetCover+5+4] // it seems that always the same + if !bytes.Equal(coverLen2, bCoverLen) { + logging.Log().Warn("special file found! 2 cover length filed no the same!") + } + }*/ + + iCoverLen := binary.LittleEndian.Uint32(bCoverLen) + f.offsetAudio = coverLenStart + 4 + iCoverLen + if iCoverLen == 0 { + return errors.New("no any cover data found") + } + f.Cover = f.data[coverLenStart+4 : 4+coverLenStart+iCoverLen] + return nil +} + +func (f *Decoder) readAudioData() error { + if f.offsetAudio == 0 || f.offsetAudio > f.fileLength { + return errors.New("invalid audio offset") + } + audioRaw := f.data[f.offsetAudio:] + audioLen := len(audioRaw) + f.Audio = make([]byte, audioLen) + for i := uint32(0); i < uint32(audioLen); i++ { + f.Audio[i] = f.box[i&0xff] ^ audioRaw[i] + } + return nil +} + +func (f *Decoder) Decode() error { + if err := f.readKeyData(); err != nil { + return err + } + f.buildKeyBox() + + err := f.readMetaData() + if err == nil { + err = f.parseMeta() + } + if err != nil { + logging.Log().Warn("parse ncm meta data failed", zap.Error(err)) + } + + err = f.readCoverData() + if err != nil { + logging.Log().Warn("parse ncm cover data failed", zap.Error(err)) + } + + return f.readAudioData() +} + +func (f Decoder) GetAudioExt() string { + if f.Meta != nil { + return f.Meta.GetFormat() + } + return "" +} + +func (f Decoder) GetAudioData() []byte { + return f.Audio +} + +func (f Decoder) GetCoverImage() []byte { + if f.Cover != nil { + return f.Cover + } + { + imgURL := f.Meta.GetAlbumImageURL() + if f.Meta != nil && !strings.HasPrefix(imgURL, "http") { + return nil + } + resp, err := http.Get(imgURL) + if err != nil { + logging.Log().Warn("download image failed", zap.Error(err), zap.String("url", imgURL)) + return nil + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + logging.Log().Warn("download image failed", zap.String("http", resp.Status), + zap.String("url", imgURL)) + return nil + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + logging.Log().Warn("download image failed", zap.Error(err), zap.String("url", imgURL)) + return nil + } + return data + } +} + +func (f Decoder) GetMeta() common.Meta { + return f.Meta +}