Compare commits

...

46 Commits
1.3.0 ... main

Author SHA1 Message Date
e29fdaf850
update: Action build type to MinSizeRel 2024-11-02 21:37:22 +08:00
b812fe4e30
ignore: build dirs 2024-11-02 21:12:21 +08:00
8c4ec2ab0a
Update bug_report.md 2024-10-29 13:50:02 +08:00
22768594ba
chore: typo 2024-10-21 16:15:12 +08:00
6af854b376
removed: copyright comment 2024-10-21 15:21:05 +08:00
ed6c05b4bf
taglib: bump version to 2.0.2 2024-09-27 20:57:41 +08:00
dc2830d5c3
update: README.md 2024-09-25 23:21:43 +08:00
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
67cba29a8f
chore: enhance output 2024-09-13 10:58:29 +08:00
2e5fc75fee
Merge pull request #27 from um-lsr/main
fix: ncm 3.x version compatibility issues
2024-09-13 10:47:32 +08:00
鲁树人
f58c6c3880
fix: cover image memory leak
Signed-off-by: 鲁树人 <lu.shuren@um-react.app>
2024-09-13 00:01:26 +01:00
鲁树人
2b52a367f2
fix #26: correctly calculate cover frame size
Signed-off-by: 鲁树人 <lu.shuren@um-react.app>
2024-09-13 00:00:35 +01:00
36e6801f29 fix: bug report template 2024-08-17 09:01:46 +08:00
2cd7d69aa5 fix: bug report 2024-08-17 09:01:09 +08:00
1aaaab10dc feat: notice of bug report 2024-08-17 09:00:15 +08:00
e04415db9b removed: feature request template 2024-08-17 08:57:24 +08:00
911beb25bc enhanced: bug report template 2024-08-17 08:56:06 +08:00
0a6ac353ff removed: expect.bin 2024-04-17 16:56:43 +08:00
f89ea7eab6 fix: cannot process in folder 2024-04-07 13:59:03 +08:00
6eb3649753
update: README.md 2024-04-06 12:29:08 +08:00
8a13d0fe07
update: parse UTF-8 to lib in C# 2024-04-06 12:10:56 +08:00
321e5f342e
fix: parse UTF-8 path to lib 2024-04-06 12:09:03 +08:00
dbdb73c120
fix: close file after fixing metadata(#17) 2024-04-06 10:47:49 +08:00
7ad6f27b75
feat: star history 2024-04-06 10:36:58 +08:00
dd8b2d6fec
chore: typo 2024-04-02 09:19:31 +08:00
981913d729
feat: 1.3.0 update README.md 2024-03-31 09:55:24 +08:00
5fbfc2db81 fix: Action cannot find builds 2024-03-25 12:09:35 +08:00
e5cd9bddde fix: cannot find build target 2024-03-25 12:05:50 +08:00
068b1a6512 fix: missing header 2024-03-25 11:51:55 +08:00
265fe6765d fix: target name 2024-03-25 10:55:00 +08:00
5c970f4a09 feat: add header for lib 2024-03-25 09:07:01 +08:00
53b0d99431
feat: library example 2024-03-24 18:00:07 +08:00
37acbf07a7
fix: missing include and link options 2024-03-24 17:48:04 +08:00
d47d29426e
chore: only build lib on Windows 2024-03-24 17:44:51 +08:00
602c6ed43a
add: unix/linux lib support; upload libs 2024-03-24 17:36:44 +08:00
d8a0944810
feat: add dynamic library build 2024-03-24 17:27:41 +08:00
aa7ae6128d
refactor: project file structure 2024-03-24 09:20:25 +08:00
wshon
ae08d4475c
fix: exception when artist is empty (#16)
Co-authored-by: wshon <me@wshon.com>
2024-03-24 09:01:38 +08:00
26 changed files with 3492 additions and 225 deletions

View File

@ -1,31 +1,32 @@
--- ---
name: Bug report name: "[请按照此模板填写] 报告 Bug"
about: Create a bug report to help us improve about: "创建一个 Bug 报告,不按照模板的 Issue 会被直接关闭,不予回复。"
title: "[Bug] Summarize the bug here" title: "[Bug] 总结你的 Bug 报告"
labels: bug labels: bug
assignees: taurusxin assignees: taurusxin
--- ---
**Describe the bug** **Bug 描述**
A clear and concise description of what the bug is.
**To Reproduce** 清晰地描述一下 Bug 的大致问题,例如无法转换,或者其他问题。
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior** **复现方法**
A clear and concise description of what you expected to happen.
**Screenshots** 复现此 Bug 的方法
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** 1. 使用本项目处理文件 '...'
- OS: [e.g. Windows 11] 2. 发生报错
**屏幕截图**
**Additional context** 如果可以,屏幕截图可以更好地阐述你的问题。
Add any other context about the problem here.
**环境**
- 操作系统: Windows / macOS / Linux
- 网易云版本(重要): [e.g. 3.0.1]
- 所选择的音质: 极高、无损等
**附加内容**
如果遇到无法转换的问题,请将样本附加到这里,便于分析。

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature] Summarize new feature here"
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -1,6 +1,6 @@
name: CI name: CI
env: env:
BUILD_TYPE: Release BUILD_TYPE: MinSizeRel
BUILD_PATH: build BUILD_PATH: build
on: on:
push: push:
@ -35,7 +35,7 @@ jobs:
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: Windows amd64 Build - MinGW name: windows_amd64_build_msys2_exe
path: ${{ env.BUILD_PATH }}/ncmdump.exe path: ${{ env.BUILD_PATH }}/ncmdump.exe
build_on_windows_msvc: build_on_windows_msvc:
@ -53,13 +53,19 @@ jobs:
- name: Build - name: Build
run: cmake --build ${{ env.BUILD_PATH }} -j 4 --config ${{ env.BUILD_TYPE }} run: cmake --build ${{ env.BUILD_PATH }} -j 4 --config ${{ env.BUILD_TYPE }}
- name: Upload artifact - name: Upload artifact executable
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: Windows amd64 Build - MSVC name: windows_amd64_build_msvc_exe
path: ${{ env.BUILD_PATH }}/${{ env.BUILD_TYPE }}/ncmdump.exe path: ${{ env.BUILD_PATH }}/${{ env.BUILD_TYPE }}/ncmdump.exe
build_on_linux: - name: Upload artifact DLL
uses: actions/upload-artifact@v4
with:
name: windows_amd64_build_msvc_dll
path: ${{ env.BUILD_PATH }}/${{ env.BUILD_TYPE }}/libncmdump.dll
build_on_linux_amd64:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -82,10 +88,10 @@ jobs:
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: Linux amd64 Build name: linux_build_amd64
path: ${{ env.BUILD_PATH }}/ncmdump path: ${{ env.BUILD_PATH }}/ncmdump
build_on_macos: build_on_macos_amd64:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
@ -99,7 +105,7 @@ jobs:
brew install git cmake brew install git cmake
- name: Configure build - 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 - name: Build
run: cmake --build ${{ env.BUILD_PATH }} -j 4 run: cmake --build ${{ env.BUILD_PATH }} -j 4
@ -107,5 +113,30 @@ jobs:
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: 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 path: ${{ env.BUILD_PATH }}/ncmdump

2
.gitignore vendored
View File

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

View File

@ -14,34 +14,73 @@ if(MSVC)
add_compile_options("$<$<CXX_COMPILER_ID:MSVC>:/execution-charset:utf-8>") add_compile_options("$<$<CXX_COMPILER_ID:MSVC>:/execution-charset:utf-8>")
endif() endif()
FILE(GLOB HEADERS ./*.h) FILE(GLOB COMMON_HEADERS src/include/*.h)
FILE(GLOB COMMON_SOURCES cJSON.cpp aes.cpp main.cpp ncmcrypt.cpp) FILE(GLOB COMMON_SOURCES src/ncmcrypt.cpp src/utils/*.cpp)
FILE(GLOB WIN_SOURCES win32_init.cpp) FILE(GLOB EXECUTABLE_SOURCES src/main.cpp)
FILE(GLOB LIBRARY_HEADERS src/lib/libncmdump.h)
FILE(GLOB LIBRARY_SOURCES src/lib/*.cpp)
FILE(GLOB WIN_SOURCES src/platform/win32_init.cpp)
if(WIN32) if(WIN32)
set(SOURCES ${COMMON_SOURCES} ${WIN_SOURCES}) add_executable(ncmdump_exec
else() ${COMMON_HEADERS}
set(SOURCES ${COMMON_SOURCES}) ${COMMON_SOURCES}
endif() ${EXECUTABLE_SOURCES}
add_executable(ncmdump ${WIN_SOURCES}
${HEADERS} )
${SOURCES} add_library(ncmdump_lib SHARED
) ${COMMON_HEADERS}
target_link_libraries(ncmdump tag) ${COMMON_SOURCES}
target_include_directories(ncmdump PRIVATE taglib) ${LIBRARY_HEADERS}
target_include_directories(ncmdump PRIVATE taglib/taglib) ${LIBRARY_SOURCES}
target_include_directories(ncmdump PRIVATE taglib/taglib/toolkit) ${WIN_SOURCES}
target_include_directories(ncmdump PRIVATE taglib/taglib/mpeg/id3v2) )
set_target_properties(ncmdump_lib PROPERTIES OUTPUT_NAME "libncmdump")
target_include_directories(ncmdump_lib PRIVATE src/include)
target_link_libraries(ncmdump_lib tag)
target_include_directories(ncmdump_lib PRIVATE taglib)
target_include_directories(ncmdump_lib PRIVATE taglib/taglib)
target_include_directories(ncmdump_lib PRIVATE taglib/taglib/toolkit)
target_include_directories(ncmdump_lib PRIVATE taglib/taglib/mpeg/id3v2)
if(WIN32)
if(CMAKE_COMPILER_IS_GNUCXX) if(CMAKE_COMPILER_IS_GNUCXX)
target_link_options(ncmdump PRIVATE -static) # static link when using MinGW
target_link_options(ncmdump_exec PRIVATE -static)
target_link_options(ncmdump_lib PRIVATE -static)
endif() endif()
else()
add_executable(ncmdump_exec
${COMMON_HEADERS}
${COMMON_SOURCES}
${EXECUTABLE_SOURCES}
)
endif() endif()
# all executable target
set_target_properties(ncmdump_exec PROPERTIES OUTPUT_NAME "ncmdump")
target_include_directories(ncmdump_exec PRIVATE src/include)
target_link_libraries(ncmdump_exec tag)
target_include_directories(ncmdump_exec PRIVATE taglib)
target_include_directories(ncmdump_exec PRIVATE taglib/taglib)
target_include_directories(ncmdump_exec PRIVATE taglib/taglib/toolkit)
target_include_directories(ncmdump_exec PRIVATE taglib/taglib/mpeg/id3v2)
include(GNUInstallDirs) include(GNUInstallDirs)
install(TARGETS ncmdump
BUNDLE DESTINATION . if(WIN32)
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} install(TARGETS ncmdump_exec ncmdump_lib
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} BUNDLE DESTINATION .
) LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
else()
install(TARGETS ncmdump_exec
BUNDLE DESTINATION .
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
endif()

View File

@ -6,8 +6,9 @@
该版本为最早的 C++ 版本,也是作者开发的市面上第一个支持 ncm 转换的程序 该版本为最早的 C++ 版本,也是作者开发的市面上第一个支持 ncm 转换的程序
源码复刻自 anonymous5l/ncmdump感谢前辈的付出 源码复刻自 anonymous5l/ncmdump感谢前辈的付出做了 Windows 下的移植,修复了一些编译问题
做了 Windows 下的移植,修复了一些编译问题
1.3.0 版本更新说明因为之前有小伙伴反馈无法解密带有特殊字符的文件名例如中文、日文、韩文或者是表情符号等在1.3.0以及之后的版本彻底修复了这个问题,所有的 UTF-8 字符都可以正常解密。并且还自带了 dll 的构建可以供其他应用程序C#、Python、Java等调用。
## 传送门 ## 传送门
@ -15,7 +16,13 @@
## 使用 ## 使用
你可以使用 Homebrew 来安装 ncmdump 注意:自网易云音乐 3.0 版本开始,下载的 ncm 文件均不内置歌曲专辑的封面图片,需要从网络获取,介于在一个小工具中嵌入庞大网络库的非必要性,可以移步我的另一个仓库(<https://git.taurusxin.com/taurusxin/ncmdump-go>),下载完全使用 Golang 重写的新版本,支持从网络自动获取封面图并嵌入到目标音乐文件。
### 命令行工具
**[不推荐]** 你可以使用 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 ```shell
brew install ncmdump brew install ncmdump
@ -29,18 +36,49 @@ brew install ncmdump
ncmdump -h ncmdump -h
``` ```
命令行下输入一个或多个文件 使用 `-v``--version` 参数来打印版本信息
```shell ```shell
ncmdump file1 file2... ncmdump -v
``` ```
你可以使用 `-d` 参数来指定一个文件夹,对文件夹下的所有文件批量处理 处理单个或多个文件
```shell ```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
```
### 动态库
或者,如果你想利用此项目进行二次开发,例如在你的 C#、Python、Java 等项目中调用,你可以使用 `libncmdump` 动态库,具体使用方法见仓库的 `example` 文件夹
请注意!如果你在 Windows 下开发,传递到库构造函数的文件名编码**必须为 UTF-8 编码**,否则会抛出运行时错误。
## 编译项目 ## 编译项目
克隆本仓库 克隆本仓库
@ -67,6 +105,10 @@ cmake -G "Visual Studio 17 2022" -A x64 -B build
# Linux / macOS # Linux / macOS
cmake -DCMAKE_BUILD_TYPE=Release -B build 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
``` ```
编译项目 编译项目
@ -78,3 +120,9 @@ cmake --build build -j 8 --config Release
# Windows MinGW / Linux / macOS # Windows MinGW / Linux / macOS
cmake --build build -j 8 cmake --build build -j 8
``` ```
你可以在 `build` 文件夹下找到编译好的二进制文件,以及一个可供其它项目使用的动态库(Windows Only),使用方法见仓库的 `example` 文件夹
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=taurusxin/ncmdump&type=Date)](https://star-history.com/#taurusxin/ncmdump&Date)

View File

@ -0,0 +1,74 @@
using System;
using System.Runtime.InteropServices;
namespace libncmdump_demo_cli
{
/// <summary>
/// NeteaseCrypt C# Wrapper
/// </summary>
class NeteaseCrypt
{
const string DLL_PATH = "libncmdump.dll";
[DllImport(DLL_PATH, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr CreateNeteaseCrypt(IntPtr path);
[DllImport(DLL_PATH, CallingConvention = CallingConvention.Cdecl)]
private static extern int Dump(IntPtr NeteaseCrypt, IntPtr outputPath);
[DllImport(DLL_PATH, CallingConvention = CallingConvention.Cdecl)]
private static extern void FixMetadata(IntPtr NeteaseCrypt);
[DllImport(DLL_PATH, CallingConvention = CallingConvention.Cdecl)]
private static extern void DestroyNeteaseCrypt(IntPtr NeteaseCrypt);
private IntPtr NeteaseCryptClass = IntPtr.Zero;
/// <summary>
/// 创建 NeteaseCrypt 类的实例。
/// </summary>
/// <param name="FileName">网易云音乐 ncm 加密文件路径</param>
public NeteaseCrypt(string FileName)
{
byte[] bytes = Encoding.UTF8.GetBytes(FileName);
IntPtr inputPtr = Marshal.AllocHGlobal(bytes.Length + 1);
Marshal.Copy(bytes, 0, inputPtr, bytes.Length);
Marshal.WriteByte(inputPtr, bytes.Length, 0);
NeteaseCryptClass = CreateNeteaseCrypt(inputPtr);
}
/// <summary>
/// 启动转换过程。
/// </summary>
/// <param name="OutputPath">指定一个路径输出,如果为空,则输出到原路径</param>
/// <returns>返回一个整数指示转储过程的结果。如果成功返回0如果失败返回1。</returns>
public int Dump(string OutputPath)
{
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>
/// 修复音乐文件元数据。
/// </summary>
public void FixMetadata()
{
FixMetadata(NeteaseCryptClass);
}
/// <summary>
/// 销毁 NeteaseCrypt 类的实例。
/// </summary>
public void Destroy()
{
DestroyNeteaseCrypt(NeteaseCryptClass);
}
}
}

26
example/csharp/Program.cs Normal file
View File

@ -0,0 +1,26 @@
using System;
using System.Threading.Tasks;
namespace libncmdump_demo_cli
{
internal class Program
{
static void Main(string[] args)
{
// 文件名
string filePath = "test.ncm";
// 创建 NeteaseCrypt 类的实例
NeteaseCrypt neteaseCrypt = new NeteaseCrypt(filePath);
// 启动转换过程
int result = neteaseCrypt.Dump(""); // 为空则输出到源
// 修复元数据
neteaseCrypt.FixMetadata();
// [务必]销毁 NeteaseCrypt 类的实例
neteaseCrypt.Destroy();
}
}
}

124
main.cpp
View File

@ -1,124 +0,0 @@
#include "ncmcrypt.h"
#include <iostream>
#include <stdexcept>
#include <vector>
#include <filesystem>
#if defined(_WIN32)
#include "platform.h"
#endif
#include "color.h"
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)
{
if (fs::exists(filePath) == false)
{
std::cerr << BOLDRED << "Error: " << RESET << "file '" << filePath.u8string() << "' does not exist." << std::endl;
return;
}
try
{
NeteaseCrypt crypt(filePath.u8string());
crypt.Dump();
crypt.FixMetadata();
std::cout << BOLDGREEN << "Done: " << RESET << "'" << crypt.dumpFilepath().u8string() << "'" << std::endl;
}
catch (const std::invalid_argument &e)
{
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());
}
}
}
int main(int argc, char **argv)
{
#if defined(_WIN32)
win32_utf8argv(&argc, &argv);
#endif
if (argc <= 1)
{
displayHelp();
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 FOLDER "-d"
for (int i = 1; i < argc; ++i)
{
if (COMPARE_STR(argv[i], HELP_SHORT) || COMPARE_STR(argv[i], HELP_LONG))
{
displayHelp();
return 0;
}
else if (COMPARE_STR(argv[i], FOLDER))
{
processFolders = true;
if (i + 1 < argc && argv[i + 1][0] != '-')
{
folderProvided = true;
processFilesInFolder(argv[i + 1]);
// Skip the folder name
++i;
}
else
{
std::cerr << "Error: -d option requires a folder path." << std::endl;
return 1;
}
}
else
{
fs::path path = fs::u8path(argv[i]);
files.push_back(path);
}
}
for (const auto &file : files)
{
if (processFolders && fs::is_directory(file))
{
processFilesInFolder(file);
}
else
{
processFile(file);
}
}
return 0;
}

2909
src/include/cxxopts.hpp Normal file

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(); void Dump(std::string const&);
void FixMetadata(); 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

31
src/lib/libncmdump.cpp Normal file
View File

@ -0,0 +1,31 @@
#include "libncmdump.h"
#include <filesystem>
namespace fs = std::filesystem;
extern "C" {
API NeteaseCrypt* CreateNeteaseCrypt(const char* path) {
fs::path fPath = fs::u8path(path);
return new NeteaseCrypt(fPath.u8string());
}
API int Dump(NeteaseCrypt* neteaseCrypt, const char* outputPath) {
try
{
neteaseCrypt->Dump(outputPath);
}
catch (const std::invalid_argument& e)
{
return 1;
}
return 0;
}
API void FixMetadata(NeteaseCrypt* neteaseCrypt) {
neteaseCrypt->FixMetadata();
}
API void DestroyNeteaseCrypt(NeteaseCrypt* neteaseCrypt) {
delete neteaseCrypt;
}
}

14
src/lib/libncmdump.h Normal file
View File

@ -0,0 +1,14 @@
#include "ncmcrypt.h"
#ifdef _WIN32
#define API __declspec(dllexport)
#else
#define API
#endif
extern "C" {
API NeteaseCrypt* CreateNeteaseCrypt(const char* path);
API int Dump(NeteaseCrypt* neteaseCrypt, const char* outputPath);
API void FixMetadata(NeteaseCrypt* neteaseCrypt);
API void DestroyNeteaseCrypt(NeteaseCrypt* neteaseCrypt);
}

216
src/main.cpp Normal file
View File

@ -0,0 +1,216 @@
#include "ncmcrypt.h"
#include <iostream>
#include <stdexcept>
#include <vector>
#include <filesystem>
#if defined(_WIN32)
#include "platform.h"
#endif
#include "color.h"
#include "version.h"
#include "cxxopts.hpp"
namespace fs = std::filesystem;
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;
return;
}
// skip if not ending with ".ncm"
if (!filePath.has_extension() || filePath.extension().u8string() != ".ncm")
{
return;
}
try
{
NeteaseCrypt crypt(filePath.u8string());
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;
}
catch (...)
{
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); // Convert command line arguments to UTF-8 under Windows
#endif
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;
}
// print usage message if unrecognised options are present
if (result.unmatched().size() > 0)
{
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))
{
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
{
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

@ -2,6 +2,7 @@
#include "aes.h" #include "aes.h"
#include "base64.h" #include "base64.h"
#include "cJSON.h" #include "cJSON.h"
#include "color.h"
#define TAGLIB_STATIC #define TAGLIB_STATIC
#include "taglib/toolkit/tfile.h" #include "taglib/toolkit/tfile.h"
@ -15,6 +16,9 @@
#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};
@ -94,12 +98,18 @@ NeteaseMusicMetadata::NeteaseMusicMetadata(cJSON *raw)
artistLen = cJSON_GetArraySize(swap); artistLen = cJSON_GetArraySize(swap);
i = 0; i = 0;
for (i = 0; i < artistLen - 1; i++) for (i = 0; i < artistLen; i++)
{ {
mArtist += std::string(cJSON_GetStringValue(cJSON_GetArrayItem(cJSON_GetArrayItem(swap, i), 0))); auto artist = cJSON_GetArrayItem(swap, i);
mArtist += "/"; if (cJSON_GetArraySize(artist) > 0)
{
if (!mArtist.empty())
{
mArtist += "/";
}
mArtist += std::string(cJSON_GetStringValue(cJSON_GetArrayItem(artist, 0)));
}
} }
mArtist += std::string(cJSON_GetStringValue(cJSON_GetArrayItem(cJSON_GetArrayItem(swap, i), 0)));
} }
swap = cJSON_GetObjectItem(raw, "bitrate"); swap = cJSON_GetObjectItem(raw, "bitrate");
@ -214,7 +224,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.length() > 0) if (!mImageData.empty())
{ {
TagLib::ID3v2::AttachedPictureFrame *frame = new TagLib::ID3v2::AttachedPictureFrame; TagLib::ID3v2::AttachedPictureFrame *frame = new TagLib::ID3v2::AttachedPictureFrame;
@ -229,7 +239,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.length() > 0) if (!mImageData.empty())
{ {
TagLib::FLAC::Picture *cover = new TagLib::FLAC::Picture; TagLib::FLAC::Picture *cover = new TagLib::FLAC::Picture;
cover->setMimeType(mimeType(mImageData)); cover->setMimeType(mimeType(mImageData));
@ -247,14 +257,20 @@ 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 netease copyright protected dump tool. author 5L", TagLib::String::UTF8)); // tag->setComment(TagLib::String("Create by taurusxin/ncmdump.", TagLib::String::UTF8));
audioFile->save(); audioFile->save();
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); std::vector<unsigned char> buffer(0x8000);
@ -285,7 +301,7 @@ void NeteaseCrypt::Dump()
mFormat = NeteaseCrypt::FLAC; mFormat = NeteaseCrypt::FLAC;
} }
output.open(mDumpFilepath, output.out | output.binary); output.open(mDumpFilepath, std::ofstream::out | std::ofstream::binary);
} }
output.write((char *)buffer.data(), n); output.write((char *)buffer.data(), n);
@ -351,7 +367,7 @@ NeteaseCrypt::NeteaseCrypt(std::string const &path)
if (n <= 0) if (n <= 0)
{ {
std::cout << "[Warn] " << path << " missing metadata infomation can't fix some infomation!" << std::endl; std::cout << BOLDYELLOW << "[Warn] " << RESET << "'" << path << "' missing metadata infomation can't fix some infomation!" << std::endl;
mMetaData = NULL; mMetaData = NULL;
} }
@ -384,23 +400,24 @@ NeteaseCrypt::NeteaseCrypt(std::string const &path)
mMetaData = new NeteaseMusicMetadata(cJSON_Parse(modifyDecryptData.c_str())); mMetaData = new NeteaseMusicMetadata(cJSON_Parse(modifyDecryptData.c_str()));
} }
// skip crc32 & unuse charset // skip crc32 & image version
if (!mFile.seekg(9, mFile.cur)) if (!mFile.seekg(5, mFile.cur))
{ {
throw std::invalid_argument("can't seek file"); throw std::invalid_argument("can't seek file");
} }
uint32_t cover_frame_len{0};
read(reinterpret_cast<char *>(&cover_frame_len), 4);
read(reinterpret_cast<char *>(&n), sizeof(n)); read(reinterpret_cast<char *>(&n), sizeof(n));
if (n > 0) if (n > 0)
{ {
char *imageData = (char *)malloc(n); mImageData = std::string(n, '\0');
read(imageData, n); read(&mImageData[0], n);
mImageData = std::string(imageData, n);
} }
else else
{ {
std::cout << "[Warn] " << path << " missing album can't fix album image!" << std::endl; std::cout << BOLDYELLOW << "[Warn] " << RESET << "'" << path << "' missing album can't fix album image!" << std::endl;
} }
mFile.seekg(cover_frame_len - n, mFile.cur);
} }

2
taglib

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

Binary file not shown.