Обнновлено до .net10

This commit is contained in:
FrigaT
2026-04-10 15:05:32 +03:00
parent 11d0b0d72f
commit 8444fc5f8e
386 changed files with 6361 additions and 7164 deletions

View File

@@ -1,103 +1,70 @@
using System.Net;
using YandexMusic.API.Common.Debug;
using YandexMusic.API.Common.Providers;
using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Common;
namespace YandexMusic.API.Common
namespace YandexMusic.API.Common;
/// <summary>
/// Хранилище данных пользователя
/// </summary>
public class AuthStorage
{
/// <summary>
/// Хранилище данных пользователя
/// Http-контекст
/// </summary>
public class AuthStorage
public HttpContext Context { get; }
/// <summary>
/// Флаг авторизации
/// </summary>
public bool IsAuthorized { get; internal set; }
/// <summary>
/// Идентификатор устройства
/// </summary>
public string DeviceId { get; set; } = "csharp";
/// <summary>
/// Токен авторизации
/// </summary>
public string Token { get; internal set; }
/// <summary>
/// Аккаунт
/// </summary>
public YAccount User { get; set; }
/// <summary>
/// Провайдер запросов
/// </summary>
public IRequestProvider Provider { get; }
/// <summary>
/// Токен доступа
/// </summary>
public YAccessToken AccessToken { get; set; }
internal YAuthToken AuthToken { get; set; }
/// <summary>
/// Конструктор
/// </summary>
public AuthStorage(IRequestProvider provider)
{
#region Свойства
/// <summary>
/// Http-контекст
/// </summary>
public HttpContext Context { get; }
public DebugSettings Debug { get; set; }
/// <summary>
/// Флаг авторизации
/// </summary>
public bool IsAuthorized { get; internal set; }
/// <summary>
/// Идентификатор устройства
/// </summary>
public string DeviceId { get; set; } = "csharp";
/// <summary>
/// Токен авторизации
/// </summary>
public string Token { get; internal set; }
/// <summary>
/// Аккаунт
/// </summary>
public YAccount User { get; set; }
/// <summary>
/// Провайдер запросов
/// </summary>
public IRequestProvider Provider { get; }
/// <summary>
/// Токен доступа
/// </summary>
public YAccessToken AccessToken { get; set; }
internal YAuthToken AuthToken { get; set; }
#endregion Свойства
/// <summary>
/// Конструктор
/// </summary>
public AuthStorage(DebugSettings settings = null)
{
User = new YAccount();
Context = new HttpContext();
Debug = settings;
Provider = new DefaultRequestProvider(this);
if (Debug is { ClearDirectory: true })
{
Debug.Clear();
}
}
/// <summary>
/// Конструктор
/// </summary>
public AuthStorage(IRequestProvider provider, DebugSettings settings = null)
{
User = new YAccount();
Context = new HttpContext();
Debug = settings;
Provider = provider;
if (Debug is { ClearDirectory: true })
{
Debug.Clear();
}
}
/// <summary>
/// Установка прокси для пользователия
/// </summary>
/// <param name="proxy">Прокси</param>
public void SetProxy(IWebProxy proxy)
{
Context.WebProxy = proxy;
}
User = new YAccount();
Context = new HttpContext();
Provider = provider;
}
/// <summary>
/// Установка прокси для пользователия
/// </summary>
/// <param name="proxy">Прокси</param>
public void SetProxy(IWebProxy proxy)
{
Context.WebProxy = proxy;
}
}

View File

@@ -1,44 +1,43 @@
using System.Net;
namespace YandexMusic.API.Common
namespace YandexMusic.API.Common;
/// <summary>
/// Загрузчик файлов по ссылке
/// </summary>
public class DataDownloader
{
/// <summary>
/// Загрузчик файлов по ссылке
/// </summary>
public class DataDownloader
private AuthStorage authStorage;
private async Task<HttpContent> GetResponseContent(string url, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
{
private AuthStorage authStorage;
HttpRequestMessage message = new(new HttpMethod(WebRequestMethods.Http.Get), url);
private async Task<HttpContent> GetResponseContent(string url, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
{
HttpRequestMessage message = new(new HttpMethod(WebRequestMethods.Http.Get), url);
HttpResponseMessage response = await authStorage.Provider.GetWebResponseAsync(message, httpCompletionOption);
return response.Content;
}
HttpResponseMessage response = await authStorage.Provider.GetWebResponseAsync(message, httpCompletionOption);
return response.Content;
}
public async Task<Stream> AsStream(string url, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
{
HttpContent content = await GetResponseContent(url, httpCompletionOption);
return await content.ReadAsStreamAsync();
}
public async Task<Stream> AsStream(string url, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
{
HttpContent content = await GetResponseContent(url, httpCompletionOption);
return await content.ReadAsStreamAsync();
}
public async Task<byte[]> AsBytes(string url)
{
HttpContent content = await GetResponseContent(url);
return await content.ReadAsByteArrayAsync();
}
public async Task<byte[]> AsBytes(string url)
{
HttpContent content = await GetResponseContent(url);
return await content.ReadAsByteArrayAsync();
}
public async Task ToFile(string url, string fileName)
{
using Stream stream = await AsStream(url);
using FileStream fs = File.Create(fileName);
await stream.CopyToAsync(fs);
}
public async Task ToFile(string url, string fileName)
{
using Stream stream = await AsStream(url);
using FileStream fs = File.Create(fileName);
await stream.CopyToAsync(fs);
}
public DataDownloader(AuthStorage storage)
{
authStorage = storage;
}
public DataDownloader(AuthStorage storage)
{
authStorage = storage;
}
}

View File

@@ -1,75 +1,63 @@
using System.Security.Cryptography;
using System.Text;
namespace YandexMusic.API.Common
namespace YandexMusic.API.Common;
/// <summary>
/// Класс для шифровки
/// </summary>
public class Encryptor
{
/// <summary>
/// Класс для шифровки
/// </summary>
public class Encryptor
private readonly string IV = "encryption";
private readonly byte[] IVHash;
private readonly byte[] keyHash;
private readonly MD5 md5;
private readonly Aes aesAlg;
private byte[] GetHash(string value)
{
#region Поля
private readonly string IV = "encryption";
private readonly byte[] IVHash;
private readonly byte[] keyHash;
private readonly MD5 md5;
private readonly Aes aesAlg;
#endregion Поля
#region Вспомогательные функции
private byte[] GetHash(string value)
{
return md5.ComputeHash(Encoding.UTF8.GetBytes(value));
}
#endregion Вспомогательные функции
public Encryptor(string key)
{
md5 = MD5.Create();
aesAlg = Aes.Create();
aesAlg.BlockSize = 128;
aesAlg.Padding = PaddingMode.PKCS7;
keyHash = GetHash(key);
IVHash = GetHash(IV);
}
public byte[] Encrypt(byte[] data)
{
using MemoryStream ms = new();
using CryptoStream csEncrypt = new(ms, aesAlg.CreateEncryptor(keyHash, IVHash), CryptoStreamMode.Write);
csEncrypt.Write(data, 0, data.Length);
if (!csEncrypt.HasFlushedFinalBlock)
csEncrypt.FlushFinalBlock();
return ms.ToArray();
}
public byte[] Decrypt(byte[] data)
{
using MemoryStream ms = new();
using CryptoStream csDecrypt = new(ms, aesAlg.CreateDecryptor(keyHash, IVHash), CryptoStreamMode.Write);
csDecrypt.Write(data, 0, data.Length);
if (!csDecrypt.HasFlushedFinalBlock)
csDecrypt.FlushFinalBlock();
return ms.ToArray();
}
return md5.ComputeHash(Encoding.UTF8.GetBytes(value));
}
public Encryptor(string key)
{
md5 = MD5.Create();
aesAlg = Aes.Create();
aesAlg.BlockSize = 128;
aesAlg.Padding = PaddingMode.PKCS7;
keyHash = GetHash(key);
IVHash = GetHash(IV);
}
public byte[] Encrypt(byte[] data)
{
using MemoryStream ms = new();
using CryptoStream csEncrypt = new(ms, aesAlg.CreateEncryptor(keyHash, IVHash), CryptoStreamMode.Write);
csEncrypt.Write(data, 0, data.Length);
if (!csEncrypt.HasFlushedFinalBlock)
csEncrypt.FlushFinalBlock();
return ms.ToArray();
}
public byte[] Decrypt(byte[] data)
{
using MemoryStream ms = new();
using CryptoStream csDecrypt = new(ms, aesAlg.CreateDecryptor(keyHash, IVHash), CryptoStreamMode.Write);
csDecrypt.Write(data, 0, data.Length);
if (!csDecrypt.HasFlushedFinalBlock)
csDecrypt.FlushFinalBlock();
return ms.ToArray();
}
}

View File

@@ -1,60 +1,56 @@
using YandexMusic.API.Models.Common;
using System.Text.Json;
using System.Text.Json.Serialization;
using YandexMusic.API.Models.Common;
namespace YandexMusic.API.Common.Providers
namespace YandexMusic.API.Common.Providers;
/// <summary>Базовый провайдер HTTP-запросов с общей логикой десериализации.</summary>
public abstract class CommonRequestProvider : IRequestProvider
{
public class CommonRequestProvider : IRequestProvider
/// <summary>Хранилище данных авторизации.</summary>
protected readonly AuthStorage storage;
/// <summary>Настройки сериализации JSON (регистронезависимые, поддержка enum-строк).</summary>
private static readonly JsonSerializerOptions JsonOptions = new()
{
#region Поля
PropertyNameCaseInsensitive = true,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};
protected AuthStorage storage;
/// <summary>Инициализирует новый экземпляр провайдера.</summary>
/// <param name="authStorage">Хранилище авторизации.</param>
protected CommonRequestProvider(AuthStorage authStorage)
{
storage = authStorage;
}
#endregion Поля
/// <summary>Выполняет HTTP-запрос и возвращает ответ.</summary>
public abstract Task<HttpResponseMessage> GetWebResponseAsync(
HttpRequestMessage message,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead);
/// <summary>Преобразует HTTP-ответ в объект типа T.</summary>
public virtual async Task<T> GetDataFromResponseAsync<T>(
YandexMusicApi api,
HttpResponseMessage response)
{
var json = await response.Content.ReadAsStringAsync();
public CommonRequestProvider(AuthStorage authStorage)
if (!response.IsSuccessStatusCode)
{
storage = authStorage;
var error = JsonSerializer.Deserialize<YErrorResponse>(json, JsonOptions);
throw error ?? new Exception("Ошибка десериализации ответа с ошибкой.");
}
#region IRequestProvider
public virtual Task<HttpResponseMessage> GetWebResponseAsync(HttpRequestMessage message, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
try
{
throw new NotImplementedException();
// Если нужен контекст выполнения, он добавляется через кастомный конвертер
return JsonSerializer.Deserialize<T>(json, JsonOptions)
?? throw new JsonException("Десериализация вернула null");
}
public virtual async Task<T> GetDataFromResponseAsync<T>(YandexMusicApi api, HttpResponseMessage response)
catch (Exception ex)
{
string result = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
YErrorResponse exception = JsonConvert.DeserializeObject<YErrorResponse>(result);
throw exception ?? new Exception("Ошибка десериализации ответа с ошибкой.");
}
try
{
JsonSerializerSettings settings = new()
{
Converters = new List<JsonConverter> {
new YExecutionContextConverter(api, storage)
}
};
return storage.Debug != null
? storage.Debug.Deserialize<T>(response.RequestMessage?.RequestUri?.AbsolutePath, result, settings)
: JsonConvert.DeserializeObject<T>(result, settings);
}
catch (Exception ex)
{
throw new Exception($"Ошибка десериализации {ex}");
}
throw new Exception($"Ошибка десериализации: {ex.Message}", ex);
}
#endregion IRequestProvider
}
}

View File

@@ -1,69 +1,45 @@
using System.Net;
using YandexMusic.API.Models.Common;
namespace YandexMusic.API.Common.Providers;
namespace YandexMusic.API.Common.Providers
/// <summary>Стандартный провайдер HTTP-запросов с использованием HttpClient.</summary>
public class DefaultRequestProvider : CommonRequestProvider
{
/// <summary>
/// Стандартный провайдер запросов
/// </summary>
public class DefaultRequestProvider : CommonRequestProvider
/// <summary>Инициализирует новый экземпляр провайдера.</summary>
/// <param name="authStorage">Хранилище авторизации.</param>
public DefaultRequestProvider(AuthStorage authStorage) : base(authStorage) { }
/// <summary>Выполняет HTTP-запрос и возвращает ответ.</summary>
/// <param name="message">HTTP-запрос.</param>
/// <param name="completionOption">Опция завершения запроса.</param>
/// <returns>HTTP-ответ.</returns>
public override async Task<HttpResponseMessage> GetWebResponseAsync(
HttpRequestMessage message,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
#region Вспомогательные функции
private Exception ProcessException(Exception ex)
using var handler = new SocketsHttpHandler
{
if (ex is not WebException webException)
return ex;
Proxy = storage.Context.WebProxy,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
UseCookies = true,
CookieContainer = storage.Context.Cookies,
AllowAutoRedirect = true,
MaxAutomaticRedirections = 10
};
if (webException.Response is null)
return ex;
using var client = new HttpClient(handler);
Stream s = webException.Response.GetResponseStream();
if (s is null)
return ex;
using StreamReader sr = new(s);
string result = sr.ReadToEnd();
YErrorResponse exception = JsonConvert.DeserializeObject<YErrorResponse>(result);
return exception ?? ex;
}
#endregion Вспомогательные функции
public DefaultRequestProvider(AuthStorage authStorage) : base(authStorage)
try
{
return await client.SendAsync(message, completionOption);
}
#region IRequestProvider
public override Task<HttpResponseMessage> GetWebResponseAsync(HttpRequestMessage message,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
catch (HttpRequestException ex)
{
try
{
HttpClient client = new(new SocketsHttpHandler
{
Proxy = storage.Context.WebProxy,
AutomaticDecompression = DecompressionMethods.GZip,
UseCookies = true,
CookieContainer = storage.Context.Cookies,
});
// Пытаемся извлечь тело ошибки, если оно доступно
if (ex.InnerException == null)
throw;
return client.SendAsync(message, completionOption);
}
catch (Exception ex)
{
throw ProcessException(ex);
}
throw new Exception($"Ошибка HTTP-запроса: {ex.Message}", ex);
}
#endregion IRequestProvider
}
}

View File

@@ -9,7 +9,6 @@
public MockRequestProvider(AuthStorage authStorage) : base(authStorage)
{
storage = authStorage;
}

View File

@@ -0,0 +1,43 @@
using System.Text;
using System.Text.Json;
namespace YandexMusic.API.Common.Ynison;
/// <summary>Политика именования в формате UPPER_SNAKE_CASE (все буквы верхнего регистра, слова через подчёркивание).</summary>
public class UpperSnakeCaseNamingPolicy : SnakeCaseNamingPolicy
{
/// <summary>Преобразует имя свойства в формат UPPER_SNAKE_CASE.</summary>
public override string ConvertName(string name)
{
var snakeCase = base.ConvertName(name);
return snakeCase.ToUpperInvariant();
}
}
/// <summary>Базовая политика именования в формате snake_case (все буквы нижнего регистра, слова через подчёркивание).</summary>
public class SnakeCaseNamingPolicy : JsonNamingPolicy
{
/// <summary>Преобразует имя свойства в формат snake_case.</summary>
public override string ConvertName(string name)
{
if (string.IsNullOrEmpty(name))
return name;
var sb = new StringBuilder();
for (int i = 0; i < name.Length; i++)
{
char c = name[i];
if (char.IsUpper(c))
{
if (i > 0)
sb.Append('_');
sb.Append(char.ToLowerInvariant(c));
}
else
{
sb.Append(c);
}
}
return sb.ToString();
}
}

View File

@@ -1,7 +0,0 @@
namespace YandexMusic.API.Common.Ynison
{
public class UpperSnakeCaseNamingStrategy : SnakeCaseNamingStrategy
{
protected override string ResolvePropertyName(string name) => base.ResolvePropertyName(name).ToUpper();
}
}

View File

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

View File

@@ -1,179 +1,142 @@
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
namespace YandexMusic.API.Common.Ynison
namespace YandexMusic.API.Common.Ynison;
/// <summary>WebSocket-клиент для взаимодействия с протоколом Ynison.</summary>
public class YnisonWebSocket : IDisposable
{
public class YnisonWebSocket : IDisposable
private readonly ClientWebSocket _socketClient = new();
private CancellationTokenSource? _cancellationTokenSource;
private CancellationToken _cancellationToken;
private readonly StringBuilder _data = new();
private const int BufferSize = 4096;
/// <summary>Флаг, указывает, открыто ли соединение.</summary>
public bool IsConnected => _socketClient.State == WebSocketState.Open;
/// <summary>Событие получения сообщения.</summary>
public event EventHandler<ReceiveEventArgs>? OnReceive;
/// <summary>Событие закрытия соединения.</summary>
public event EventHandler<CloseEventArgs>? OnClose;
/// <summary>Аргументы события получения данных.</summary>
public class ReceiveEventArgs : EventArgs
{
#region Поля
/// <summary>Полученные данные (JSON-строка).</summary>
public string Data { 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; }
}
private static string GetProtocolData(string deviceId, string? redirectTicket)
{
var deviceInfo = new Dictionary<string, object>
{
Converters = new List<JsonConverter> {
new StringEnumConverter {
NamingStrategy = new CamelCaseNamingStrategy()
}
},
NullValueHandling = NullValueHandling.Ignore
{ "app_name", "Chrome" },
{ "type", 1 }
};
private readonly ClientWebSocket socketClient = new();
private CancellationTokenSource cancellationTokenSource = new();
private CancellationToken cancellation;
private readonly StringBuilder data = new();
private readonly int size = 4096;
#endregion Поля
#region Свойства
public bool IsConnected => socketClient.State == WebSocketState.Open;
#endregion Свойства
#region События
public class ReceiveEventArgs
var protocol = new Dictionary<string, string>
{
public string Data { get; internal set; }
}
{ "Ynison-Device-Id", deviceId },
{ "Ynison-Device-Info", JsonSerializer.Serialize(deviceInfo) }
};
if (!string.IsNullOrEmpty(redirectTicket))
protocol.Add("Ynison-Redirect-Ticket", redirectTicket);
return JsonSerializer.Serialize(protocol);
}
public delegate void OnReceiveEventHandler(YnisonWebSocket socket, ReceiveEventArgs args);
/// <summary>
/// Получение данных
/// </summary>
public event OnReceiveEventHandler OnReceive;
public class CloseEventArgs
private async Task<string> ReadSocketContentAsync()
{
var buffer = new byte[BufferSize];
WebSocketReceiveResult result;
do
{
public WebSocketCloseStatus? Status { get; set; }
public string Description { get; set; }
}
result = await _socketClient.ReceiveAsync(new ArraySegment<byte>(buffer), _cancellationToken);
_data.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
} while (!result.EndOfMessage);
return _data.ToString();
}
public delegate void OnCloseEventHandler(YnisonWebSocket socket, CloseEventArgs args);
/// <summary>
/// Закрытие соединения
/// </summary>
public event OnCloseEventHandler OnClose;
/// <summary>Подключается к WebSocket.</summary>
/// <param name="storage">Хранилище авторизации.</param>
/// <param name="url">URL WebSocket.</param>
/// <param name="redirectTicket">Тикет перенаправления (опционально).</param>
public async Task ConnectAsync(AuthStorage storage, string url, string? redirectTicket = null)
{
_socketClient.Options.AddSubProtocol("Bearer");
var protocolData = GetProtocolData(storage.DeviceId, redirectTicket);
_socketClient.Options.SetRequestHeader("Sec-WebSocket-Protocol", $"Bearer, v2, {protocolData}");
_socketClient.Options.SetRequestHeader("Origin", "https://music.yandex.ru");
_socketClient.Options.SetRequestHeader("Authorization", $"OAuth {storage.Token}");
_socketClient.Options.Proxy = storage.Context.WebProxy;
#endregion События
_cancellationTokenSource = new CancellationTokenSource();
_cancellationToken = _cancellationTokenSource.Token;
#region Вспомогательные функции
await _socketClient.ConnectAsync(new Uri(url), CancellationToken.None);
}
private string SerializeJson(object obj)
/// <summary>Начинает асинхронный приём сообщений.</summary>
public async Task BeginReceiveAsync()
{
if (_socketClient.State != WebSocketState.Open)
return;
try
{
return JsonConvert.SerializeObject(obj, jsonSettings);
}
private string GetProtocolData(string deviceId, string redirectTicket)
{
Dictionary<string, object> deviceInfo = new() {
{ "app_name", "Chrome" },
{ "type", 1 }
};
Dictionary<string, string> protocol = new() {
{ "Ynison-Device-Id", deviceId },
{ "Ynison-Device-Info", SerializeJson(deviceInfo) }
};
if (!string.IsNullOrEmpty(redirectTicket))
protocol.Add("Ynison-Redirect-Ticket", redirectTicket);
return SerializeJson(protocol);
}
private async Task<string> ReadSocketContent()
{
byte[] buffer = new byte[size];
WebSocketReceiveResult result;
do
while (!_cancellationToken.IsCancellationRequested && _socketClient.State == WebSocketState.Open)
{
result = await socketClient.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
data.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
} while (!result.EndOfMessage);
return data.ToString();
var content = await ReadSocketContentAsync();
OnReceive?.Invoke(this, new ReceiveEventArgs { Data = content });
_data.Clear();
}
}
#endregion Вспомогательные функции
public bool Connect(AuthStorage storage, string url, string redirectTicket = null)
catch (OperationCanceledException)
{
socketClient.Options.AddSubProtocol("Bearer");
socketClient.Options.SetRequestHeader("Sec-WebSocket-Protocol", $"Bearer, v2, {GetProtocolData(storage.DeviceId, redirectTicket)}");
socketClient.Options.SetRequestHeader("Origin", "https://music.yandex.ru");
socketClient.Options.SetRequestHeader("Authorization", $"OAuth {storage.Token}");
socketClient.Options.Proxy = storage.Context.WebProxy;
socketClient.ConnectAsync(new Uri(url), CancellationToken.None)
.GetAwaiter()
.GetResult();
cancellation = cancellationTokenSource.Token;
return socketClient.State == WebSocketState.Open;
// Ожидаемая отмена
}
public async Task BeginReceive()
finally
{
if (socketClient.State != WebSocketState.Open)
return;
do
{
string content = await ReadSocketContent();
OnReceive?.Invoke(this, new ReceiveEventArgs
{
Data = content
});
data.Clear();
} while (!cancellation.IsCancellationRequested && socketClient.State == WebSocketState.Open);
OnClose?.Invoke(this, new CloseEventArgs
{
Status = socketClient.CloseStatus,
Description = socketClient.CloseStatusDescription
});
await socketClient.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
var closeStatus = _socketClient.CloseStatus;
var closeDesc = _socketClient.CloseStatusDescription;
OnClose?.Invoke(this, new CloseEventArgs { Status = closeStatus, Description = closeDesc });
if (_socketClient.State == WebSocketState.Open)
await _socketClient.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
}
}
/// <summary>Отправляет JSON-сообщение.</summary>
/// <param name="json">JSON-строка.</param>
public async ValueTask SendAsync(string json)
{
var bytes = Encoding.UTF8.GetBytes(json);
await _socketClient.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, _cancellationToken);
}
public ValueTask Send(string json)
/// <summary>Останавливает приём сообщений.</summary>
public async Task StopReceiveAsync()
{
if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
{
ReadOnlyMemory<byte> message = new(Encoding.UTF8.GetBytes(json));
return socketClient.SendAsync(message, WebSocketMessageType.Text, false, CancellationToken.None);
await _cancellationTokenSource.CancelAsync();
}
}
public Task StopReceive()
{
if (socketClient.State != WebSocketState.Open)
return Task.CompletedTask;
cancellationTokenSource.Cancel(false);
return Task.CompletedTask;
}
#region IDisposable
public void Dispose()
{
socketClient?.Dispose();
cancellationTokenSource?.Dispose();
}
#endregion IDisposable
/// <summary>Освобождает ресурсы.</summary>
public void Dispose()
{
_cancellationTokenSource?.Dispose();
_socketClient.Dispose();
GC.SuppressFinalize(this);
}
}