Files
YandexMusic/YandexMusic.API/Common/Ynison/YnisonWebSocket.cs
2026-04-10 15:05:32 +03:00

142 lines
5.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}