142 lines
5.7 KiB
C#
142 lines
5.7 KiB
C#
using System.Net.WebSockets;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
|
||
namespace YandexMusic.API.Common.Ynison;
|
||
|
||
/// <summary>WebSocket-клиент для взаимодействия с протоколом Ynison.</summary>
|
||
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
|
||
{
|
||
/// <summary>Полученные данные (JSON-строка).</summary>
|
||
public string Data { get; init; } = null!;
|
||
}
|
||
|
||
/// <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>
|
||
{
|
||
{ "app_name", "Chrome" },
|
||
{ "type", 1 }
|
||
};
|
||
var protocol = new Dictionary<string, string>
|
||
{
|
||
{ "Ynison-Device-Id", deviceId },
|
||
{ "Ynison-Device-Info", JsonSerializer.Serialize(deviceInfo) }
|
||
};
|
||
if (!string.IsNullOrEmpty(redirectTicket))
|
||
protocol.Add("Ynison-Redirect-Ticket", redirectTicket);
|
||
return JsonSerializer.Serialize(protocol);
|
||
}
|
||
|
||
private async Task<string> ReadSocketContentAsync()
|
||
{
|
||
var buffer = new byte[BufferSize];
|
||
WebSocketReceiveResult result;
|
||
do
|
||
{
|
||
result = await _socketClient.ReceiveAsync(new ArraySegment<byte>(buffer), _cancellationToken);
|
||
_data.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
|
||
} while (!result.EndOfMessage);
|
||
return _data.ToString();
|
||
}
|
||
|
||
/// <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;
|
||
|
||
_cancellationTokenSource = new CancellationTokenSource();
|
||
_cancellationToken = _cancellationTokenSource.Token;
|
||
|
||
await _socketClient.ConnectAsync(new Uri(url), CancellationToken.None);
|
||
}
|
||
|
||
/// <summary>Начинает асинхронный приём сообщений.</summary>
|
||
public async Task BeginReceiveAsync()
|
||
{
|
||
if (_socketClient.State != WebSocketState.Open)
|
||
return;
|
||
|
||
try
|
||
{
|
||
while (!_cancellationToken.IsCancellationRequested && _socketClient.State == WebSocketState.Open)
|
||
{
|
||
var content = await ReadSocketContentAsync();
|
||
OnReceive?.Invoke(this, new ReceiveEventArgs { Data = content });
|
||
_data.Clear();
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// Ожидаемая отмена
|
||
}
|
||
finally
|
||
{
|
||
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);
|
||
}
|
||
|
||
/// <summary>Останавливает приём сообщений.</summary>
|
||
public async Task StopReceiveAsync()
|
||
{
|
||
if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
|
||
{
|
||
await _cancellationTokenSource.CancelAsync();
|
||
}
|
||
}
|
||
|
||
/// <summary>Освобождает ресурсы.</summary>
|
||
public void Dispose()
|
||
{
|
||
_cancellationTokenSource?.Dispose();
|
||
_socketClient.Dispose();
|
||
GC.SuppressFinalize(this);
|
||
}
|
||
} |