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); } }