Add NCM Decoder
parent
bf2846b950
commit
98a645a45c
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue