From a46abba2f9d4e6c0c8cd6e70eb85598a43813da5 Mon Sep 17 00:00:00 2001 From: real-zony Date: Sun, 11 Dec 2022 21:28:25 +0800 Subject: [PATCH] feat: Use MusicDecrypto as a decryption driver. --- README.md | 14 +- .../Commands/SubCommand/UtilityCommand.cs | 70 ++----- .../Infrastructure/Logging/IWarpLogger.cs | 3 + .../Lyrics/LyricsDownloader.cs | 1 + .../MusicDecryption/DecryptionResult.cs | 25 ++- .../MusicDecryption/DefaultMusicDecryptor.cs | 44 +++++ .../MusicDecryption/IMusicDecryptor.cs | 15 +- .../MusicDecryption/NcmMusicDecryptor.cs | 180 ------------------ .../ZonyLrcTools.Common.csproj | 4 + .../Lyrics/NetEaseLyricsProviderTests.cs | 7 + .../NcmMusicDecryptor_Tests.cs | 23 +-- 11 files changed, 123 insertions(+), 263 deletions(-) create mode 100644 src/ZonyLrcTools.Common/MusicDecryption/DefaultMusicDecryptor.cs delete mode 100644 src/ZonyLrcTools.Common/MusicDecryption/NcmMusicDecryptor.cs diff --git a/README.md b/README.md index 5e2c400..741df94 100644 --- a/README.md +++ b/README.md @@ -29,29 +29,31 @@ macOS 和 Linux 用户请打开终端,切换到软件目录,一样执行命 子命令为 `download`,可用于下载歌词数据和专辑图像,支持多个下载器进行下载。 ```shell -./ZonyLrcTools.Cli.exe download -d|dir [-l|--lyric] [-a|--album] [-n|--number] +.\ZonyLrcTools.Cli.exe download -d|dir [-l|--lyric] [-a|--album] [-n|--number] -./ZonyLrcTools.Cli.exe download -h|--help +.\ZonyLrcTools.Cli.exe download -h|--help ``` **例子** ```shell # 下载歌词 -./ZonyLrcTools.Cli.exe download -d "C:\歌曲目录" -l -n 2 +.\ZonyLrcTools.Cli.exe download -d "C:\歌曲目录" -l -n 2 # 下载专辑封面 -./ZonyLrcTools.Cli.exe download -d "C:\歌曲目录" -a -n 2 +.\ZonyLrcTools.Cli.exe download -d "C:\歌曲目录" -a -n 2 ``` #### 加密格式转换 子命令为 `util`,可用于转换部分加密歌曲,**仅供个人研究学习使用,思路与源码都来自于网络**。 -目前软件支持 NCM、QCM(开发中...🚧) 格式的音乐文件转换,命令如下。 +具体支持的格式请参考项目 [MusicDecrypto](https://github.com/davidxuang/MusicDecrypto/blob/master/MusicDecrypto.Library/DecryptoFactory.cs#L23),本工具仅做一个集成,替换掉原本自己的一些实现。现在不需要指定对应的类型参数,程序会自动根据文件后缀选择适合的解密算法。 + +命令只需要一个参数 `-s`,指定需要转换的文件夹或者是文件路径。 ```shell -./ZonyLrcTools.Cli.exe util -t=Ncm D:\CloudMusic +.\ZonyLrcTools.Cli.exe util -s D:\CloudMusic ``` ### 配置文件 diff --git a/src/ZonyLrcTools.Cli/Commands/SubCommand/UtilityCommand.cs b/src/ZonyLrcTools.Cli/Commands/SubCommand/UtilityCommand.cs index c08bbb1..250dc32 100644 --- a/src/ZonyLrcTools.Cli/Commands/SubCommand/UtilityCommand.cs +++ b/src/ZonyLrcTools.Cli/Commands/SubCommand/UtilityCommand.cs @@ -1,24 +1,16 @@ -using System; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Threading.Tasks; using McMaster.Extensions.CommandLineUtils; using Microsoft.Extensions.Logging; -using Newtonsoft.Json.Linq; -using ZonyLrcTools.Common.Infrastructure.Exceptions; +using MusicDecrypto.Library; using ZonyLrcTools.Common.Infrastructure.IO; using ZonyLrcTools.Common.Infrastructure.Threading; using ZonyLrcTools.Common.MusicDecryption; namespace ZonyLrcTools.Cli.Commands.SubCommand { - public enum SupportFileType - { - Ncm = 1, - Qcm = 2 - } - /// /// 工具类相关命令。 /// @@ -28,13 +20,9 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand private readonly ILogger _logger; private readonly IMusicDecryptor _musicDecryptor; - [Required(ErrorMessage = "音乐格式为必须参数,请指定 -t 参数。")] - [Option("-t|--type", CommandOptionType.SingleValue, Description = "需要转换的文件格式,参数[Ncm、Qcm]。", ShowInHelpText = true)] - public SupportFileType Type { get; set; } - - [Required(ErrorMessage = "文件路径为必须按参数,请传入有效路径。")] - [Argument(0, "FilePath", "指定需要转换的音乐文件路径,支持目录和文件路径。")] - public string FilePath { get; set; } + [Required(ErrorMessage = "请指定需要解密的歌曲文件或文件夹路径。")] + [Option("-s|--source", CommandOptionType.SingleValue, Description = "需要解密的歌曲文件或文件夹路径。", ShowInHelpText = true)] + public string Source { get; set; } private readonly IFileScanner _fileScanner; @@ -49,57 +37,41 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand protected override async Task OnExecuteAsync(CommandLineApplication app) { - if (Directory.Exists(FilePath)) + if (Directory.Exists(Source)) { _logger.LogInformation("开始扫描文件夹,请稍等..."); - var files = (await _fileScanner.ScanAsync(FilePath, new[] { "*.ncm" })) + var files = (await _fileScanner.ScanAsync(Source, DecryptoFactory.KnownExtensions)) .SelectMany(f => f.FilePaths) .ToList(); _logger.LogInformation($"扫描完成,共 {files.Count} 个文件,准备转换。"); var wrapTask = new WarpTask(4); - var tasks = files.Select(path => wrapTask.RunAsync(() => Convert(path))); + var tasks = files.Select(path => wrapTask.RunAsync(async () => + { + _logger.LogInformation($"开始转换文件:{path}"); + var result = await _musicDecryptor.ConvertMusicAsync(path); + if (result.IsSuccess) + { + _logger.LogInformation($"转换完成,文件保存在:{result.OutputFilePath}"); + } + else + { + _logger.LogError($"转换失败,原因:{result.ErrorMessage}"); + } + })); await Task.WhenAll(tasks); } - else if (File.Exists(FilePath)) + else if (File.Exists(Source)) { - await Convert(FilePath); + await _musicDecryptor.ConvertMusicAsync(Source); } _logger.LogInformation("所有文件已经转换完成..."); return 0; } - - private async Task Convert(string filePath) - { - if (Type != SupportFileType.Ncm) - { - throw new ErrorCodeException(ErrorCodes.OnlySupportNcmFormatFile); - } - - var memoryStream = new MemoryStream(); - await using var file = File.Open(filePath, FileMode.Open); - { - var buffer = new Memory(new byte[2048]); - while (await file.ReadAsync(buffer) > 0) - { - // TODO: Large Object Issue!!!!! - await memoryStream.WriteAsync(buffer); - } - } - - // TODO: Large Object Issue!!!!! - var result = await _musicDecryptor.ConvertMusic(memoryStream.ToArray()); - var newFileName = Path.Combine(Path.GetDirectoryName(filePath), - $"{Path.GetFileNameWithoutExtension(filePath)}.{((JObject)result.ExtensionObjects["JSON"]).SelectToken("$.format").Value()}"); - - await using var musicFileStream = File.Create(newFileName); - await musicFileStream.WriteAsync(result.Data); - await musicFileStream.FlushAsync(); - } } } \ No newline at end of file diff --git a/src/ZonyLrcTools.Common/Infrastructure/Logging/IWarpLogger.cs b/src/ZonyLrcTools.Common/Infrastructure/Logging/IWarpLogger.cs index c522a63..4c0563d 100644 --- a/src/ZonyLrcTools.Common/Infrastructure/Logging/IWarpLogger.cs +++ b/src/ZonyLrcTools.Common/Infrastructure/Logging/IWarpLogger.cs @@ -1,5 +1,8 @@ namespace ZonyLrcTools.Common.Infrastructure.Logging; +/// +/// 日志记录器,包装了 CLI 和网页日志的两种输出方式。 +/// public interface IWarpLogger { Task DebugAsync(string message, Exception? exception = null); diff --git a/src/ZonyLrcTools.Common/Lyrics/LyricsDownloader.cs b/src/ZonyLrcTools.Common/Lyrics/LyricsDownloader.cs index 8234b46..b74d57d 100644 --- a/src/ZonyLrcTools.Common/Lyrics/LyricsDownloader.cs +++ b/src/ZonyLrcTools.Common/Lyrics/LyricsDownloader.cs @@ -113,6 +113,7 @@ public class LyricsDownloader : ILyricsDownloader, ISingletonDependency } } + // Convert UTF-8 to selected encoding. private byte[] Utf8ToSelectedEncoding(LyricsItemCollection lyrics) { var supportEncodings = Encoding.GetEncodings(); diff --git a/src/ZonyLrcTools.Common/MusicDecryption/DecryptionResult.cs b/src/ZonyLrcTools.Common/MusicDecryption/DecryptionResult.cs index a5e53fc..2ed1d9f 100644 --- a/src/ZonyLrcTools.Common/MusicDecryption/DecryptionResult.cs +++ b/src/ZonyLrcTools.Common/MusicDecryption/DecryptionResult.cs @@ -1,14 +1,29 @@ namespace ZonyLrcTools.Common.MusicDecryption { - public class DecryptionResult + public sealed class DecryptionResult { - public byte[] Data { get; protected set; } + public bool IsSuccess { get; set; } = false; - public Dictionary ExtensionObjects { get; set; } + public string? OutputFilePath { get; set; } = default; - public DecryptionResult(byte[] data) + public string? ErrorMessage { get; set; } = default; + + public static DecryptionResult Failed(string errorMessage) { - Data = data; + return new DecryptionResult + { + IsSuccess = false, + ErrorMessage = errorMessage + }; + } + + public static DecryptionResult Success(string outputFilePath) + { + return new DecryptionResult + { + IsSuccess = true, + OutputFilePath = outputFilePath + }; } } } \ No newline at end of file diff --git a/src/ZonyLrcTools.Common/MusicDecryption/DefaultMusicDecryptor.cs b/src/ZonyLrcTools.Common/MusicDecryption/DefaultMusicDecryptor.cs new file mode 100644 index 0000000..1e44629 --- /dev/null +++ b/src/ZonyLrcTools.Common/MusicDecryption/DefaultMusicDecryptor.cs @@ -0,0 +1,44 @@ +using MusicDecrypto.Library; +using ZonyLrcTools.Common.Infrastructure.DependencyInject; +using ZonyLrcTools.Common.Infrastructure.Logging; + +namespace ZonyLrcTools.Common.MusicDecryption +{ + /// + public class DefaultMusicDecryptor : IMusicDecryptor, ITransientDependency + { + private readonly IWarpLogger _warpLogger; + + public DefaultMusicDecryptor(IWarpLogger warpLogger) + { + _warpLogger = warpLogger; + } + + public async Task ConvertMusicAsync(string filePath) + { + try + { + await using var buffer = new MarshalMemoryStream(); + await using var file = new FileStream(filePath, FileMode.Open, FileAccess.Read); + buffer.SetLengthWithPadding(file.Length); + await file.CopyToAsync(buffer); + + using var decrypto = DecryptoFactory.Create(buffer, Path.GetFileName(filePath), message => { }); + var outFileName = decrypto.Decrypt().NewName; + var outFilePath = Path.Combine(Path.GetDirectoryName(filePath)!, outFileName); + + if (!File.Exists(outFilePath)) + { + await using var outFile = new FileStream(outFilePath, FileMode.Create, FileAccess.Write, FileShare.None); + await buffer.CopyToAsync(outFile); + } + + return DecryptionResult.Success(outFilePath); + } + catch (Exception e) + { + return DecryptionResult.Failed(e.Message); + } + } + } +} \ No newline at end of file diff --git a/src/ZonyLrcTools.Common/MusicDecryption/IMusicDecryptor.cs b/src/ZonyLrcTools.Common/MusicDecryption/IMusicDecryptor.cs index 0e3a707..b0c146d 100644 --- a/src/ZonyLrcTools.Common/MusicDecryption/IMusicDecryptor.cs +++ b/src/ZonyLrcTools.Common/MusicDecryption/IMusicDecryptor.cs @@ -1,15 +1,20 @@ -namespace ZonyLrcTools.Common.MusicDecryption +using MusicDecrypto.Library; + +namespace ZonyLrcTools.Common.MusicDecryption { /// /// 音乐解密器,用于将加密的歌曲数据,转换为可识别的歌曲格式。 + /// + /// 这个类型仅仅是对 相关功能的封装,方便在本工具进行单元测试。 + /// /// public interface IMusicDecryptor { /// - /// 将加密数据转换为可识别的歌曲格式。 + /// 将加密数据歌曲文件转换为可识别的歌曲格式。 /// - /// 源加密的歌曲数据。 - /// 解密完成的歌曲数据。 - Task ConvertMusic(byte[] sourceBytes); + /// 加密歌曲文件的路径。 + /// 转换结果。 + Task ConvertMusicAsync(string filePath); } } \ No newline at end of file diff --git a/src/ZonyLrcTools.Common/MusicDecryption/NcmMusicDecryptor.cs b/src/ZonyLrcTools.Common/MusicDecryption/NcmMusicDecryptor.cs deleted file mode 100644 index 96154de..0000000 --- a/src/ZonyLrcTools.Common/MusicDecryption/NcmMusicDecryptor.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -using Newtonsoft.Json.Linq; -using ZonyLrcTools.Common.Infrastructure.DependencyInject; - -namespace ZonyLrcTools.Common.MusicDecryption -{ - /// - /// NCM 音乐转换器,用于将 NCM 格式的音乐转换为可播放的格式。 - /// - public class NcmMusicDecryptor : IMusicDecryptor, ITransientDependency - { - protected readonly byte[] AesCoreKey = { 0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D, 0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E, 0x62, 0x61, 0x78, 0x57 }; - protected readonly byte[] AesModifyKey = { 0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 0x28 }; - - public async Task ConvertMusic(byte[] sourceBytes) - { - var stream = new MemoryStream(sourceBytes); - var streamReader = new BinaryReader(stream); - - var lengthBytes = new byte[4]; - lengthBytes = streamReader.ReadBytes(4); - if (BitConverter.ToInt32(lengthBytes) != 0x4e455443) - { - throw new Exception(); - } - - lengthBytes = streamReader.ReadBytes(4); - if (BitConverter.ToInt32(lengthBytes) != 0x4d414446) - { - throw new Exception(); - } - - stream.Seek(2, SeekOrigin.Current); - stream.Read(lengthBytes); - - var keyBytes = new byte[BitConverter.ToInt32(lengthBytes)]; - stream.Read(keyBytes); - - // 对已经加密的数据进行异或操作。 - for (int i = 0; i < keyBytes.Length; i++) - { - keyBytes[i] ^= 0x64; - } - - var coreKeyBytes = GetBytesByOffset(DecryptAes128Ecb(AesCoreKey, keyBytes), 17); - - var modifyDataBytes = new byte[streamReader.ReadInt32()]; - stream.Read(modifyDataBytes); - for (int i = 0; i < modifyDataBytes.Length; i++) - { - modifyDataBytes[i] ^= 0x63; - } - - var decryptBase64Bytes = Convert.FromBase64String(Encoding.UTF8.GetString(GetBytesByOffset(modifyDataBytes, 22))); - var decryptModifyData = DecryptAes128Ecb(AesModifyKey, decryptBase64Bytes); - - var musicInfoJson = JObject.Parse(Encoding.UTF8.GetString(GetBytesByOffset(decryptModifyData, 6))); - - // CRC 校验 - stream.Seek(4, SeekOrigin.Current); - stream.Seek(5, SeekOrigin.Current); - - GetAlbumImageBytes(stream, streamReader); - - var sBox = BuildKeyBox(coreKeyBytes); - return new DecryptionResult(GetMusicBytes(sBox, stream).ToArray()) - { - ExtensionObjects = new Dictionary - { - { "JSON", musicInfoJson } - } - }; - } - - private byte[] GetBytesByOffset(byte[] srcBytes, int offset = 0) - { - var resultBytes = new byte[srcBytes.Length - offset]; - Array.Copy(srcBytes, offset, resultBytes, 0, srcBytes.Length - offset); - return resultBytes; - } - - private byte[] DecryptAes128Ecb(byte[] keyBytes, byte[] data) - { - var aes = Aes.Create(); - aes.Padding = PaddingMode.PKCS7; - aes.Mode = CipherMode.ECB; - using var decryptor = aes.CreateDecryptor(keyBytes, null); - var result = decryptor.TransformFinalBlock(data, 0, data.Length); - - return result; - } - - /// - /// RC4 加密,生成 KeyBox。 - /// - /// - /// - private byte[] BuildKeyBox(byte[] key) - { - byte[] box = new byte[256]; - for (int i = 0; i < 256; ++i) - { - box[i] = (byte)i; - } - - byte keyLength = (byte)key.Length; - byte c; - byte lastByte = 0; - byte keyOffset = 0; - byte swap; - - for (int i = 0; i < 256; ++i) - { - swap = box[i]; - c = (byte)((swap + lastByte + key[keyOffset++]) & 0xff); - - if (keyOffset >= keyLength) - { - keyOffset = 0; - } - - box[i] = box[c]; - box[c] = swap; - lastByte = c; - } - - return box; - } - - /// - /// 获得歌曲的专辑图像信息。 - /// - /// 原始文件流。 - /// 二进制读取器。 - private byte[] GetAlbumImageBytes(Stream stream, BinaryReader streamReader) - { - var imgLength = streamReader.ReadInt32(); - - if (imgLength <= 0) - { - return null; - } - - var imgBuffer = streamReader.ReadBytes(imgLength); - - return imgBuffer; - } - - /// - /// 获得歌曲的完整数据。 - /// - /// - /// 原始文件流。 - private MemoryStream GetMusicBytes(byte[] sBox, Stream stream) - { - var n = 0x8000; - var memoryStream = new MemoryStream(); - - while (true) - { - var tb = new byte[n]; - var result = stream.Read(tb); - if (result <= 0) break; - - for (int i = 0; i < n; i++) - { - var j = (byte)((i + 1) & 0xff); - tb[i] ^= sBox[sBox[j] + sBox[(sBox[j] + j) & 0xff] & 0xff]; - } - - memoryStream.Write(tb); - } - - memoryStream.Flush(); - - return memoryStream; - } - } -} \ No newline at end of file diff --git a/src/ZonyLrcTools.Common/ZonyLrcTools.Common.csproj b/src/ZonyLrcTools.Common/ZonyLrcTools.Common.csproj index fa11a04..1f2e7b4 100644 --- a/src/ZonyLrcTools.Common/ZonyLrcTools.Common.csproj +++ b/src/ZonyLrcTools.Common/ZonyLrcTools.Common.csproj @@ -20,4 +20,8 @@ + + + + diff --git a/tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/NetEaseLyricsProviderTests.cs b/tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/NetEaseLyricsProviderTests.cs index ef8673a..85a9190 100644 --- a/tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/NetEaseLyricsProviderTests.cs +++ b/tests/ZonyLrcTools.Tests/Infrastructure/Lyrics/NetEaseLyricsProviderTests.cs @@ -98,5 +98,12 @@ namespace ZonyLrcTools.Tests.Infrastructure.Lyrics var lyric = await _lyricsProvider.DownloadAsync("Bones", "Image Dragons"); lyric.ToString().ShouldNotContain("Gimme, gimme, gimme some time to think"); } + + [Fact] + public async Task DownloadAsync_Issue123_Test() + { + var lyric = await _lyricsProvider.DownloadAsync("橄榄树", "苏曼"); + lyric.ToString().ShouldNotBeNullOrEmpty(); + } } } \ No newline at end of file diff --git a/tests/ZonyLrcTools.Tests/Infrastructure/MusicDecryption/NcmMusicDecryptor_Tests.cs b/tests/ZonyLrcTools.Tests/Infrastructure/MusicDecryption/NcmMusicDecryptor_Tests.cs index 5ce79d2..2060b7f 100644 --- a/tests/ZonyLrcTools.Tests/Infrastructure/MusicDecryption/NcmMusicDecryptor_Tests.cs +++ b/tests/ZonyLrcTools.Tests/Infrastructure/MusicDecryption/NcmMusicDecryptor_Tests.cs @@ -1,7 +1,7 @@ using System.IO; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json.Linq; +using Shouldly; using Xunit; using ZonyLrcTools.Common.MusicDecryption; @@ -10,26 +10,13 @@ namespace ZonyLrcTools.Tests.Infrastructure.MusicDecryption public class NcmMusicDecryptorTests : TestBase { [Fact] - public async Task ConvertMusic_Test() + public async Task ConvertMusicAsync_Test() { var decryptor = ServiceProvider.GetRequiredService(); + var ncmFilePath = Path.Combine(Directory.GetCurrentDirectory(), "MusicFiles", "Loren Gray - Queen.ncm"); - await using var fs = File.Open(Path.Combine(Directory.GetCurrentDirectory(), "MusicFiles", "Loren Gray - Queen.ncm"), FileMode.Open); - using var reader = new BinaryReader(fs); - var response = await decryptor.ConvertMusic(reader.ReadBytes((int) fs.Length)); - - var musicFilePath = Path.Combine(Directory.GetCurrentDirectory(), - "MusicFiles", - $"Loren Gray - Queen.{((JObject) response.ExtensionObjects["JSON"]).SelectToken("$.format").Value()}"); - - if (File.Exists(musicFilePath)) - { - File.Delete(musicFilePath); - } - - await using var musicFileStream = File.Create(musicFilePath); - await musicFileStream.WriteAsync(response.Data); - await musicFileStream.FlushAsync(); + var result = await decryptor.ConvertMusicAsync(ncmFilePath); + result.IsSuccess.ShouldBeTrue(); } } } \ No newline at end of file