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

181 lines
7.1 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.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 readonly IWebProxy? _proxy;
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
{
public YYnisonState State { get; init; } = null!;
}
/// <summary>Аргументы события закрытия соединения.</summary>
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<T>(YYnisonMessageType messageType, string data)
=> 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(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));
}
/// <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);
}
}