using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
namespace YandexMusic.API.Common.Ynison;
/// WebSocket-клиент для взаимодействия с протоколом Ynison.
public class YnisonWebSocket : IDisposable
{
private ClientWebSocket? _socketClient;
private CancellationTokenSource? _cancellationTokenSource;
private CancellationToken _cancellationToken;
private readonly StringBuilder _data = new();
private const int BufferSize = 4096;
private readonly IWebProxy? _proxy;
/// Флаг, указывает, открыто ли соединение.
public bool IsConnected => _socketClient?.State == WebSocketState.Open;
/// Событие получения сообщения.
public event EventHandler? OnReceive;
/// Событие закрытия соединения.
public event EventHandler? OnClose;
/// Аргументы события получения данных.
public class ReceiveEventArgs : EventArgs
{
public string Data { get; init; } = null!;
}
/// Аргументы события закрытия соединения.
public class CloseEventArgs : EventArgs
{
public WebSocketCloseStatus? Status { get; init; }
public string? Description { get; init; }
}
///
/// Инициализирует новый экземпляр WebSocket-клиента.
///
/// Прокси-сервер (опционально).
public YnisonWebSocket(IWebProxy? proxy = null)
{
_proxy = proxy;
}
private static string GetProtocolData(string deviceId, string? redirectTicket)
{
var deviceInfo = new Dictionary
{
{ "app_name", "Chrome" },
{ "type", 1 }
};
var protocol = new Dictionary
{
{ "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 ReadSocketContentAsync()
{
if (_socketClient == null)
throw new InvalidOperationException("WebSocket не инициализирован");
var buffer = new byte[BufferSize];
WebSocketReceiveResult result;
do
{
result = await _socketClient.ReceiveAsync(new ArraySegment(buffer), _cancellationToken);
_data.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
} while (!result.EndOfMessage);
return _data.ToString();
}
/// Подключается к WebSocket.
/// Хранилище авторизации (для токена и deviceId).
/// URL WebSocket.
/// Тикет перенаправления (опционально).
public async Task ConnectAsync(AuthStorage storage, string url, string? redirectTicket = null)
{
_socketClient = new ClientWebSocket();
_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}");
if (_proxy != null)
_socketClient.Options.Proxy = _proxy;
_cancellationTokenSource = new CancellationTokenSource();
_cancellationToken = _cancellationTokenSource.Token;
await _socketClient.ConnectAsync(new Uri(url), CancellationToken.None);
}
/// Начинает асинхронный приём сообщений.
public async Task BeginReceiveAsync()
{
if (_socketClient == null || _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);
}
}
/// Отправляет JSON-сообщение.
public async ValueTask SendAsync(string json)
{
if (_socketClient == null)
throw new InvalidOperationException("WebSocket не инициализирован");
var bytes = Encoding.UTF8.GetBytes(json);
await _socketClient.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, _cancellationToken);
}
/// Останавливает приём сообщений.
public async Task StopReceiveAsync()
{
if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
await _cancellationTokenSource.CancelAsync();
}
/// Освобождает ресурсы.
public void Dispose()
{
_cancellationTokenSource?.Dispose();
_socketClient?.Dispose();
GC.SuppressFinalize(this);
}
}