Обнновлено до .net10
This commit is contained in:
@@ -1,315 +1,183 @@
|
||||
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
|
||||
namespace YandexMusic.API.Common.Ynison;
|
||||
|
||||
/// <summary>Плеер для управления воспроизведением через протокол Ynison (WebSocket).</summary>
|
||||
public class YnisonPlayer : IDisposable
|
||||
{
|
||||
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
|
||||
{
|
||||
#region Поля
|
||||
/// <summary>Состояние плеера.</summary>
|
||||
public YYnisonState State { get; init; } = null!;
|
||||
}
|
||||
|
||||
private readonly JsonSerializerSettings jsonSettings = new()
|
||||
/// <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
|
||||
{
|
||||
Converters = new List<JsonConverter> {
|
||||
new StringEnumConverter(new UpperSnakeCaseNamingStrategy())
|
||||
},
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter(new UpperSnakeCaseNamingPolicy(), false) }
|
||||
};
|
||||
_redirector = new YnisonWebSocket();
|
||||
_state = new YnisonWebSocket();
|
||||
}
|
||||
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
ContractResolver = new DefaultContractResolver
|
||||
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
|
||||
{
|
||||
// Важно! Унисон отдаёт данные в SnakeCase
|
||||
NamingStrategy = new SnakeCaseNamingStrategy()
|
||||
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 AuthStorage storage;
|
||||
private YnisonWebSocket redirector;
|
||||
private YnisonWebSocket state;
|
||||
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();
|
||||
}
|
||||
|
||||
#endregion Поля
|
||||
|
||||
#region Свойства
|
||||
|
||||
/// <summary>
|
||||
/// API
|
||||
/// </summary>
|
||||
public YandexMusicApi API { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Состояние
|
||||
/// </summary>
|
||||
public YYnisonState State { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Текущий проигрываемый трек
|
||||
/// </summary>
|
||||
public YTrack Current => GetCurrent();
|
||||
|
||||
#endregion Свойства
|
||||
|
||||
#region События
|
||||
|
||||
public class ReceiveEventArgs
|
||||
private async Task UpdateStateAsync()
|
||||
{
|
||||
if (State == null) return;
|
||||
var update = new YYnisonUpdatePlayerStateMessage
|
||||
{
|
||||
public YYnisonState State { get; internal set; }
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
public delegate void OnReceiveEventHandler(YnisonPlayer player, ReceiveEventArgs args);
|
||||
|
||||
/// <summary>
|
||||
/// Получение данных
|
||||
/// </summary>
|
||||
public event OnReceiveEventHandler OnReceive;
|
||||
|
||||
|
||||
public class CloseEventArgs
|
||||
/// <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) =>
|
||||
{
|
||||
public WebSocketCloseStatus? Status { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
|
||||
public delegate void OnCloseEventHandler(YnisonPlayer player, CloseEventArgs args);
|
||||
|
||||
/// <summary>
|
||||
/// Получение данных
|
||||
/// </summary>
|
||||
public event OnCloseEventHandler OnClose;
|
||||
|
||||
#endregion События
|
||||
|
||||
#region Вспомогательные функции
|
||||
|
||||
private string SerializeJson(object data)
|
||||
{
|
||||
return JsonConvert.SerializeObject(data, jsonSettings);
|
||||
}
|
||||
|
||||
private T Deserialize<T>(YYnisonMessageType messageType, string data)
|
||||
{
|
||||
return storage.Debug != null
|
||||
? storage.Debug.Deserialize<T>($"Ynison{messageType}", data, jsonSettings)
|
||||
: JsonConvert.DeserializeObject<T>(data, jsonSettings);
|
||||
}
|
||||
|
||||
private T DeserializeMessage<T>(YYnisonMessageType messageType, string data)
|
||||
{
|
||||
JObject o = JObject.Parse(data);
|
||||
// Сообщение с ошибкой
|
||||
if (o.ContainsKey("error"))
|
||||
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) =>
|
||||
{
|
||||
YYnisonErrorMessage exception = Deserialize<YYnisonErrorMessage>(YYnisonMessageType.Error, data);
|
||||
throw exception ?? new Exception("Ошибка десериализации ответа с ошибкой.");
|
||||
}
|
||||
|
||||
return Deserialize<T>(messageType, data);
|
||||
}
|
||||
|
||||
private string DefaultState()
|
||||
{
|
||||
YYnisonVersion version = new()
|
||||
{
|
||||
DeviceId = storage.DeviceId,
|
||||
Version = "0"
|
||||
var message = DeserializeMessage<YYnisonState>(YYnisonMessageType.State, d.Data);
|
||||
State = message;
|
||||
OnReceive?.Invoke(this, new ReceiveEventArgs { State = State });
|
||||
};
|
||||
|
||||
YYnisonUpdateFullStateMessage fullState = new()
|
||||
_state.OnClose += (s, args) =>
|
||||
{
|
||||
UpdateFullState = new()
|
||||
{
|
||||
Device = new()
|
||||
{
|
||||
Capabilities = new()
|
||||
{
|
||||
CanBePlayer = true
|
||||
},
|
||||
Info = new()
|
||||
{
|
||||
DeviceId = storage.DeviceId,
|
||||
AppName = "Yandex Music API",
|
||||
AppVersion = "0.0.1",
|
||||
Type = "WEB",
|
||||
Title = "YandexMusicAPI"
|
||||
},
|
||||
IsShadow = true
|
||||
},
|
||||
PlayerState = new()
|
||||
{
|
||||
PlayerQueue = new()
|
||||
{
|
||||
Version = version
|
||||
},
|
||||
Status = new()
|
||||
{
|
||||
Version = version
|
||||
}
|
||||
}
|
||||
}
|
||||
OnClose?.Invoke(this, new CloseEventArgs { Status = args.Status, Description = args.Description });
|
||||
};
|
||||
_ = _state.BeginReceiveAsync();
|
||||
await _state.SendAsync(DefaultState());
|
||||
};
|
||||
await _redirector.BeginReceiveAsync();
|
||||
}
|
||||
|
||||
return SerializeJson(fullState);
|
||||
}
|
||||
/// <summary>Отключается от Ynison.</summary>
|
||||
public async Task DisconnectAsync()
|
||||
{
|
||||
if (_state != null) await _state.StopReceiveAsync();
|
||||
if (_redirector != null) await _redirector.StopReceiveAsync();
|
||||
}
|
||||
|
||||
private YTrack GetCurrent()
|
||||
{
|
||||
if (State == null)
|
||||
return null;
|
||||
|
||||
int index = State.PlayerState.PlayerQueue.CurrentPlayableIndex;
|
||||
if (index < 0 || index > State.PlayerState.PlayerQueue.PlayableList.Count)
|
||||
return null;
|
||||
|
||||
YYnisonPlayableItem item = State.PlayerState.PlayerQueue.PlayableList[index];
|
||||
|
||||
return API.Track.Get(storage, item.PlayableId)
|
||||
.Result
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private void UpdateState()
|
||||
{
|
||||
YYnisonUpdatePlayerStateMessage update = new()
|
||||
{
|
||||
UpdatePlayerState = State.PlayerState
|
||||
};
|
||||
|
||||
update.UpdatePlayerState.Status.Version = new()
|
||||
{
|
||||
DeviceId = storage.DeviceId
|
||||
};
|
||||
|
||||
update.UpdatePlayerState.PlayerQueue.Version = new()
|
||||
{
|
||||
DeviceId = storage.DeviceId
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
state.Send(SerializeJson(update));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Вспомогательные функции
|
||||
|
||||
|
||||
|
||||
#region Подключение
|
||||
|
||||
public void Connect()
|
||||
{
|
||||
redirector.Connect(storage, "wss://ynison.music.yandex.ru/redirector.YnisonRedirectService/GetRedirectToYnison");
|
||||
redirector.OnReceive += (socket, data) =>
|
||||
{
|
||||
YYnisonRedirect redirectInfo = Deserialize<YYnisonRedirect>(YYnisonMessageType.Redirect, data.Data);
|
||||
|
||||
if (state.IsConnected)
|
||||
return;
|
||||
|
||||
state.Connect(storage, $"wss://{redirectInfo.Host}/ynison_state.YnisonStateService/PutYnisonState", redirectInfo.RedirectTicket);
|
||||
state.OnReceive += (s, d) =>
|
||||
{
|
||||
YYnisonState 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.BeginReceive();
|
||||
// Отправка изначального состояния
|
||||
state.Send(DefaultState());
|
||||
};
|
||||
|
||||
redirector.BeginReceive();
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
state?.StopReceive();
|
||||
redirector?.StopReceive();
|
||||
}
|
||||
|
||||
#endregion Подключение
|
||||
|
||||
#region Плеер
|
||||
|
||||
/*
|
||||
public void Play()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void Next()
|
||||
{
|
||||
List<YYnisonPlayableItem> list = State.PlayerState.PlayerQueue.PlayableList;
|
||||
|
||||
if (State.PlayerState.PlayerQueue.EntityType == YYnisonEntityType.Radio)
|
||||
{
|
||||
YYnisonPlayableItem next = State.PlayerState.PlayerQueue.Queue.WaveQueue.RecommendedPlayableList
|
||||
.FirstOrDefault();
|
||||
|
||||
list.RemoveAt(0);
|
||||
list.Add(next);
|
||||
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
if (State.PlayerState.PlayerQueue.CurrentPlayableIndex < list.Count - 1)
|
||||
{
|
||||
State.PlayerState.PlayerQueue.CurrentPlayableIndex++;
|
||||
UpdateState();
|
||||
}
|
||||
}
|
||||
|
||||
public void Previous()
|
||||
{
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
#endregion Плеер
|
||||
|
||||
internal YnisonPlayer(YandexMusicApi api, AuthStorage authStorage)
|
||||
{
|
||||
API = api;
|
||||
storage = authStorage;
|
||||
|
||||
redirector = new();
|
||||
state = new();
|
||||
}
|
||||
|
||||
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
redirector?.StopReceive();
|
||||
redirector?.Dispose();
|
||||
}
|
||||
|
||||
#endregion IDisposable
|
||||
/// <summary>Освобождает ресурсы.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_redirector?.Dispose();
|
||||
_state?.Dispose();
|
||||
_redirector = null;
|
||||
_state = null;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user