完成Web ASM
This commit is contained in:
+130
-94
@@ -20,9 +20,7 @@ import (
|
||||
type NcmFormat = string
|
||||
|
||||
const (
|
||||
Mp3 NcmFormat = "mp3"
|
||||
)
|
||||
const (
|
||||
Mp3 NcmFormat = "mp3"
|
||||
Flac NcmFormat = "flac"
|
||||
)
|
||||
|
||||
@@ -31,56 +29,37 @@ type NeteaseCloudMusic struct {
|
||||
sModifyKey [17]byte
|
||||
mPng [8]byte
|
||||
|
||||
mFilePath string
|
||||
|
||||
mFilePath string
|
||||
mDumpFilePath string
|
||||
mFormat NcmFormat
|
||||
mImageData []byte
|
||||
mFileStream *os.File
|
||||
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.mFileStream.Read(*buffer)
|
||||
res, err := ncm.mReader.Read((*buffer)[:size])
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (ncm *NeteaseCloudMusic) openFile() bool {
|
||||
file, err := os.Open(ncm.mFilePath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ncm.mFileStream = file
|
||||
return true
|
||||
}
|
||||
|
||||
func (ncm *NeteaseCloudMusic) isNcmFile() bool {
|
||||
header := make([]byte, 4)
|
||||
|
||||
// check magic header 4E455443 4D414446
|
||||
if ncm.read(&header, 4) != 4 {
|
||||
return false
|
||||
}
|
||||
if int(binary.LittleEndian.Uint32(header)) != 0x4E455443 {
|
||||
return false
|
||||
}
|
||||
if ncm.read(&header, 4) != 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
if int(binary.LittleEndian.Uint32(header)) != 0x4D414446 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
func (ncm *NeteaseCloudMusic) skip(n int64) error {
|
||||
_, err := ncm.mReader.Seek(n, io.SeekCurrent)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ncm *NeteaseCloudMusic) buildKeyBox(key []byte, keyLen int) {
|
||||
@@ -113,7 +92,6 @@ func (ncm *NeteaseCloudMusic) mimeType() string {
|
||||
return "image/jpeg"
|
||||
}
|
||||
|
||||
// Dump encrypted ncm file to normal music file. If `targetDir` is "", the converted file will be saved to the original directory.
|
||||
func (ncm *NeteaseCloudMusic) Dump(targetDir string) (bool, error) {
|
||||
ncm.mDumpFilePath = ncm.mFilePath
|
||||
var outputStream *os.File
|
||||
@@ -123,7 +101,6 @@ func (ncm *NeteaseCloudMusic) Dump(targetDir string) (bool, error) {
|
||||
|
||||
for {
|
||||
n := ncm.read(&buffer, len(buffer))
|
||||
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
@@ -141,7 +118,7 @@ func (ncm *NeteaseCloudMusic) Dump(targetDir string) (bool, error) {
|
||||
ncm.mFormat = Flac
|
||||
ncm.mDumpFilePath = utils.ReplaceExtension(ncm.mDumpFilePath, ".flac")
|
||||
}
|
||||
if targetDir != "" { // change save dir
|
||||
if targetDir != "" {
|
||||
ncm.mDumpFilePath = filepath.Join(targetDir, filepath.Base(ncm.mDumpFilePath))
|
||||
}
|
||||
findFormatFlag = true
|
||||
@@ -160,12 +137,63 @@ func (ncm *NeteaseCloudMusic) Dump(targetDir string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// FixMetadata will fix the missing metadata for target music file, the source of the metadata comes from origin ncm file.
|
||||
// Since NeteaseCloudMusic version 3.0, the album cover image is no longer embedded in the ncm file. If the parameter is true, it means downloading the image from the NetEase server and embedding it into the target music file (network connection required)
|
||||
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) {
|
||||
// only fetch album image from remote when it's not embedded in the ncm file
|
||||
if ncm.mMetadata == nil {
|
||||
return true, nil
|
||||
}
|
||||
if len(ncm.mImageData) <= 0 && fetchAlbumImageFromRemote {
|
||||
// get the album pic from url
|
||||
resp, err := http.Get(ncm.mAlbumPicUrl)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -237,7 +265,6 @@ func (ncm *NeteaseCloudMusic) FixMetadata(fetchAlbumImageFromRemote bool) (bool,
|
||||
cmts = flacvorbis.New()
|
||||
}
|
||||
|
||||
// flac 可能自带元数据 当且仅当没有该项时才向目标添加元数据
|
||||
if res, _ := cmts.Get(flacvorbis.FIELD_TITLE); len(res) == 0 {
|
||||
_ = cmts.Add(flacvorbis.FIELD_TITLE, ncm.mMetadata.mName)
|
||||
}
|
||||
@@ -257,7 +284,6 @@ func (ncm *NeteaseCloudMusic) FixMetadata(fetchAlbumImageFromRemote bool) (bool,
|
||||
}
|
||||
|
||||
err = audioFile.Save(ncm.mDumpFilePath)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -265,7 +291,6 @@ func (ncm *NeteaseCloudMusic) FixMetadata(fetchAlbumImageFromRemote bool) (bool,
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GetDumpFilePath returns the absolute path of dumped music file
|
||||
func (ncm *NeteaseCloudMusic) GetDumpFilePath() string {
|
||||
path, err := filepath.Abs(ncm.mDumpFilePath)
|
||||
if err != nil {
|
||||
@@ -274,38 +299,32 @@ func (ncm *NeteaseCloudMusic) GetDumpFilePath() string {
|
||||
return path
|
||||
}
|
||||
|
||||
// NewNeteaseCloudMusic returns a new NeteaseCloudMusic instance, if the format of the file is incorrect, the error will be returned.
|
||||
func NewNeteaseCloudMusic(filePath string) (*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},
|
||||
func parseNcmHeader(ncm *NeteaseCloudMusic) error {
|
||||
header := make([]byte, 4)
|
||||
|
||||
mFilePath: filePath,
|
||||
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 !ncm.openFile() {
|
||||
return nil, fmt.Errorf("open file failed")
|
||||
if err := ncm.skip(2); err != nil {
|
||||
return fmt.Errorf("seek version failed")
|
||||
}
|
||||
|
||||
if !ncm.isNcmFile() {
|
||||
return nil, fmt.Errorf("not a ncm file")
|
||||
}
|
||||
|
||||
// actually this 2 bytes is the version, now we just skip it
|
||||
if _, err := ncm.mFileStream.Seek(2, 1); err != nil {
|
||||
return nil, fmt.Errorf("seek version failed")
|
||||
}
|
||||
|
||||
// the length of the RC4 key, encrypted by AES128
|
||||
n := make([]byte, 4)
|
||||
|
||||
if ncm.read(&n, len(n)) != 4 {
|
||||
return nil, fmt.Errorf("read key len failed")
|
||||
return fmt.Errorf("read key len failed")
|
||||
}
|
||||
|
||||
keyLen := int(binary.LittleEndian.Uint32(n))
|
||||
|
||||
keydata := make([]byte, keyLen)
|
||||
ncm.read(&keydata, keyLen)
|
||||
|
||||
@@ -314,25 +333,19 @@ func NewNeteaseCloudMusic(filePath string) (*NeteaseCloudMusic, error) {
|
||||
}
|
||||
|
||||
mKeyData, err := utils.AesEcbDecrypt(ncm.sCoreKey[:16], keydata)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt key failed")
|
||||
return fmt.Errorf("decrypt key failed")
|
||||
}
|
||||
|
||||
// build the key box
|
||||
ncm.buildKeyBox(mKeyData[17:], len(mKeyData)-17)
|
||||
|
||||
if ncm.read(&n, len(n)) != 4 {
|
||||
return nil, fmt.Errorf("read metadata len failed")
|
||||
return fmt.Errorf("read metadata len failed")
|
||||
}
|
||||
|
||||
metadataLen := int(binary.LittleEndian.Uint32(n))
|
||||
|
||||
if metadataLen <= 0 {
|
||||
// process meta here
|
||||
ncm.mMetadata = nil
|
||||
} else {
|
||||
// read metadata
|
||||
if metadataLen > 0 {
|
||||
modifyData := make([]byte, metadataLen)
|
||||
ncm.read(&modifyData, metadataLen)
|
||||
|
||||
@@ -340,43 +353,32 @@ func NewNeteaseCloudMusic(filePath string) (*NeteaseCloudMusic, error) {
|
||||
modifyData[i] ^= 0x63
|
||||
}
|
||||
|
||||
// escape `163 key(Don't modify):`
|
||||
swapModifyData := string(modifyData[22:])
|
||||
|
||||
modifyOutData, err := base64.StdEncoding.DecodeString(swapModifyData)
|
||||
if err != nil {
|
||||
panic("base64 decode modify data failed")
|
||||
return fmt.Errorf("base64 decode modify data failed")
|
||||
}
|
||||
|
||||
modifyDecryptData, err := utils.AesEcbDecrypt(ncm.sModifyKey[:16], modifyOutData)
|
||||
|
||||
if err != nil {
|
||||
panic("decrypt modify data failed")
|
||||
return fmt.Errorf("decrypt modify data failed")
|
||||
}
|
||||
|
||||
// escape `music:`
|
||||
mMetadataString := string(modifyDecryptData[6:])
|
||||
|
||||
// extract the album pic url
|
||||
ncm.mAlbumPicUrl = GetAlbumPicUrl(mMetadataString)
|
||||
|
||||
ncm.mMetadata = NewNeteaseCloudMusicMetadata(mMetadataString)
|
||||
}
|
||||
|
||||
// skip the 5 bytes gap
|
||||
if _, err := ncm.mFileStream.Seek(5, 1); err != nil {
|
||||
return nil, fmt.Errorf("seek gap failed")
|
||||
if err := ncm.skip(5); err != nil {
|
||||
return fmt.Errorf("seek gap failed")
|
||||
}
|
||||
|
||||
// read the cover frame
|
||||
coverFrameLen := make([]byte, 4)
|
||||
|
||||
if ncm.read(&coverFrameLen, len(coverFrameLen)) != 4 {
|
||||
return nil, fmt.Errorf("read cover frame len failed")
|
||||
return fmt.Errorf("read cover frame len failed")
|
||||
}
|
||||
|
||||
if ncm.read(&n, len(n)) != 4 {
|
||||
return nil, fmt.Errorf("read cover frame data len failed")
|
||||
return fmt.Errorf("read cover frame data len failed")
|
||||
}
|
||||
|
||||
coverFrameLenInt := int(binary.LittleEndian.Uint32(coverFrameLen))
|
||||
@@ -386,7 +388,41 @@ func NewNeteaseCloudMusic(filePath string) (*NeteaseCloudMusic, error) {
|
||||
ncm.read(&ncm.mImageData, coverFrameDataLen)
|
||||
}
|
||||
|
||||
ncm.mFileStream.Seek(int64(coverFrameLenInt)-int64(coverFrameDataLen), 1)
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user