Files
YandexMusic/YandexMusic.API/Common/Ynison/YnisonWebSocket.cs
FrigaT 36e28ce3fe
All checks were successful
Release / pack-and-publish (release) Successful in 36s
Полностью переписанное api
2026-04-19 17:00:05 +03:00

155 lines
6.2 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;
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 ClientWebSocket? _socketClient;
private CancellationTokenSource? _cancellationTokenSource;
private CancellationToken _cancellationToken;
private readonly StringBuilder _data = new();
private const int BufferSize = 4096;
private readonly IWebProxy? _proxy;
/// <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
{
public string Data { get; init; } = null!;
}
/// <summary>Аргументы события закрытия соединения.</summary>
public class CloseEventArgs : EventArgs
{
public WebSocketCloseStatus? Status { get; init; }
public string? Description { get; init; }
}
/// <summary>
/// Инициализирует новый экземпляр WebSocket-клиента.
/// </summary>
/// <param name="proxy">Прокси-сервер (опционально).</param>
public YnisonWebSocket(IWebProxy? proxy = null)
{
_proxy = proxy;
}
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()
{
if (_socketClient == null)
throw new InvalidOperationException("WebSocket не инициализирован");
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">Хранилище авторизации (для токена и deviceId).</param>
/// <param name="url">URL WebSocket.</param>
/// <param name="redirectTicket">Тикет перенаправления (опционально).</param>
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);
}
/// <summary>Начинает асинхронный приём сообщений.</summary>
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);
}
}
/// <summary>Отправляет JSON-сообщение.</summary>
public async ValueTask SendAsync(string json)
{
if (_socketClient == null)
throw new InvalidOperationException("WebSocket не инициализирован");
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);
}
}