Compare commits
7 Commits
0c9fc960ff
...
1.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
0bb001e7e2
|
|||
|
b104e1352f
|
|||
|
30a50c1eae
|
|||
|
d396c91e90
|
|||
|
ee68843d9e
|
|||
|
ab1c9fcf0b
|
|||
|
da31a12acb
|
32
build.sh
32
build.sh
@@ -1,13 +1,37 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
VERSION=1.6.0
|
||||||
|
|
||||||
|
# Clean up the build directory
|
||||||
|
rm -rf build
|
||||||
|
mkdir build
|
||||||
|
|
||||||
# Linux amd64
|
# Linux amd64
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o ./build/ncmdump_linux_amd64 ncmdump
|
echo "Building for Linux amd64..."
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o build/ncmdump github.com/taurusxin/ncmdump-go
|
||||||
|
tar zcf build/ncmdump_linux_amd64_$VERSION.tar.gz -C build ncmdump
|
||||||
|
rm build/ncmdump
|
||||||
|
|
||||||
|
# Linux arm64
|
||||||
|
echo "Building for Linux arm64..."
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -o build/ncmdump github.com/taurusxin/ncmdump-go
|
||||||
|
tar zcf build/ncmdump_linux_arm64_$VERSION.tar.gz -C build ncmdump
|
||||||
|
rm build/ncmdump
|
||||||
|
|
||||||
# macOS amd64
|
# macOS amd64
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-w -s" -o ./build/ncmdump_darwin_amd64 ncmdump
|
echo "Building for macOS amd64..."
|
||||||
|
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-w -s" -o build/ncmdump github.com/taurusxin/ncmdump-go
|
||||||
|
tar zcf build/ncmdump_darwin_amd64_$VERSION.tar.gz -C build ncmdump
|
||||||
|
rm build/ncmdump
|
||||||
|
|
||||||
# macOS arm64
|
# macOS arm64
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-w -s" -o ./build/ncmdump_darwin_arm64 ncmdump
|
echo "Building for macOS arm64..."
|
||||||
|
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-w -s" -o build/ncmdump github.com/taurusxin/ncmdump-go
|
||||||
|
tar zcf build/ncmdump_darwin_arm64_$VERSION.tar.gz -C build ncmdump
|
||||||
|
rm build/ncmdump
|
||||||
|
|
||||||
# Windows amd64
|
# Windows amd64
|
||||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-w -s" -o ./build/ncmdump_windows_amd64.exe ncmdump
|
echo "Building for Windows amd64..."
|
||||||
|
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-w -s" -o build/ncmdump.exe github.com/taurusxin/ncmdump-go
|
||||||
|
zip -q -j build/ncmdump_windows_amd64_$VERSION.zip ./build/ncmdump.exe
|
||||||
|
rm build/ncmdump.exe
|
||||||
118
main.go
118
main.go
@@ -10,21 +10,27 @@ import (
|
|||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func processFile(filePath string) error {
|
func processFile(filePath string, outputDir string) error {
|
||||||
|
// skip if the extension is not .ncm
|
||||||
|
if filePath[len(filePath)-4:] != ".ncm" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// process the file
|
||||||
currentFile, err := ncmcrypt.NewNeteaseCloudMusic(filePath)
|
currentFile, err := ncmcrypt.NewNeteaseCloudMusic(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ErrorPrintfln("Reading '%s' failed: '%s'", filePath, err.Error())
|
utils.ErrorPrintfln("Reading '%s' failed: %s", filePath, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dump, err := currentFile.Dump(filepath.Dir(filePath))
|
dump, err := currentFile.Dump(outputDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ErrorPrintfln("Processing '%s' failed: '%s'", filePath, err.Error())
|
utils.ErrorPrintfln("Processing '%s' failed: %s", filePath, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if dump {
|
if dump {
|
||||||
metadata, err := currentFile.FixMetadata(true)
|
metadata, err := currentFile.FixMetadata(true)
|
||||||
if !metadata {
|
if !metadata {
|
||||||
utils.WarningPrintfln("Fix metadata for '%s' failed: '%s'", filePath, err.Error())
|
utils.WarningPrintfln("Fix metadata for '%s' failed: %s", filePath, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
utils.DonePrintfln("'%s' -> '%s'", filePath, currentFile.GetDumpFilePath())
|
utils.DonePrintfln("'%s' -> '%s'", filePath, currentFile.GetDumpFilePath())
|
||||||
@@ -33,10 +39,13 @@ func processFile(filePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var folderPath string
|
var sourceDir string
|
||||||
|
var outputDir string
|
||||||
showHelp := flag.BoolP("help", "h", false, "Display help message")
|
showHelp := flag.BoolP("help", "h", false, "Display help message")
|
||||||
showVersion := flag.BoolP("version", "v", false, "Display version information")
|
showVersion := flag.BoolP("version", "v", false, "Display version information")
|
||||||
flag.StringVarP(&folderPath, "dir", "d", "", "Process all files in the directory")
|
processRecursive := flag.BoolP("recursive", "r", false, "Process all files in the directory recursively")
|
||||||
|
flag.StringVarP(&outputDir, "output", "o", "", "Output directory for the dump files")
|
||||||
|
flag.StringVarP(&sourceDir, "dir", "d", "", "Process all files in the directory")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if len(os.Args) == 1 {
|
if len(os.Args) == 1 {
|
||||||
@@ -50,55 +59,86 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if *showVersion {
|
if *showVersion {
|
||||||
fmt.Println("ncmdump version 1.5.0")
|
fmt.Println("ncmdump version 1.6.0")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if folderPath != "" {
|
if !flag.Lookup("dir").Changed && sourceDir == "" && len(flag.Args()) == 0 {
|
||||||
// check if the folder exists
|
flag.Usage()
|
||||||
info, err := os.Stat(folderPath)
|
os.Exit(1)
|
||||||
if err != nil {
|
}
|
||||||
utils.ErrorPrintfln("Unable to access directory: '%s'", folderPath)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !info.IsDir() {
|
if flag.Lookup("recursive").Changed && !flag.Lookup("dir").Changed {
|
||||||
utils.ErrorPrintfln("Not a directory: '%s'", folderPath)
|
utils.ErrorPrintfln("The -r option can only be used with the -d option")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dump files in the folder
|
outputDirSpecified := flag.Lookup("output").Changed
|
||||||
files, err := os.ReadDir(folderPath)
|
|
||||||
if err != nil {
|
|
||||||
utils.ErrorPrintfln("Unable to read directory: '%s'", folderPath)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range files {
|
if outputDirSpecified {
|
||||||
if file.IsDir() {
|
if utils.PathExists(outputDir) {
|
||||||
continue
|
if !utils.IsDir(outputDir) {
|
||||||
|
utils.ErrorPrintfln("Output directory '%s' is not valid.", outputDir)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
_ = os.MkdirAll(outputDir, os.ModePerm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
filePath := filepath.Join(folderPath, file.Name())
|
if sourceDir != "" {
|
||||||
// skip if the extension is not .ncm
|
if !utils.IsDir(sourceDir) {
|
||||||
if filePath[len(filePath)-4:] != ".ncm" {
|
utils.ErrorPrintfln("The source directory '%s' is not valid.", sourceDir)
|
||||||
continue
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
err = processFile(filePath)
|
|
||||||
|
if *processRecursive {
|
||||||
|
_ = filepath.WalkDir(sourceDir, func(p string, d os.DirEntry, err_ error) error {
|
||||||
|
if !outputDirSpecified {
|
||||||
|
outputDir = sourceDir
|
||||||
|
}
|
||||||
|
relativePath := utils.GetRelativePath(sourceDir, p)
|
||||||
|
destinationPath := filepath.Join(outputDir, relativePath)
|
||||||
|
|
||||||
|
if utils.IsRegularFile(p) {
|
||||||
|
parentDir := filepath.Dir(destinationPath)
|
||||||
|
_ = os.MkdirAll(parentDir, os.ModePerm)
|
||||||
|
_ = processFile(p, parentDir)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// dump files in the folder
|
||||||
|
files, err := os.ReadDir(sourceDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
utils.ErrorPrintfln("Unable to read directory: '%s'", sourceDir)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(sourceDir, file.Name())
|
||||||
|
if outputDirSpecified {
|
||||||
|
_ = processFile(filePath, outputDir)
|
||||||
|
} else {
|
||||||
|
_ = processFile(filePath, sourceDir)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// dump file from args
|
// process files from args
|
||||||
for _, filePath := range flag.Args() {
|
for _, filePath := range flag.Args() {
|
||||||
// skip if the extension is not .ncm
|
// skip if the extension is not .ncm
|
||||||
if filePath[len(filePath)-4:] != ".ncm" {
|
if filePath[len(filePath)-4:] != ".ncm" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err := processFile(filePath)
|
if outputDirSpecified {
|
||||||
if err != nil {
|
_ = processFile(filePath, outputDir)
|
||||||
continue
|
} else {
|
||||||
|
_ = processFile(filePath, sourceDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ func (ncm *NeteaseCloudMusic) mimeType() string {
|
|||||||
return "image/jpeg"
|
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) {
|
func (ncm *NeteaseCloudMusic) Dump(targetDir string) (bool, error) {
|
||||||
ncm.mDumpFilePath = ncm.mFilePath
|
ncm.mDumpFilePath = ncm.mFilePath
|
||||||
var outputStream *os.File
|
var outputStream *os.File
|
||||||
@@ -157,6 +158,8 @@ func (ncm *NeteaseCloudMusic) Dump(targetDir string) (bool, error) {
|
|||||||
return true, nil
|
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) FixMetadata(fetchAlbumImageFromRemote bool) (bool, error) {
|
func (ncm *NeteaseCloudMusic) FixMetadata(fetchAlbumImageFromRemote bool) (bool, error) {
|
||||||
if fetchAlbumImageFromRemote {
|
if fetchAlbumImageFromRemote {
|
||||||
// get the album pic from url
|
// get the album pic from url
|
||||||
@@ -226,10 +229,16 @@ func (ncm *NeteaseCloudMusic) FixMetadata(fetchAlbumImageFromRemote bool) (bool,
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDumpFilePath returns the absolute path of dumped music file
|
||||||
func (ncm *NeteaseCloudMusic) GetDumpFilePath() string {
|
func (ncm *NeteaseCloudMusic) GetDumpFilePath() string {
|
||||||
return ncm.mDumpFilePath
|
path, err := filepath.Abs(ncm.mDumpFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return ncm.mDumpFilePath
|
||||||
|
}
|
||||||
|
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) {
|
func NewNeteaseCloudMusic(filePath string) (*NeteaseCloudMusic, error) {
|
||||||
ncm := &NeteaseCloudMusic{
|
ncm := &NeteaseCloudMusic{
|
||||||
sCoreKey: [17]byte{0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D, 0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E, 0x62, 0x61, 0x78, 0x57, 0},
|
sCoreKey: [17]byte{0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D, 0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E, 0x62, 0x61, 0x78, 0x57, 0},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -9,3 +10,39 @@ func ReplaceExtension(filepathStr, newExt string) string {
|
|||||||
ext := filepath.Ext(filepathStr)
|
ext := filepath.Ext(filepathStr)
|
||||||
return strings.TrimSuffix(filepathStr, ext) + newExt
|
return strings.TrimSuffix(filepathStr, ext) + newExt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PathExists(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
if err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsDir(path string) bool {
|
||||||
|
s, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRelativePath(from, to string) string {
|
||||||
|
rel, err := filepath.Rel(from, to)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return rel
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsRegularFile(path string) bool {
|
||||||
|
s, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s.Mode().IsRegular()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user