Полностью переписанное api
All checks were successful
Release / pack-and-publish (release) Successful in 36s

This commit is contained in:
FrigaT
2026-04-19 17:00:05 +03:00
parent 5541d0ad27
commit 36e28ce3fe
111 changed files with 1552 additions and 3358 deletions

View File

@@ -1,4 +1,5 @@
using System.Net.WebSockets;
using System.Net;
using System.Net.WebSockets;
using System.Text.Json;
using System.Text.Json.Serialization;
using YandexMusic.API.Models.Track;
@@ -12,6 +13,7 @@ public class YnisonPlayer : IDisposable
{
private readonly JsonSerializerOptions _jsonOptions;
private readonly AuthStorage _storage;
private readonly IWebProxy? _proxy;
private YnisonWebSocket? _redirector;
private YnisonWebSocket? _state;
@@ -33,40 +35,36 @@ public class YnisonPlayer : IDisposable
/// <summary>Аргументы события получения состояния.</summary>
public class ReceiveEventArgs : EventArgs
{
/// <summary>Состояние плеера.</summary>
public YYnisonState State { get; init; } = null!;
}
/// <summary>Аргументы события закрытия соединения.</summary>
public class CloseEventArgs : EventArgs
{
/// <summary>Статус закрытия.</summary>
public WebSocketCloseStatus? Status { get; init; }
/// <summary>Описание причины закрытия.</summary>
public string? Description { get; init; }
}
internal YnisonPlayer(YandexMusicApi api, AuthStorage authStorage)
internal YnisonPlayer(YandexMusicApi api, AuthStorage authStorage, IWebProxy? proxy = null)
{
API = api;
_storage = authStorage;
_proxy = proxy;
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(new UpperSnakeCaseNamingPolicy(), false) }
};
_redirector = new YnisonWebSocket();
_state = new YnisonWebSocket();
_redirector = new YnisonWebSocket(_proxy);
_state = new YnisonWebSocket(_proxy);
}
private string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions);
private T Deserialize<T>(YYnisonMessageType messageType, string data)
{
return JsonSerializer.Deserialize<T>(data, _jsonOptions)
=> JsonSerializer.Deserialize<T>(data, _jsonOptions)
?? throw new JsonException("Десериализация вернула null");
}
private T DeserializeMessage<T>(YYnisonMessageType messageType, string data)
{
@@ -120,8 +118,8 @@ public class YnisonPlayer : IDisposable
if (index < 0 || index >= State.PlayerState.PlayerQueue.PlayableList.Count)
return null;
var item = State.PlayerState.PlayerQueue.PlayableList[index];
var response = await API.Track.GetAsync(_storage, item.PlayableId);
return response?.Result?.FirstOrDefault();
var response = await API.Track.GetAsync(item.PlayableId);
return response;
}
private async Task UpdateStateAsync()

View File

@@ -1,4 +1,5 @@
using System.Net.WebSockets;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
@@ -7,14 +8,15 @@ namespace YandexMusic.API.Common.Ynison;
/// <summary>WebSocket-клиент для взаимодействия с протоколом Ynison.</summary>
public class YnisonWebSocket : IDisposable
{
private readonly ClientWebSocket _socketClient = new();
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;
public bool IsConnected => _socketClient?.State == WebSocketState.Open;
/// <summary>Событие получения сообщения.</summary>
public event EventHandler<ReceiveEventArgs>? OnReceive;
@@ -25,19 +27,25 @@ public class YnisonWebSocket : IDisposable
/// <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; }
}
/// <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>
@@ -57,6 +65,9 @@ public class YnisonWebSocket : IDisposable
private async Task<string> ReadSocketContentAsync()
{
if (_socketClient == null)
throw new InvalidOperationException("WebSocket не инициализирован");
var buffer = new byte[BufferSize];
WebSocketReceiveResult result;
do
@@ -68,17 +79,19 @@ public class YnisonWebSocket : IDisposable
}
/// <summary>Подключается к WebSocket.</summary>
/// <param name="storage">Хранилище авторизации.</param>
/// <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}");
_socketClient.Options.Proxy = storage.Context.WebProxy;
if (_proxy != null)
_socketClient.Options.Proxy = _proxy;
_cancellationTokenSource = new CancellationTokenSource();
_cancellationToken = _cancellationTokenSource.Token;
@@ -89,7 +102,7 @@ public class YnisonWebSocket : IDisposable
/// <summary>Начинает асинхронный приём сообщений.</summary>
public async Task BeginReceiveAsync()
{
if (_socketClient.State != WebSocketState.Open)
if (_socketClient == null || _socketClient.State != WebSocketState.Open)
return;
try
@@ -116,9 +129,11 @@ public class YnisonWebSocket : IDisposable
}
/// <summary>Отправляет JSON-сообщение.</summary>
/// <param name="json">JSON-строка.</param>
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);
}
@@ -127,16 +142,14 @@ public class YnisonWebSocket : IDisposable
public async Task StopReceiveAsync()
{
if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
{
await _cancellationTokenSource.CancelAsync();
}
}
/// <summary>Освобождает ресурсы.</summary>
public void Dispose()
{
_cancellationTokenSource?.Dispose();
_socketClient.Dispose();
_socketClient?.Dispose();
GC.SuppressFinalize(this);
}
}