181 lines
7.1 KiB
C#
181 lines
7.1 KiB
C#
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);
|
||
}
|
||
} |