9 Commits
1.4.0 ... 1.5.0

Author SHA1 Message Date
cb7a84662f update: README.md; bump version to 1.5.0; 2024-09-25 23:18:18 +08:00
39adf93e46 feat: handle exception 2024-09-25 23:08:14 +08:00
0befc5bc93 feat: allow sepecify output dir; process dir recursively
use cxxopts library to parse command line options
2024-09-25 22:41:33 +08:00
b9299d8988 feat: comment for ncm 3.0 2024-09-23 20:10:20 +08:00
84b5c0044d chore: typo 2024-09-21 00:10:26 +08:00
f060bee5ad feat: autobuild for macOS arm64 2024-09-21 00:08:51 +08:00
bc5719ac11 feat: docs for cross build on macOS 2024-09-20 23:55:46 +08:00
5e41d41874 update: README.md 2024-09-13 11:46:44 +08:00
3906a49f7a update: README.md 2024-09-13 11:39:00 +08:00
11 changed files with 3163 additions and 86 deletions

View File

@@ -35,7 +35,7 @@ jobs:
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: Windows amd64 Build - MinGW
name: windows_amd64_build_msys2_exe
path: ${{ env.BUILD_PATH }}/ncmdump.exe
build_on_windows_msvc:
@@ -56,16 +56,16 @@ jobs:
- name: Upload artifact executable
uses: actions/upload-artifact@v4
with:
name: Windows amd64 Build - MSVC
name: windows_amd64_build_msvc_exe
path: ${{ env.BUILD_PATH }}/${{ env.BUILD_TYPE }}/ncmdump.exe
- name: Upload artifact DLL
uses: actions/upload-artifact@v4
with:
name: Windows amd64 Build - MSVC DLL
name: windows_amd64_build_msvc_dll
path: ${{ env.BUILD_PATH }}/${{ env.BUILD_TYPE }}/libncmdump.dll
build_on_linux:
build_on_linux_amd64:
runs-on: ubuntu-latest
steps:
@@ -88,10 +88,10 @@ jobs:
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: Linux amd64 Build
name: linux_build_amd64
path: ${{ env.BUILD_PATH }}/ncmdump
build_on_macos:
build_on_macos_amd64:
runs-on: macos-latest
steps:
@@ -105,7 +105,7 @@ jobs:
brew install git cmake
- name: Configure build
run: cmake -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} -B ${{ env.BUILD_PATH }}
run: cmake -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} -B ${{ env.BUILD_PATH }} -DCMAKE_OSX_ARCHITECTURES=x86_64
- name: Build
run: cmake --build ${{ env.BUILD_PATH }} -j 4
@@ -113,5 +113,30 @@ jobs:
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: macOS amd64 Build
name: macOS_build_amd64
path: ${{ env.BUILD_PATH }}/ncmdump
build_on_macos_arm64:
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies
run: |
brew install git cmake
- name: Configure build
run: cmake -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} -B ${{ env.BUILD_PATH }} -DCMAKE_OSX_ARCHITECTURES=arm64
- name: Build
run: cmake --build ${{ env.BUILD_PATH }} -j 4
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: macOS_build_arm64
path: ${{ env.BUILD_PATH }}/ncmdump

View File

@@ -16,9 +16,13 @@
## 使用
注意:自网易云音乐 3.0 版本开始,下载的 ncm 文件均不内置歌曲专辑的封面图片,需要从网络获取,介于在一个小工具中嵌入庞大网络库的非必要性,可以移步我的另一个仓库(<https://git.taurusxin.com/taurusxin/ncmdump-go>),下载完全使用 Golang 重写的新版本,支持从网络自动获取封面图并嵌入到目标音乐文件。
### 命令行工具
你可以使用 Homebrew 来安装 ncmdump 的 cli 版本
**[不推荐]** 你可以使用 Homebrew 来安装 Linux 或者 macOS 下的 ncmdump
注意:由于本项目依赖的库 taglib 的 2.0 版本尚未发布到 homebrew-core主要是因为 taglib 2.0 导致其他 brew 包存在版本兼容问题),目前的 cmake 构建链无法在 macOS 上正常使用。根据 brew 的要求,如果依赖库已有官方的 brew 包,构建时必须使用官方仓库中的包,不能从 git 获取。而 taglib 2.0 版本开始才支持 cmake 构建。因此ncmdump 在 homebrew 上只能发布到 `1.2.1` 版本。建议直接通过二进制方式安装,`1.3.0` 后版本修复了许多 bug使用体验会更好。
```shell
brew install ncmdump
@@ -32,16 +36,41 @@ brew install ncmdump
ncmdump -h
```
命令行下输入一个或多个文件
使用 `-v``--version` 参数来打印版本信息
```shell
ncmdump file1 file2...
ncmdump -v
```
你可以使用 `-d` 参数来指定一个文件夹,对文件夹下的所有文件批量处理
处理单个或多个文件
```shell
ncmdump -d folder
ncmdump 1.ncm 2.ncm...
```
你可以使用 `-d` 参数来指定一个文件夹,对文件夹下的所有以 ncm 为扩展名的文件进行批量处理
```shell
ncmdump -d source_dir
```
你可以使用 `-r` 配合 `-d` 参数来递归处理文件夹下的所有以 ncm 为扩展名的文件
```shell
ncmdump -d source_dir -r
```
你可以使用 `-o` 参数来指定输出目录,将转换后的文件输出到指定目录,该参数支持与 `-r` 参数一起使用
```shell
# 处理单个或多个文件并输出到指定目录
ncmdump 1.ncm 2.ncm -o output_dir
# 处理文件夹下的所有以 ncm 为扩展名并输出到指定目录,不包含子文件夹
ncmdump -d source_dir -o output_dir
# 递归处理文件夹并输出到指定目录,并保留目录结构
ncmdump -d source_dir -o output_dir -r
```
### 动态库
@@ -76,6 +105,10 @@ cmake -G "Visual Studio 17 2022" -A x64 -B build
# Linux / macOS
cmake -DCMAKE_BUILD_TYPE=Release -B build
# 如果需要在 macOS 下交叉编译,可以指定 `CMAKE_OSX_ARCHITECTURES` 变量来指明目标系统架构
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -B build # arm64
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -B build # Intel-based
```
编译项目

View File

@@ -14,7 +14,7 @@ namespace libncmdump_demo_cli
private static extern IntPtr CreateNeteaseCrypt(IntPtr path);
[DllImport(DLL_PATH, CallingConvention = CallingConvention.Cdecl)]
private static extern int Dump(IntPtr NeteaseCrypt);
private static extern int Dump(IntPtr NeteaseCrypt, IntPtr outputPath);
[DllImport(DLL_PATH, CallingConvention = CallingConvention.Cdecl)]
private static extern void FixMetadata(IntPtr NeteaseCrypt);
@@ -42,10 +42,17 @@ namespace libncmdump_demo_cli
/// <summary>
/// 启动转换过程。
/// </summary>
/// <param name="OutputPath">指定一个路径输出,如果为空,则输出到原路径</param>
/// <returns>返回一个整数指示转储过程的结果。如果成功返回0如果失败返回1。</returns>
public int Dump()
public int Dump(string OutputPath)
{
return Dump(NeteaseCryptClass);
byte[] bytes = Encoding.UTF8.GetBytes(OutputPath);
IntPtr outputPtr = Marshal.AllocHGlobal(bytes.Length + 1);
Marshal.Copy(bytes, 0, outputPtr, bytes.Length);
Marshal.WriteByte(outputPtr, bytes.Length, 0);
return Dump(NeteaseCryptClass, outputPtr);
}
/// <summary>

View File

@@ -14,7 +14,7 @@ namespace libncmdump_demo_cli
NeteaseCrypt neteaseCrypt = new NeteaseCrypt(filePath);
// 启动转换过程
int result = neteaseCrypt.Dump();
int result = neteaseCrypt.Dump(""); // 为空则输出到源
// 修复元数据
neteaseCrypt.FixMetadata();

2909
src/include/cxxopts.hpp Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -66,6 +66,6 @@ public:
~NeteaseCrypt();
public:
void Dump();
void Dump(std::string const&);
void FixMetadata();
};

3
src/include/version.h Normal file
View File

@@ -0,0 +1,3 @@
#define VERSION_MAJOR 1
#define VERSION_MINOR 5
#define VERSION_PATCH 0

View File

@@ -9,10 +9,10 @@ extern "C" {
return new NeteaseCrypt(fPath.u8string());
}
API int Dump(NeteaseCrypt* neteaseCrypt) {
API int Dump(NeteaseCrypt* neteaseCrypt, const char* outputPath) {
try
{
neteaseCrypt->Dump();
neteaseCrypt->Dump(outputPath);
}
catch (const std::invalid_argument& e)
{

View File

@@ -8,7 +8,7 @@
extern "C" {
API NeteaseCrypt* CreateNeteaseCrypt(const char* path);
API int Dump(NeteaseCrypt* neteaseCrypt);
API int Dump(NeteaseCrypt* neteaseCrypt, const char* outputPath);
API void FixMetadata(NeteaseCrypt* neteaseCrypt);
API void DestroyNeteaseCrypt(NeteaseCrypt* neteaseCrypt);
}

View File

@@ -10,115 +10,207 @@
#endif
#include "color.h"
#include "version.h"
#include "cxxopts.hpp"
namespace fs = std::filesystem;
void displayHelp()
{
std::cout << "Usage: ncmdump [-d] [-h] file1 file2 ..." << std::endl;
std::cout << "Options:" << std::endl;
std::cout << " -d Process files in a folder (requires folder path)" << std::endl;
std::cout << " -h, --help Display this help message" << std::endl;
}
void processFile(const fs::path &filePath)
void processFile(const fs::path &filePath, const fs::path &outputFolder)
{
if (fs::exists(filePath) == false)
{
std::cerr << BOLDRED << "Error: " << RESET << "file '" << filePath.u8string() << "' does not exist." << std::endl;
std::cerr << BOLDRED << "[Error] " << RESET << "file '" << filePath.u8string() << "' does not exist." << std::endl;
return;
}
// skip if not ending with ".ncm"
if (!filePath.has_extension() || filePath.extension().u8string() != ".ncm")
{
return;
}
try
{
NeteaseCrypt crypt(filePath.u8string());
crypt.Dump();
crypt.Dump(outputFolder.u8string());
crypt.FixMetadata();
std::cout << BOLDGREEN << "[Done] " << RESET << "'" << filePath.u8string() << "' -> '" << crypt.dumpFilepath().u8string() << "'" << std::endl;
}
catch (const std::invalid_argument &e)
{
std::cerr << BOLDRED << "Exception: " << RESET << RED << e.what() << RESET << " '" << filePath.u8string() << "'" << std::endl;
std::cerr << BOLDRED << "[Exception] " << RESET << RED << e.what() << RESET << " '" << filePath.u8string() << "'" << std::endl;
}
catch (...)
{
std::cerr << BOLDRED << "Unexpected exception while processing file: " << RESET << filePath.u8string() << std::endl;
}
}
void processFilesInFolder(const fs::path &folderPath)
{
for (const auto &entry : fs::directory_iterator(folderPath))
{
if (fs::is_regular_file(entry.status()))
{
processFile(entry.path());
}
std::cerr << BOLDRED << "[Error] Unexpected exception while processing file: " << RESET << filePath.u8string() << std::endl;
}
}
int main(int argc, char **argv)
{
#if defined(_WIN32)
win32_utf8argv(&argc, &argv);
win32_utf8argv(&argc, &argv); // Convert command line arguments to UTF-8 under Windows
#endif
if (argc <= 1)
{
displayHelp();
cxxopts::Options options("ncmdump");
options.add_options()
("h,help", "Print usage")
("d,directory", "Process files in a folder (requires folder path)", cxxopts::value<std::string>())
("r,recursive", "Process files recursively (requires -d option)", cxxopts::value<bool>()->default_value("false"))
("o,output", "Output folder (default: original file folder)", cxxopts::value<std::string>())
("v,version", "Print version information", cxxopts::value<bool>()->default_value("false"))
("filenames", "Input files", cxxopts::value<std::vector<std::string>>());
options.positional_help("<files>");
options.parse_positional({"filenames"});
options.allow_unrecognised_options();
// Parse options the usual way
cxxopts::ParseResult result;
try {
result = options.parse(argc, argv);
} catch(cxxopts::exceptions::parsing const& e) {
std::cout << options.help() << std::endl;
return 1;
}
std::vector<fs::path> files;
bool processFolders = false;
bool folderProvided = false;
#define COMPARE_STR(s1, s2) (strcmp(s1, s2) == 0)
#define HELP_SHORT "-h"
#define HELP_LONG "--help"
#define PROCESS_FOLDER "-d"
for (int i = 1; i < argc; ++i)
// print usage message if unrecognised options are present
if (result.unmatched().size() > 0)
{
if (COMPARE_STR(argv[i], HELP_SHORT) || COMPARE_STR(argv[i], HELP_LONG))
std::cout << options.help() << std::endl;
return 1;
}
// display help message
if (result.count("help"))
{
std::cout << options.help() << std::endl;
return 0;
}
// display version information
if (result.count("version"))
{
std::cout << "ncmdump version " << VERSION_MAJOR << "." << VERSION_MINOR << "." << VERSION_PATCH << std::endl;
return 0;
}
// no input files or folder provided
if (result.count("directory") == 0 && result.count("filenames") == 0)
{
std::cout << options.help() << std::endl;
return 1;
}
// only -r option without -d option
if (result.count("recursive") && result.count("directory") == 0)
{
std::cerr << BOLDRED << "[Error] " << RESET << "-r option requires -d option." << std::endl;
return 1;
}
// check output folder
fs::path outputDir = fs::u8path("");
bool outputDirSpecified = result.count("output") > 0;
if (outputDirSpecified)
{
outputDir = fs::u8path(result["output"].as<std::string>());
if (fs::exists(outputDir))
{
displayHelp();
return 0;
}
else if (COMPARE_STR(argv[i], PROCESS_FOLDER))
{
processFolders = true;
if (i + 1 < argc && argv[i + 1][0] != '-')
if (!fs::is_directory(outputDir))
{
folderProvided = true;
processFilesInFolder(fs::u8path(argv[i + 1]));
// Skip the folder name
++i;
}
else
{
std::cerr << "Error: -d option requires a folder path." << std::endl;
std::cerr << BOLDRED << "[Error] " << RESET << "'" << outputDir.u8string() << "' is not a valid directory." << std::endl;
return 1;
}
}
else
{
fs::path path = fs::u8path(argv[i]);
files.push_back(path);
}
fs::create_directories(outputDir);
}
for (const auto &file : files)
// process files in a folder
if (result.count("directory"))
{
if (processFolders && fs::is_directory(file))
fs::path sourceDir = fs::u8path(result["directory"].as<std::string>());
if (!fs::is_directory(sourceDir))
{
processFilesInFolder(file);
std::cerr << BOLDRED << "[Error] " << RESET << "'" << sourceDir.u8string() << "' is not a valid directory." << std::endl;
return 1;
}
bool recursive = result["recursive"].as<bool>();
if (recursive)
{
// 递归遍历源目录
for (const auto &entry : fs::recursive_directory_iterator(sourceDir))
{
// 没有指定输出目录,则使用源目录作为输出目录
if (!outputDirSpecified)
{
outputDir = sourceDir;
}
// 获得递归遍历的相对路径
const auto &path = fs::u8path(entry.path().u8string());
auto relativePath = fs::relative(path, sourceDir);
fs::path destinationPath = outputDir / relativePath;
if (fs::is_regular_file(path))
{
// 确保输出文件的目录存在
fs::create_directories(destinationPath.parent_path());
// 处理文件
processFile(path, destinationPath.parent_path());
}
}
}
else
{
processFile(file);
for (const auto &entry : fs::directory_iterator(sourceDir))
{
const auto &path = fs::u8path(entry.path().u8string());
if (entry.is_regular_file())
{
if (outputDirSpecified)
{
processFile(path, outputDir);
}
else
{
processFile(path, "");
}
}
}
}
return 0;
}
// process individual files
if (result.count("filenames"))
{
for (const auto &filePath : result["filenames"].as<std::vector<std::string>>())
{
fs::path filePathU8 = fs::u8path(filePath);
if (!fs::is_regular_file(filePathU8))
{
std::cerr << BOLDRED << "[Error] " << RESET << "'" << filePathU8.u8string() << "' is not a valid file." << std::endl;
continue;
}
if (outputDirSpecified)
{
processFile(filePathU8, outputDir);
}
else
{
processFile(filePathU8, "");
}
}
}
return 0;
}

View File

@@ -16,6 +16,9 @@
#include <string>
#include <filesystem>
#pragma warning(disable:4267)
#pragma warning(disable:4244)
const unsigned char NeteaseCrypt::sCoreKey[17] = {0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D, 0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E, 0x62, 0x61, 0x78, 0x57, 0};
const unsigned char NeteaseCrypt::sModifyKey[17] = {0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 0x28, 0};
@@ -260,9 +263,14 @@ void NeteaseCrypt::FixMetadata()
audioFile->~File();
}
void NeteaseCrypt::Dump()
void NeteaseCrypt::Dump(std::string const &outputDir = "")
{
mDumpFilepath = std::filesystem::u8path(mFilepath);
if (outputDir.empty())
{
mDumpFilepath = std::filesystem::u8path(mFilepath);
} else {
mDumpFilepath = std::filesystem::u8path(outputDir) / std::filesystem::u8path(mFilepath).filename();
}
std::vector<unsigned char> buffer(0x8000);