mirror of
https://github.com/real-zony/ZonyLrcToolsX.git
synced 2025-07-02 05:10:42 +00:00
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:
parent
afe1a7013c
commit
b916323986
@ -72,6 +72,11 @@ namespace ZonyLrcTools.Cli.Commands.SubCommand
|
|||||||
|
|
||||||
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
|
protected override async Task<int> OnExecuteAsync(CommandLineApplication app)
|
||||||
{
|
{
|
||||||
|
if (!DownloadAlbum && !DownloadLyric)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("请至少指定一个下载选项,例如 -l(下载歌词) 或 -a(下载专辑图像)。");
|
||||||
|
}
|
||||||
|
|
||||||
if (DownloadLyric)
|
if (DownloadLyric)
|
||||||
{
|
{
|
||||||
await _lyricsDownloader.DownloadAsync(await GetMusicInfosAsync(Scanner), ParallelNumber);
|
await _lyricsDownloader.DownloadAsync(await GetMusicInfosAsync(Scanner), ParallelNumber);
|
||||||
|
@ -11,7 +11,7 @@ namespace ZonyLrcTools.Cli.Commands
|
|||||||
{
|
{
|
||||||
protected virtual Task<int> OnExecuteAsync(CommandLineApplication app)
|
protected virtual Task<int> OnExecuteAsync(CommandLineApplication app)
|
||||||
{
|
{
|
||||||
if (Environment.UserInteractive)
|
if (!Environment.UserInteractive)
|
||||||
{
|
{
|
||||||
Console.WriteLine("请使用终端运行此程序,如果你不知道如何操作,请访问 https://soft.myzony.com 或添加 QQ 群 337656932 寻求帮助。");
|
Console.WriteLine("请使用终端运行此程序,如果你不知道如何操作,请访问 https://soft.myzony.com 或添加 QQ 群 337656932 寻求帮助。");
|
||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
|
@ -16,6 +16,8 @@ using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
|||||||
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||||
using ZonyLrcTools.Common.Infrastructure.Network;
|
using ZonyLrcTools.Common.Infrastructure.Network;
|
||||||
|
|
||||||
|
// ReSharper disable ClassNeverInstantiated.Global
|
||||||
|
|
||||||
namespace ZonyLrcTools.Cli
|
namespace ZonyLrcTools.Cli
|
||||||
{
|
{
|
||||||
[Command("lyric-tool")]
|
[Command("lyric-tool")]
|
||||||
@ -63,12 +65,12 @@ namespace ZonyLrcTools.Cli
|
|||||||
.WriteTo.Async(c => c.Console(theme: CustomConsoleTheme.Code))
|
.WriteTo.Async(c => c.Console(theme: CustomConsoleTheme.Code))
|
||||||
.WriteTo.Logger(lc =>
|
.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.Async(c => c.File("Logs/warnings.txt"));
|
||||||
})
|
})
|
||||||
.WriteTo.Logger(lc =>
|
.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"));
|
.WriteTo.Async(c => c.File("Logs/errors.txt"));
|
||||||
})
|
})
|
||||||
.CreateLogger();
|
.CreateLogger();
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" 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="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.Extensions.Hosting" Version="5.0.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
|
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
|
||||||
|
@ -20,6 +20,7 @@ public static class NetEaseMusicEncryptionHelper
|
|||||||
public const string Nonce = "0CoJUm6Qyw8W8jud";
|
public const string Nonce = "0CoJUm6Qyw8W8jud";
|
||||||
public const string PubKey = "010001";
|
public const string PubKey = "010001";
|
||||||
public const string Vi = "0102030405060708";
|
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)
|
public static string RsaEncode(string text)
|
||||||
{
|
{
|
||||||
@ -90,4 +91,19 @@ public static class NetEaseMusicEncryptionHelper
|
|||||||
|
|
||||||
return sb.ToString();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,5 +1,9 @@
|
|||||||
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using QRCoder;
|
||||||
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||||
using ZonyLrcTools.Common.Infrastructure.Encryption;
|
using ZonyLrcTools.Common.Infrastructure.Encryption;
|
||||||
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||||
@ -8,28 +12,43 @@ using ZonyLrcTools.Common.MusicScanner.JsonModel;
|
|||||||
|
|
||||||
namespace ZonyLrcTools.Common.MusicScanner;
|
namespace ZonyLrcTools.Common.MusicScanner;
|
||||||
|
|
||||||
public class NetEaseMusicSongListMusicScanner : ITransientDependency
|
public class NetEaseMusicSongListMusicScanner : ISingletonDependency
|
||||||
{
|
{
|
||||||
private readonly IWarpHttpClient _warpHttpClient;
|
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;
|
_warpHttpClient = warpHttpClient;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<MusicInfo>> GetMusicInfoFromNetEaseMusicSongListAsync(string songListId, string outputDirectory, string pattern)
|
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 secretKey = NetEaseMusicEncryptionHelper.CreateSecretKey(16);
|
||||||
var encSecKey = NetEaseMusicEncryptionHelper.RsaEncode(secretKey);
|
var encSecKey = NetEaseMusicEncryptionHelper.RsaEncode(secretKey);
|
||||||
|
var response = await _warpHttpClient.PostAsync<GetMusicInfoFromNetEaseMusicSongListResponse>(
|
||||||
var response = await _warpHttpClient.PostAsync<GetMusicInfoFromNetEaseMusicSongListResponse>("https://music.163.com/weapi/v6/playlist/detail?csrf_token=", requestOption:
|
$"{Host}/weapi/v6/playlist/detail?csrf_token=e5044820d8b66e14b8c31d39f9651a98", requestOption:
|
||||||
request =>
|
request =>
|
||||||
{
|
{
|
||||||
|
request.Headers.Add("Cookie", Cookie);
|
||||||
request.Content = new FormUrlEncodedContent(HandleRequest(new
|
request.Content = new FormUrlEncodedContent(HandleRequest(new
|
||||||
{
|
{
|
||||||
csrf_token = "",
|
csrf_token = CsrfToken,
|
||||||
id = songListId,
|
id = songListId,
|
||||||
n = 100000,
|
n = 1000,
|
||||||
offset = 0,
|
offset = 0,
|
||||||
total = true,
|
total = true,
|
||||||
limit = 1000,
|
limit = 1000,
|
||||||
@ -66,4 +85,87 @@ public class NetEaseMusicSongListMusicScanner : ITransientDependency
|
|||||||
{ "encSecKey", encSecKey }
|
{ "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");
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
@ -14,6 +14,7 @@
|
|||||||
<PackageReference Include="MusicDecrypto.Library" Version="2.2.0" />
|
<PackageReference Include="MusicDecrypto.Library" Version="2.2.0" />
|
||||||
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="2.2.0" />
|
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="2.2.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||||
|
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using QRCoder;
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using ZonyLrcTools.Common.Infrastructure.IO;
|
using ZonyLrcTools.Common.Infrastructure.IO;
|
||||||
@ -27,5 +28,14 @@ namespace ZonyLrcTools.Tests
|
|||||||
|
|
||||||
File.Delete(tempMusicFilePath);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user