mirror of
https://github.com/real-zony/ZonyLrcToolsX.git
synced 2025-07-01 12:11:13 +00:00
feat: Now, it's possible to fetch playlists from NetEase Cloud Music and download lyrics.
This commit is contained in:
parent
5dbabab5a6
commit
18d9c2d32c
@ -4,7 +4,8 @@
|
||||
"10002": "需要扫描的目录不存在,请确认路径是否正确。",
|
||||
"10003": "不能获取文件的后缀信息。",
|
||||
"10004": "没有扫描到任何音乐文件。",
|
||||
"10005": "指定的编码不受支持,请检查配置,所有受支持的编码名称,请参考: https://docs.microsoft.com/en-us/dotnet/api/system.text.encodinginfo.codepage?view=net-6.0#system-text-encodinginfo-codepage。"
|
||||
"10005": "指定的编码不受支持,请检查配置,所有受支持的编码名称,请参考: https://docs.microsoft.com/en-us/dotnet/api/system.text.encodinginfo.codepage?view=net-6.0#system-text-encodinginfo-codepage。",
|
||||
"10006": "无法从网易云音乐获取歌曲列表。"
|
||||
},
|
||||
"Warning": {
|
||||
"50001": "扫描文件时出现了错误。",
|
||||
|
@ -0,0 +1,93 @@
|
||||
using System.Numerics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace ZonyLrcTools.Common.Infrastructure.Encryption;
|
||||
|
||||
/// <summary>
|
||||
/// 提供网易云音乐 API 的相关加密方法。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 加密方法参考以下开源项目:
|
||||
/// 1. https://github.com/jitwxs/163MusicLyrics/blob/master/MusicLyricApp/Api/Music/NetEaseMusicNativeApi.cs
|
||||
/// 2. https://github.com/mos9527/pyncm/blob/ad0a84b2ed5f1affa9890d5f54f6170c2cf99bbb/pyncm/utils/crypto.py#L53
|
||||
/// </remarks>
|
||||
public static class NetEaseMusicEncryptionHelper
|
||||
{
|
||||
public const string Modulus =
|
||||
"00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7";
|
||||
|
||||
public const string Nonce = "0CoJUm6Qyw8W8jud";
|
||||
public const string PubKey = "010001";
|
||||
public const string Vi = "0102030405060708";
|
||||
|
||||
public static string RsaEncode(string text)
|
||||
{
|
||||
var srText = new string(text.Reverse().ToArray());
|
||||
var a = BCHexDec(BitConverter.ToString(Encoding.Default.GetBytes(srText)).Replace("-", string.Empty));
|
||||
var b = BCHexDec(PubKey);
|
||||
var c = BCHexDec(Modulus);
|
||||
var key = BigInteger.ModPow(a, b, c).ToString("x");
|
||||
key = key.PadLeft(256, '0');
|
||||
|
||||
return key.Length > 256 ? key.Substring(key.Length - 256, 256) : key;
|
||||
}
|
||||
|
||||
public static BigInteger BCHexDec(string hex)
|
||||
{
|
||||
var dec = new BigInteger(0);
|
||||
var len = hex.Length;
|
||||
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
dec += BigInteger.Multiply(new BigInteger(Convert.ToInt32(hex[i].ToString(), 16)),
|
||||
BigInteger.Pow(new BigInteger(16), len - i - 1));
|
||||
}
|
||||
|
||||
return dec;
|
||||
}
|
||||
|
||||
public static string AesEncode(string secretData, string secret = "TA3YiYCfY2dDJQgg")
|
||||
{
|
||||
byte[] encrypted;
|
||||
var iv = Encoding.UTF8.GetBytes(Vi);
|
||||
|
||||
using (var aes = Aes.Create())
|
||||
{
|
||||
aes.Key = Encoding.UTF8.GetBytes(secret);
|
||||
aes.IV = iv;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
using (var encryptor = aes.CreateEncryptor())
|
||||
{
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
using (var cryptoStream = new CryptoStream(stream, encryptor, CryptoStreamMode.Write))
|
||||
{
|
||||
using (var sw = new StreamWriter(cryptoStream))
|
||||
{
|
||||
sw.Write(secretData);
|
||||
}
|
||||
|
||||
encrypted = stream.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Convert.ToBase64String(encrypted);
|
||||
}
|
||||
|
||||
public static string CreateSecretKey(int length)
|
||||
{
|
||||
const string str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
var sb = new StringBuilder(length);
|
||||
var rnd = new Random();
|
||||
|
||||
for (var i = 0; i < length; ++i)
|
||||
{
|
||||
sb.Append(str[rnd.Next(0, str.Length)]);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
@ -31,6 +31,11 @@ namespace ZonyLrcTools.Common.Infrastructure.Exceptions
|
||||
/// 文本: 指定的编码不受支持,请检查配置,所有受支持的编码名称。
|
||||
/// </summary>
|
||||
public const int NotSupportedFileEncoding = 10005;
|
||||
|
||||
/// <summary>
|
||||
/// 文本: 无法从网易云音乐获取歌曲列表。
|
||||
/// </summary>
|
||||
public const int UnableGetSongListFromNetEaseCloudMusic = 10006;
|
||||
|
||||
#endregion
|
||||
|
||||
|
@ -0,0 +1,80 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace ZonyLrcTools.Common.MusicScanner.JsonModel;
|
||||
|
||||
public sealed class GetMusicInfoFromNetEaseMusicSongListResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求结果代码,为 200 时请求成功。
|
||||
/// </summary>
|
||||
[JsonProperty("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌单信息。
|
||||
/// </summary>
|
||||
[JsonProperty("playlist")]
|
||||
public PlayListModel? PlayList { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PlayListModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 歌单的歌曲列表。
|
||||
/// </summary>
|
||||
[JsonProperty("tracks")]
|
||||
public ICollection<PlayListSongModel>? SongList { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PlayListSongModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 歌曲的名称。
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 歌曲的艺术家信息,可能会有多位艺术家/歌手。
|
||||
/// </summary>
|
||||
[JsonProperty("al")]
|
||||
[JsonConverter(typeof(PlayListSongArtistModelJsonConverter))]
|
||||
public ICollection<PlayListSongArtistModel>? Artists { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PlayListSongArtistModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 艺术家的名称。
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
public class PlayListSongArtistModelJsonConverter : JsonConverter
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var token = JToken.Load(reader);
|
||||
switch (token.Type)
|
||||
{
|
||||
case JTokenType.Array:
|
||||
return token.ToObject(objectType);
|
||||
case JTokenType.Object:
|
||||
return new List<PlayListSongArtistModel> { token.ToObject<PlayListSongArtistModel>() };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(ICollection<PlayListSongArtistModel>);
|
||||
}
|
||||
}
|
@ -1,109 +1,69 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Net.Http.Headers;
|
||||
using Newtonsoft.Json;
|
||||
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
|
||||
using ZonyLrcTools.Common.Infrastructure.Encryption;
|
||||
using ZonyLrcTools.Common.Infrastructure.Exceptions;
|
||||
using ZonyLrcTools.Common.Infrastructure.Network;
|
||||
using ZonyLrcTools.Common.MusicScanner.JsonModel;
|
||||
|
||||
namespace ZonyLrcTools.Common.MusicScanner;
|
||||
|
||||
public class NetEaseMusicSongListMusicScanner : ITransientDependency
|
||||
{
|
||||
}
|
||||
private readonly IWarpHttpClient _warpHttpClient;
|
||||
|
||||
public interface INetEaseMusicSongListMusicScanner
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Reference github links:
|
||||
/// https://github.com/Quandong-Zhang/easynet-playlist-downloader/blob/main/main.py
|
||||
/// https://github.com/mos9527/pyncm/blob/master/pyncm/apis/login.py
|
||||
/// </remarks>
|
||||
public class NetEaseMusicSessionManager : INetEaseMusicSongListMusicScanner, ISingletonDependency
|
||||
{
|
||||
}
|
||||
|
||||
public class NetEaseCrypto
|
||||
{
|
||||
private readonly (long, long) WEAPI_RSA_PUBKEY = (
|
||||
Convert.ToInt64(
|
||||
"00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7",
|
||||
16),
|
||||
Convert.ToInt64("10001", 16) // textbook rsa without padding
|
||||
);
|
||||
|
||||
private const string Base62 = "PJArHa0dpwhvMNYqKnTbitWfEmosQ9527ZBx46IXUgOzD81VuSFyckLRljG3eC";
|
||||
private const string Base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
private const string WEAPI_AES_KEY = "0CoJUm6Qyw8W8jud"; // cbc
|
||||
private const string WEAPI_AES_IV = "0102030405060708"; // cbc
|
||||
private const string LINUXAPI_AES_KEY = "rFgB&h#%2?^eDg:Q"; // ecb
|
||||
private const string EAPI_DIGEST_SALT = "nobody%(url)suse%(text)smd5forencrypt";
|
||||
private const string EAPI_DATA_SALT = "%(url)s-36cd479b6b5-%(text)s-36cd479b6b5-%(digest)s";
|
||||
private const string EAPI_AES_KEY = "e82ckenh8dichen8"; // ecb
|
||||
|
||||
public string WeApiEncrypt(string @params, string aesKey2 = null)
|
||||
public NetEaseMusicSongListMusicScanner(IWarpHttpClient warpHttpClient)
|
||||
{
|
||||
aesKey2 ??= RandomString(16);
|
||||
// 1st go,encrypt the text with aes_key and aes_iv.
|
||||
@params = AesEncrypt(@params, WEAPI_AES_KEY, WEAPI_AES_IV, CipherMode.CBC);
|
||||
// 2nd go,encrypt the ENCRYPTED text again,with the 2nd key and aes_iv.
|
||||
@params = AesEncrypt(@params, aesKey2, WEAPI_AES_IV);
|
||||
// 3rd go,generate RSA encrypted encSecKey.
|
||||
var encSecKey = HexDigest(RsaEncrypt(aesKey2, WEAPI_RSA_PUBKEY.Item1, WEAPI_RSA_PUBKEY.Item2));
|
||||
return $"params={@params}&encSecKey={encSecKey}";
|
||||
_warpHttpClient = warpHttpClient;
|
||||
}
|
||||
|
||||
public string AesEncrypt(string data, string key, string iv, CipherMode mode = CipherMode.CBC)
|
||||
public async Task<List<MusicInfo>> GetMusicInfoFromNetEaseMusicSongListAsync(string songListId, ManualDownloadOptions options)
|
||||
{
|
||||
using (var aes = Aes.Create())
|
||||
{
|
||||
aes.Key = Encoding.UTF8.GetBytes(key);
|
||||
aes.IV = Encoding.UTF8.GetBytes(iv);
|
||||
aes.Mode = mode;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
using (var encryptor = aes.CreateEncryptor(aes.Key, aes.IV))
|
||||
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:
|
||||
request =>
|
||||
{
|
||||
var plainBytes = System.Text.Encoding.UTF8.GetBytes(Pkcs7Pad(data));
|
||||
var encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
|
||||
return Convert.ToBase64String(encryptedBytes);
|
||||
}
|
||||
request.Content = new FormUrlEncodedContent(HandleRequest(new
|
||||
{
|
||||
csrf_token = "",
|
||||
id = songListId,
|
||||
n = 100000,
|
||||
offset = 0,
|
||||
total = true,
|
||||
limit = 1000,
|
||||
}, secretKey, encSecKey));
|
||||
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
|
||||
});
|
||||
|
||||
if (response.Code != 200 || response.PlayList?.SongList == null)
|
||||
{
|
||||
throw new ErrorCodeException(ErrorCodes.NotSupportedFileEncoding);
|
||||
}
|
||||
|
||||
return response.PlayList.SongList
|
||||
.Where(song => !string.IsNullOrEmpty(song.Name))
|
||||
.Select(song =>
|
||||
{
|
||||
var artistName = song.Artists?.FirstOrDefault()?.Name ?? string.Empty;
|
||||
var fakeFilePath = Path.Combine(options.OutputDirectory, options.OutputFileNamePattern.Replace("{Name}", song.Name).Replace("{Artist}", artistName));
|
||||
|
||||
var info = new MusicInfo(fakeFilePath, song.Name!, artistName);
|
||||
return info;
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private string Pkcs7Pad(string data, int bs = 16)
|
||||
private Dictionary<string, string> HandleRequest(object srcParams, string secretKey, string encSecKey)
|
||||
{
|
||||
var padding = bs - data.Length % bs;
|
||||
return data + new string((char)padding, padding);
|
||||
}
|
||||
|
||||
private string RandomString(int length, string chars = Base62)
|
||||
{
|
||||
var random = new Random();
|
||||
return string.Join("", Enumerable.Range(0, length).Select(_ => chars[random.Next(chars.Length)]));
|
||||
}
|
||||
|
||||
private string HexDigest(byte[] data)
|
||||
{
|
||||
return string.Join("", data.Select(b => b.ToString("X2")));
|
||||
}
|
||||
|
||||
static byte[] RsaEncrypt(string data, long n, long e, bool reverse = true)
|
||||
{
|
||||
var m = reverse ? data.Reverse() : data;
|
||||
var mBytes = Encoding.UTF8.GetBytes(string.Join("", m));
|
||||
var mHex = BitConverter.ToString(mBytes).Replace("-", string.Empty);
|
||||
var mInt = int.Parse(mHex, System.Globalization.NumberStyles.HexNumber);
|
||||
var r = (int)Math.Pow(mInt, e) % n;
|
||||
return BitConverter.GetBytes(r);
|
||||
}
|
||||
|
||||
static string HexCompose(string hex)
|
||||
{
|
||||
return BitConverter.ToString(Enumerable.Range(0, hex.Length)
|
||||
.Where(x => x % 2 == 0)
|
||||
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
|
||||
.ToArray());
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
{
|
||||
"params", NetEaseMusicEncryptionHelper.AesEncode(
|
||||
NetEaseMusicEncryptionHelper.AesEncode(
|
||||
JsonConvert.SerializeObject(srcParams), NetEaseMusicEncryptionHelper.Nonce), secretKey)
|
||||
},
|
||||
{ "encSecKey", encSecKey }
|
||||
};
|
||||
}
|
||||
}
|
@ -1,5 +1,19 @@
|
||||
namespace ZonyLrcTools.Tests.MusicScanner;
|
||||
using System.Threading.Tasks;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZonyLrcTools.Common.MusicScanner;
|
||||
|
||||
namespace ZonyLrcTools.Tests.MusicScanner;
|
||||
|
||||
public class NetEaseMusicSongListMusicScannerTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetMusicInfoFromNetEaseMusicSongListAsync_Test()
|
||||
{
|
||||
var musicScanner = GetService<NetEaseMusicSongListMusicScanner>();
|
||||
var musicInfo = await musicScanner.GetMusicInfoFromNetEaseMusicSongListAsync("7224428149", new ManualDownloadOptions());
|
||||
|
||||
musicInfo.ShouldNotBeNull();
|
||||
musicInfo.Count.ShouldBeGreaterThan(10);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user