using System.Net.WebSockets; using System.Text; namespace YandexMusic.API.Common.Ynison { public class YnisonWebSocket : IDisposable { #region Поля private readonly JsonSerializerSettings jsonSettings = new() { Converters = new List { new StringEnumConverter { NamingStrategy = new CamelCaseNamingStrategy() } }, NullValueHandling = NullValueHandling.Ignore }; 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 { public string Data { get; internal set; } } public delegate void OnReceiveEventHandler(YnisonWebSocket socket, ReceiveEventArgs args); /// /// Получение данных /// public event OnReceiveEventHandler OnReceive; public class CloseEventArgs { public WebSocketCloseStatus? Status { get; set; } public string Description { get; set; } } public delegate void OnCloseEventHandler(YnisonWebSocket socket, CloseEventArgs args); /// /// Закрытие соединения /// public event OnCloseEventHandler OnClose; #endregion События #region Вспомогательные функции private string SerializeJson(object obj) { return JsonConvert.SerializeObject(obj, jsonSettings); } private string GetProtocolData(string deviceId, string redirectTicket) { Dictionary deviceInfo = new() { { "app_name", "Chrome" }, { "type", 1 } }; Dictionary 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 ReadSocketContent() { byte[] buffer = new byte[size]; WebSocketReceiveResult result; do { result = await socketClient.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); data.Append(Encoding.UTF8.GetString(buffer, 0, result.Count)); } while (!result.EndOfMessage); return data.ToString(); } #endregion Вспомогательные функции public bool Connect(AuthStorage storage, string url, string redirectTicket = null) { 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() { 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); } public ValueTask Send(string json) { ReadOnlyMemory message = new(Encoding.UTF8.GetBytes(json)); return socketClient.SendAsync(message, WebSocketMessageType.Text, false, CancellationToken.None); } 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 } }