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); } }