Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
628bea38be | ||
|
|
a62f5156f0 | ||
|
|
747343162b | ||
|
|
676641d5f9 | ||
|
|
96dc36f112 | ||
|
|
54da5b7a35
|
||
|
|
4d0622073a
|
||
|
|
49e554931e |
@@ -2,18 +2,51 @@
|
|||||||
|
|
||||||
基于 https://github.com/taurusxin/ncmdump 的 Golang 移植版
|
基于 https://github.com/taurusxin/ncmdump 的 Golang 移植版
|
||||||
|
|
||||||
支持网易云音乐最新的 3.x 版本,但需要注意:从该版本开始网易云音乐不再在 ncm 文件中内置封面图片,本工具支持从网易服务器上自动下载对应歌曲的封面图并写入到最终的音乐文件中
|
支持网易云音乐最新的 3.x 版本,但需要注意:从 3. x开始的某些网易云音乐版本不再在 ncm 文件中内置封面图片,本项目支持从网易服务器上自动下载对应歌曲的封面图并写入到最终的音乐文件中
|
||||||
|
|
||||||
|
你也可以去 https://git.taurusxin.com/taurusxin/ncmdump-gui 下载基于本项目的 gui 可视化图形应用,只需简单点击即可自动转换。
|
||||||
|
|
||||||
|
## 如何提 Issue
|
||||||
|
|
||||||
|
由于本站恶意机器人注册过多,已关闭账号注册,如果需要提 Issue 请前往 [GitHub](https://github.com/taurusxin/ncmdump),必须注明 Issue 的主题为 ncmdump-go,敬请谅解。
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
你可以使用去 [releases](https://git.taurusxin.com/taurusxin/ncmdump-go/releases/latest) 下载最新版预编译好的二进制文件,或者你也可以用包管理器来安装
|
你可以前往 [releases](https://git.taurusxin.com/taurusxin/ncmdump-go/releases/latest) 下载最新版预编译好的二进制文件,或者使用包管理器安装。
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
项目同时支持 Apple Silicon(M1/M2/M3/M4 等)和 Intel Mac:
|
||||||
|
|
||||||
|
- Apple Silicon 下载文件名包含 `darwin_arm64` 的压缩包;
|
||||||
|
- Intel Mac 下载文件名包含 `darwin_amd64` 的压缩包;
|
||||||
|
- 不确定芯片架构时,可运行 `uname -m`:输出 `arm64` 对应 Apple Silicon,输出 `x86_64` 对应 Intel。
|
||||||
|
|
||||||
|
下载并解压后,在终端执行:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
chmod +x ncmdump-go
|
||||||
|
sudo install -d /usr/local/bin
|
||||||
|
sudo install -m 0755 ncmdump-go /usr/local/bin/ncmdump-go
|
||||||
|
ncmdump-go --version
|
||||||
|
```
|
||||||
|
|
||||||
|
新发布的 macOS 压缩包已经包含可执行权限;这里保留 `chmod`,以兼容早期发布包。如果 macOS 提示无法验证开发者,可在确认文件来自本项目发布页后、执行安装命令前移除下载隔离属性:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
xattr -d com.apple.quarantine ncmdump-go
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以安装 Go 1.23 或更高版本后从源码构建:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go build -o ncmdump-go .
|
||||||
|
```
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Windows Scoop
|
# Windows Scoop
|
||||||
scoop bucket add taurusxin https://git.taurusxin.com/taurusxin/scoop-bucket.git # 添加 scoop 源
|
scoop bucket add taurusxin https://git.taurusxin.com/taurusxin/scoop-bucket.git # 添加 scoop 源
|
||||||
scoop install ncmdump-go # 安装 ncmdump-go
|
scoop install ncmdump-go # 安装 ncmdump-go
|
||||||
|
|
||||||
# macOS & Linux 之后会支持
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
@@ -63,10 +96,10 @@ ncmdump-go -d source_dir -o output_dir -r
|
|||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
使用 go module 下载 ncmdump-go 包
|
使用 Go module 下载 ncmdump-go 包
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
go get -u git.taurusxin.com/taurusxin/ncmdump-go
|
go get git.taurusxin.com/taurusxin/ncmdump-go@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
导入并使用
|
导入并使用
|
||||||
@@ -105,4 +138,3 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,63 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
VERSION=1.7.3
|
set -euo pipefail
|
||||||
|
|
||||||
# Clean up the build directory
|
VERSION="${VERSION:-1.7.5}"
|
||||||
rm -rf build
|
VERSION="${VERSION#v}"
|
||||||
mkdir build
|
BUILD_DIR="${BUILD_DIR:-build}"
|
||||||
|
PACKAGE="git.taurusxin.com/taurusxin/ncmdump-go"
|
||||||
|
LDFLAGS="-w -s -X main.version=${VERSION}"
|
||||||
|
|
||||||
# Linux amd64
|
mkdir -p "${BUILD_DIR}"
|
||||||
echo "Building for Linux amd64..."
|
rm -f \
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o build/ncmdump-go git.taurusxin.com/taurusxin/ncmdump-go
|
"${BUILD_DIR}/ncmdump-go" \
|
||||||
tar zcf build/ncmdump-go_linux_amd64_$VERSION.tar.gz -C build ncmdump-go
|
"${BUILD_DIR}/ncmdump-go.exe" \
|
||||||
rm build/ncmdump-go
|
"${BUILD_DIR}/ncmdump-go_linux_amd64_${VERSION}.tar.gz" \
|
||||||
|
"${BUILD_DIR}/ncmdump-go_linux_arm64_${VERSION}.tar.gz" \
|
||||||
|
"${BUILD_DIR}/ncmdump-go_darwin_amd64_${VERSION}.tar.gz" \
|
||||||
|
"${BUILD_DIR}/ncmdump-go_darwin_arm64_${VERSION}.tar.gz" \
|
||||||
|
"${BUILD_DIR}/ncmdump-go_windows_amd64_${VERSION}.zip"
|
||||||
|
|
||||||
# Linux arm64
|
create_tar_archive() {
|
||||||
echo "Building for Linux arm64..."
|
local os="$1"
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -o build/ncmdump-go git.taurusxin.com/taurusxin/ncmdump-go
|
local arch="$2"
|
||||||
tar zcf build/ncmdump-go_linux_arm64_$VERSION.tar.gz -C build ncmdump-go
|
local binary="${BUILD_DIR}/ncmdump-go"
|
||||||
rm build/ncmdump-go
|
local archive="${BUILD_DIR}/ncmdump-go_${os}_${arch}_${VERSION}.tar.gz"
|
||||||
|
local tar_version
|
||||||
|
|
||||||
# macOS amd64
|
echo "Building for ${os} ${arch}..."
|
||||||
echo "Building for macOS amd64..."
|
CGO_ENABLED=0 GOOS="${os}" GOARCH="${arch}" \
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-w -s" -o build/ncmdump-go git.taurusxin.com/taurusxin/ncmdump-go
|
go build -trimpath -ldflags="${LDFLAGS}" -o "${binary}" "${PACKAGE}"
|
||||||
tar zcf build/ncmdump-go_darwin_amd64_$VERSION.tar.gz -C build ncmdump-go
|
|
||||||
rm build/ncmdump-go
|
|
||||||
|
|
||||||
# macOS arm64
|
# Builds are sometimes produced from Git Bash on Windows, where chmod does
|
||||||
echo "Building for macOS arm64..."
|
# not change NTFS permissions. Force the executable bit into GNU tar's
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-w -s" -o build/ncmdump-go git.taurusxin.com/taurusxin/ncmdump-go
|
# header so macOS/Linux users can run the extracted binary immediately.
|
||||||
tar zcf build/ncmdump-go_darwin_arm64_$VERSION.tar.gz -C build ncmdump-go
|
chmod 0755 "${binary}"
|
||||||
rm build/ncmdump-go
|
tar_version="$(tar --version 2>/dev/null || true)"
|
||||||
|
if [[ "${tar_version}" == *"GNU tar"* ]]; then
|
||||||
|
tar --mode=0755 -zcf "${archive}" -C "${BUILD_DIR}" ncmdump-go
|
||||||
|
else
|
||||||
|
tar -zcf "${archive}" -C "${BUILD_DIR}" ncmdump-go
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! tar -tvzf "${archive}" | awk 'NR == 1 { exit(substr($1, 1, 10) == "-rwxr-xr-x" ? 0 : 1) }'; then
|
||||||
|
echo "Archive validation failed: ncmdump-go is not executable in ${archive}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm "${binary}"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_tar_archive linux amd64
|
||||||
|
create_tar_archive linux arm64
|
||||||
|
create_tar_archive darwin amd64
|
||||||
|
create_tar_archive darwin arm64
|
||||||
|
|
||||||
# Windows amd64
|
# Windows amd64
|
||||||
echo "Building for Windows amd64..."
|
echo "Building for Windows amd64..."
|
||||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-w -s" -o build/ncmdump-go.exe git.taurusxin.com/taurusxin/ncmdump-go
|
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \
|
||||||
zip -q -j build/ncmdump-go_windows_amd64_$VERSION.zip ./build/ncmdump-go.exe
|
go build -trimpath -ldflags="${LDFLAGS}" -o "${BUILD_DIR}/ncmdump-go.exe" "${PACKAGE}"
|
||||||
rm build/ncmdump-go.exe
|
zip -q -j "${BUILD_DIR}/ncmdump-go_windows_amd64_${VERSION}.zip" "${BUILD_DIR}/ncmdump-go.exe"
|
||||||
|
rm "${BUILD_DIR}/ncmdump-go.exe"
|
||||||
|
|
||||||
|
echo "Release archives written to ${BUILD_DIR}/"
|
||||||
|
|||||||
@@ -2,17 +2,27 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.taurusxin.com/taurusxin/ncmdump-go/ncmcrypt"
|
|
||||||
"git.taurusxin.com/taurusxin/ncmdump-go/utils"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.taurusxin.com/taurusxin/ncmdump-go/ncmcrypt"
|
||||||
|
"git.taurusxin.com/taurusxin/ncmdump-go/utils"
|
||||||
|
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// version is replaced by build.sh for release builds.
|
||||||
|
var version = "1.7.5"
|
||||||
|
|
||||||
|
func isNCMFile(filePath string) bool {
|
||||||
|
baseName := filepath.Base(filePath)
|
||||||
|
return !strings.HasPrefix(baseName, "._") && strings.EqualFold(filepath.Ext(baseName), ".ncm")
|
||||||
|
}
|
||||||
|
|
||||||
func processFile(filePath string, outputDir string) error {
|
func processFile(filePath string, outputDir string) error {
|
||||||
// skip if the extension is not .ncm
|
// skip if the extension is not .ncm
|
||||||
if filePath[len(filePath)-4:] != ".ncm" {
|
if !isNCMFile(filePath) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +69,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if *showVersion {
|
if *showVersion {
|
||||||
fmt.Println("ncmdump version 1.7.3")
|
fmt.Printf("ncmdump version %s\n", version)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +142,7 @@ func main() {
|
|||||||
// process files 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 !isNCMFile(filePath) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if outputDirSpecified {
|
if outputDirSpecified {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestIsNCMFile(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "lowercase", path: "/Users/test/Music/song.ncm", want: true},
|
||||||
|
{name: "uppercase", path: "/Users/test/Music/song.NCM", want: true},
|
||||||
|
{name: "unicode path", path: "/Users/test/音乐/歌曲.ncm", want: true},
|
||||||
|
{name: "macOS AppleDouble file", path: "/Users/test/Music/._song.ncm", want: false},
|
||||||
|
{name: "short name", path: "a", want: false},
|
||||||
|
{name: "extension only", path: ".ncm", want: true},
|
||||||
|
{name: "different extension", path: "song.flac", want: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := isNCMFile(tt.path); got != tt.want {
|
||||||
|
t.Fatalf("isNCMFile(%q) = %v, want %v", tt.path, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-11
@@ -5,15 +5,16 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/bogem/id3v2/v2"
|
|
||||||
"github.com/go-flac/flacpicture"
|
|
||||||
"github.com/go-flac/flacvorbis"
|
|
||||||
"github.com/go-flac/go-flac"
|
|
||||||
"git.taurusxin.com/taurusxin/ncmdump-go/utils"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"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
|
type NcmFormat = string
|
||||||
@@ -152,7 +153,7 @@ func (ncm *NeteaseCloudMusic) Dump(targetDir string) (bool, error) {
|
|||||||
outputStream = output
|
outputStream = output
|
||||||
}
|
}
|
||||||
|
|
||||||
outputStream.Write(buffer)
|
outputStream.Write(buffer[:n])
|
||||||
}
|
}
|
||||||
|
|
||||||
outputStream.Close()
|
outputStream.Close()
|
||||||
@@ -179,7 +180,8 @@ func (ncm *NeteaseCloudMusic) FixMetadata(fetchAlbumImageFromRemote bool) (bool,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ncm.mFormat == Mp3 {
|
switch ncm.mFormat {
|
||||||
|
case Mp3:
|
||||||
audioFile, err := id3v2.Open(ncm.mDumpFilePath, id3v2.Options{Parse: true})
|
audioFile, err := id3v2.Open(ncm.mDumpFilePath, id3v2.Options{Parse: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -205,7 +207,7 @@ func (ncm *NeteaseCloudMusic) FixMetadata(fetchAlbumImageFromRemote bool) (bool,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
} else if ncm.mFormat == Flac {
|
case Flac:
|
||||||
audioFile, err := flac.ParseFile(ncm.mDumpFilePath)
|
audioFile, err := flac.ParseFile(ncm.mDumpFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -235,13 +237,14 @@ func (ncm *NeteaseCloudMusic) FixMetadata(fetchAlbumImageFromRemote bool) (bool,
|
|||||||
cmts = flacvorbis.New()
|
cmts = flacvorbis.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
if res, _ := cmts.Get(flacvorbis.FIELD_TITLE); res[0] != ncm.mMetadata.mName {
|
// flac 可能自带元数据 当且仅当没有该项时才向目标添加元数据
|
||||||
|
if res, _ := cmts.Get(flacvorbis.FIELD_TITLE); len(res) == 0 {
|
||||||
_ = cmts.Add(flacvorbis.FIELD_TITLE, ncm.mMetadata.mName)
|
_ = cmts.Add(flacvorbis.FIELD_TITLE, ncm.mMetadata.mName)
|
||||||
}
|
}
|
||||||
if res, _ := cmts.Get(flacvorbis.FIELD_ARTIST); res[0] != ncm.mMetadata.mArtist {
|
if res, _ := cmts.Get(flacvorbis.FIELD_ARTIST); len(res) == 0 {
|
||||||
_ = cmts.Add(flacvorbis.FIELD_ARTIST, ncm.mMetadata.mArtist)
|
_ = cmts.Add(flacvorbis.FIELD_ARTIST, ncm.mMetadata.mArtist)
|
||||||
}
|
}
|
||||||
if res, _ := cmts.Get(flacvorbis.FIELD_ALBUM); res[0] != ncm.mMetadata.mAlbum {
|
if res, _ := cmts.Get(flacvorbis.FIELD_ALBUM); len(res) == 0 {
|
||||||
_ = cmts.Add(flacvorbis.FIELD_ALBUM, ncm.mMetadata.mAlbum)
|
_ = cmts.Add(flacvorbis.FIELD_ALBUM, ncm.mMetadata.mAlbum)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user