Compare commits

..

No commits in common. "e29fdaf8502541780bea64a28f45bb01c2285086" and "b9299d8988ccd6b4313b82c855afb52877543e49" have entirely different histories.

14 changed files with 95 additions and 3138 deletions

View File

@ -1,21 +1,25 @@
--- ---
name: "[请按照此模板填写] 报告 Bug" name: "[请按照此模板填写] 报告 Bug"
about: "创建一个 Bug 报告,不按照模板的 Issue 会被直接关闭,不予回复。" about: "创建一个 Bug 报告,不按照模板的 Issue 会被关闭。"
title: "[Bug] 总结你的 Bug 报告" title: "[Bug] 总结你的 Bug 报告"
labels: bug labels: bug
assignees: taurusxin assignees: taurusxin
--- ---
**Bug 描述** **Bug 描述**
清晰地描述一下 Bug 的大致问题。
清晰地描述一下 Bug 的大致问题,例如无法转换,或者其他问题。
**复现方法** **复现方法**
复现此 Bug 的方法 复现此 Bug 的方法
1. 使用本项目处理文件 '...' 1. 打开 '...'
2. 发生报错 2. 点击 '....'
3. 发生报错
**预期行为**
解释一下原本应该出现的结果。
**屏幕截图** **屏幕截图**
@ -23,10 +27,9 @@ assignees: taurusxin
**环境** **环境**
- 操作系统: Windows / macOS / Linux - OS: [e.g. Windows 11]
- 网易云版本(重要): [e.g. 3.0.1] - 软件版本: [e.g. 1.3.2]
- 所选择的音质: 极高、无损等
**附加内容** **附加内容**
如果遇到无法转换的问题,请将样本附加到这里,便于分析 添加更多其他内容以帮助开发者更好地了解这个 Bug

View File

@ -1,6 +1,6 @@
name: CI name: CI
env: env:
BUILD_TYPE: MinSizeRel BUILD_TYPE: Release
BUILD_PATH: build BUILD_PATH: build
on: on:
push: push:

2
.gitignore vendored
View File

@ -5,5 +5,3 @@ ncmdump
.idea .idea
build build
cmake-build-*

View File

@ -36,41 +36,16 @@ brew install ncmdump
ncmdump -h ncmdump -h
``` ```
使用 `-v``--version` 参数来打印版本信息 命令行下输入一个或多个文件
```shell ```shell
ncmdump -v ncmdump file1 file2...
``` ```
处理单个或多个文件 你可以使用 `-d` 参数来指定一个文件夹,对文件夹下的所有文件批量处理
```shell ```shell
ncmdump 1.ncm 2.ncm... ncmdump -d folder
```
使用 `-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
``` ```
### 动态库 ### 动态库

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,7 @@ private:
NcmFormat mFormat; NcmFormat mFormat;
std::string mImageData; std::string mImageData;
std::ifstream mFile; std::ifstream mFile;
unsigned char mKeyBox[256]{}; unsigned char mKeyBox[256];
NeteaseMusicMetadata* mMetaData; NeteaseMusicMetadata* mMetaData;
private: private:
@ -66,6 +66,6 @@ public:
~NeteaseCrypt(); ~NeteaseCrypt();
public: public:
void Dump(std::string const&); void Dump();
void FixMetadata(); void FixMetadata();
}; };

View File

@ -1,3 +0,0 @@
#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()); return new NeteaseCrypt(fPath.u8string());
} }
API int Dump(NeteaseCrypt* neteaseCrypt, const char* outputPath) { API int Dump(NeteaseCrypt* neteaseCrypt) {
try try
{ {
neteaseCrypt->Dump(outputPath); neteaseCrypt->Dump();
} }
catch (const std::invalid_argument& e) catch (const std::invalid_argument& e)
{ {

View File

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

View File

@ -10,207 +10,115 @@
#endif #endif
#include "color.h" #include "color.h"
#include "version.h"
#include "cxxopts.hpp"
namespace fs = std::filesystem; namespace fs = std::filesystem;
void processFile(const fs::path &filePath, const fs::path &outputFolder) 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)
{ {
if (fs::exists(filePath) == false) 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; return;
} }
try try
{ {
NeteaseCrypt crypt(filePath.u8string()); NeteaseCrypt crypt(filePath.u8string());
crypt.Dump(outputFolder.u8string()); crypt.Dump();
crypt.FixMetadata(); crypt.FixMetadata();
std::cout << BOLDGREEN << "[Done] " << RESET << "'" << filePath.u8string() << "' -> '" << crypt.dumpFilepath().u8string() << "'" << std::endl; std::cout << BOLDGREEN << "[Done] " << RESET << "'" << filePath.u8string() << "' -> '" << crypt.dumpFilepath().u8string() << "'" << std::endl;
} }
catch (const std::invalid_argument &e) 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 (...) catch (...)
{ {
std::cerr << BOLDRED << "[Error] Unexpected exception while processing file: " << RESET << filePath.u8string() << std::endl; 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());
}
} }
} }
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
#if defined(_WIN32) #if defined(_WIN32)
win32_utf8argv(&argc, &argv); // Convert command line arguments to UTF-8 under Windows win32_utf8argv(&argc, &argv);
#endif #endif
if (argc <= 1)
cxxopts::Options options("ncmdump"); {
displayHelp();
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; return 1;
} }
// print usage message if unrecognised options are present std::vector<fs::path> files;
if (result.unmatched().size() > 0) bool processFolders = false;
{
std::cout << options.help() << std::endl;
return 1;
}
// display help message bool folderProvided = false;
if (result.count("help"))
#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)
{ {
std::cout << options.help() << std::endl; if (COMPARE_STR(argv[i], HELP_SHORT) || COMPARE_STR(argv[i], HELP_LONG))
{
displayHelp();
return 0; return 0;
} }
else if (COMPARE_STR(argv[i], PROCESS_FOLDER))
// display version information
if (result.count("version"))
{ {
std::cout << "ncmdump version " << VERSION_MAJOR << "." << VERSION_MINOR << "." << VERSION_PATCH << std::endl; processFolders = true;
return 0; if (i + 1 < argc && argv[i + 1][0] != '-')
{
folderProvided = true;
processFilesInFolder(fs::u8path(argv[i + 1]));
// Skip the folder name
++i;
} }
else
// no input files or folder provided
if (result.count("directory") == 0 && result.count("filenames") == 0)
{ {
std::cout << options.help() << std::endl; std::cerr << "Error: -d option requires a folder path." << std::endl;
return 1; 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))
{
if (!fs::is_directory(outputDir))
{
std::cerr << BOLDRED << "[Error] " << RESET << "'" << outputDir.u8string() << "' is not a valid directory." << std::endl;
return 1;
}
}
fs::create_directories(outputDir);
}
// process files in a folder
if (result.count("directory"))
{
fs::path sourceDir = fs::u8path(result["directory"].as<std::string>());
if (!fs::is_directory(sourceDir))
{
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 else
{ {
for (const auto &entry : fs::directory_iterator(sourceDir)) fs::path path = fs::u8path(argv[i]);
files.push_back(path);
}
}
for (const auto &file : files)
{ {
const auto &path = fs::u8path(entry.path().u8string()); if (processFolders && fs::is_directory(file))
if (entry.is_regular_file())
{ {
if (outputDirSpecified) processFilesInFolder(file);
{
processFile(path, outputDir);
} }
else else
{ {
processFile(path, ""); processFile(file);
} }
} }
}
}
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; return 0;
} }

View File

@ -16,9 +16,6 @@
#include <string> #include <string>
#include <filesystem> #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::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}; const unsigned char NeteaseCrypt::sModifyKey[17] = {0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 0x28, 0};
@ -224,7 +221,7 @@ void NeteaseCrypt::FixMetadata()
audioFile = new TagLib::MPEG::File(mDumpFilepath.c_str()); audioFile = new TagLib::MPEG::File(mDumpFilepath.c_str());
tag = dynamic_cast<TagLib::MPEG::File *>(audioFile)->ID3v2Tag(true); tag = dynamic_cast<TagLib::MPEG::File *>(audioFile)->ID3v2Tag(true);
if (!mImageData.empty()) if (mImageData.length() > 0)
{ {
TagLib::ID3v2::AttachedPictureFrame *frame = new TagLib::ID3v2::AttachedPictureFrame; TagLib::ID3v2::AttachedPictureFrame *frame = new TagLib::ID3v2::AttachedPictureFrame;
@ -239,7 +236,7 @@ void NeteaseCrypt::FixMetadata()
audioFile = new TagLib::FLAC::File(mDumpFilepath.c_str()); audioFile = new TagLib::FLAC::File(mDumpFilepath.c_str());
tag = audioFile->tag(); tag = audioFile->tag();
if (!mImageData.empty()) if (mImageData.length() > 0)
{ {
TagLib::FLAC::Picture *cover = new TagLib::FLAC::Picture; TagLib::FLAC::Picture *cover = new TagLib::FLAC::Picture;
cover->setMimeType(mimeType(mImageData)); cover->setMimeType(mimeType(mImageData));
@ -257,20 +254,15 @@ void NeteaseCrypt::FixMetadata()
tag->setAlbum(TagLib::String(mMetaData->album(), TagLib::String::UTF8)); tag->setAlbum(TagLib::String(mMetaData->album(), TagLib::String::UTF8));
} }
// tag->setComment(TagLib::String("Create by taurusxin/ncmdump.", TagLib::String::UTF8)); tag->setComment(TagLib::String("Create by taurusxin/ncmdump.", TagLib::String::UTF8));
audioFile->save(); audioFile->save();
audioFile->~File(); audioFile->~File();
} }
void NeteaseCrypt::Dump(std::string const &outputDir = "") void NeteaseCrypt::Dump()
{ {
if (outputDir.empty())
{
mDumpFilepath = std::filesystem::u8path(mFilepath); mDumpFilepath = std::filesystem::u8path(mFilepath);
} else {
mDumpFilepath = std::filesystem::u8path(outputDir) / std::filesystem::u8path(mFilepath).filename();
}
std::vector<unsigned char> buffer(0x8000); std::vector<unsigned char> buffer(0x8000);
@ -301,7 +293,7 @@ void NeteaseCrypt::Dump(std::string const &outputDir = "")
mFormat = NeteaseCrypt::FLAC; mFormat = NeteaseCrypt::FLAC;
} }
output.open(mDumpFilepath, std::ofstream::out | std::ofstream::binary); output.open(mDumpFilepath, output.out | output.binary);
} }
output.write((char *)buffer.data(), n); output.write((char *)buffer.data(), n);

2
taglib

@ -1 +1 @@
Subproject commit e3de03501ff66221d1f1f971022b248d5b38ba06 Subproject commit 0896fb90920c125e55248360d271d1a1674e2a4d