using System.Net;
using System.Net.WebSockets;
using System.Text.Json;
using System.Text.Json.Serialization;
using YandexMusic.API.Models.Track;
using YandexMusic.API.Models.Ynison;
using YandexMusic.API.Models.Ynison.Messages;
namespace YandexMusic.API.Common.Ynison;
/// Плеер для управления воспроизведением через протокол Ynison (WebSocket).
public class YnisonPlayer : IDisposable
{
private readonly JsonSerializerOptions _jsonOptions;
private readonly AuthStorage _storage;
private readonly IWebProxy? _proxy;
private YnisonWebSocket? _redirector;
private YnisonWebSocket? _state;
/// API Яндекс Музыки.
public YandexMusicApi API { get; }
/// Текущее состояние плеера.
public YYnisonState? State { get; private set; }
/// Текущий проигрываемый трек.
public YTrack? Current => GetCurrentAsync().GetAwaiter().GetResult();
/// Событие получения нового состояния.
public event EventHandler? OnReceive;
/// Событие закрытия соединения.
public event EventHandler? OnClose;
/// Аргументы события получения состояния.
public class ReceiveEventArgs : EventArgs
{
public YYnisonState State { get; init; } = null!;
}
/// Аргументы события закрытия соединения.
public class CloseEventArgs : EventArgs
{
public WebSocketCloseStatus? Status { get; init; }
public string? Description { get; init; }
}
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(_proxy);
_state = new YnisonWebSocket(_proxy);
}
private string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions);
private T Deserialize(YYnisonMessageType messageType, string data)
=> JsonSerializer.Deserialize(data, _jsonOptions)
?? throw new JsonException("Десериализация вернула null");
private T DeserializeMessage(YYnisonMessageType messageType, string data)
{
using var doc = JsonDocument.Parse(data);
if (doc.RootElement.TryGetProperty("error", out _))
{
var error = Deserialize(YYnisonMessageType.Error, data);
throw error ?? new Exception("Ошибка десериализации ответа с ошибкой.");
}
return Deserialize(messageType, data);
}
private string DefaultState()
{
var version = new YYnisonVersion
{
DeviceId = _storage.DeviceId,
Version = "0"
};
var fullState = new YYnisonUpdateFullStateMessage
{
UpdateFullState = new YYnisonFullState
{
Device = new YYnisonDevice
{
Capabilities = new YYnisonDeviceCapabilities { CanBePlayer = true },
Info = new YYnisonDeviceInfo
{
DeviceId = _storage.DeviceId,
AppName = "Yandex Music API",
AppVersion = "0.0.1",
Type = "WEB",
Title = "YandexMusicAPI"
},
IsShadow = true
},
PlayerState = new YYnisonPlayerState
{
PlayerQueue = new YYnisonPlayerQueue { Version = version },
Status = new YYnisonPlayerStateStatus { Version = version }
}
}
};
return SerializeJson(fullState);
}
private async Task GetCurrentAsync()
{
if (State == null) return null;
int index = State.PlayerState.PlayerQueue.CurrentPlayableIndex;
if (index < 0 || index >= State.PlayerState.PlayerQueue.PlayableList.Count)
return null;
var item = State.PlayerState.PlayerQueue.PlayableList[index];
var response = await API.Track.GetAsync(item.PlayableId);
return response;
}
private async Task UpdateStateAsync()
{
if (State == null) return;
var update = new YYnisonUpdatePlayerStateMessage
{
UpdatePlayerState = State.PlayerState
};
update.UpdatePlayerState.Status.Version = new YYnisonVersion { DeviceId = _storage.DeviceId };
update.UpdatePlayerState.PlayerQueue.Version = new YYnisonVersion { DeviceId = _storage.DeviceId };
if (_state != null)
await _state.SendAsync(SerializeJson(update));
}
/// Подключается к Ynison и начинает получение состояния.
public async Task ConnectAsync()
{
if (_redirector == null) throw new ObjectDisposedException(nameof(YnisonPlayer));
await _redirector.ConnectAsync(_storage, "wss://ynison.music.yandex.ru/redirector.YnisonRedirectService/GetRedirectToYnison");
_redirector.OnReceive += async (socket, data) =>
{
var redirectInfo = Deserialize(YYnisonMessageType.Redirect, data.Data);
if (_state == null) return;
if (_state.IsConnected) return;
await _state.ConnectAsync(_storage, $"wss://{redirectInfo.Host}/ynison_state.YnisonStateService/PutYnisonState", redirectInfo.RedirectTicket);
_state.OnReceive += (s, d) =>
{
var message = DeserializeMessage(YYnisonMessageType.State, d.Data);
State = message;
OnReceive?.Invoke(this, new ReceiveEventArgs { State = State });
};
_state.OnClose += (s, args) =>
{
OnClose?.Invoke(this, new CloseEventArgs { Status = args.Status, Description = args.Description });
};
_ = _state.BeginReceiveAsync();
await _state.SendAsync(DefaultState());
};
await _redirector.BeginReceiveAsync();
}
/// Отключается от Ynison.
public async Task DisconnectAsync()
{
if (_state != null) await _state.StopReceiveAsync();
if (_redirector != null) await _redirector.StopReceiveAsync();
}
/// Освобождает ресурсы.
public void Dispose()
{
_redirector?.Dispose();
_state?.Dispose();
_redirector = null;
_state = null;
GC.SuppressFinalize(this);
}
}