mirror of
https://github.com/LanZhan-Harmony/WindowsMusicPlayer-TheUntamedMusicPlayer.git
synced 2026-05-07 03:25:48 +08:00
更新C#14扩展属性
This commit is contained in:
3
.csharpierignore
Normal file
3
.csharpierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
App.xaml.cs
|
||||
**/*.xaml
|
||||
**/*.csproj
|
||||
@@ -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<string, string> 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<string, string> replacements)
|
||||
{
|
||||
var template = _resourceLoader.GetString(resourceKey);
|
||||
foreach (var (placeholder, value) in replacements)
|
||||
{
|
||||
template = template.Replace(placeholder, value);
|
||||
}
|
||||
return template;
|
||||
}
|
||||
return template;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T>(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<T?> ReadAsync<T>(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<T?> ReadAsync<T>(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<T>(fileContent);
|
||||
}
|
||||
|
||||
public async Task SaveAsync<T>(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<byte[]?> 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<StorageFile> 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<byte[]?> 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<T?> ReadAsync<T>(string key)
|
||||
{
|
||||
object? obj;
|
||||
|
||||
if (settings.Values.TryGetValue(key, out obj))
|
||||
{
|
||||
return await Json.ToObjectAsync<T>((string)obj);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
var file = await folder.GetFileAsync($"{name}.json");
|
||||
var fileContent = await FileIO.ReadTextAsync(file);
|
||||
|
||||
return await Json.ToObjectAsync<T>(fileContent);
|
||||
}
|
||||
|
||||
public static async Task SaveAsync<T>(
|
||||
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<T?> ReadAsync<T>(this ApplicationDataContainer settings, string key)
|
||||
{
|
||||
object? obj;
|
||||
|
||||
if (settings.Values.TryGetValue(key, out obj))
|
||||
public async Task SaveAsync<T>(string key, T value)
|
||||
{
|
||||
return await Json.ToObjectAsync<T>((string)obj);
|
||||
settings.SaveString(key, await Json.StringifyAsync(value!));
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public static async Task<StorageFile> 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<byte[]?> 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<byte[]?> 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)
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
#pragma warning disable
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace UntamedMusicPlayer.OnlineAPIs.CloudMusicAPI.Extensions;
|
||||
|
||||
internal static class ExceptionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取最内层异常
|
||||
/// </summary>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
public static Exception GetInmostException(this Exception exception)
|
||||
extension(Exception exception)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(exception);
|
||||
/// <summary>
|
||||
/// 获取最内层异常
|
||||
/// </summary>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
public Exception GetInmostException()
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(exception);
|
||||
|
||||
return exception.InnerException is null
|
||||
? exception
|
||||
: exception.InnerException.GetInmostException();
|
||||
}
|
||||
return exception.InnerException is null
|
||||
? exception
|
||||
: exception.InnerException.GetInmostException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回一个字符串,其中包含异常的所有信息。
|
||||
/// </summary>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToFullString(this Exception exception)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(exception);
|
||||
/// <summary>
|
||||
/// 返回一个字符串,其中包含异常的所有信息。
|
||||
/// </summary>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
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)
|
||||
|
||||
@@ -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<HttpResponseMessage> SendAsync(
|
||||
this HttpClient client,
|
||||
HttpMethod method,
|
||||
string url
|
||||
) => client.SendAsync(method, url, null, null);
|
||||
|
||||
public static Task<HttpResponseMessage> SendAsync(
|
||||
this HttpClient client,
|
||||
HttpMethod method,
|
||||
string url,
|
||||
IEnumerable<KeyValuePair<string, string>> queries,
|
||||
IEnumerable<KeyValuePair<string, string>> headers
|
||||
) =>
|
||||
client.SendAsync(
|
||||
method,
|
||||
url,
|
||||
queries,
|
||||
headers,
|
||||
(byte[])null,
|
||||
"application/x-www-form-urlencoded"
|
||||
);
|
||||
|
||||
public static Task<HttpResponseMessage> SendAsync(
|
||||
this HttpClient client,
|
||||
HttpMethod method,
|
||||
string url,
|
||||
IEnumerable<KeyValuePair<string, string>> queries,
|
||||
IEnumerable<KeyValuePair<string, string>> headers,
|
||||
string content,
|
||||
string contentType
|
||||
) =>
|
||||
client.SendAsync(
|
||||
method,
|
||||
url,
|
||||
queries,
|
||||
headers,
|
||||
content is null ? null : Encoding.UTF8.GetBytes(content),
|
||||
contentType
|
||||
);
|
||||
|
||||
public static Task<HttpResponseMessage> SendAsync(
|
||||
this HttpClient client,
|
||||
HttpMethod method,
|
||||
string url,
|
||||
IEnumerable<KeyValuePair<string, string>> queries,
|
||||
IEnumerable<KeyValuePair<string, string>> headers,
|
||||
byte[] content,
|
||||
string contentType
|
||||
)
|
||||
extension(HttpClient client)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(client);
|
||||
public Task<HttpResponseMessage> SendAsync(HttpMethod method, string? url) =>
|
||||
client.SendAsync(method, url, null, null);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(method);
|
||||
public Task<HttpResponseMessage> SendAsync(
|
||||
HttpMethod method,
|
||||
string? url,
|
||||
IEnumerable<KeyValuePair<string, string>>? queries,
|
||||
IEnumerable<KeyValuePair<string, string>>? headers
|
||||
) =>
|
||||
client.SendAsync(
|
||||
method,
|
||||
url,
|
||||
queries,
|
||||
headers,
|
||||
(byte[]?)null,
|
||||
"application/x-www-form-urlencoded"
|
||||
);
|
||||
|
||||
if (string.IsNullOrEmpty(url))
|
||||
public Task<HttpResponseMessage> SendAsync(
|
||||
HttpMethod method,
|
||||
string? url,
|
||||
IEnumerable<KeyValuePair<string, string>>? queries,
|
||||
IEnumerable<KeyValuePair<string, string>>? headers,
|
||||
string? content,
|
||||
string? contentType
|
||||
) =>
|
||||
client.SendAsync(
|
||||
method,
|
||||
url,
|
||||
queries,
|
||||
headers,
|
||||
content is null ? null : Encoding.UTF8.GetBytes(content),
|
||||
contentType
|
||||
);
|
||||
|
||||
public Task<HttpResponseMessage> SendAsync(
|
||||
HttpMethod method,
|
||||
string? url,
|
||||
IEnumerable<KeyValuePair<string, string>>? queries,
|
||||
IEnumerable<KeyValuePair<string, string>>? 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,18 @@ namespace UntamedMusicPlayer.OnlineAPIs.CloudMusicAPI.Extensions;
|
||||
|
||||
internal static class HttpExtensions
|
||||
{
|
||||
public static string ToQueryString(this IEnumerable<KeyValuePair<string, string>> queries)
|
||||
extension(IEnumerable<KeyValuePair<string, string>> 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)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, string> queries)
|
||||
private async Task<(bool, JsonObject?)> HandleCheckMusicAsync(
|
||||
Dictionary<string, string> 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<string, string> queries)
|
||||
private async Task<(bool, JsonObject?)> HandleLoginAsync(Dictionary<string, string> 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<string, string> 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<Match>()
|
||||
.Select(match => new JsonObject
|
||||
|
||||
@@ -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<KeyValuePair<string, string>> GetData(Dictionary<string, string> 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;
|
||||
}
|
||||
|
||||
@@ -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<string, string> WEApi(object @object)
|
||||
public static Dictionary<string, string> 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<string, string>
|
||||
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<string, string> LinuxApi(object @object)
|
||||
public static Dictionary<string, string> LinuxApi(object obj)
|
||||
{
|
||||
string text;
|
||||
|
||||
text = JsonSerializer.Serialize(@object);
|
||||
return new Dictionary<string, string>
|
||||
var text = JsonSerializer.Serialize(obj);
|
||||
return new()
|
||||
{
|
||||
{
|
||||
"eparams",
|
||||
@@ -66,18 +53,13 @@ internal static class Crypto
|
||||
};
|
||||
}
|
||||
|
||||
public static Dictionary<string, string> EApi(string url, object @object)
|
||||
public static Dictionary<string, string> 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<string, string>
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TKey, TValue>(
|
||||
this Dictionary<TKey, TValue> dictionary,
|
||||
TKey key,
|
||||
TValue defaultValue
|
||||
) => dictionary.TryGetValue(key, out var value) ? value : defaultValue;
|
||||
extension<TKey, TValue>(Dictionary<TKey, TValue> dictionary)
|
||||
{
|
||||
public TValue GetValueOrDefault(TKey key, TValue defaultValue) =>
|
||||
dictionary.TryGetValue(key, out var value) ? value : defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
@@ -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<string, string> headers;
|
||||
Dictionary<string, string> data;
|
||||
JsonObject answer;
|
||||
HttpResponseMessage response;
|
||||
|
||||
headers = new Dictionary<string, string>
|
||||
var headers = new Dictionary<string, string>
|
||||
{
|
||||
["User-Agent"] = ChooseUserAgent(options.ua),
|
||||
["User-Agent"] = ChooseUserAgent(options.UA),
|
||||
["Cookie"] = string.Join(
|
||||
"; ",
|
||||
options
|
||||
.cookie.Cast<Cookie>()
|
||||
.Cookie.Cast<Cookie>()
|
||||
.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<string, string>();
|
||||
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<string, object>
|
||||
{
|
||||
{
|
||||
data = Crypto.LinuxApi(
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "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<string, string> 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<string, string> header;
|
||||
|
||||
csrfToken = cookie["__csrf"]?.Value ?? string.Empty;
|
||||
header = new Dictionary<string, string>()
|
||||
cookie = [];
|
||||
foreach (Cookie item in options.Cookie)
|
||||
{
|
||||
cookie.Add(new Cookie(item.Name, item.Value));
|
||||
}
|
||||
|
||||
csrfToken = cookie["__csrf"]?.Value ?? string.Empty;
|
||||
header = new Dictionary<string, string>()
|
||||
{
|
||||
{ "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<string> 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<byte> _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)]
|
||||
|
||||
@@ -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
|
||||
/// 渐变配置
|
||||
/// </summary>
|
||||
public sealed record GradientConfig(List<Color> Colors, double Angle);
|
||||
|
||||
/// <summary>
|
||||
/// 扩展方法
|
||||
/// </summary>
|
||||
internal static class Extensions
|
||||
{
|
||||
public static void Let<T>(this T obj, Action<T> action) => action(obj);
|
||||
}
|
||||
|
||||
@@ -11,292 +11,213 @@ namespace UntamedMusicPlayer.Services;
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user