From b916323986cb531dbbae2030c9f999b8a6960f33 Mon Sep 17 00:00:00 2001 From: real-zony Date: Sun, 12 Mar 2023 23:19:27 +0800 Subject: [PATCH] feat: We have improved the search and download logic for NetEase Cloud Music playlists and added support for logging in. --- .../Commands/SubCommand/DownloadCommand.cs | 5 + .../Commands/ToolCommandBase.cs | 2 +- src/ZonyLrcTools.Cli/Program.cs | 6 +- src/ZonyLrcTools.Cli/ZonyLrcTools.Cli.csproj | 1 + .../NetEaseMusicEncryptionHelper.cs | 16 +++ .../NetEaseMusicSongListMusicScanner.cs | 114 +++++++++++++++++- .../ZonyLrcTools.Common.csproj | 1 + tests/ZonyLrcTools.Tests/FileScannerTests.cs | 10 ++ 8 files changed, 146 insertions(+), 9 deletions(-) diff --git a/src/ZonyLrcTools.Cli/Commands/SubCommand/DownloadCommand.cs b/src/ZonyLrcTools.Cli/Commands/SubCommand/DownloadCommand.cs index 9576214..cec3311 100644 --- a/src/ZonyLrcTools.Cli/Commands/SubCommand/DownloadCommand.cs +++ b/src/ZonyLrcTools.Cli/Commands/SubCommand/DownloadCommand.cs @@ -72,6 +72,11 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand protected override async Task OnExecuteAsync(CommandLineApplication app) { + if (!DownloadAlbum && !DownloadLyric) + { + throw new ArgumentException("请至少指定一个下载选项,例如 -l(下载歌词) 或 -a(下载专辑图像)。"); + } + if (DownloadLyric) { await _lyricsDownloader.DownloadAsync(await GetMusicInfosAsync(Scanner), ParallelNumber); diff --git a/src/ZonyLrcTools.Cli/Commands/ToolCommandBase.cs b/src/ZonyLrcTools.Cli/Commands/ToolCommandBase.cs index b2021da..dc57c50 100644 --- a/src/ZonyLrcTools.Cli/Commands/ToolCommandBase.cs +++ b/src/ZonyLrcTools.Cli/Commands/ToolCommandBase.cs @@ -11,7 +11,7 @@ namespace ZonyLrcTools.Cli.Commands { protected virtual Task OnExecuteAsync(CommandLineApplication app) { - if (Environment.UserInteractive) + if (!Environment.UserInteractive) { Console.WriteLine("请使用终端运行此程序,如果你不知道如何操作,请访问 https://soft.myzony.com 或添加 QQ 群 337656932 寻求帮助。"); Console.ReadKey(); diff --git a/src/ZonyLrcTools.Cli/Program.cs b/src/ZonyLrcTools.Cli/Program.cs index 8ca0fc3..972e029 100644 --- a/src/ZonyLrcTools.Cli/Program.cs +++ b/src/ZonyLrcTools.Cli/Program.cs @@ -16,6 +16,8 @@ using ZonyLrcTools.Common.Infrastructure.DependencyInject; using ZonyLrcTools.Common.Infrastructure.Exceptions; using ZonyLrcTools.Common.Infrastructure.Network; +// ReSharper disable ClassNeverInstantiated.Global + namespace ZonyLrcTools.Cli { [Command("lyric-tool")] @@ -63,12 +65,12 @@ namespace ZonyLrcTools.Cli .WriteTo.Async(c => c.Console(theme: CustomConsoleTheme.Code)) .WriteTo.Logger(lc => { - lc.Filter.ByIncludingOnly(lc => lc.Level == LogEventLevel.Warning) + lc.Filter.ByIncludingOnly(warningLog => warningLog.Level == LogEventLevel.Warning) .WriteTo.Async(c => c.File("Logs/warnings.txt")); }) .WriteTo.Logger(lc => { - lc.Filter.ByIncludingOnly(lc => lc.Level == LogEventLevel.Error) + lc.Filter.ByIncludingOnly(errLog => errLog.Level == LogEventLevel.Error) .WriteTo.Async(c => c.File("Logs/errors.txt")); }) .CreateLogger(); diff --git a/src/ZonyLrcTools.Cli/ZonyLrcTools.Cli.csproj b/src/ZonyLrcTools.Cli/ZonyLrcTools.Cli.csproj index 898e4ba..f46bc70 100644 --- a/src/ZonyLrcTools.Cli/ZonyLrcTools.Cli.csproj +++ b/src/ZonyLrcTools.Cli/ZonyLrcTools.Cli.csproj @@ -15,6 +15,7 @@ + diff --git a/src/ZonyLrcTools.Common/Infrastructure/Encryption/NetEaseMusicEncryptionHelper.cs b/src/ZonyLrcTools.Common/Infrastructure/Encryption/NetEaseMusicEncryptionHelper.cs index 657e47e..d5e2a46 100644 --- a/src/ZonyLrcTools.Common/Infrastructure/Encryption/NetEaseMusicEncryptionHelper.cs +++ b/src/ZonyLrcTools.Common/Infrastructure/Encryption/NetEaseMusicEncryptionHelper.cs @@ -20,6 +20,7 @@ public static class NetEaseMusicEncryptionHelper public const string Nonce = "0CoJUm6Qyw8W8jud"; public const string PubKey = "010001"; public const string Vi = "0102030405060708"; + public static readonly byte[] ID_XOR_KEY_1 = "3go8&$8*3*3h0k(2)2"u8.ToArray(); public static string RsaEncode(string text) { @@ -90,4 +91,19 @@ public static class NetEaseMusicEncryptionHelper return sb.ToString(); } + + public static string CloudMusicDllEncode(string deviceId) + { + var xored = new byte[deviceId.Length]; + for (var i = 0; i < deviceId.Length; i++) + { + xored[i] = (byte)(deviceId[i] ^ ID_XOR_KEY_1[i % ID_XOR_KEY_1.Length]); + } + + using (var md5 = MD5.Create()) + { + var digest = md5.ComputeHash(xored); + return Convert.ToBase64String(digest); + } + } } \ No newline at end of file diff --git a/src/ZonyLrcTools.Common/MusicScanner/NetEaseMusicSongListMusicScanner.cs b/src/ZonyLrcTools.Common/MusicScanner/NetEaseMusicSongListMusicScanner.cs index 44cf038..f4c5c4e 100644 --- a/src/ZonyLrcTools.Common/MusicScanner/NetEaseMusicSongListMusicScanner.cs +++ b/src/ZonyLrcTools.Common/MusicScanner/NetEaseMusicSongListMusicScanner.cs @@ -1,5 +1,9 @@ +using System.Net; using System.Net.Http.Headers; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using QRCoder; using ZonyLrcTools.Common.Infrastructure.DependencyInject; using ZonyLrcTools.Common.Infrastructure.Encryption; using ZonyLrcTools.Common.Infrastructure.Exceptions; @@ -8,28 +12,43 @@ using ZonyLrcTools.Common.MusicScanner.JsonModel; namespace ZonyLrcTools.Common.MusicScanner; -public class NetEaseMusicSongListMusicScanner : ITransientDependency +public class NetEaseMusicSongListMusicScanner : ISingletonDependency { private readonly IWarpHttpClient _warpHttpClient; + private readonly ILogger _logger; + private const string Host = "https://music.163.com"; - public NetEaseMusicSongListMusicScanner(IWarpHttpClient warpHttpClient) + private string Cookie { get; set; } = string.Empty; + private string CsrfToken { get; set; } = string.Empty; + + public NetEaseMusicSongListMusicScanner(IWarpHttpClient warpHttpClient, + ILogger logger) { _warpHttpClient = warpHttpClient; + _logger = logger; } public async Task> GetMusicInfoFromNetEaseMusicSongListAsync(string songListId, string outputDirectory, string pattern) { + if (string.IsNullOrEmpty(Cookie)) + { + var loginResponse = await LoginViqQrCodeAsync(); + Cookie = loginResponse.cookieContainer?.GetCookieHeader(new Uri(Host)) ?? string.Empty; + CsrfToken = loginResponse.csrfToken ?? string.Empty; + } + var secretKey = NetEaseMusicEncryptionHelper.CreateSecretKey(16); var encSecKey = NetEaseMusicEncryptionHelper.RsaEncode(secretKey); - - var response = await _warpHttpClient.PostAsync("https://music.163.com/weapi/v6/playlist/detail?csrf_token=", requestOption: + var response = await _warpHttpClient.PostAsync( + $"{Host}/weapi/v6/playlist/detail?csrf_token=e5044820d8b66e14b8c31d39f9651a98", requestOption: request => { + request.Headers.Add("Cookie", Cookie); request.Content = new FormUrlEncodedContent(HandleRequest(new { - csrf_token = "", + csrf_token = CsrfToken, id = songListId, - n = 100000, + n = 1000, offset = 0, total = true, limit = 1000, @@ -66,4 +85,87 @@ public class NetEaseMusicSongListMusicScanner : ITransientDependency { "encSecKey", encSecKey } }; } + + private async Task<(string? csrfToken, CookieContainer? cookieContainer)> LoginViqQrCodeAsync() + { + // Get unikey. + var qrCodeKeyJson = await (await PostAsync($"{Host}/weapi/login/qrcode/unikey", new + { + type = 1 + })).Content.ReadAsStringAsync(); + var uniKey = JObject.Parse(qrCodeKeyJson).SelectToken("$.unikey")!.Value(); + if (string.IsNullOrEmpty(uniKey)) return (null, null); + + // Generate QR code. + var qrGenerator = new QRCodeGenerator(); + var qrCodeData = qrGenerator.CreateQrCode($"{Host}/login?codekey={uniKey}", + QRCodeGenerator.ECCLevel.L); + var qrCode = new AsciiQRCode(qrCodeData); + var asciiQrCodeString = qrCode.GetGraphic(1, drawQuietZones: false); + + _logger.LogInformation("请使用网易云 APP 扫码登录:"); + _logger.LogInformation(asciiQrCodeString); + + // Wait for login success. + var isLogin = false; + while (!isLogin) + { + var (isSuccess, cookieContainer) = await CheckIsLoginAsync(uniKey); + isLogin = isSuccess; + + if (!isLogin) + { + await Task.Delay(2000); + } + else + { + return (cookieContainer?.GetCookies(new Uri(Host))["__csrf"]?.Value, cookieContainer); + } + } + + return (null, null); + } + + private async Task<(bool isSuccess, CookieContainer? cookieContainer)> CheckIsLoginAsync(string uniKey) + { + var responseMessage = await PostAsync($"{Host}/weapi/login/qrcode/client/login", new + { + key = uniKey, + type = 1 + }); + + var responseString = await responseMessage.Content.ReadAsStringAsync(); + var responseCode = JObject.Parse(responseString)["code"]?.Value(); + + if (responseCode != 803) + { + return (false, null); + } + + if (!responseMessage.Headers.TryGetValues("Set-Cookie", out var cookies)) + { + return (false, null); + } + + var cookieContainer = new CookieContainer(); + foreach (var cookie in cookies) + { + cookieContainer.SetCookies(new Uri(Host), cookie); + } + + return (true, cookieContainer); + } + + private async Task PostAsync(string url, object @params) + { + var secretKey = NetEaseMusicEncryptionHelper.CreateSecretKey(16); + var encSecKey = NetEaseMusicEncryptionHelper.RsaEncode(secretKey); + + return await _warpHttpClient.PostReturnHttpResponseAsync(url, requestOption: + request => + { + request.Content = new FormUrlEncodedContent(HandleRequest(@params, secretKey, encSecKey)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); + }); + } } \ No newline at end of file diff --git a/src/ZonyLrcTools.Common/ZonyLrcTools.Common.csproj b/src/ZonyLrcTools.Common/ZonyLrcTools.Common.csproj index a01f686..33ee1db 100644 --- a/src/ZonyLrcTools.Common/ZonyLrcTools.Common.csproj +++ b/src/ZonyLrcTools.Common/ZonyLrcTools.Common.csproj @@ -14,6 +14,7 @@ + diff --git a/tests/ZonyLrcTools.Tests/FileScannerTests.cs b/tests/ZonyLrcTools.Tests/FileScannerTests.cs index fd25654..6c3334b 100644 --- a/tests/ZonyLrcTools.Tests/FileScannerTests.cs +++ b/tests/ZonyLrcTools.Tests/FileScannerTests.cs @@ -2,6 +2,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using QRCoder; using Shouldly; using Xunit; using ZonyLrcTools.Common.Infrastructure.IO; @@ -27,5 +28,14 @@ namespace ZonyLrcTools.Tests File.Delete(tempMusicFilePath); } + + [Fact] + public void TestConsole() + { + QRCodeGenerator qrGenerator = new QRCodeGenerator(); + QRCodeData qrCodeData = qrGenerator.CreateQrCode("https://y.music.163.com/m/login?codekey=2f0da1d0-759e-478b-9153-35058b3", QRCodeGenerator.ECCLevel.L); + AsciiQRCode qrCode = new AsciiQRCode(qrCodeData); + string qrCodeAsAsciiArt = qrCode.GetGraphic(1, drawQuietZones: false); + } } } \ No newline at end of file