Files
2026-06-08 22:09:35 +08:00

429 lines
10 KiB
Go

package ncmcrypt
import (
"bytes"
"encoding/base64"
"encoding/binary"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"git.taurusxin.com/taurusxin/ncmdump-go/utils"
"github.com/bogem/id3v2/v2"
"github.com/go-flac/flacpicture"
"github.com/go-flac/flacvorbis"
"github.com/go-flac/go-flac"
)
type NcmFormat = string
const (
Mp3 NcmFormat = "mp3"
Flac NcmFormat = "flac"
)
type NeteaseCloudMusic struct {
sCoreKey [17]byte
sModifyKey [17]byte
mPng [8]byte
mFilePath string
mDumpFilePath string
mFormat NcmFormat
mImageData []byte
mReader io.ReadSeeker
mKeyBox [256]byte
mMetadata *NeteaseClousMusicMetadata
mAlbumPicUrl string
}
func (ncm *NeteaseCloudMusic) Close() {
if closer, ok := ncm.mReader.(io.Closer); ok {
closer.Close()
}
ncm.mReader = nil
}
func (ncm *NeteaseCloudMusic) read(buffer *[]byte, size int) int {
if len(*buffer) < size {
*buffer = make([]byte, size)
}
res, err := ncm.mReader.Read((*buffer)[:size])
if err != nil {
return 0
}
return res
}
func (ncm *NeteaseCloudMusic) skip(n int64) error {
_, err := ncm.mReader.Seek(n, io.SeekCurrent)
return err
}
func (ncm *NeteaseCloudMusic) buildKeyBox(key []byte, keyLen int) {
for i := 0; i < 256; i++ {
ncm.mKeyBox[i] = byte(i)
}
var swap uint8 = 0
var c uint8 = 0
var lastByte uint8 = 0
var keyOffset uint8 = 0
for i := 0; i < 256; i++ {
swap = ncm.mKeyBox[i]
c = (swap + lastByte + key[keyOffset]) & 0xff
keyOffset++
if int(keyOffset) >= keyLen {
keyOffset = 0
}
ncm.mKeyBox[i] = ncm.mKeyBox[c]
ncm.mKeyBox[c] = swap
lastByte = c
}
}
func (ncm *NeteaseCloudMusic) mimeType() string {
if bytes.HasPrefix(ncm.mImageData, ncm.mPng[:]) {
return "image/png"
}
return "image/jpeg"
}
func (ncm *NeteaseCloudMusic) Dump(targetDir string) (bool, error) {
ncm.mDumpFilePath = ncm.mFilePath
var outputStream *os.File
buffer := make([]byte, 0x8000)
findFormatFlag := false
for {
n := ncm.read(&buffer, len(buffer))
if n == 0 {
break
}
for i := 0; i < n; i++ {
j := (i + 1) & 0xff
buffer[i] ^= ncm.mKeyBox[(ncm.mKeyBox[j]+ncm.mKeyBox[(int(ncm.mKeyBox[j])+j)&0xff])&0xff]
}
if !findFormatFlag {
if buffer[0] == 0x49 && buffer[1] == 0x44 && buffer[2] == 0x33 {
ncm.mFormat = Mp3
ncm.mDumpFilePath = utils.ReplaceExtension(ncm.mDumpFilePath, ".mp3")
} else {
ncm.mFormat = Flac
ncm.mDumpFilePath = utils.ReplaceExtension(ncm.mDumpFilePath, ".flac")
}
if targetDir != "" {
ncm.mDumpFilePath = filepath.Join(targetDir, filepath.Base(ncm.mDumpFilePath))
}
findFormatFlag = true
output, err := os.Create(ncm.mDumpFilePath)
if err != nil {
return false, fmt.Errorf("create output file failed")
}
outputStream = output
}
outputStream.Write(buffer[:n])
}
outputStream.Close()
return true, nil
}
func (ncm *NeteaseCloudMusic) DumpStream(w io.Writer, onFormat func(NcmFormat)) error {
buffer := make([]byte, 0x8000)
findFormatFlag := false
for {
n := ncm.read(&buffer, len(buffer))
if n == 0 {
break
}
for i := 0; i < n; i++ {
j := (i + 1) & 0xff
buffer[i] ^= ncm.mKeyBox[(ncm.mKeyBox[j]+ncm.mKeyBox[(int(ncm.mKeyBox[j])+j)&0xff])&0xff]
}
if !findFormatFlag {
if buffer[0] == 0x49 && buffer[1] == 0x44 && buffer[2] == 0x33 {
ncm.mFormat = Mp3
} else {
ncm.mFormat = Flac
}
findFormatFlag = true
if onFormat != nil {
onFormat(ncm.mFormat)
}
}
if _, err := w.Write(buffer[:n]); err != nil {
return fmt.Errorf("stream write failed: %w", err)
}
}
return nil
}
func (ncm *NeteaseCloudMusic) GetFormat() NcmFormat {
return ncm.mFormat
}
func (ncm *NeteaseCloudMusic) GetMetadata() map[string]string {
m := map[string]string{}
if ncm.mMetadata != nil {
m["title"] = ncm.mMetadata.mName
m["artist"] = ncm.mMetadata.mArtist
m["album"] = ncm.mMetadata.mAlbum
}
if ncm.mAlbumPicUrl != "" {
m["coverUrl"] = ncm.mAlbumPicUrl
}
return m
}
func (ncm *NeteaseCloudMusic) FixMetadata(fetchAlbumImageFromRemote bool) (bool, error) {
if ncm.mMetadata == nil {
return true, nil
}
if len(ncm.mImageData) <= 0 && fetchAlbumImageFromRemote {
resp, err := http.Get(ncm.mAlbumPicUrl)
if err != nil {
return false, err
}
if resp != nil {
if resp.StatusCode == http.StatusOK {
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return false, err
}
ncm.mImageData = bodyBytes
}
}
}
switch ncm.mFormat {
case Mp3:
audioFile, err := id3v2.Open(ncm.mDumpFilePath, id3v2.Options{Parse: true})
if err != nil {
return false, err
}
defer audioFile.Close()
audioFile.SetDefaultEncoding(id3v2.EncodingUTF8)
audioFile.SetTitle(ncm.mMetadata.mName)
audioFile.SetArtist(ncm.mMetadata.mArtist)
audioFile.SetAlbum(ncm.mMetadata.mAlbum)
if len(ncm.mImageData) > 0 {
pic := id3v2.PictureFrame{
Encoding: id3v2.EncodingUTF8,
MimeType: ncm.mimeType(),
PictureType: id3v2.PTFrontCover,
Description: "",
Picture: ncm.mImageData,
}
audioFile.AddAttachedPicture(pic)
}
err = audioFile.Save()
if err != nil {
return false, err
}
case Flac:
audioFile, err := flac.ParseFile(ncm.mDumpFilePath)
if err != nil {
return false, err
}
if len(ncm.mImageData) > 0 {
pic, err := flacpicture.NewFromImageData(flacpicture.PictureTypeFrontCover, "",
ncm.mImageData, ncm.mimeType())
if err != nil {
return false, err
}
pictureMeta := pic.Marshal()
audioFile.Meta = append(audioFile.Meta, &pictureMeta)
}
var cmts *flacvorbis.MetaDataBlockVorbisComment
var cmtIdx int
for idx, meta := range audioFile.Meta {
if meta.Type == flac.VorbisComment {
cmts, err = flacvorbis.ParseFromMetaDataBlock(*meta)
cmtIdx = idx
if err != nil {
return false, err
}
}
}
if cmts == nil && cmtIdx > 0 {
cmts = flacvorbis.New()
}
if res, _ := cmts.Get(flacvorbis.FIELD_TITLE); len(res) == 0 {
_ = cmts.Add(flacvorbis.FIELD_TITLE, ncm.mMetadata.mName)
}
if res, _ := cmts.Get(flacvorbis.FIELD_ARTIST); len(res) == 0 {
_ = cmts.Add(flacvorbis.FIELD_ARTIST, ncm.mMetadata.mArtist)
}
if res, _ := cmts.Get(flacvorbis.FIELD_ALBUM); len(res) == 0 {
_ = cmts.Add(flacvorbis.FIELD_ALBUM, ncm.mMetadata.mAlbum)
}
cmtsmeta := cmts.Marshal()
if cmtIdx > 0 {
audioFile.Meta[cmtIdx] = &cmtsmeta
} else {
audioFile.Meta = append(audioFile.Meta, &cmtsmeta)
}
err = audioFile.Save(ncm.mDumpFilePath)
if err != nil {
return false, err
}
}
return true, nil
}
func (ncm *NeteaseCloudMusic) GetDumpFilePath() string {
path, err := filepath.Abs(ncm.mDumpFilePath)
if err != nil {
return ncm.mDumpFilePath
}
return path
}
func parseNcmHeader(ncm *NeteaseCloudMusic) error {
header := make([]byte, 4)
if ncm.read(&header, 4) != 4 {
return fmt.Errorf("read header failed")
}
if int(binary.LittleEndian.Uint32(header)) != 0x4E455443 {
return fmt.Errorf("not a ncm file")
}
if ncm.read(&header, 4) != 4 {
return fmt.Errorf("read header failed")
}
if int(binary.LittleEndian.Uint32(header)) != 0x4D414446 {
return fmt.Errorf("not a ncm file")
}
if err := ncm.skip(2); err != nil {
return fmt.Errorf("seek version failed")
}
n := make([]byte, 4)
if ncm.read(&n, len(n)) != 4 {
return fmt.Errorf("read key len failed")
}
keyLen := int(binary.LittleEndian.Uint32(n))
keydata := make([]byte, keyLen)
ncm.read(&keydata, keyLen)
for i := range keydata {
keydata[i] ^= 0x64
}
mKeyData, err := utils.AesEcbDecrypt(ncm.sCoreKey[:16], keydata)
if err != nil {
return fmt.Errorf("decrypt key failed")
}
ncm.buildKeyBox(mKeyData[17:], len(mKeyData)-17)
if ncm.read(&n, len(n)) != 4 {
return fmt.Errorf("read metadata len failed")
}
metadataLen := int(binary.LittleEndian.Uint32(n))
if metadataLen > 0 {
modifyData := make([]byte, metadataLen)
ncm.read(&modifyData, metadataLen)
for i := range modifyData {
modifyData[i] ^= 0x63
}
swapModifyData := string(modifyData[22:])
modifyOutData, err := base64.StdEncoding.DecodeString(swapModifyData)
if err != nil {
return fmt.Errorf("base64 decode modify data failed")
}
modifyDecryptData, err := utils.AesEcbDecrypt(ncm.sModifyKey[:16], modifyOutData)
if err != nil {
return fmt.Errorf("decrypt modify data failed")
}
mMetadataString := string(modifyDecryptData[6:])
ncm.mAlbumPicUrl = GetAlbumPicUrl(mMetadataString)
ncm.mMetadata = NewNeteaseCloudMusicMetadata(mMetadataString)
}
if err := ncm.skip(5); err != nil {
return fmt.Errorf("seek gap failed")
}
coverFrameLen := make([]byte, 4)
if ncm.read(&coverFrameLen, len(coverFrameLen)) != 4 {
return fmt.Errorf("read cover frame len failed")
}
if ncm.read(&n, len(n)) != 4 {
return fmt.Errorf("read cover frame data len failed")
}
coverFrameLenInt := int(binary.LittleEndian.Uint32(coverFrameLen))
coverFrameDataLen := int(binary.LittleEndian.Uint32(n))
if coverFrameDataLen > 0 {
ncm.read(&ncm.mImageData, coverFrameDataLen)
}
ncm.skip(int64(coverFrameLenInt) - int64(coverFrameDataLen))
return nil
}
func NewNeteaseCloudMusic(filePath string) (*NeteaseCloudMusic, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("open file failed")
}
ncm := &NeteaseCloudMusic{
sCoreKey: [17]byte{0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D, 0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E, 0x62, 0x61, 0x78, 0x57, 0},
sModifyKey: [17]byte{0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 0x28, 0},
mPng: [8]byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A},
mFilePath: filePath,
mReader: f,
}
if err := parseNcmHeader(ncm); err != nil {
f.Close()
return nil, err
}
return ncm, nil
}
func NewNeteaseCloudMusicFromBytes(data []byte) (*NeteaseCloudMusic, error) {
ncm := &NeteaseCloudMusic{
sCoreKey: [17]byte{0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D, 0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E, 0x62, 0x61, 0x78, 0x57, 0},
sModifyKey: [17]byte{0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 0x28, 0},
mPng: [8]byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A},
mReader: bytes.NewReader(data),
}
if err := parseNcmHeader(ncm); err != nil {
return nil, err
}
return ncm, nil
}