From 607e0ef4cf8f9450e82bfbab0b8e0136de4e57e6 Mon Sep 17 00:00:00 2001 From: LanZhan Date: Tue, 20 Jan 2026 20:04:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0C#14=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .csharpierignore | 3 + .../Helpers/ResourceExtensions.cs | 35 +- .../Helpers/SettingsStorageExtensions.cs | 195 +++---- .../Extensions/ExceptionExtensions.cs | 49 +- .../Extensions/HttpClientExtensions.cs | 186 ++++--- .../Extensions/HttpExtensions.cs | 17 +- .../CloudMusicAPI/NeteaseCloudMusicApi.cs | 72 +-- .../NeteaseCloudMusicApiProvider.cs | 18 +- .../OnlineAPIs/CloudMusicAPI/Utils/Crypto.cs | 215 ++------ .../CloudMusicAPI/Utils/Extensions.cs | 41 +- .../OnlineAPIs/CloudMusicAPI/Utils/Options.cs | 10 +- .../OnlineAPIs/CloudMusicAPI/Utils/Request.cs | 233 ++++----- .../Services/ColorExtractionService.cs | 21 +- .../Services/ZLoggerExtensions.cs | 477 ++++++++---------- 14 files changed, 622 insertions(+), 950 deletions(-) create mode 100644 .csharpierignore diff --git a/.csharpierignore b/.csharpierignore new file mode 100644 index 0000000..907c886 --- /dev/null +++ b/.csharpierignore @@ -0,0 +1,3 @@ +App.xaml.cs +**/*.xaml +**/*.csproj \ No newline at end of file diff --git a/UntamedMusicPlayer/Helpers/ResourceExtensions.cs b/UntamedMusicPlayer/Helpers/ResourceExtensions.cs index 2a9c9e0..2fea9c4 100644 --- a/UntamedMusicPlayer/Helpers/ResourceExtensions.cs +++ b/UntamedMusicPlayer/Helpers/ResourceExtensions.cs @@ -6,29 +6,24 @@ public static class ResourceExtensions { private static readonly ResourceLoader _resourceLoader = new(); - public static string GetLocalized(this string resourceKey) => - _resourceLoader.GetString(resourceKey); - - public static string GetLocalizedWithReplace( - this string resourceKey, - string placeholder, - string value - ) + extension(string resourceKey) { - var template = _resourceLoader.GetString(resourceKey); - return template.Replace(placeholder, value); - } + public string GetLocalized() => _resourceLoader.GetString(resourceKey); - public static string GetLocalizedWithReplace( - this string resourceKey, - IDictionary replacements - ) - { - var template = _resourceLoader.GetString(resourceKey); - foreach (var (placeholder, value) in replacements) + public string GetLocalizedWithReplace(string placeholder, string value) { - template = template.Replace(placeholder, value); + var template = _resourceLoader.GetString(resourceKey); + return template.Replace(placeholder, value); + } + + public string GetLocalizedWithReplace(IDictionary replacements) + { + var template = _resourceLoader.GetString(resourceKey); + foreach (var (placeholder, value) in replacements) + { + template = template.Replace(placeholder, value); + } + return template; } - return template; } } diff --git a/UntamedMusicPlayer/Helpers/SettingsStorageExtensions.cs b/UntamedMusicPlayer/Helpers/SettingsStorageExtensions.cs index 3145252..3ff084a 100644 --- a/UntamedMusicPlayer/Helpers/SettingsStorageExtensions.cs +++ b/UntamedMusicPlayer/Helpers/SettingsStorageExtensions.cs @@ -7,110 +7,117 @@ public static class SettingsStorageExtensions { private const string FileExtension = ".json"; - public static bool IsRoamingStorageAvailable(this ApplicationData appData) + extension(ApplicationData appData) { - return appData.RoamingStorageQuota == 0; - } - - public static async Task SaveAsync(this StorageFolder folder, string name, T content) - { - var file = await folder.CreateFileAsync( - GetFileName(name), - CreationCollisionOption.ReplaceExisting - ); - var fileContent = await Json.StringifyAsync(content!); - - await FileIO.WriteTextAsync(file, fileContent); - } - - public static async Task ReadAsync(this StorageFolder folder, string name) - { - if (!File.Exists(Path.Combine(folder.Path, GetFileName(name)))) + public bool IsRoamingStorageAvailable() { + return appData.RoamingStorageQuota == 0; + } + } + + extension(StorageFolder folder) + { + public async Task ReadAsync(string name) + { + if (!File.Exists(Path.Combine(folder.Path, GetFileName(name)))) + { + return default; + } + + var file = await folder.GetFileAsync($"{name}.json"); + var fileContent = await FileIO.ReadTextAsync(file); + + return await Json.ToObjectAsync(fileContent); + } + + public async Task SaveAsync(string name, T content) + { + var file = await folder.CreateFileAsync( + GetFileName(name), + CreationCollisionOption.ReplaceExisting + ); + var fileContent = await Json.StringifyAsync(content!); + + await FileIO.WriteTextAsync(file, fileContent); + } + + public async Task ReadFileAsync(string fileName) + { + var item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false); + + if ((item is not null) && item.IsOfType(StorageItemTypes.File)) + { + var storageFile = await folder.GetFileAsync(fileName); + var content = await storageFile.ReadBytesAsync(); + return content; + } + + return null; + } + + public async Task SaveFileAsync( + byte[] content, + string fileName, + CreationCollisionOption options = CreationCollisionOption.ReplaceExisting + ) + { + ArgumentNullException.ThrowIfNull(content); + + if (string.IsNullOrEmpty(fileName)) + { + throw new ArgumentException( + "File name is null or empty. Specify a valid file name", + nameof(fileName) + ); + } + + var storageFile = await folder.CreateFileAsync(fileName, options); + await FileIO.WriteBytesAsync(storageFile, content); + return storageFile; + } + } + + extension(StorageFile file) + { + public async Task ReadBytesAsync() + { + if (file is not null) + { + using IRandomAccessStream stream = await file.OpenReadAsync(); + using var reader = new DataReader(stream.GetInputStreamAt(0)); + await reader.LoadAsync((uint)stream.Size); + var bytes = new byte[stream.Size]; + reader.ReadBytes(bytes); + return bytes; + } + + return null; + } + } + + extension(ApplicationDataContainer settings) + { + public async Task ReadAsync(string key) + { + object? obj; + + if (settings.Values.TryGetValue(key, out obj)) + { + return await Json.ToObjectAsync((string)obj); + } + return default; } - var file = await folder.GetFileAsync($"{name}.json"); - var fileContent = await FileIO.ReadTextAsync(file); - - return await Json.ToObjectAsync(fileContent); - } - - public static async Task SaveAsync( - this ApplicationDataContainer settings, - string key, - T value - ) - { - settings.SaveString(key, await Json.StringifyAsync(value!)); - } - - public static void SaveString(this ApplicationDataContainer settings, string key, string value) - { - settings.Values[key] = value; - } - - public static async Task ReadAsync(this ApplicationDataContainer settings, string key) - { - object? obj; - - if (settings.Values.TryGetValue(key, out obj)) + public async Task SaveAsync(string key, T value) { - return await Json.ToObjectAsync((string)obj); + settings.SaveString(key, await Json.StringifyAsync(value!)); } - return default; - } - - public static async Task SaveFileAsync( - this StorageFolder folder, - byte[] content, - string fileName, - CreationCollisionOption options = CreationCollisionOption.ReplaceExisting - ) - { - ArgumentNullException.ThrowIfNull(content); - - if (string.IsNullOrEmpty(fileName)) + public void SaveString(string key, string value) { - throw new ArgumentException( - "File name is null or empty. Specify a valid file name", - nameof(fileName) - ); + settings.Values[key] = value; } - - var storageFile = await folder.CreateFileAsync(fileName, options); - await FileIO.WriteBytesAsync(storageFile, content); - return storageFile; - } - - public static async Task ReadFileAsync(this StorageFolder folder, string fileName) - { - var item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false); - - if ((item is not null) && item.IsOfType(StorageItemTypes.File)) - { - var storageFile = await folder.GetFileAsync(fileName); - var content = await storageFile.ReadBytesAsync(); - return content; - } - - return null; - } - - public static async Task ReadBytesAsync(this StorageFile file) - { - if (file is not null) - { - using IRandomAccessStream stream = await file.OpenReadAsync(); - using var reader = new DataReader(stream.GetInputStreamAt(0)); - await reader.LoadAsync((uint)stream.Size); - var bytes = new byte[stream.Size]; - reader.ReadBytes(bytes); - return bytes; - } - - return null; } private static string GetFileName(string name) diff --git a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Extensions/ExceptionExtensions.cs b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Extensions/ExceptionExtensions.cs index 4bbe2ef..6bf2c8d 100644 --- a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Extensions/ExceptionExtensions.cs +++ b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Extensions/ExceptionExtensions.cs @@ -1,37 +1,38 @@ -#pragma warning disable - using System.Text; namespace UntamedMusicPlayer.OnlineAPIs.CloudMusicAPI.Extensions; internal static class ExceptionExtensions { - /// - /// 获取最内层异常 - /// - /// - /// - public static Exception GetInmostException(this Exception exception) + extension(Exception exception) { - ArgumentNullException.ThrowIfNull(exception); + /// + /// 获取最内层异常 + /// + /// + /// + public Exception GetInmostException() + { + ArgumentNullException.ThrowIfNull(exception); - return exception.InnerException is null - ? exception - : exception.InnerException.GetInmostException(); - } + return exception.InnerException is null + ? exception + : exception.InnerException.GetInmostException(); + } - /// - /// 返回一个字符串,其中包含异常的所有信息。 - /// - /// - /// - public static string ToFullString(this Exception exception) - { - ArgumentNullException.ThrowIfNull(exception); + /// + /// 返回一个字符串,其中包含异常的所有信息。 + /// + /// + /// + public string ToFullString() + { + ArgumentNullException.ThrowIfNull(exception); - var sb = new StringBuilder(); - DumpException(exception, sb); - return sb.ToString(); + var sb = new StringBuilder(); + DumpException(exception, sb); + return sb.ToString(); + } } private static void DumpException(Exception exception, StringBuilder sb) diff --git a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Extensions/HttpClientExtensions.cs b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Extensions/HttpClientExtensions.cs index 174623b..24303f4 100644 --- a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Extensions/HttpClientExtensions.cs +++ b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Extensions/HttpClientExtensions.cs @@ -1,5 +1,3 @@ -#pragma warning disable - using System.Net.Http.Headers; using System.Text; @@ -7,114 +5,104 @@ namespace UntamedMusicPlayer.OnlineAPIs.CloudMusicAPI.Extensions; internal static class HttpClientExtensions { - public static Task SendAsync( - this HttpClient client, - HttpMethod method, - string url - ) => client.SendAsync(method, url, null, null); - - public static Task SendAsync( - this HttpClient client, - HttpMethod method, - string url, - IEnumerable> queries, - IEnumerable> headers - ) => - client.SendAsync( - method, - url, - queries, - headers, - (byte[])null, - "application/x-www-form-urlencoded" - ); - - public static Task SendAsync( - this HttpClient client, - HttpMethod method, - string url, - IEnumerable> queries, - IEnumerable> headers, - string content, - string contentType - ) => - client.SendAsync( - method, - url, - queries, - headers, - content is null ? null : Encoding.UTF8.GetBytes(content), - contentType - ); - - public static Task SendAsync( - this HttpClient client, - HttpMethod method, - string url, - IEnumerable> queries, - IEnumerable> headers, - byte[] content, - string contentType - ) + extension(HttpClient client) { - ArgumentNullException.ThrowIfNull(client); + public Task SendAsync(HttpMethod method, string? url) => + client.SendAsync(method, url, null, null); - ArgumentNullException.ThrowIfNull(method); + public Task SendAsync( + HttpMethod method, + string? url, + IEnumerable>? queries, + IEnumerable>? headers + ) => + client.SendAsync( + method, + url, + queries, + headers, + (byte[]?)null, + "application/x-www-form-urlencoded" + ); - if (string.IsNullOrEmpty(url)) + public Task SendAsync( + HttpMethod method, + string? url, + IEnumerable>? queries, + IEnumerable>? headers, + string? content, + string? contentType + ) => + client.SendAsync( + method, + url, + queries, + headers, + content is null ? null : Encoding.UTF8.GetBytes(content), + contentType + ); + + public Task SendAsync( + HttpMethod method, + string? url, + IEnumerable>? queries, + IEnumerable>? headers, + byte[]? content, + string? contentType + ) { - throw new ArgumentNullException(nameof(url)); - } + ArgumentNullException.ThrowIfNull(client); + ArgumentNullException.ThrowIfNull(method); - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - UriBuilder uriBuilder; - HttpRequestMessage request; - - uriBuilder = new UriBuilder(url); - if (queries is not null) - { - string query; - - query = queries.ToQueryString(); - if (!string.IsNullOrEmpty(query)) + if (string.IsNullOrEmpty(url)) { - if (string.IsNullOrEmpty(uriBuilder.Query)) + throw new ArgumentNullException(nameof(url)); + } + + if (string.IsNullOrEmpty(contentType)) + { + throw new ArgumentNullException(nameof(contentType)); + } + + var uriBuilder = new UriBuilder(url); + HttpRequestMessage request; + + if (queries is not null) + { + var query = queries.ToQueryString(); + if (!string.IsNullOrEmpty(query)) { - uriBuilder.Query = query; - } - else - { - uriBuilder.Query += "&" + query; + if (string.IsNullOrEmpty(uriBuilder.Query)) + { + uriBuilder.Query = query; + } + else + { + uriBuilder.Query += "&" + query; + } } } - } - request = new HttpRequestMessage(method, uriBuilder.Uri); - if (content is not null) - { - request.Content = new ByteArrayContent(content); - } - else if (queries is not null && method != HttpMethod.Get) - { - request.Content = new FormUrlEncodedContent(queries); - } - - if (request.Content is not null) - { - request.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); - } - - if (headers is not null) - { - foreach (var header in headers) + request = new HttpRequestMessage(method, uriBuilder.Uri); + if (content is not null) { - request.Headers.TryAddWithoutValidation(header.Key, header.Value); + request.Content = new ByteArrayContent(content); + } + else if (queries is not null && method != HttpMethod.Get) + { + request.Content = new FormUrlEncodedContent(queries); } - } - return client.SendAsync(request); + request.Content?.Headers.ContentType = new MediaTypeHeaderValue(contentType); + + if (headers is not null) + { + foreach (var header in headers) + { + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + } + + return client.SendAsync(request); + } } } diff --git a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Extensions/HttpExtensions.cs b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Extensions/HttpExtensions.cs index 729cb5c..869a580 100644 --- a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Extensions/HttpExtensions.cs +++ b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Extensions/HttpExtensions.cs @@ -2,13 +2,18 @@ namespace UntamedMusicPlayer.OnlineAPIs.CloudMusicAPI.Extensions; internal static class HttpExtensions { - public static string ToQueryString(this IEnumerable> queries) + extension(IEnumerable> queries) { - ArgumentNullException.ThrowIfNull(queries); + public string ToQueryString() + { + ArgumentNullException.ThrowIfNull(queries); - return string.Join( - "&", - queries.Select(t => Uri.EscapeDataString(t.Key) + "=" + Uri.EscapeDataString(t.Value)) - ); + return string.Join( + "&", + queries.Select(t => + Uri.EscapeDataString(t.Key) + "=" + Uri.EscapeDataString(t.Value) + ) + ); + } } } diff --git a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/NeteaseCloudMusicApi.cs b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/NeteaseCloudMusicApi.cs index 1895267..507d455 100644 --- a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/NeteaseCloudMusicApi.cs +++ b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/NeteaseCloudMusicApi.cs @@ -1,5 +1,4 @@ #pragma warning disable - using System.Net; using System.Text; using System.Text.Json.Nodes; @@ -75,22 +74,21 @@ public sealed partial class NeteaseCloudMusicApi : IDisposable ) { ArgumentNullException.ThrowIfNull(provider); - ArgumentNullException.ThrowIfNull(queries); if (provider == CloudMusicApiProviders.CheckMusic) { return HandleCheckMusicAsync(queries); } - else if (provider == CloudMusicApiProviders.Login) + if (provider == CloudMusicApiProviders.Login) { return HandleLoginAsync(queries); } - else if (provider == CloudMusicApiProviders.LoginStatus) + if (provider == CloudMusicApiProviders.LoginStatus) { return HandleLoginStatusAsync(); } - else if (provider == CloudMusicApiProviders.RelatedPlaylist) + if (provider == CloudMusicApiProviders.RelatedPlaylist) { return HandleRelatedPlaylistAsync(queries); } @@ -111,17 +109,11 @@ public sealed partial class NeteaseCloudMusicApi : IDisposable ) { ArgumentNullException.ThrowIfNull(method); - ArgumentNullException.ThrowIfNull(url); - ArgumentNullException.ThrowIfNull(data); - ArgumentNullException.ThrowIfNull(options); - bool isOk; - JsonObject json; - - (isOk, json) = await Request.CreateRequest(_client, method, url, data, options); + var (isOk, json) = await Request.CreateRequest(_client, method, url, data, options); json = (JsonObject)json["body"]; if (!isOk && (int?)json["code"] == 301) { @@ -131,16 +123,13 @@ public sealed partial class NeteaseCloudMusicApi : IDisposable return (isOk, json); } - private async Task<(bool, JsonObject)> HandleCheckMusicAsync(Dictionary queries) + private async Task<(bool, JsonObject?)> HandleCheckMusicAsync( + Dictionary queries + ) { - NeteaseCloudMusicApiProvider provider; - bool isOk; - JsonObject json; - JsonObject result; - bool playable; + var provider = CloudMusicApiProviders.CheckMusic; - provider = CloudMusicApiProviders.CheckMusic; - (isOk, json) = await RequestAsync( + var (isOk, json) = await RequestAsync( provider.Method, provider.Url(queries), provider.Data(queries), @@ -151,12 +140,12 @@ public sealed partial class NeteaseCloudMusicApi : IDisposable return (false, null); } - playable = + var playable = (int?)json["code"] == 200 && json["data"] is JsonArray dataArray && dataArray.Count > 0 && (int?)dataArray[0]?["code"] == 200; - result = new JsonObject + var result = new JsonObject { { "success", playable }, { "message", playable ? "ok" : "亲爱的,暂无版权" }, @@ -164,14 +153,11 @@ public sealed partial class NeteaseCloudMusicApi : IDisposable return (true, result); } - private async Task<(bool, JsonObject)> HandleLoginAsync(Dictionary queries) + private async Task<(bool, JsonObject?)> HandleLoginAsync(Dictionary queries) { - NeteaseCloudMusicApiProvider provider; - bool isOk; - JsonObject json; + var provider = CloudMusicApiProviders.Login; - provider = CloudMusicApiProviders.Login; - (isOk, json) = await RequestAsync( + var (isOk, json) = await RequestAsync( provider.Method, provider.Url(queries), provider.Data(queries), @@ -197,27 +183,21 @@ public sealed partial class NeteaseCloudMusicApi : IDisposable private async Task<(bool, JsonObject)> HandleLoginStatusAsync() { - HttpResponseMessage response; - - response = null; + HttpResponseMessage? response = null; try { const string GUSER = "GUser="; const string GBINDS = "GBinds="; - string s; - int index; - JsonObject json; - response = await _client.GetAsync("https://music.163.com"); - s = Encoding.UTF8.GetString(await response.Content.ReadAsByteArrayAsync()); - index = s.IndexOf(GUSER, StringComparison.Ordinal); + var s = Encoding.UTF8.GetString(await response.Content.ReadAsByteArrayAsync()); + var index = s.IndexOf(GUSER, StringComparison.Ordinal); if (index == -1) { goto errorExit; } - json = new JsonObject { { "code", 200 } }; + var json = new JsonObject { { "code", 200 } }; var profileJson = JsonNode.Parse(s[(index + GUSER.Length)..]); if (profileJson is not null) { @@ -246,7 +226,7 @@ public sealed partial class NeteaseCloudMusicApi : IDisposable { response?.Dispose(); } - errorExit: + errorExit: return (false, new JsonObject { { "code", 301 } }); } @@ -254,24 +234,18 @@ public sealed partial class NeteaseCloudMusicApi : IDisposable Dictionary queries ) { - HttpResponseMessage response; - - response = null; + HttpResponseMessage? response = null; try { - string s; - MatchCollection matchs; - JsonArray playlists; - response = await _client.SendAsync( HttpMethod.Get, "https://music.163.com/playlist", new QueryCollection { { "id", queries["id"] } }, new QueryCollection { { "User-Agent", Request.ChooseUserAgent("pc") } } ); - s = Encoding.UTF8.GetString(await response.Content.ReadAsByteArrayAsync()); - matchs = MyRegex().Matches(s); - playlists = new JsonArray(); + var s = Encoding.UTF8.GetString(await response.Content.ReadAsByteArrayAsync()); + var matchs = MyRegex().Matches(s); + var playlists = new JsonArray(); matchs .Cast() .Select(match => new JsonObject diff --git a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/NeteaseCloudMusicApiProvider.cs b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/NeteaseCloudMusicApiProvider.cs index 218ac64..984f315 100644 --- a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/NeteaseCloudMusicApiProvider.cs +++ b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/NeteaseCloudMusicApiProvider.cs @@ -1,6 +1,6 @@ #pragma warning disable - using System.Net; +using System.Security.Cryptography; using System.Text.Json; using System.Text.RegularExpressions; using UntamedMusicPlayer.OnlineAPIs.CloudMusicAPI.Utils; @@ -83,14 +83,13 @@ public sealed class NeteaseCloudMusicApiProvider private IEnumerable> GetData(Dictionary queries) { - QueryCollection data; + var data = new QueryCollection(); if (_parameterInfos.Length == 0) { return _emptyData; } - data = []; foreach (var parameterInfo in _parameterInfos) { switch (parameterInfo.Type) @@ -1903,7 +1902,10 @@ public static partial class CloudMusicApiProviders ], BuildOptions( "linuxapi", - [new("os", "pc"), new("_ntes_nuid", new Random().RandomBytes(16).ToHexStringLower())] + [ + new("os", "pc"), + new("_ntes_nuid", RandomNumberGenerator.GetBytes(16).ToHexStringLower()), + ] ) ); @@ -2321,10 +2323,10 @@ public static partial class CloudMusicApiProviders options = new Options { - crypto = crypto, - cookie = cookieCollection, - ua = ua, - url = url, + Crypto = crypto, + Cookie = cookieCollection, + UA = ua, + Url = url, }; return options; } diff --git a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Utils/Crypto.cs b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Utils/Crypto.cs index 1052c79..f12cd3b 100644 --- a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Utils/Crypto.cs +++ b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Utils/Crypto.cs @@ -1,5 +1,4 @@ #pragma warning disable - using System.Numerics; using System.Security.Cryptography; using System.Text; @@ -16,18 +15,14 @@ internal static class Crypto private const string publicKey = "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----"; private static readonly byte[] eapiKey = Encoding.ASCII.GetBytes("e82ckenh8dichen8"); - private static RSAParameters? _cachedPublicKey; - public static Dictionary WEApi(object @object) + public static Dictionary WEApi(object obj) { - string text; - byte[] secretKey; - - text = JsonSerializer.Serialize(@object); - secretKey = new Random().RandomBytes(16); + var text = JsonSerializer.Serialize(obj); + var secretKey = RandomNumberGenerator.GetBytes(16); secretKey = [.. secretKey.Select(n => (byte)base62[n % 62])]; - return new Dictionary + return new() { { "params", @@ -41,22 +36,14 @@ internal static class Crypto ) .ToBase64String() }, - { - "encSecKey", - RsaEncrypt( - [.. secretKey.AsEnumerable().Reverse()] /*, publicKey*/ - ) - .ToHexStringLower() - }, + { "encSecKey", RsaEncrypt([.. secretKey.Reverse()]).ToHexStringLower() }, }; } - public static Dictionary LinuxApi(object @object) + public static Dictionary LinuxApi(object obj) { - string text; - - text = JsonSerializer.Serialize(@object); - return new Dictionary + var text = JsonSerializer.Serialize(obj); + return new() { { "eparams", @@ -66,18 +53,13 @@ internal static class Crypto }; } - public static Dictionary EApi(string url, object @object) + public static Dictionary EApi(string url, object obj) { - string text; - string message; - string digest; - string data; - - text = JsonSerializer.Serialize(@object); - message = $"nobody{url}use{text}md5forencrypt"; - digest = message.ToByteArrayUtf8().ComputeMd5().ToHexStringLower(); - data = $"{url}-36cd479b6b5-{text}-36cd479b6b5-{digest}"; - return new Dictionary + var text = JsonSerializer.Serialize(obj); + var message = $"nobody{url}use{text}md5forencrypt"; + var digest = message.ToByteArrayUtf8().ComputeMd5().ToHexStringLower(); + var data = $"{url}-36cd479b6b5-{text}-36cd479b6b5-{digest}"; + return new() { { "params", @@ -89,45 +71,40 @@ internal static class Crypto public static byte[] Decrypt(byte[] cipherBuffer) => AesDecrypt(cipherBuffer, CipherMode.ECB, eapiKey, null); - private static byte[] AesEncrypt(byte[] buffer, CipherMode mode, byte[] key, byte[] iv) + private static byte[] AesEncrypt(byte[] buffer, CipherMode mode, byte[] key, byte[]? iv) { using var aes = Aes.Create(); - aes.BlockSize = 128; aes.Key = key; - if (iv is not null) + return mode switch { - aes.IV = iv; - } - - aes.Mode = mode; - using var cryptoTransform = aes.CreateEncryptor(); - return cryptoTransform.TransformFinalBlock(buffer, 0, buffer.Length); + CipherMode.CBC => aes.EncryptCbc(buffer, iv!, PaddingMode.PKCS7), + CipherMode.ECB => aes.EncryptEcb(buffer, PaddingMode.PKCS7), + _ => throw new NotSupportedException($"Mode {mode} is not supported."), + }; } - private static byte[] AesDecrypt(byte[] buffer, CipherMode mode, byte[] key, byte[] iv) + private static byte[] AesDecrypt(byte[] buffer, CipherMode mode, byte[] key, byte[]? iv) { using var aes = Aes.Create(); - aes.BlockSize = 128; aes.Key = key; - if (iv is not null) + return mode switch { - aes.IV = iv; - } - - aes.Mode = mode; - using var cryptoTransform = aes.CreateDecryptor(); - return cryptoTransform.TransformFinalBlock(buffer, 0, buffer.Length); + CipherMode.CBC => aes.DecryptCbc(buffer, iv!, PaddingMode.PKCS7), + CipherMode.ECB => aes.DecryptEcb(buffer, PaddingMode.PKCS7), + _ => throw new NotSupportedException($"Mode {mode} is not supported."), + }; } - private static byte[] RsaEncrypt( - byte[] buffer /*, string key*/ - ) + private static byte[] RsaEncrypt(byte[] buffer) { - RSAParameters rsaParameters; + if (_cachedPublicKey == null) + { + using var rsa = RSA.Create(); + rsa.ImportFromPem(publicKey); + _cachedPublicKey = rsa.ExportParameters(false); + } - _cachedPublicKey ??= ParsePublicKey(publicKey); - - rsaParameters = _cachedPublicKey.Value; + var rsaParameters = _cachedPublicKey.Value; return BigInteger .ModPow( new BigInteger(buffer, true, true), @@ -136,128 +113,4 @@ internal static class Crypto ) .ToByteArray(true, true); } - - private static RSAParameters ParsePublicKey(string _publicKey) - { - _publicKey = _publicKey.Replace("\n", string.Empty); - _publicKey = _publicKey[26..^24]; - using var _stream = new MemoryStream(Convert.FromBase64String(_publicKey)); - using var _reader = new BinaryReader(_stream); - ushort _i16; - byte[] _oid; - byte _i8; - byte _low; - byte _high; - int _modulusLength; - byte[] _modulus; - int _exponentLength; - byte[] _exponent; - - _i16 = _reader.ReadUInt16(); - if (_i16 == 0x8130) - { - _reader.ReadByte(); - } - else if (_i16 == 0x8230) - { - _reader.ReadInt16(); - } - else - { - throw new ArgumentException(null, nameof(_publicKey)); - } - - _oid = _reader.ReadBytes(15); - if ( - !_oid.SequenceEqual( - new byte[] - { - 0x30, - 0x0D, - 0x06, - 0x09, - 0x2A, - 0x86, - 0x48, - 0x86, - 0xF7, - 0x0D, - 0x01, - 0x01, - 0x01, - 0x05, - 0x00, - } - ) - ) - { - throw new ArgumentException(null, nameof(_publicKey)); - } - - _i16 = _reader.ReadUInt16(); - if (_i16 == 0x8103) - { - _reader.ReadByte(); - } - else if (_i16 == 0x8203) - { - _reader.ReadInt16(); - } - else - { - throw new ArgumentException(null, nameof(_publicKey)); - } - - _i8 = _reader.ReadByte(); - if (_i8 != 0x00) - { - throw new ArgumentException(null, nameof(_publicKey)); - } - - _i16 = _reader.ReadUInt16(); - if (_i16 == 0x8130) - { - _reader.ReadByte(); - } - else if (_i16 == 0x8230) - { - _reader.ReadInt16(); - } - else - { - throw new ArgumentException(null, nameof(_publicKey)); - } - - _i16 = _reader.ReadUInt16(); - if (_i16 == 0x8102) - { - _high = 0; - _low = _reader.ReadByte(); - } - else if (_i16 == 0x8202) - { - _high = _reader.ReadByte(); - _low = _reader.ReadByte(); - } - else - { - throw new ArgumentException(null, nameof(_publicKey)); - } - - _modulusLength = BitConverter.ToInt32([_low, _high, 0x00, 0x00], 0); - if (_reader.PeekChar() == 0x00) - { - _reader.ReadByte(); - _modulusLength -= 1; - } - _modulus = _reader.ReadBytes(_modulusLength); - if (_reader.ReadByte() != 0x02) - { - throw new ArgumentException(null, nameof(_publicKey)); - } - - _exponentLength = _reader.ReadByte(); - _exponent = _reader.ReadBytes(_exponentLength); - return new RSAParameters { Modulus = _modulus, Exponent = _exponent }; - } } diff --git a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Utils/Extensions.cs b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Utils/Extensions.cs index 3ff6e37..1e6f082 100644 --- a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Utils/Extensions.cs +++ b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Utils/Extensions.cs @@ -1,5 +1,4 @@ #pragma warning disable - using System.Security.Cryptography; using System.Text; @@ -7,36 +6,30 @@ namespace UntamedMusicPlayer.OnlineAPIs.CloudMusicAPI.Utils; internal static class Extensions { - private static readonly MD5 _md5 = MD5.Create(); - - public static byte[] ToByteArrayUtf8(this string value) => Encoding.UTF8.GetBytes(value); - - public static string ToHexStringLower(this byte[] value) + extension(string value) { - return Convert.ToHexString(value).ToLowerInvariant(); + public byte[] ToByteArrayUtf8() => Encoding.UTF8.GetBytes(value); } - public static string ToHexStringUpper(this byte[] value) + extension(byte[] value) { - return Convert.ToHexString(value); + public string ToHexStringLower() => Convert.ToHexString(value).ToLowerInvariant(); + + public string ToHexStringUpper() => Convert.ToHexString(value); + + public string ToBase64String() => Convert.ToBase64String(value); + + public byte[] ComputeMd5() => MD5.HashData(value); } - public static string ToBase64String(this byte[] value) => Convert.ToBase64String(value); - - public static byte[] ComputeMd5(this byte[] value) => _md5.ComputeHash(value); - - public static byte[] RandomBytes(this Random random, int length) + extension(Random random) { - byte[] buffer; - - buffer = new byte[length]; - random.NextBytes(buffer); - return buffer; + public byte[] RandomBytes(int length) => RandomNumberGenerator.GetBytes(length); } - public static TValue GetValueOrDefault( - this Dictionary dictionary, - TKey key, - TValue defaultValue - ) => dictionary.TryGetValue(key, out var value) ? value : defaultValue; + extension(Dictionary dictionary) + { + public TValue GetValueOrDefault(TKey key, TValue defaultValue) => + dictionary.TryGetValue(key, out var value) ? value : defaultValue; + } } diff --git a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Utils/Options.cs b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Utils/Options.cs index 1f0ca01..b107417 100644 --- a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Utils/Options.cs +++ b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Utils/Options.cs @@ -1,13 +1,11 @@ -#pragma warning disable - using System.Net; namespace UntamedMusicPlayer.OnlineAPIs.CloudMusicAPI.Utils; internal sealed class Options { - public string crypto; - public CookieCollection cookie; - public string ua; - public string url; + public string Crypto { get; set; } = null!; + public CookieCollection Cookie { get; set; } = null!; + public string UA { get; set; } = null!; + public string Url { get; set; } = null!; } diff --git a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Utils/Request.cs b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Utils/Request.cs index c1c5099..c65ab82 100644 --- a/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Utils/Request.cs +++ b/UntamedMusicPlayer/OnlineAPIs/CloudMusicAPI/Utils/Request.cs @@ -1,5 +1,4 @@ #pragma warning disable - using System.IO.Compression; using System.Net; using System.Text; @@ -14,31 +13,31 @@ internal static partial class Request { private static readonly string[] userAgentList = [ - "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1", - "Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6.1 Mobile/15E148 Safari/604.1", - "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_7_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 EdgiOS/134.3124.77 Mobile/15E148 Safari/605.1.15", + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/134.0.6998.99 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (Linux; Android 16; AGT-AN00; HMSCore 6.14.0.309; GMSCore 25.45.34) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.196 HuaweiBrowser/16.0.9.303 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 16; Pixel 3 XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.135 Mobile Safari/537.36 EdgA/134.0.3124.68", + "Mozilla/5.0 (Linux; Android 16; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.135 Mobile Safari/537.36 EdgA/134.0.3124.68", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Mobile/14F89;GameHelper", "Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1", - "Mozilla/5.0 (iPad; CPU OS 10_0 like Mac OS X) AppleWebKit/602.1.38 (KHTML, like Gecko) Version/10.0 Mobile/14A300 Safari/602.1", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:130.0) Gecko/20100101 Firefox/130.0", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0", + "Mozilla/5.0 (iPad; CPU OS 17_7_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.7; rv:136.0) Gecko/20100101 Firefox/136.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3 Safari/605.1.15", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0", ]; - public static string ChooseUserAgent(string ua) + public static string ChooseUserAgent(string UA) { - return ua switch + return UA switch { - "mobile" => userAgentList[(int)Math.Floor(new Random().NextDouble() * 7)], - "pc" => userAgentList[(int)Math.Floor(new Random().NextDouble() * 5) + 8], - _ => string.IsNullOrEmpty(ua) - ? userAgentList[(int)Math.Floor(new Random().NextDouble() * userAgentList.Length)] - : ua, + "mobile" => userAgentList[Random.Shared.Next(8)], + "pc" => userAgentList[Random.Shared.Next(6) + 8], + _ => string.IsNullOrEmpty(UA) + ? userAgentList[Random.Shared.Next(userAgentList.Length)] + : UA, }; } @@ -51,27 +50,18 @@ internal static partial class Request ) { ArgumentNullException.ThrowIfNull(client); - ArgumentNullException.ThrowIfNull(method); - ArgumentNullException.ThrowIfNull(url); - ArgumentNullException.ThrowIfNull(data_); - ArgumentNullException.ThrowIfNull(options); - Dictionary headers; - Dictionary data; - JsonObject answer; - HttpResponseMessage response; - - headers = new Dictionary + var headers = new Dictionary { - ["User-Agent"] = ChooseUserAgent(options.ua), + ["User-Agent"] = ChooseUserAgent(options.UA), ["Cookie"] = string.Join( "; ", options - .cookie.Cast() + .Cookie.Cast() .Select(t => Uri.EscapeDataString(t.Name) + "=" + Uri.EscapeDataString(t.Value)) ), }; @@ -85,98 +75,101 @@ internal static partial class Request headers["Referer"] = "https://music.163.com"; } - data = []; + var data = new Dictionary(); foreach (var item in data_) { data.Add(item.Key, item.Value); } - switch (options.crypto) + switch (options.Crypto) { case "weapi": - { - data["csrf_token"] = options.cookie["__csrf"]?.Value ?? string.Empty; - data = Crypto.WEApi(data); - url = MyRegex1().Replace(url, "weapi"); - break; - } + { + data["csrf_token"] = options.Cookie["__csrf"]?.Value ?? string.Empty; + data = Crypto.WEApi(data); + url = MyRegex1().Replace(url, "weapi"); + break; + } case "linuxapi": - { - data = Crypto.LinuxApi( - new Dictionary - { + { + data = Crypto.LinuxApi( + new Dictionary + { { "method", method.Method }, { "url", MyRegex1().Replace(url, "api") }, { "params", data }, - } - ); - headers["User-Agent"] = - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"; - url = "https://music.163.com/api/linux/forward"; - break; - } - case "eapi": - { - CookieCollection cookie; - string csrfToken; - Dictionary header; - - cookie = []; - foreach (Cookie item in options.cookie) - { - cookie.Add(new Cookie(item.Name, item.Value)); } + ); + headers["User-Agent"] = + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"; + url = "https://music.163.com/api/linux/forward"; + break; + } + case "eapi": + { + CookieCollection cookie; + string csrfToken; + Dictionary header; - csrfToken = cookie["__csrf"]?.Value ?? string.Empty; - header = new Dictionary() + cookie = []; + foreach (Cookie item in options.Cookie) + { + cookie.Add(new Cookie(item.Name, item.Value)); + } + + csrfToken = cookie["__csrf"]?.Value ?? string.Empty; + header = new Dictionary() { { "osver", cookie["osver"]?.Value ?? string.Empty }, // 系统版本 { "deviceId", cookie["deviceId"]?.Value ?? string.Empty }, // encrypt.base64.encode(imei + '\t02:00:00:00:00:00\t5106025eb79a5247\t70ffbaac7') { "appver", cookie["appver"]?.Value ?? "6.1.1" }, // app版本 { "versioncode", cookie["versioncode"]?.Value ?? "140" }, // 版本号 { "mobilename", cookie["mobilename"]?.Value ?? string.Empty }, // 设备model - { "buildver", cookie["buildver"]?.Value ?? $"{GetCurrentTotalSeconds()}" }, + { + "buildver", + cookie["buildver"]?.Value ?? $"{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}" + }, { "resolution", cookie["resolution"]?.Value ?? "1920x1080" }, // 设备分辨率 { "__csrf", csrfToken }, { "os", cookie["os"]?.Value ?? "android" }, { "channel", cookie["channel"]?.Value ?? string.Empty }, { "requestId", - $"{GetCurrentTotalMilliseconds()}_{$"{Math.Floor(new Random().NextDouble() * 1000)}".PadLeft(4, '0')}" + $"{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{Random.Shared.Next(1000):D4}" }, }; - if (cookie["MUSIC_U"] is not null) - { - header["MUSIC_U"] = cookie["MUSIC_U"].Value; - } - - if (cookie["MUSIC_A"] is not null) - { - header["MUSIC_A"] = cookie["MUSIC_A"].Value; - } - - headers["Cookie"] = string.Join( - "; ", - header.Select(t => - Uri.EscapeDataString(t.Key) + "=" + Uri.EscapeDataString(t.Value) - ) - ); - data["header"] = JsonSerializer.Serialize(header); - data = Crypto.EApi(options.url, data); - url = MyRegex1().Replace(url, "eapi"); - break; + if (cookie["MUSIC_U"] is not null) + { + header["MUSIC_U"] = cookie["MUSIC_U"].Value; } + + if (cookie["MUSIC_A"] is not null) + { + header["MUSIC_A"] = cookie["MUSIC_A"].Value; + } + + headers["Cookie"] = string.Join( + "; ", + header.Select(t => + Uri.EscapeDataString(t.Key) + "=" + Uri.EscapeDataString(t.Value) + ) + ); + data["header"] = JsonSerializer.Serialize(header); + data = Crypto.EApi(options.Url, data); + url = MyRegex1().Replace(url, "eapi"); + break; + } } - answer = new JsonObject + var answer = new JsonObject { { "status", 500 }, { "body", null }, { "cookie", null }, }; - response = null; + + HttpResponseMessage? response = null; try { - IEnumerable temp1; JsonValue temp2; int temp3; @@ -193,7 +186,7 @@ internal static partial class Request throw new HttpRequestException(); } - if (!response.Headers.TryGetValues("set-cookie", out temp1)) + if (!response.Headers.TryGetValues("set-cookie", out var temp1)) { temp1 = []; } @@ -205,28 +198,23 @@ internal static partial class Request .ToList() .ForEach(x => cookieArray.Add(x)); answer["cookie"] = cookieArray; - if (options.crypto == "eapi") + if (options.Crypto == "eapi") { - DeflateStream stream; byte[] buffer; - - stream = null; try { - stream = new DeflateStream( + using var stream = new DeflateStream( await response.Content.ReadAsStreamAsync(), CompressionMode.Decompress ); - buffer = ReadStream(stream); + using var ms = new MemoryStream(); + stream.CopyTo(ms); + buffer = ms.ToArray(); } catch { buffer = await response.Content.ReadAsByteArrayAsync(); } - finally - { - stream?.Dispose(); - } try { answer["body"] = JsonObject.Parse( @@ -266,53 +254,6 @@ internal static partial class Request { response?.Dispose(); } - - ulong GetCurrentTotalSeconds() - { - TimeSpan _timeSpan; - - _timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1); - return (ulong)_timeSpan.TotalSeconds; - } - - ulong GetCurrentTotalMilliseconds() - { - TimeSpan _timeSpan; - - _timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1); - return (ulong)_timeSpan.TotalMilliseconds; - } - - byte[] ReadStream(Stream _stream) - { - byte[] _buffer; - List _byteList; - - _buffer = new byte[0x1000]; - _byteList = []; - for (var i = 0; i < int.MaxValue; i++) - { - int count; - - count = _stream.Read(_buffer, 0, _buffer.Length); - if (count == 0x1000) - { - _byteList.AddRange(_buffer); - } - else if (count == 0) - { - return [.. _byteList]; - } - else - { - for (var j = 0; j < count; j++) - { - _byteList.Add(_buffer[j]); - } - } - } - throw new OutOfMemoryException(); - } } [GeneratedRegex(@"\w*api", RegexOptions.Compiled)] diff --git a/UntamedMusicPlayer/Services/ColorExtractionService.cs b/UntamedMusicPlayer/Services/ColorExtractionService.cs index 1637d60..0ed0e08 100644 --- a/UntamedMusicPlayer/Services/ColorExtractionService.cs +++ b/UntamedMusicPlayer/Services/ColorExtractionService.cs @@ -2,6 +2,7 @@ using System.Runtime.InteropServices.WindowsRuntime; using Microsoft.Extensions.Logging; using Microsoft.Graphics.Canvas; using UntamedMusicPlayer.Contracts.Services; +using Windows.Foundation; using Windows.Graphics; using Windows.Storage.Streams; using Windows.UI; @@ -145,12 +146,7 @@ public sealed class ColorExtractionService : IColorExtractionService { using var renderTarget = new CanvasRenderTarget(device, newSize.Width, newSize.Height, 96); using var session = renderTarget.CreateDrawingSession(); - - session.DrawImage( - original, - new Windows.Foundation.Rect(0, 0, newSize.Width, newSize.Height) - ); - + session.DrawImage(original, new Rect(0, 0, newSize.Width, newSize.Height)); return CanvasBitmap.CreateFromDirect3D11Surface(device, renderTarget); } @@ -252,7 +248,10 @@ public sealed class ColorExtractionService : IColorExtractionService foreach (var child in node.Children) { - child?.Let(c => CollectColors(c, colorStats)); + if (child is not null) + { + CollectColors(child, colorStats); + } } } @@ -352,11 +351,3 @@ internal class OctreeNode /// 渐变配置 /// public sealed record GradientConfig(List Colors, double Angle); - -/// -/// 扩展方法 -/// -internal static class Extensions -{ - public static void Let(this T obj, Action action) => action(obj); -} diff --git a/UntamedMusicPlayer/Services/ZLoggerExtensions.cs b/UntamedMusicPlayer/Services/ZLoggerExtensions.cs index 1d7bb6b..92aa457 100644 --- a/UntamedMusicPlayer/Services/ZLoggerExtensions.cs +++ b/UntamedMusicPlayer/Services/ZLoggerExtensions.cs @@ -11,292 +11,213 @@ namespace UntamedMusicPlayer.Services; /// public static class ZLoggerExtensions { - // 应用程序生命周期日志 - public static void ApplicationStarting(this ILogger logger) => - logger.ZLogInformation($"应用程序正在启动"); - - public static void ApplicationStarted(this ILogger logger) => - logger.ZLogInformation($"应用程序启动完成"); - - public static void ApplicationShuttingDown(this ILogger logger) => - logger.ZLogInformation($"应用程序正在关闭"); - - // 音乐库相关日志 - public static void SavingLibraryData(this ILogger logger, string path) => - logger.ZLogInformation($"正在保存音乐库数据到: {path}"); - - public static void LibraryDataSaved( - this ILogger logger, - string path, - double elapsedMs, - int songCount, - int albumCount - ) => - logger.ZLogInformation( - $"音乐库数据已保存到: {path}, 耗时: {elapsedMs}ms, 歌曲数: {songCount}, 专辑数: {albumCount}" - ); - - public static void LoadingLibraryData(this ILogger logger, string path) => - logger.ZLogInformation($"正在加载音乐库数据从: {path}"); - - public static void LibraryDataLoaded( - this ILogger logger, - string path, - double elapsedMs, - int songCount, - int albumCount - ) => - logger.ZLogInformation( - $"音乐库数据已加载从: {path}, 耗时: {elapsedMs}ms, 歌曲数: {songCount}, 专辑数: {albumCount}" - ); - - public static void LibraryScanning( - this ILogger logger, - int processedCount, - double progressPercent - ) => - logger.ZLogDebug( - $"正在扫描音乐库: 已处理 {processedCount} 个文件, 进度: {progressPercent}%" - ); - - // 播放器相关日志 - public static void SongStartedPlaying(this ILogger logger, string title, string artist) => - logger.ZLogInformation($"开始播放歌曲: {title} - {artist}"); - - public static void SongPlaybackError( - this ILogger logger, - string title, - Exception? exception = null - ) => - logger.ZLogError( - exception, - $"{"Error_SongPlayback".GetLocalizedWithReplace("{title}", title)}" - ); - - public static void PlaybackDeviceBusy(this ILogger logger, Exception? exception = null) => - logger.ZLogError(exception, $"{"Error_PlaybackDeviceBusy".GetLocalized()}"); - - public static void SongPlaybackPosition(this ILogger logger, string title, long positionMs) => - logger.ZLogTrace($"歌曲播放位置更新: {title}, 位置: {positionMs}ms"); - - // 下载相关日志 - public static void DownloadStarted(this ILogger logger, string title, string url) => - logger.ZLogInformation($"开始下载: {title}, URL: {url}"); - - public static void DownloadCompleted( - this ILogger logger, - string title, - double elapsedMs, - long fileSizeBytes - ) => - logger.ZLogInformation( - $"下载完成: {title}, 耗时: {elapsedMs}ms, 文件大小: {fileSizeBytes} 字节" - ); - - public static void DownloadFailed( - this ILogger logger, - string title, - string error, - Exception? exception = null - ) => logger.ZLogError(exception, $"下载失败: {title}, 错误: {error}"); - - public static void DownloadProgress(this ILogger logger, string title, int progressPercent) => - logger.ZLogTrace($"下载进度: {title}, 进度: {progressPercent}%"); - - // 用户界面相关日志 - public static void NavigationOccurred(this ILogger logger, string pageName) => - logger.ZLogDebug($"页面导航: {pageName}"); - - public static void UserAction(this ILogger logger, string actionName) => - logger.ZLogDebug($"用户操作: {actionName}"); - - // 性能相关日志 - public static void PerformanceMetric(this ILogger logger, string metricName, double value) => - logger.ZLogInformation($"性能指标: {metricName}, 值: {value}ms"); - - public static void MemoryUsage(this ILogger logger, long memoryBytes, int generation) => - logger.ZLogInformation($"内存使用情况: {memoryBytes} 字节, GC代数: {generation}"); - - // 错误和异常日志 - public static void UnexpectedException( - this ILogger logger, - string message, - Exception exception - ) => logger.ZLogError(exception, $"未处理的异常: {message}"); - - public static void OperationFailed( - this ILogger logger, - string operationName, - string error, - Exception? exception = null - ) => logger.ZLogError(exception, $"操作失败: {operationName}, 错误: {error}"); - - public static void CriticalError( - this ILogger logger, - string message, - Exception? exception = null - ) => logger.ZLogCritical(exception, $"严重错误: {message}"); - - // 歌曲信息编辑日志 - public static void EditingSongInfoIO(this ILogger logger, string title) => - logger.ZLogError($"{"Error_EditingSongInfoIO".GetLocalizedWithReplace("{title}", title)}"); - - public static void EditingSongInfoOther( - this ILogger logger, - string title, - Exception exception - ) => - logger.ZLogError( - exception, - $"{"Error_EditingSongInfoOther".GetLocalizedWithReplace("{title}", title)}" - ); - - // 播放列表相关日志 - public static void SamePlaylistName(this ILogger logger) => - logger.ZLogError($"{"Error_SamePlaylistName".GetLocalized()}"); - - // 高性能性能监控日志 - public static void LogPerformanceStart( - this ILogger logger, - string operationName, - int operationId - ) => logger.ZLogDebug($"[{operationId:X8}] 开始执行: {operationName}"); - - public static void LogPerformanceEnd( - this ILogger logger, - string operationName, - int operationId, - double elapsedMs - ) => - logger.ZLogInformation( - $"[{operationId:X8}] 完成执行: {operationName}, 耗时: {elapsedMs}ms" - ); - - public static void LogPerformanceEnd( - this ILogger logger, - string operationName, - int operationId, - TimeSpan elapsed - ) => - logger.ZLogInformation( - $"[{operationId:X8}] 完成执行: {operationName}, 耗时: {elapsed.TotalMilliseconds}ms" - ); - - // 音乐播放专用高性能日志 - public static void PlaybackStateChanged( - this ILogger logger, - string songTitle, - string newState - ) => logger.ZLogDebug($"播放状态变更: {songTitle} -> {newState}"); - - public static void AudioStreamCreated( - this ILogger logger, - string filePath, - int streamHandle, - double durationSeconds - ) => - logger.ZLogDebug( - $"音频流已创建: {filePath}, 句柄: {streamHandle}, 时长: {durationSeconds}s" - ); - - public static void AudioStreamReleased(this ILogger logger, int streamHandle) => - logger.ZLogDebug($"音频流已释放: 句柄 {streamHandle}"); - - public static void PlaybackBufferUnderrun( - this ILogger logger, - string songTitle, - int bufferLevel - ) => logger.ZLogWarning($"播放缓冲区不足: {songTitle}, 缓冲区水平: {bufferLevel}%"); - - // 网络请求日志 - public static void HttpRequestStarted( - this ILogger logger, - string method, - string url, - int requestId - ) => logger.ZLogDebug($"[{requestId:X8}] HTTP请求开始: {method} {url}"); - - public static void HttpRequestCompleted( - this ILogger logger, - int requestId, - int statusCode, - double elapsedMs - ) => - logger.ZLogInformation($"[{requestId:X8}] HTTP请求完成: {statusCode}, 耗时: {elapsedMs}ms"); - - public static void HttpRequestFailed( - this ILogger logger, - int requestId, - string error, - Exception? exception = null - ) => logger.ZLogError(exception, $"[{requestId:X8}] HTTP请求失败: {error}"); - - // 文件操作日志 - public static void FileOperationStarted( - this ILogger logger, - string operation, - string filePath, - int operationId - ) => logger.ZLogDebug($"[{operationId:X8}] 文件操作开始: {operation} -> {filePath}"); - - public static void FileOperationCompleted( - this ILogger logger, - int operationId, - double elapsedMs, - long? fileSize = null - ) + extension(ILogger logger) { - if (fileSize.HasValue) - { + // 应用程序生命周期日志 + public void ApplicationStarting() => logger.ZLogInformation($"应用程序正在启动"); + + public void ApplicationStarted() => logger.ZLogInformation($"应用程序启动完成"); + + public void ApplicationShuttingDown() => logger.ZLogInformation($"应用程序正在关闭"); + + // 音乐库相关日志 + public void SavingLibraryData(string path) => + logger.ZLogInformation($"正在保存音乐库数据到: {path}"); + + public void LibraryDataSaved( + string path, + double elapsedMs, + int songCount, + int albumCount + ) => logger.ZLogInformation( - $"[{operationId:X8}] 文件操作完成, 耗时: {elapsedMs}ms, 大小: {fileSize} 字节" + $"音乐库数据已保存到: {path}, 耗时: {elapsedMs}ms, 歌曲数: {songCount}, 专辑数: {albumCount}" ); - } - else - { - logger.ZLogInformation($"[{operationId:X8}] 文件操作完成, 耗时: {elapsedMs}ms"); - } - } - public static void FileOperationFailed( - this ILogger logger, - int operationId, - string error, - Exception? exception = null - ) => logger.ZLogError(exception, $"[{operationId:X8}] 文件操作失败: {error}"); + public void LoadingLibraryData(string path) => + logger.ZLogInformation($"正在加载音乐库数据从: {path}"); - // 数据库操作日志 - public static void DatabaseOperationStarted( - this ILogger logger, - string operation, - int operationId - ) => logger.ZLogDebug($"[{operationId:X8}] 数据库操作开始: {operation}"); - - public static void DatabaseOperationCompleted( - this ILogger logger, - int operationId, - double elapsedMs, - int? affectedRows = null - ) - { - if (affectedRows.HasValue) - { + public void LibraryDataLoaded( + string path, + double elapsedMs, + int songCount, + int albumCount + ) => logger.ZLogInformation( - $"[{operationId:X8}] 数据库操作完成, 耗时: {elapsedMs}ms, 影响行数: {affectedRows}" + $"音乐库数据已加载从: {path}, 耗时: {elapsedMs}ms, 歌曲数: {songCount}, 专辑数: {albumCount}" ); - } - else - { - logger.ZLogInformation($"[{operationId:X8}] 数据库操作完成, 耗时: {elapsedMs}ms"); - } - } - public static void DatabaseOperationFailed( - this ILogger logger, - int operationId, - string error, - Exception? exception = null - ) => logger.ZLogError(exception, $"[{operationId:X8}] 数据库操作失败: {error}"); + public void LibraryScanning(int processedCount, double progressPercent) => + logger.ZLogDebug( + $"正在扫描音乐库: 已处理 {processedCount} 个文件, 进度: {progressPercent}%" + ); + + // 播放器相关日志 + public void SongStartedPlaying(string title, string artist) => + logger.ZLogInformation($"开始播放歌曲: {title} - {artist}"); + + public void SongPlaybackError(string title, Exception? exception = null) => + logger.ZLogError( + exception, + $"{"Error_SongPlayback".GetLocalizedWithReplace("{title}", title)}" + ); + + public void PlaybackDeviceBusy(Exception? exception = null) => + logger.ZLogError(exception, $"{"Error_PlaybackDeviceBusy".GetLocalized()}"); + + public void SongPlaybackPosition(string title, long positionMs) => + logger.ZLogTrace($"歌曲播放位置更新: {title}, 位置: {positionMs}ms"); + + // 下载相关日志 + public void DownloadStarted(string title, string url) => + logger.ZLogInformation($"开始下载: {title}, URL: {url}"); + + public void DownloadCompleted(string title, double elapsedMs, long fileSizeBytes) => + logger.ZLogInformation( + $"下载完成: {title}, 耗时: {elapsedMs}ms, 文件大小: {fileSizeBytes} 字节" + ); + + public void DownloadFailed(string title, string error, Exception? exception = null) => + logger.ZLogError(exception, $"下载失败: {title}, 错误: {error}"); + + public void DownloadProgress(string title, int progressPercent) => + logger.ZLogTrace($"下载进度: {title}, 进度: {progressPercent}%"); + + // 用户界面相关日志 + public void NavigationOccurred(string pageName) => + logger.ZLogDebug($"页面导航: {pageName}"); + + public void UserAction(string actionName) => logger.ZLogDebug($"用户操作: {actionName}"); + + // 性能相关日志 + public void PerformanceMetric(string metricName, double value) => + logger.ZLogInformation($"性能指标: {metricName}, 值: {value}ms"); + + public void MemoryUsage(long memoryBytes, int generation) => + logger.ZLogInformation($"内存使用情况: {memoryBytes} 字节, GC代数: {generation}"); + + // 错误和异常日志 + public void UnexpectedException(string message, Exception exception) => + logger.ZLogError(exception, $"未处理的异常: {message}"); + + public void OperationFailed( + string operationName, + string error, + Exception? exception = null + ) => logger.ZLogError(exception, $"操作失败: {operationName}, 错误: {error}"); + + public void CriticalError(string message, Exception? exception = null) => + logger.ZLogCritical(exception, $"严重错误: {message}"); + + // 歌曲信息编辑日志 + public void EditingSongInfoIO(string title) => + logger.ZLogError( + $"{"Error_EditingSongInfoIO".GetLocalizedWithReplace("{title}", title)}" + ); + + public void EditingSongInfoOther(string title, Exception exception) => + logger.ZLogError( + exception, + $"{"Error_EditingSongInfoOther".GetLocalizedWithReplace("{title}", title)}" + ); + + // 播放列表相关日志 + public void SamePlaylistName() => + logger.ZLogError($"{"Error_SamePlaylistName".GetLocalized()}"); + + // 高性能性能监控日志 + public void LogPerformanceStart(string operationName, int operationId) => + logger.ZLogDebug($"[{operationId:X8}] 开始执行: {operationName}"); + + public void LogPerformanceEnd(string operationName, int operationId, double elapsedMs) => + logger.ZLogInformation( + $"[{operationId:X8}] 完成执行: {operationName}, 耗时: {elapsedMs}ms" + ); + + public void LogPerformanceEnd(string operationName, int operationId, TimeSpan elapsed) => + logger.ZLogInformation( + $"[{operationId:X8}] 完成执行: {operationName}, 耗时: {elapsed.TotalMilliseconds}ms" + ); + + // 音乐播放专用高性能日志 + public void PlaybackStateChanged(string songTitle, string newState) => + logger.ZLogDebug($"播放状态变更: {songTitle} -> {newState}"); + + public void AudioStreamCreated(string filePath, int streamHandle, double durationSeconds) => + logger.ZLogDebug( + $"音频流已创建: {filePath}, 句柄: {streamHandle}, 时长: {durationSeconds}s" + ); + + public void AudioStreamReleased(int streamHandle) => + logger.ZLogDebug($"音频流已释放: 句柄 {streamHandle}"); + + public void PlaybackBufferUnderrun(string songTitle, int bufferLevel) => + logger.ZLogWarning($"播放缓冲区不足: {songTitle}, 缓冲区水平: {bufferLevel}%"); + + // 网络请求日志 + public void HttpRequestStarted(string method, string url, int requestId) => + logger.ZLogDebug($"[{requestId:X8}] HTTP请求开始: {method} {url}"); + + public void HttpRequestCompleted(int requestId, int statusCode, double elapsedMs) => + logger.ZLogInformation( + $"[{requestId:X8}] HTTP请求完成: {statusCode}, 耗时: {elapsedMs}ms" + ); + + public void HttpRequestFailed(int requestId, string error, Exception? exception = null) => + logger.ZLogError(exception, $"[{requestId:X8}] HTTP请求失败: {error}"); + + // 文件操作日志 + public void FileOperationStarted(string operation, string filePath, int operationId) => + logger.ZLogDebug($"[{operationId:X8}] 文件操作开始: {operation} -> {filePath}"); + + public void FileOperationCompleted(int operationId, double elapsedMs, long? fileSize = null) + { + if (fileSize.HasValue) + { + logger.ZLogInformation( + $"[{operationId:X8}] 文件操作完成, 耗时: {elapsedMs}ms, 大小: {fileSize} 字节" + ); + } + else + { + logger.ZLogInformation($"[{operationId:X8}] 文件操作完成, 耗时: {elapsedMs}ms"); + } + } + + public void FileOperationFailed( + int operationId, + string error, + Exception? exception = null + ) => logger.ZLogError(exception, $"[{operationId:X8}] 文件操作失败: {error}"); + + // 数据库操作日志 + public void DatabaseOperationStarted(string operation, int operationId) => + logger.ZLogDebug($"[{operationId:X8}] 数据库操作开始: {operation}"); + + public void DatabaseOperationCompleted( + int operationId, + double elapsedMs, + int? affectedRows = null + ) + { + if (affectedRows.HasValue) + { + logger.ZLogInformation( + $"[{operationId:X8}] 数据库操作完成, 耗时: {elapsedMs}ms, 影响行数: {affectedRows}" + ); + } + else + { + logger.ZLogInformation($"[{operationId:X8}] 数据库操作完成, 耗时: {elapsedMs}ms"); + } + } + + public void DatabaseOperationFailed( + int operationId, + string error, + Exception? exception = null + ) => logger.ZLogError(exception, $"[{operationId:X8}] 数据库操作失败: {error}"); + } } ///