feat: We have improved the search and download logic for NetEase Cloud Music playlists and added support for logging in.

This commit is contained in:
real-zony 2023-03-12 23:19:27 +08:00
parent afe1a7013c
commit b916323986
8 changed files with 146 additions and 9 deletions

View File

@ -72,6 +72,11 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
{
if (!DownloadAlbum && !DownloadLyric)
{
throw new ArgumentException("请至少指定一个下载选项,例如 -l(下载歌词) 或 -a(下载专辑图像)。");
}
if (DownloadLyric)
{
await _lyricsDownloader.DownloadAsync(await GetMusicInfosAsync(Scanner), ParallelNumber);

View File

@ -11,7 +11,7 @@ namespace ZonyLrcTools.Cli.Commands
{
protected virtual Task<int> OnExecuteAsync(CommandLineApplication app)
{
if (Environment.UserInteractive)
if (!Environment.UserInteractive)
{
Console.WriteLine("请使用终端运行此程序,如果你不知道如何操作,请访问 https://soft.myzony.com 或添加 QQ 群 337656932 寻求帮助。");
Console.ReadKey();

View File

@ -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();

View File

@ -15,6 +15,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />

View File

@ -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);
}
}
}

View File

@ -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<NetEaseMusicSongListMusicScanner> _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<NetEaseMusicSongListMusicScanner> logger)
{
_warpHttpClient = warpHttpClient;
_logger = logger;
}
public async Task<List<MusicInfo>> 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<GetMusicInfoFromNetEaseMusicSongListResponse>("https://music.163.com/weapi/v6/playlist/detail?csrf_token=", requestOption:
var response = await _warpHttpClient.PostAsync<GetMusicInfoFromNetEaseMusicSongListResponse>(
$"{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<string>();
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<int>();
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<HttpResponseMessage> 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");
});
}
}

View File

@ -14,6 +14,7 @@
<PackageReference Include="MusicDecrypto.Library" Version="2.2.0" />
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
</ItemGroup>

View File

@ -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);
}
}
}