Обнновлено до .net10
This commit is contained in:
@@ -1,103 +1,70 @@
|
||||
using System.Net;
|
||||
|
||||
using YandexMusic.API.Common.Debug;
|
||||
using YandexMusic.API.Common.Providers;
|
||||
using YandexMusic.API.Models.Account;
|
||||
using YandexMusic.API.Requests.Common;
|
||||
|
||||
namespace YandexMusic.API.Common
|
||||
namespace YandexMusic.API.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Хранилище данных пользователя
|
||||
/// </summary>
|
||||
public class AuthStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Хранилище данных пользователя
|
||||
/// Http-контекст
|
||||
/// </summary>
|
||||
public class AuthStorage
|
||||
public HttpContext Context { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Флаг авторизации
|
||||
/// </summary>
|
||||
public bool IsAuthorized { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Идентификатор устройства
|
||||
/// </summary>
|
||||
public string DeviceId { get; set; } = "csharp";
|
||||
|
||||
/// <summary>
|
||||
/// Токен авторизации
|
||||
/// </summary>
|
||||
public string Token { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Аккаунт
|
||||
/// </summary>
|
||||
public YAccount User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Провайдер запросов
|
||||
/// </summary>
|
||||
public IRequestProvider Provider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Токен доступа
|
||||
/// </summary>
|
||||
public YAccessToken AccessToken { get; set; }
|
||||
|
||||
internal YAuthToken AuthToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Конструктор
|
||||
/// </summary>
|
||||
public AuthStorage(IRequestProvider provider)
|
||||
{
|
||||
#region Свойства
|
||||
|
||||
/// <summary>
|
||||
/// Http-контекст
|
||||
/// </summary>
|
||||
public HttpContext Context { get; }
|
||||
|
||||
public DebugSettings Debug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Флаг авторизации
|
||||
/// </summary>
|
||||
public bool IsAuthorized { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Идентификатор устройства
|
||||
/// </summary>
|
||||
public string DeviceId { get; set; } = "csharp";
|
||||
|
||||
/// <summary>
|
||||
/// Токен авторизации
|
||||
/// </summary>
|
||||
public string Token { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Аккаунт
|
||||
/// </summary>
|
||||
public YAccount User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Провайдер запросов
|
||||
/// </summary>
|
||||
public IRequestProvider Provider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Токен доступа
|
||||
/// </summary>
|
||||
public YAccessToken AccessToken { get; set; }
|
||||
|
||||
internal YAuthToken AuthToken { get; set; }
|
||||
|
||||
#endregion Свойства
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Конструктор
|
||||
/// </summary>
|
||||
public AuthStorage(DebugSettings settings = null)
|
||||
{
|
||||
User = new YAccount();
|
||||
Context = new HttpContext();
|
||||
Debug = settings;
|
||||
Provider = new DefaultRequestProvider(this);
|
||||
|
||||
if (Debug is { ClearDirectory: true })
|
||||
{
|
||||
Debug.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Конструктор
|
||||
/// </summary>
|
||||
public AuthStorage(IRequestProvider provider, DebugSettings settings = null)
|
||||
{
|
||||
User = new YAccount();
|
||||
Context = new HttpContext();
|
||||
Debug = settings;
|
||||
Provider = provider;
|
||||
|
||||
if (Debug is { ClearDirectory: true })
|
||||
{
|
||||
Debug.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Установка прокси для пользователия
|
||||
/// </summary>
|
||||
/// <param name="proxy">Прокси</param>
|
||||
public void SetProxy(IWebProxy proxy)
|
||||
{
|
||||
Context.WebProxy = proxy;
|
||||
}
|
||||
|
||||
|
||||
User = new YAccount();
|
||||
Context = new HttpContext();
|
||||
Provider = provider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Установка прокси для пользователия
|
||||
/// </summary>
|
||||
/// <param name="proxy">Прокси</param>
|
||||
public void SetProxy(IWebProxy proxy)
|
||||
{
|
||||
Context.WebProxy = proxy;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,44 +1,43 @@
|
||||
using System.Net;
|
||||
|
||||
namespace YandexMusic.API.Common
|
||||
namespace YandexMusic.API.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Загрузчик файлов по ссылке
|
||||
/// </summary>
|
||||
public class DataDownloader
|
||||
{
|
||||
/// <summary>
|
||||
/// Загрузчик файлов по ссылке
|
||||
/// </summary>
|
||||
public class DataDownloader
|
||||
private AuthStorage authStorage;
|
||||
|
||||
private async Task<HttpContent> GetResponseContent(string url, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
private AuthStorage authStorage;
|
||||
HttpRequestMessage message = new(new HttpMethod(WebRequestMethods.Http.Get), url);
|
||||
|
||||
private async Task<HttpContent> GetResponseContent(string url, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
HttpRequestMessage message = new(new HttpMethod(WebRequestMethods.Http.Get), url);
|
||||
HttpResponseMessage response = await authStorage.Provider.GetWebResponseAsync(message, httpCompletionOption);
|
||||
return response.Content;
|
||||
}
|
||||
|
||||
HttpResponseMessage response = await authStorage.Provider.GetWebResponseAsync(message, httpCompletionOption);
|
||||
return response.Content;
|
||||
}
|
||||
public async Task<Stream> AsStream(string url, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
HttpContent content = await GetResponseContent(url, httpCompletionOption);
|
||||
return await content.ReadAsStreamAsync();
|
||||
}
|
||||
|
||||
public async Task<Stream> AsStream(string url, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
HttpContent content = await GetResponseContent(url, httpCompletionOption);
|
||||
return await content.ReadAsStreamAsync();
|
||||
}
|
||||
public async Task<byte[]> AsBytes(string url)
|
||||
{
|
||||
HttpContent content = await GetResponseContent(url);
|
||||
return await content.ReadAsByteArrayAsync();
|
||||
}
|
||||
|
||||
public async Task<byte[]> AsBytes(string url)
|
||||
{
|
||||
HttpContent content = await GetResponseContent(url);
|
||||
return await content.ReadAsByteArrayAsync();
|
||||
}
|
||||
public async Task ToFile(string url, string fileName)
|
||||
{
|
||||
using Stream stream = await AsStream(url);
|
||||
using FileStream fs = File.Create(fileName);
|
||||
await stream.CopyToAsync(fs);
|
||||
}
|
||||
|
||||
public async Task ToFile(string url, string fileName)
|
||||
{
|
||||
using Stream stream = await AsStream(url);
|
||||
using FileStream fs = File.Create(fileName);
|
||||
await stream.CopyToAsync(fs);
|
||||
}
|
||||
|
||||
public DataDownloader(AuthStorage storage)
|
||||
{
|
||||
authStorage = storage;
|
||||
}
|
||||
public DataDownloader(AuthStorage storage)
|
||||
{
|
||||
authStorage = storage;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +1,63 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace YandexMusic.API.Common
|
||||
namespace YandexMusic.API.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Класс для шифровки
|
||||
/// </summary>
|
||||
public class Encryptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Класс для шифровки
|
||||
/// </summary>
|
||||
public class Encryptor
|
||||
private readonly string IV = "encryption";
|
||||
private readonly byte[] IVHash;
|
||||
|
||||
private readonly byte[] keyHash;
|
||||
|
||||
private readonly MD5 md5;
|
||||
private readonly Aes aesAlg;
|
||||
|
||||
private byte[] GetHash(string value)
|
||||
{
|
||||
#region Поля
|
||||
|
||||
private readonly string IV = "encryption";
|
||||
private readonly byte[] IVHash;
|
||||
|
||||
private readonly byte[] keyHash;
|
||||
|
||||
private readonly MD5 md5;
|
||||
private readonly Aes aesAlg;
|
||||
|
||||
|
||||
#endregion Поля
|
||||
|
||||
#region Вспомогательные функции
|
||||
|
||||
private byte[] GetHash(string value)
|
||||
{
|
||||
return md5.ComputeHash(Encoding.UTF8.GetBytes(value));
|
||||
}
|
||||
|
||||
#endregion Вспомогательные функции
|
||||
|
||||
|
||||
|
||||
public Encryptor(string key)
|
||||
{
|
||||
md5 = MD5.Create();
|
||||
|
||||
aesAlg = Aes.Create();
|
||||
aesAlg.BlockSize = 128;
|
||||
aesAlg.Padding = PaddingMode.PKCS7;
|
||||
|
||||
keyHash = GetHash(key);
|
||||
IVHash = GetHash(IV);
|
||||
}
|
||||
|
||||
public byte[] Encrypt(byte[] data)
|
||||
{
|
||||
using MemoryStream ms = new();
|
||||
using CryptoStream csEncrypt = new(ms, aesAlg.CreateEncryptor(keyHash, IVHash), CryptoStreamMode.Write);
|
||||
|
||||
csEncrypt.Write(data, 0, data.Length);
|
||||
|
||||
if (!csEncrypt.HasFlushedFinalBlock)
|
||||
csEncrypt.FlushFinalBlock();
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
public byte[] Decrypt(byte[] data)
|
||||
{
|
||||
using MemoryStream ms = new();
|
||||
using CryptoStream csDecrypt = new(ms, aesAlg.CreateDecryptor(keyHash, IVHash), CryptoStreamMode.Write);
|
||||
|
||||
csDecrypt.Write(data, 0, data.Length);
|
||||
|
||||
if (!csDecrypt.HasFlushedFinalBlock)
|
||||
csDecrypt.FlushFinalBlock();
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
|
||||
return md5.ComputeHash(Encoding.UTF8.GetBytes(value));
|
||||
}
|
||||
|
||||
public Encryptor(string key)
|
||||
{
|
||||
md5 = MD5.Create();
|
||||
|
||||
aesAlg = Aes.Create();
|
||||
aesAlg.BlockSize = 128;
|
||||
aesAlg.Padding = PaddingMode.PKCS7;
|
||||
|
||||
keyHash = GetHash(key);
|
||||
IVHash = GetHash(IV);
|
||||
}
|
||||
|
||||
public byte[] Encrypt(byte[] data)
|
||||
{
|
||||
using MemoryStream ms = new();
|
||||
using CryptoStream csEncrypt = new(ms, aesAlg.CreateEncryptor(keyHash, IVHash), CryptoStreamMode.Write);
|
||||
|
||||
csEncrypt.Write(data, 0, data.Length);
|
||||
|
||||
if (!csEncrypt.HasFlushedFinalBlock)
|
||||
csEncrypt.FlushFinalBlock();
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
public byte[] Decrypt(byte[] data)
|
||||
{
|
||||
using MemoryStream ms = new();
|
||||
using CryptoStream csDecrypt = new(ms, aesAlg.CreateDecryptor(keyHash, IVHash), CryptoStreamMode.Write);
|
||||
|
||||
csDecrypt.Write(data, 0, data.Length);
|
||||
|
||||
if (!csDecrypt.HasFlushedFinalBlock)
|
||||
csDecrypt.FlushFinalBlock();
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,60 +1,56 @@
|
||||
using YandexMusic.API.Models.Common;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using YandexMusic.API.Models.Common;
|
||||
|
||||
namespace YandexMusic.API.Common.Providers
|
||||
namespace YandexMusic.API.Common.Providers;
|
||||
|
||||
/// <summary>Базовый провайдер HTTP-запросов с общей логикой десериализации.</summary>
|
||||
public abstract class CommonRequestProvider : IRequestProvider
|
||||
{
|
||||
public class CommonRequestProvider : IRequestProvider
|
||||
/// <summary>Хранилище данных авторизации.</summary>
|
||||
protected readonly AuthStorage storage;
|
||||
|
||||
/// <summary>Настройки сериализации JSON (регистронезависимые, поддержка enum-строк).</summary>
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
#region Поля
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
||||
};
|
||||
|
||||
protected AuthStorage storage;
|
||||
/// <summary>Инициализирует новый экземпляр провайдера.</summary>
|
||||
/// <param name="authStorage">Хранилище авторизации.</param>
|
||||
protected CommonRequestProvider(AuthStorage authStorage)
|
||||
{
|
||||
storage = authStorage;
|
||||
}
|
||||
|
||||
#endregion Поля
|
||||
/// <summary>Выполняет HTTP-запрос и возвращает ответ.</summary>
|
||||
public abstract Task<HttpResponseMessage> GetWebResponseAsync(
|
||||
HttpRequestMessage message,
|
||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead);
|
||||
|
||||
/// <summary>Преобразует HTTP-ответ в объект типа T.</summary>
|
||||
public virtual async Task<T> GetDataFromResponseAsync<T>(
|
||||
YandexMusicApi api,
|
||||
HttpResponseMessage response)
|
||||
{
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
|
||||
|
||||
public CommonRequestProvider(AuthStorage authStorage)
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
storage = authStorage;
|
||||
var error = JsonSerializer.Deserialize<YErrorResponse>(json, JsonOptions);
|
||||
throw error ?? new Exception("Ошибка десериализации ответа с ошибкой.");
|
||||
}
|
||||
|
||||
|
||||
|
||||
#region IRequestProvider
|
||||
|
||||
public virtual Task<HttpResponseMessage> GetWebResponseAsync(HttpRequestMessage message, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
||||
try
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
// Если нужен контекст выполнения, он добавляется через кастомный конвертер
|
||||
return JsonSerializer.Deserialize<T>(json, JsonOptions)
|
||||
?? throw new JsonException("Десериализация вернула null");
|
||||
}
|
||||
|
||||
public virtual async Task<T> GetDataFromResponseAsync<T>(YandexMusicApi api, HttpResponseMessage response)
|
||||
catch (Exception ex)
|
||||
{
|
||||
string result = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
YErrorResponse exception = JsonConvert.DeserializeObject<YErrorResponse>(result);
|
||||
throw exception ?? new Exception("Ошибка десериализации ответа с ошибкой.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
JsonSerializerSettings settings = new()
|
||||
{
|
||||
Converters = new List<JsonConverter> {
|
||||
new YExecutionContextConverter(api, storage)
|
||||
}
|
||||
};
|
||||
|
||||
return storage.Debug != null
|
||||
? storage.Debug.Deserialize<T>(response.RequestMessage?.RequestUri?.AbsolutePath, result, settings)
|
||||
: JsonConvert.DeserializeObject<T>(result, settings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Ошибка десериализации {ex}");
|
||||
}
|
||||
throw new Exception($"Ошибка десериализации: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
#endregion IRequestProvider
|
||||
}
|
||||
}
|
||||
@@ -1,69 +1,45 @@
|
||||
using System.Net;
|
||||
|
||||
using YandexMusic.API.Models.Common;
|
||||
namespace YandexMusic.API.Common.Providers;
|
||||
|
||||
namespace YandexMusic.API.Common.Providers
|
||||
/// <summary>Стандартный провайдер HTTP-запросов с использованием HttpClient.</summary>
|
||||
public class DefaultRequestProvider : CommonRequestProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Стандартный провайдер запросов
|
||||
/// </summary>
|
||||
public class DefaultRequestProvider : CommonRequestProvider
|
||||
/// <summary>Инициализирует новый экземпляр провайдера.</summary>
|
||||
/// <param name="authStorage">Хранилище авторизации.</param>
|
||||
public DefaultRequestProvider(AuthStorage authStorage) : base(authStorage) { }
|
||||
|
||||
/// <summary>Выполняет HTTP-запрос и возвращает ответ.</summary>
|
||||
/// <param name="message">HTTP-запрос.</param>
|
||||
/// <param name="completionOption">Опция завершения запроса.</param>
|
||||
/// <returns>HTTP-ответ.</returns>
|
||||
public override async Task<HttpResponseMessage> GetWebResponseAsync(
|
||||
HttpRequestMessage message,
|
||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
#region Вспомогательные функции
|
||||
|
||||
private Exception ProcessException(Exception ex)
|
||||
using var handler = new SocketsHttpHandler
|
||||
{
|
||||
if (ex is not WebException webException)
|
||||
return ex;
|
||||
Proxy = storage.Context.WebProxy,
|
||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
|
||||
UseCookies = true,
|
||||
CookieContainer = storage.Context.Cookies,
|
||||
AllowAutoRedirect = true,
|
||||
MaxAutomaticRedirections = 10
|
||||
};
|
||||
|
||||
if (webException.Response is null)
|
||||
return ex;
|
||||
using var client = new HttpClient(handler);
|
||||
|
||||
Stream s = webException.Response.GetResponseStream();
|
||||
if (s is null)
|
||||
return ex;
|
||||
|
||||
using StreamReader sr = new(s);
|
||||
string result = sr.ReadToEnd();
|
||||
|
||||
YErrorResponse exception = JsonConvert.DeserializeObject<YErrorResponse>(result);
|
||||
|
||||
return exception ?? ex;
|
||||
}
|
||||
|
||||
#endregion Вспомогательные функции
|
||||
|
||||
|
||||
|
||||
public DefaultRequestProvider(AuthStorage authStorage) : base(authStorage)
|
||||
try
|
||||
{
|
||||
return await client.SendAsync(message, completionOption);
|
||||
}
|
||||
|
||||
|
||||
|
||||
#region IRequestProvider
|
||||
|
||||
public override Task<HttpResponseMessage> GetWebResponseAsync(HttpRequestMessage message,
|
||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpClient client = new(new SocketsHttpHandler
|
||||
{
|
||||
Proxy = storage.Context.WebProxy,
|
||||
AutomaticDecompression = DecompressionMethods.GZip,
|
||||
UseCookies = true,
|
||||
CookieContainer = storage.Context.Cookies,
|
||||
});
|
||||
// Пытаемся извлечь тело ошибки, если оно доступно
|
||||
if (ex.InnerException == null)
|
||||
throw;
|
||||
|
||||
return client.SendAsync(message, completionOption);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw ProcessException(ex);
|
||||
}
|
||||
throw new Exception($"Ошибка HTTP-запроса: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
#endregion IRequestProvider
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
public MockRequestProvider(AuthStorage authStorage) : base(authStorage)
|
||||
{
|
||||
storage = authStorage;
|
||||
}
|
||||
|
||||
|
||||
|
||||
43
YandexMusic.API/Common/Ynison/UpperSnakeCaseNamingPolicy.cs
Normal file
43
YandexMusic.API/Common/Ynison/UpperSnakeCaseNamingPolicy.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace YandexMusic.API.Common.Ynison;
|
||||
|
||||
/// <summary>Политика именования в формате UPPER_SNAKE_CASE (все буквы верхнего регистра, слова через подчёркивание).</summary>
|
||||
public class UpperSnakeCaseNamingPolicy : SnakeCaseNamingPolicy
|
||||
{
|
||||
/// <summary>Преобразует имя свойства в формат UPPER_SNAKE_CASE.</summary>
|
||||
public override string ConvertName(string name)
|
||||
{
|
||||
var snakeCase = base.ConvertName(name);
|
||||
return snakeCase.ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Базовая политика именования в формате snake_case (все буквы нижнего регистра, слова через подчёркивание).</summary>
|
||||
public class SnakeCaseNamingPolicy : JsonNamingPolicy
|
||||
{
|
||||
/// <summary>Преобразует имя свойства в формат snake_case.</summary>
|
||||
public override string ConvertName(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return name;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
for (int i = 0; i < name.Length; i++)
|
||||
{
|
||||
char c = name[i];
|
||||
if (char.IsUpper(c))
|
||||
{
|
||||
if (i > 0)
|
||||
sb.Append('_');
|
||||
sb.Append(char.ToLowerInvariant(c));
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace YandexMusic.API.Common.Ynison
|
||||
{
|
||||
public class UpperSnakeCaseNamingStrategy : SnakeCaseNamingStrategy
|
||||
{
|
||||
protected override string ResolvePropertyName(string name) => base.ResolvePropertyName(name).ToUpper();
|
||||
}
|
||||
}
|
||||
@@ -1,315 +1,183 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using YandexMusic.API.Models.Track;
|
||||
using YandexMusic.API.Models.Ynison;
|
||||
using YandexMusic.API.Models.Ynison.Messages;
|
||||
|
||||
namespace YandexMusic.API.Common.Ynison
|
||||
namespace YandexMusic.API.Common.Ynison;
|
||||
|
||||
/// <summary>Плеер для управления воспроизведением через протокол Ynison (WebSocket).</summary>
|
||||
public class YnisonPlayer : IDisposable
|
||||
{
|
||||
public class YnisonPlayer : IDisposable
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly AuthStorage _storage;
|
||||
private YnisonWebSocket? _redirector;
|
||||
private YnisonWebSocket? _state;
|
||||
|
||||
/// <summary>API Яндекс Музыки.</summary>
|
||||
public YandexMusicApi API { get; }
|
||||
|
||||
/// <summary>Текущее состояние плеера.</summary>
|
||||
public YYnisonState? State { get; private set; }
|
||||
|
||||
/// <summary>Текущий проигрываемый трек.</summary>
|
||||
public YTrack? Current => GetCurrentAsync().GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>Событие получения нового состояния.</summary>
|
||||
public event EventHandler<ReceiveEventArgs>? OnReceive;
|
||||
|
||||
/// <summary>Событие закрытия соединения.</summary>
|
||||
public event EventHandler<CloseEventArgs>? OnClose;
|
||||
|
||||
/// <summary>Аргументы события получения состояния.</summary>
|
||||
public class ReceiveEventArgs : EventArgs
|
||||
{
|
||||
#region Поля
|
||||
/// <summary>Состояние плеера.</summary>
|
||||
public YYnisonState State { get; init; } = null!;
|
||||
}
|
||||
|
||||
private readonly JsonSerializerSettings jsonSettings = new()
|
||||
/// <summary>Аргументы события закрытия соединения.</summary>
|
||||
public class CloseEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>Статус закрытия.</summary>
|
||||
public WebSocketCloseStatus? Status { get; init; }
|
||||
/// <summary>Описание причины закрытия.</summary>
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
internal YnisonPlayer(YandexMusicApi api, AuthStorage authStorage)
|
||||
{
|
||||
API = api;
|
||||
_storage = authStorage;
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
Converters = new List<JsonConverter> {
|
||||
new StringEnumConverter(new UpperSnakeCaseNamingStrategy())
|
||||
},
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter(new UpperSnakeCaseNamingPolicy(), false) }
|
||||
};
|
||||
_redirector = new YnisonWebSocket();
|
||||
_state = new YnisonWebSocket();
|
||||
}
|
||||
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
ContractResolver = new DefaultContractResolver
|
||||
private string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions);
|
||||
|
||||
private T Deserialize<T>(YYnisonMessageType messageType, string data)
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(data, _jsonOptions)
|
||||
?? throw new JsonException("Десериализация вернула null");
|
||||
}
|
||||
|
||||
private T DeserializeMessage<T>(YYnisonMessageType messageType, string data)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(data);
|
||||
if (doc.RootElement.TryGetProperty("error", out _))
|
||||
{
|
||||
var error = Deserialize<YYnisonErrorMessage>(YYnisonMessageType.Error, data);
|
||||
throw error ?? new Exception("Ошибка десериализации ответа с ошибкой.");
|
||||
}
|
||||
return Deserialize<T>(messageType, data);
|
||||
}
|
||||
|
||||
private string DefaultState()
|
||||
{
|
||||
var version = new YYnisonVersion
|
||||
{
|
||||
DeviceId = _storage.DeviceId,
|
||||
Version = "0"
|
||||
};
|
||||
var fullState = new YYnisonUpdateFullStateMessage
|
||||
{
|
||||
UpdateFullState = new YYnisonFullState
|
||||
{
|
||||
// Важно! Унисон отдаёт данные в SnakeCase
|
||||
NamingStrategy = new SnakeCaseNamingStrategy()
|
||||
Device = new YYnisonDevice
|
||||
{
|
||||
Capabilities = new YYnisonDeviceCapabilities { CanBePlayer = true },
|
||||
Info = new YYnisonDeviceInfo
|
||||
{
|
||||
DeviceId = _storage.DeviceId,
|
||||
AppName = "Yandex Music API",
|
||||
AppVersion = "0.0.1",
|
||||
Type = "WEB",
|
||||
Title = "YandexMusicAPI"
|
||||
},
|
||||
IsShadow = true
|
||||
},
|
||||
PlayerState = new YYnisonPlayerState
|
||||
{
|
||||
PlayerQueue = new YYnisonPlayerQueue { Version = version },
|
||||
Status = new YYnisonPlayerStateStatus { Version = version }
|
||||
}
|
||||
}
|
||||
};
|
||||
return SerializeJson(fullState);
|
||||
}
|
||||
|
||||
private AuthStorage storage;
|
||||
private YnisonWebSocket redirector;
|
||||
private YnisonWebSocket state;
|
||||
private async Task<YTrack?> GetCurrentAsync()
|
||||
{
|
||||
if (State == null) return null;
|
||||
int index = State.PlayerState.PlayerQueue.CurrentPlayableIndex;
|
||||
if (index < 0 || index >= State.PlayerState.PlayerQueue.PlayableList.Count)
|
||||
return null;
|
||||
var item = State.PlayerState.PlayerQueue.PlayableList[index];
|
||||
var response = await API.Track.GetAsync(_storage, item.PlayableId);
|
||||
return response?.Result?.FirstOrDefault();
|
||||
}
|
||||
|
||||
#endregion Поля
|
||||
|
||||
#region Свойства
|
||||
|
||||
/// <summary>
|
||||
/// API
|
||||
/// </summary>
|
||||
public YandexMusicApi API { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Состояние
|
||||
/// </summary>
|
||||
public YYnisonState State { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Текущий проигрываемый трек
|
||||
/// </summary>
|
||||
public YTrack Current => GetCurrent();
|
||||
|
||||
#endregion Свойства
|
||||
|
||||
#region События
|
||||
|
||||
public class ReceiveEventArgs
|
||||
private async Task UpdateStateAsync()
|
||||
{
|
||||
if (State == null) return;
|
||||
var update = new YYnisonUpdatePlayerStateMessage
|
||||
{
|
||||
public YYnisonState State { get; internal set; }
|
||||
}
|
||||
UpdatePlayerState = State.PlayerState
|
||||
};
|
||||
update.UpdatePlayerState.Status.Version = new YYnisonVersion { DeviceId = _storage.DeviceId };
|
||||
update.UpdatePlayerState.PlayerQueue.Version = new YYnisonVersion { DeviceId = _storage.DeviceId };
|
||||
if (_state != null)
|
||||
await _state.SendAsync(SerializeJson(update));
|
||||
}
|
||||
|
||||
public delegate void OnReceiveEventHandler(YnisonPlayer player, ReceiveEventArgs args);
|
||||
|
||||
/// <summary>
|
||||
/// Получение данных
|
||||
/// </summary>
|
||||
public event OnReceiveEventHandler OnReceive;
|
||||
|
||||
|
||||
public class CloseEventArgs
|
||||
/// <summary>Подключается к Ynison и начинает получение состояния.</summary>
|
||||
public async Task ConnectAsync()
|
||||
{
|
||||
if (_redirector == null) throw new ObjectDisposedException(nameof(YnisonPlayer));
|
||||
await _redirector.ConnectAsync(_storage, "wss://ynison.music.yandex.ru/redirector.YnisonRedirectService/GetRedirectToYnison");
|
||||
_redirector.OnReceive += async (socket, data) =>
|
||||
{
|
||||
public WebSocketCloseStatus? Status { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
|
||||
public delegate void OnCloseEventHandler(YnisonPlayer player, CloseEventArgs args);
|
||||
|
||||
/// <summary>
|
||||
/// Получение данных
|
||||
/// </summary>
|
||||
public event OnCloseEventHandler OnClose;
|
||||
|
||||
#endregion События
|
||||
|
||||
#region Вспомогательные функции
|
||||
|
||||
private string SerializeJson(object data)
|
||||
{
|
||||
return JsonConvert.SerializeObject(data, jsonSettings);
|
||||
}
|
||||
|
||||
private T Deserialize<T>(YYnisonMessageType messageType, string data)
|
||||
{
|
||||
return storage.Debug != null
|
||||
? storage.Debug.Deserialize<T>($"Ynison{messageType}", data, jsonSettings)
|
||||
: JsonConvert.DeserializeObject<T>(data, jsonSettings);
|
||||
}
|
||||
|
||||
private T DeserializeMessage<T>(YYnisonMessageType messageType, string data)
|
||||
{
|
||||
JObject o = JObject.Parse(data);
|
||||
// Сообщение с ошибкой
|
||||
if (o.ContainsKey("error"))
|
||||
var redirectInfo = Deserialize<YYnisonRedirect>(YYnisonMessageType.Redirect, data.Data);
|
||||
if (_state == null) return;
|
||||
if (_state.IsConnected) return;
|
||||
await _state.ConnectAsync(_storage, $"wss://{redirectInfo.Host}/ynison_state.YnisonStateService/PutYnisonState", redirectInfo.RedirectTicket);
|
||||
_state.OnReceive += (s, d) =>
|
||||
{
|
||||
YYnisonErrorMessage exception = Deserialize<YYnisonErrorMessage>(YYnisonMessageType.Error, data);
|
||||
throw exception ?? new Exception("Ошибка десериализации ответа с ошибкой.");
|
||||
}
|
||||
|
||||
return Deserialize<T>(messageType, data);
|
||||
}
|
||||
|
||||
private string DefaultState()
|
||||
{
|
||||
YYnisonVersion version = new()
|
||||
{
|
||||
DeviceId = storage.DeviceId,
|
||||
Version = "0"
|
||||
var message = DeserializeMessage<YYnisonState>(YYnisonMessageType.State, d.Data);
|
||||
State = message;
|
||||
OnReceive?.Invoke(this, new ReceiveEventArgs { State = State });
|
||||
};
|
||||
|
||||
YYnisonUpdateFullStateMessage fullState = new()
|
||||
_state.OnClose += (s, args) =>
|
||||
{
|
||||
UpdateFullState = new()
|
||||
{
|
||||
Device = new()
|
||||
{
|
||||
Capabilities = new()
|
||||
{
|
||||
CanBePlayer = true
|
||||
},
|
||||
Info = new()
|
||||
{
|
||||
DeviceId = storage.DeviceId,
|
||||
AppName = "Yandex Music API",
|
||||
AppVersion = "0.0.1",
|
||||
Type = "WEB",
|
||||
Title = "YandexMusicAPI"
|
||||
},
|
||||
IsShadow = true
|
||||
},
|
||||
PlayerState = new()
|
||||
{
|
||||
PlayerQueue = new()
|
||||
{
|
||||
Version = version
|
||||
},
|
||||
Status = new()
|
||||
{
|
||||
Version = version
|
||||
}
|
||||
}
|
||||
}
|
||||
OnClose?.Invoke(this, new CloseEventArgs { Status = args.Status, Description = args.Description });
|
||||
};
|
||||
_ = _state.BeginReceiveAsync();
|
||||
await _state.SendAsync(DefaultState());
|
||||
};
|
||||
await _redirector.BeginReceiveAsync();
|
||||
}
|
||||
|
||||
return SerializeJson(fullState);
|
||||
}
|
||||
/// <summary>Отключается от Ynison.</summary>
|
||||
public async Task DisconnectAsync()
|
||||
{
|
||||
if (_state != null) await _state.StopReceiveAsync();
|
||||
if (_redirector != null) await _redirector.StopReceiveAsync();
|
||||
}
|
||||
|
||||
private YTrack GetCurrent()
|
||||
{
|
||||
if (State == null)
|
||||
return null;
|
||||
|
||||
int index = State.PlayerState.PlayerQueue.CurrentPlayableIndex;
|
||||
if (index < 0 || index > State.PlayerState.PlayerQueue.PlayableList.Count)
|
||||
return null;
|
||||
|
||||
YYnisonPlayableItem item = State.PlayerState.PlayerQueue.PlayableList[index];
|
||||
|
||||
return API.Track.Get(storage, item.PlayableId)
|
||||
.Result
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private void UpdateState()
|
||||
{
|
||||
YYnisonUpdatePlayerStateMessage update = new()
|
||||
{
|
||||
UpdatePlayerState = State.PlayerState
|
||||
};
|
||||
|
||||
update.UpdatePlayerState.Status.Version = new()
|
||||
{
|
||||
DeviceId = storage.DeviceId
|
||||
};
|
||||
|
||||
update.UpdatePlayerState.PlayerQueue.Version = new()
|
||||
{
|
||||
DeviceId = storage.DeviceId
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
state.Send(SerializeJson(update));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Вспомогательные функции
|
||||
|
||||
|
||||
|
||||
#region Подключение
|
||||
|
||||
public void Connect()
|
||||
{
|
||||
redirector.Connect(storage, "wss://ynison.music.yandex.ru/redirector.YnisonRedirectService/GetRedirectToYnison");
|
||||
redirector.OnReceive += (socket, data) =>
|
||||
{
|
||||
YYnisonRedirect redirectInfo = Deserialize<YYnisonRedirect>(YYnisonMessageType.Redirect, data.Data);
|
||||
|
||||
if (state.IsConnected)
|
||||
return;
|
||||
|
||||
state.Connect(storage, $"wss://{redirectInfo.Host}/ynison_state.YnisonStateService/PutYnisonState", redirectInfo.RedirectTicket);
|
||||
state.OnReceive += (s, d) =>
|
||||
{
|
||||
YYnisonState message = DeserializeMessage<YYnisonState>(YYnisonMessageType.State, d.Data);
|
||||
|
||||
State = message;
|
||||
|
||||
OnReceive?.Invoke(this, new ReceiveEventArgs
|
||||
{
|
||||
State = State
|
||||
});
|
||||
};
|
||||
|
||||
state.OnClose += (s, args) =>
|
||||
{
|
||||
OnClose?.Invoke(this, new CloseEventArgs
|
||||
{
|
||||
Status = args.Status,
|
||||
Description = args.Description
|
||||
});
|
||||
};
|
||||
|
||||
state.BeginReceive();
|
||||
// Отправка изначального состояния
|
||||
state.Send(DefaultState());
|
||||
};
|
||||
|
||||
redirector.BeginReceive();
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
state?.StopReceive();
|
||||
redirector?.StopReceive();
|
||||
}
|
||||
|
||||
#endregion Подключение
|
||||
|
||||
#region Плеер
|
||||
|
||||
/*
|
||||
public void Play()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void Next()
|
||||
{
|
||||
List<YYnisonPlayableItem> list = State.PlayerState.PlayerQueue.PlayableList;
|
||||
|
||||
if (State.PlayerState.PlayerQueue.EntityType == YYnisonEntityType.Radio)
|
||||
{
|
||||
YYnisonPlayableItem next = State.PlayerState.PlayerQueue.Queue.WaveQueue.RecommendedPlayableList
|
||||
.FirstOrDefault();
|
||||
|
||||
list.RemoveAt(0);
|
||||
list.Add(next);
|
||||
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
if (State.PlayerState.PlayerQueue.CurrentPlayableIndex < list.Count - 1)
|
||||
{
|
||||
State.PlayerState.PlayerQueue.CurrentPlayableIndex++;
|
||||
UpdateState();
|
||||
}
|
||||
}
|
||||
|
||||
public void Previous()
|
||||
{
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
#endregion Плеер
|
||||
|
||||
internal YnisonPlayer(YandexMusicApi api, AuthStorage authStorage)
|
||||
{
|
||||
API = api;
|
||||
storage = authStorage;
|
||||
|
||||
redirector = new();
|
||||
state = new();
|
||||
}
|
||||
|
||||
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
redirector?.StopReceive();
|
||||
redirector?.Dispose();
|
||||
}
|
||||
|
||||
#endregion IDisposable
|
||||
/// <summary>Освобождает ресурсы.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_redirector?.Dispose();
|
||||
_state?.Dispose();
|
||||
_redirector = null;
|
||||
_state = null;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -1,179 +1,142 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace YandexMusic.API.Common.Ynison
|
||||
namespace YandexMusic.API.Common.Ynison;
|
||||
|
||||
/// <summary>WebSocket-клиент для взаимодействия с протоколом Ynison.</summary>
|
||||
public class YnisonWebSocket : IDisposable
|
||||
{
|
||||
public class YnisonWebSocket : IDisposable
|
||||
private readonly ClientWebSocket _socketClient = new();
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private CancellationToken _cancellationToken;
|
||||
private readonly StringBuilder _data = new();
|
||||
private const int BufferSize = 4096;
|
||||
|
||||
/// <summary>Флаг, указывает, открыто ли соединение.</summary>
|
||||
public bool IsConnected => _socketClient.State == WebSocketState.Open;
|
||||
|
||||
/// <summary>Событие получения сообщения.</summary>
|
||||
public event EventHandler<ReceiveEventArgs>? OnReceive;
|
||||
|
||||
/// <summary>Событие закрытия соединения.</summary>
|
||||
public event EventHandler<CloseEventArgs>? OnClose;
|
||||
|
||||
/// <summary>Аргументы события получения данных.</summary>
|
||||
public class ReceiveEventArgs : EventArgs
|
||||
{
|
||||
#region Поля
|
||||
/// <summary>Полученные данные (JSON-строка).</summary>
|
||||
public string Data { get; init; } = null!;
|
||||
}
|
||||
|
||||
private readonly JsonSerializerSettings jsonSettings = new()
|
||||
/// <summary>Аргументы события закрытия соединения.</summary>
|
||||
public class CloseEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>Статус закрытия.</summary>
|
||||
public WebSocketCloseStatus? Status { get; init; }
|
||||
/// <summary>Описание причины закрытия.</summary>
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
private static string GetProtocolData(string deviceId, string? redirectTicket)
|
||||
{
|
||||
var deviceInfo = new Dictionary<string, object>
|
||||
{
|
||||
Converters = new List<JsonConverter> {
|
||||
new StringEnumConverter {
|
||||
NamingStrategy = new CamelCaseNamingStrategy()
|
||||
}
|
||||
},
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
{ "app_name", "Chrome" },
|
||||
{ "type", 1 }
|
||||
};
|
||||
|
||||
private readonly ClientWebSocket socketClient = new();
|
||||
|
||||
private CancellationTokenSource cancellationTokenSource = new();
|
||||
private CancellationToken cancellation;
|
||||
|
||||
private readonly StringBuilder data = new();
|
||||
private readonly int size = 4096;
|
||||
|
||||
#endregion Поля
|
||||
|
||||
#region Свойства
|
||||
|
||||
public bool IsConnected => socketClient.State == WebSocketState.Open;
|
||||
|
||||
#endregion Свойства
|
||||
|
||||
#region События
|
||||
|
||||
public class ReceiveEventArgs
|
||||
var protocol = new Dictionary<string, string>
|
||||
{
|
||||
public string Data { get; internal set; }
|
||||
}
|
||||
{ "Ynison-Device-Id", deviceId },
|
||||
{ "Ynison-Device-Info", JsonSerializer.Serialize(deviceInfo) }
|
||||
};
|
||||
if (!string.IsNullOrEmpty(redirectTicket))
|
||||
protocol.Add("Ynison-Redirect-Ticket", redirectTicket);
|
||||
return JsonSerializer.Serialize(protocol);
|
||||
}
|
||||
|
||||
public delegate void OnReceiveEventHandler(YnisonWebSocket socket, ReceiveEventArgs args);
|
||||
/// <summary>
|
||||
/// Получение данных
|
||||
/// </summary>
|
||||
public event OnReceiveEventHandler OnReceive;
|
||||
|
||||
public class CloseEventArgs
|
||||
private async Task<string> ReadSocketContentAsync()
|
||||
{
|
||||
var buffer = new byte[BufferSize];
|
||||
WebSocketReceiveResult result;
|
||||
do
|
||||
{
|
||||
public WebSocketCloseStatus? Status { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
result = await _socketClient.ReceiveAsync(new ArraySegment<byte>(buffer), _cancellationToken);
|
||||
_data.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
|
||||
} while (!result.EndOfMessage);
|
||||
return _data.ToString();
|
||||
}
|
||||
|
||||
public delegate void OnCloseEventHandler(YnisonWebSocket socket, CloseEventArgs args);
|
||||
/// <summary>
|
||||
/// Закрытие соединения
|
||||
/// </summary>
|
||||
public event OnCloseEventHandler OnClose;
|
||||
/// <summary>Подключается к WebSocket.</summary>
|
||||
/// <param name="storage">Хранилище авторизации.</param>
|
||||
/// <param name="url">URL WebSocket.</param>
|
||||
/// <param name="redirectTicket">Тикет перенаправления (опционально).</param>
|
||||
public async Task ConnectAsync(AuthStorage storage, string url, string? redirectTicket = null)
|
||||
{
|
||||
_socketClient.Options.AddSubProtocol("Bearer");
|
||||
var protocolData = GetProtocolData(storage.DeviceId, redirectTicket);
|
||||
_socketClient.Options.SetRequestHeader("Sec-WebSocket-Protocol", $"Bearer, v2, {protocolData}");
|
||||
_socketClient.Options.SetRequestHeader("Origin", "https://music.yandex.ru");
|
||||
_socketClient.Options.SetRequestHeader("Authorization", $"OAuth {storage.Token}");
|
||||
_socketClient.Options.Proxy = storage.Context.WebProxy;
|
||||
|
||||
#endregion События
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_cancellationToken = _cancellationTokenSource.Token;
|
||||
|
||||
#region Вспомогательные функции
|
||||
await _socketClient.ConnectAsync(new Uri(url), CancellationToken.None);
|
||||
}
|
||||
|
||||
private string SerializeJson(object obj)
|
||||
/// <summary>Начинает асинхронный приём сообщений.</summary>
|
||||
public async Task BeginReceiveAsync()
|
||||
{
|
||||
if (_socketClient.State != WebSocketState.Open)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
return JsonConvert.SerializeObject(obj, jsonSettings);
|
||||
}
|
||||
|
||||
private string GetProtocolData(string deviceId, string redirectTicket)
|
||||
{
|
||||
Dictionary<string, object> deviceInfo = new() {
|
||||
{ "app_name", "Chrome" },
|
||||
{ "type", 1 }
|
||||
};
|
||||
|
||||
Dictionary<string, string> protocol = new() {
|
||||
{ "Ynison-Device-Id", deviceId },
|
||||
{ "Ynison-Device-Info", SerializeJson(deviceInfo) }
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(redirectTicket))
|
||||
protocol.Add("Ynison-Redirect-Ticket", redirectTicket);
|
||||
|
||||
return SerializeJson(protocol);
|
||||
}
|
||||
|
||||
private async Task<string> ReadSocketContent()
|
||||
{
|
||||
byte[] buffer = new byte[size];
|
||||
WebSocketReceiveResult result;
|
||||
|
||||
do
|
||||
while (!_cancellationToken.IsCancellationRequested && _socketClient.State == WebSocketState.Open)
|
||||
{
|
||||
result = await socketClient.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
data.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
|
||||
} while (!result.EndOfMessage);
|
||||
|
||||
return data.ToString();
|
||||
var content = await ReadSocketContentAsync();
|
||||
OnReceive?.Invoke(this, new ReceiveEventArgs { Data = content });
|
||||
_data.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Вспомогательные функции
|
||||
|
||||
|
||||
|
||||
public bool Connect(AuthStorage storage, string url, string redirectTicket = null)
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
socketClient.Options.AddSubProtocol("Bearer");
|
||||
|
||||
socketClient.Options.SetRequestHeader("Sec-WebSocket-Protocol", $"Bearer, v2, {GetProtocolData(storage.DeviceId, redirectTicket)}");
|
||||
socketClient.Options.SetRequestHeader("Origin", "https://music.yandex.ru");
|
||||
socketClient.Options.SetRequestHeader("Authorization", $"OAuth {storage.Token}");
|
||||
|
||||
socketClient.Options.Proxy = storage.Context.WebProxy;
|
||||
|
||||
socketClient.ConnectAsync(new Uri(url), CancellationToken.None)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
|
||||
cancellation = cancellationTokenSource.Token;
|
||||
|
||||
return socketClient.State == WebSocketState.Open;
|
||||
// Ожидаемая отмена
|
||||
}
|
||||
|
||||
public async Task BeginReceive()
|
||||
finally
|
||||
{
|
||||
if (socketClient.State != WebSocketState.Open)
|
||||
return;
|
||||
|
||||
do
|
||||
{
|
||||
string content = await ReadSocketContent();
|
||||
OnReceive?.Invoke(this, new ReceiveEventArgs
|
||||
{
|
||||
Data = content
|
||||
});
|
||||
|
||||
data.Clear();
|
||||
} while (!cancellation.IsCancellationRequested && socketClient.State == WebSocketState.Open);
|
||||
|
||||
OnClose?.Invoke(this, new CloseEventArgs
|
||||
{
|
||||
Status = socketClient.CloseStatus,
|
||||
Description = socketClient.CloseStatusDescription
|
||||
});
|
||||
|
||||
await socketClient.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
|
||||
var closeStatus = _socketClient.CloseStatus;
|
||||
var closeDesc = _socketClient.CloseStatusDescription;
|
||||
OnClose?.Invoke(this, new CloseEventArgs { Status = closeStatus, Description = closeDesc });
|
||||
if (_socketClient.State == WebSocketState.Open)
|
||||
await _socketClient.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Отправляет JSON-сообщение.</summary>
|
||||
/// <param name="json">JSON-строка.</param>
|
||||
public async ValueTask SendAsync(string json)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
await _socketClient.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, _cancellationToken);
|
||||
}
|
||||
|
||||
public ValueTask Send(string json)
|
||||
/// <summary>Останавливает приём сообщений.</summary>
|
||||
public async Task StopReceiveAsync()
|
||||
{
|
||||
if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
ReadOnlyMemory<byte> message = new(Encoding.UTF8.GetBytes(json));
|
||||
return socketClient.SendAsync(message, WebSocketMessageType.Text, false, CancellationToken.None);
|
||||
await _cancellationTokenSource.CancelAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopReceive()
|
||||
{
|
||||
if (socketClient.State != WebSocketState.Open)
|
||||
return Task.CompletedTask;
|
||||
|
||||
cancellationTokenSource.Cancel(false);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
socketClient?.Dispose();
|
||||
cancellationTokenSource?.Dispose();
|
||||
}
|
||||
|
||||
#endregion IDisposable
|
||||
/// <summary>Освобождает ресурсы.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_socketClient.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user