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

183 lines
7.3 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.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;
/// <summary>Плеер для управления воспроизведением через протокол Ynison (WebSocket).</summary>
public class YnisonPlayer : IDisposable
{
private readonly JsonSerializerOptions _jsonOptions;
private readonly AuthStorage _storage;
private YnisonWebSocket? _redirector;
private YnisonWebSocket? _state;
/// <summary>API Яндекс Музыки.</summary>
public YandexMusicApi API { get; }
/// <summary>Текущее состояние плеера.</summary>
public YYnisonState? State { get; private set; }
/// <summary>Текущий проигрываемый трек.</summary>
public YTrack? Current => GetCurrentAsync().GetAwaiter().GetResult();
/// <summary>Событие получения нового состояния.</summary>
public event EventHandler<ReceiveEventArgs>? OnReceive;
/// <summary>Событие закрытия соединения.</summary>
public event EventHandler<CloseEventArgs>? OnClose;
/// <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)
{
API = api;
_storage = authStorage;
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(new UpperSnakeCaseNamingPolicy(), false) }
};
_redirector = new YnisonWebSocket();
_state = new YnisonWebSocket();
}
private string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions);
private T Deserialize<T>(YYnisonMessageType messageType, string data)
{
return JsonSerializer.Deserialize<T>(data, _jsonOptions)
?? throw new JsonException("Десериализация вернула null");
}
private T DeserializeMessage<T>(YYnisonMessageType messageType, string data)
{
using var doc = JsonDocument.Parse(data);
if (doc.RootElement.TryGetProperty("error", out _))
{
var error = Deserialize<YYnisonErrorMessage>(YYnisonMessageType.Error, data);
throw error ?? new Exception("Ошибка десериализации ответа с ошибкой.");
}
return Deserialize<T>(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<YTrack?> 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(_storage, item.PlayableId);
return response?.Result?.FirstOrDefault();
}
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));
}
/// <summary>Подключается к Ynison и начинает получение состояния.</summary>
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<YYnisonRedirect>(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<YYnisonState>(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();
}
/// <summary>Отключается от Ynison.</summary>
public async Task DisconnectAsync()
{
if (_state != null) await _state.StopReceiveAsync();
if (_redirector != null) await _redirector.StopReceiveAsync();
}
/// <summary>Освобождает ресурсы.</summary>
public void Dispose()
{
_redirector?.Dispose();
_state?.Dispose();
_redirector = null;
_state = null;
GC.SuppressFinalize(this);
}
}