Полностью переписанное api
All checks were successful
Release / pack-and-publish (release) Successful in 36s
All checks were successful
Release / pack-and-publish (release) Successful in 36s
This commit is contained in:
@@ -1,80 +1,61 @@
|
||||
using System.Net;
|
||||
using YandexMusic.API.Common.Providers;
|
||||
using YandexMusic.API.Models.Account;
|
||||
using YandexMusic.API.Requests.Common;
|
||||
|
||||
namespace YandexMusic.API.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Хранилище данных пользователя
|
||||
/// Хранилище данных авторизации. Не содержит HTTP-зависимостей.
|
||||
/// </summary>
|
||||
public class AuthStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Http-контекст
|
||||
/// </summary>
|
||||
public HttpContext Context { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Флаг авторизации
|
||||
/// Флаг, указывающий, авторизован ли пользователь.
|
||||
/// </summary>
|
||||
public bool IsAuthorized { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Идентификатор устройства
|
||||
/// Идентификатор устройства (используется в заголовках).
|
||||
/// </summary>
|
||||
public string DeviceId { get; set; } = "csharp";
|
||||
|
||||
/// <summary>
|
||||
/// Токен авторизации
|
||||
/// OAuth-токен для доступа к API.
|
||||
/// </summary>
|
||||
public string Token { get; internal set; }
|
||||
public string Token { get; internal set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Аккаунт
|
||||
/// Информация об аккаунте пользователя.
|
||||
/// </summary>
|
||||
public YAccount User { get; set; }
|
||||
public YAccount User { get; internal set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Провайдер запросов
|
||||
/// Временный токен доступа (используется в некоторых сценариях авторизации).
|
||||
/// </summary>
|
||||
public IRequestProvider Provider { get; }
|
||||
public YAccessToken AccessToken { get; internal set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Токен доступа
|
||||
/// Внутренние данные авторизации (CSRF, track_id и т.д.).
|
||||
/// </summary>
|
||||
public YAccessToken AccessToken { get; set; }
|
||||
|
||||
internal YAuthToken AuthToken { get; set; }
|
||||
internal YAuthToken AuthToken { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Конструктор
|
||||
/// Устанавливает флаг авторизации и сохраняет информацию об аккаунте.
|
||||
/// </summary>
|
||||
public AuthStorage(IRequestProvider provider)
|
||||
internal void SetAuthorized(YAccount user, string token)
|
||||
{
|
||||
User = user ?? throw new ArgumentNullException(nameof(user));
|
||||
Token = token ?? throw new ArgumentNullException(nameof(token));
|
||||
IsAuthorized = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Сбрасывает состояние авторизации.
|
||||
/// </summary>
|
||||
internal void ResetAuthorization()
|
||||
{
|
||||
User = new YAccount();
|
||||
Context = new HttpContext();
|
||||
Provider = provider;
|
||||
Token = string.Empty;
|
||||
AccessToken = new YAccessToken();
|
||||
AuthToken = new YAuthToken();
|
||||
IsAuthorized = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Конструктор
|
||||
/// </summary>
|
||||
public AuthStorage()
|
||||
{
|
||||
User = new YAccount();
|
||||
Context = new HttpContext();
|
||||
Provider = new DefaultRequestProvider(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Установка прокси для пользователия
|
||||
/// </summary>
|
||||
/// <param name="proxy">Прокси</param>
|
||||
public void SetProxy(IWebProxy proxy)
|
||||
{
|
||||
Context.WebProxy = proxy;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using System.Net;
|
||||
|
||||
namespace YandexMusic.API.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Загрузчик файлов по ссылке
|
||||
/// </summary>
|
||||
public class DataDownloader
|
||||
{
|
||||
private AuthStorage authStorage;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using YandexMusic.API.Converters;
|
||||
using YandexMusic.API.Models.Common;
|
||||
|
||||
namespace YandexMusic.API.Common.Providers;
|
||||
|
||||
/// <summary>Базовый провайдер HTTP-запросов с общей логикой десериализации.</summary>
|
||||
public abstract class CommonRequestProvider : IRequestProvider
|
||||
{
|
||||
/// <summary>Хранилище данных авторизации.</summary>
|
||||
protected readonly AuthStorage storage;
|
||||
|
||||
/// <summary>Инициализирует новый экземпляр провайдера.</summary>
|
||||
/// <param name="authStorage">Хранилище авторизации.</param>
|
||||
protected CommonRequestProvider(AuthStorage authStorage)
|
||||
{
|
||||
storage = authStorage;
|
||||
}
|
||||
|
||||
/// <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();
|
||||
|
||||
JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = {
|
||||
new JsonStringEnumConverter(JsonNamingPolicy.KebabCaseLower),
|
||||
new IntToStringConverter(),
|
||||
new StringToIntConverter(),
|
||||
new YExecutionContextConverter(api, storage),
|
||||
}
|
||||
};
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = JsonSerializer.Deserialize<YErrorResponse>(json, JsonOptions);
|
||||
throw error ?? new Exception("Ошибка десериализации ответа с ошибкой.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Если нужен контекст выполнения, он добавляется через кастомный конвертер
|
||||
return JsonSerializer.Deserialize<T>(json, JsonOptions)
|
||||
?? throw new JsonException("Десериализация вернула null");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Ошибка десериализации: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using System.Net;
|
||||
|
||||
namespace YandexMusic.API.Common.Providers;
|
||||
|
||||
/// <summary>Стандартный провайдер HTTP-запросов с использованием HttpClient.</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)
|
||||
{
|
||||
using var handler = new SocketsHttpHandler
|
||||
{
|
||||
Proxy = storage.Context.WebProxy,
|
||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
|
||||
UseCookies = true,
|
||||
CookieContainer = storage.Context.Cookies,
|
||||
AllowAutoRedirect = true,
|
||||
MaxAutomaticRedirections = 10
|
||||
};
|
||||
|
||||
using var client = new HttpClient(handler);
|
||||
|
||||
try
|
||||
{
|
||||
return await client.SendAsync(message, completionOption);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
// Пытаемся извлечь тело ошибки, если оно доступно
|
||||
if (ex.InnerException == null)
|
||||
throw;
|
||||
|
||||
throw new Exception($"Ошибка HTTP-запроса: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
namespace YandexMusic.API.Common.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Интерфейс для провайдеров обработки запросов
|
||||
/// </summary>
|
||||
public interface IRequestProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Функция получения ответа
|
||||
/// </summary>
|
||||
/// <param name="message">Запрос</param>
|
||||
/// <param name="completionOption">Опция завершения запроса</param>
|
||||
/// <returns></returns>
|
||||
Task<HttpResponseMessage> GetWebResponseAsync(HttpRequestMessage message, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead);
|
||||
|
||||
/// <summary>
|
||||
/// Функция формирования ответа
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Тип объекта с ответом</typeparam>
|
||||
/// <param name="api">API</param>
|
||||
/// <param name="response">Ответ</param>
|
||||
/// <returns></returns>
|
||||
Task<T> GetDataFromResponseAsync<T>(YandexMusicApi api, HttpResponseMessage response);
|
||||
}
|
||||
}
|
||||
47
YandexMusic.API/Common/YandexMusicHttpClientFactory.cs
Normal file
47
YandexMusic.API/Common/YandexMusicHttpClientFactory.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.Net;
|
||||
|
||||
namespace YandexMusic.API.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Фабрика для создания стандартного HttpClient с поддержкой кук, прокси и автоматической декомпрессией.
|
||||
/// </summary>
|
||||
public static class YandexMusicHttpClientFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Создаёт стандартный HttpClient с автоматическим управлением куками.
|
||||
/// </summary>
|
||||
/// <param name="proxy">Прокси-сервер (опционально).</param>
|
||||
/// <param name="timeout">Таймаут запросов (по умолчанию 30 секунд).</param>
|
||||
/// <param name="userAgent">User-Agent (по умолчанию как у браузера Chrome).</param>
|
||||
/// <returns>Настроенный HttpClient.</returns>
|
||||
public static HttpClient CreateDefault(
|
||||
CookieContainer? cookieContainer = null,
|
||||
IWebProxy? proxy = null,
|
||||
TimeSpan? timeout = null,
|
||||
string? userAgent = null)
|
||||
{
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
|
||||
UseCookies = true,
|
||||
CookieContainer = cookieContainer ?? new CookieContainer(),
|
||||
AllowAutoRedirect = true,
|
||||
MaxAutomaticRedirections = 10,
|
||||
Proxy = proxy,
|
||||
UseProxy = proxy != null
|
||||
};
|
||||
|
||||
var client = new HttpClient(handler, disposeHandler: true)
|
||||
{
|
||||
Timeout = timeout ?? TimeSpan.FromSeconds(30)
|
||||
};
|
||||
|
||||
// Стандартный User-Agent, похожий на браузерный
|
||||
client.DefaultRequestHeaders.Add("User-Agent",
|
||||
userAgent ?? "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
||||
client.DefaultRequestHeaders.Add("Accept", "*/*");
|
||||
client.DefaultRequestHeaders.Add("Accept-Language", "ru-RU,ru;q=0.9,en;q=0.8");
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using YandexMusic.API.Models.Track;
|
||||
@@ -12,6 +13,7 @@ public class YnisonPlayer : IDisposable
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly AuthStorage _storage;
|
||||
private readonly IWebProxy? _proxy;
|
||||
private YnisonWebSocket? _redirector;
|
||||
private YnisonWebSocket? _state;
|
||||
|
||||
@@ -33,40 +35,36 @@ public class YnisonPlayer : IDisposable
|
||||
/// <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)
|
||||
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();
|
||||
_state = new YnisonWebSocket();
|
||||
_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)
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(data, _jsonOptions)
|
||||
=> JsonSerializer.Deserialize<T>(data, _jsonOptions)
|
||||
?? throw new JsonException("Десериализация вернула null");
|
||||
}
|
||||
|
||||
private T DeserializeMessage<T>(YYnisonMessageType messageType, string data)
|
||||
{
|
||||
@@ -120,8 +118,8 @@ public class YnisonPlayer : IDisposable
|
||||
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();
|
||||
var response = await API.Track.GetAsync(item.PlayableId);
|
||||
return response;
|
||||
}
|
||||
|
||||
private async Task UpdateStateAsync()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -7,14 +8,15 @@ namespace YandexMusic.API.Common.Ynison;
|
||||
/// <summary>WebSocket-клиент для взаимодействия с протоколом Ynison.</summary>
|
||||
public class YnisonWebSocket : IDisposable
|
||||
{
|
||||
private readonly ClientWebSocket _socketClient = new();
|
||||
private ClientWebSocket? _socketClient;
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private CancellationToken _cancellationToken;
|
||||
private readonly StringBuilder _data = new();
|
||||
private const int BufferSize = 4096;
|
||||
private readonly IWebProxy? _proxy;
|
||||
|
||||
/// <summary>Флаг, указывает, открыто ли соединение.</summary>
|
||||
public bool IsConnected => _socketClient.State == WebSocketState.Open;
|
||||
public bool IsConnected => _socketClient?.State == WebSocketState.Open;
|
||||
|
||||
/// <summary>Событие получения сообщения.</summary>
|
||||
public event EventHandler<ReceiveEventArgs>? OnReceive;
|
||||
@@ -25,19 +27,25 @@ public class YnisonWebSocket : IDisposable
|
||||
/// <summary>Аргументы события получения данных.</summary>
|
||||
public class ReceiveEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>Полученные данные (JSON-строка).</summary>
|
||||
public string Data { get; init; } = null!;
|
||||
}
|
||||
|
||||
/// <summary>Аргументы события закрытия соединения.</summary>
|
||||
public class CloseEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>Статус закрытия.</summary>
|
||||
public WebSocketCloseStatus? Status { get; init; }
|
||||
/// <summary>Описание причины закрытия.</summary>
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр WebSocket-клиента.
|
||||
/// </summary>
|
||||
/// <param name="proxy">Прокси-сервер (опционально).</param>
|
||||
public YnisonWebSocket(IWebProxy? proxy = null)
|
||||
{
|
||||
_proxy = proxy;
|
||||
}
|
||||
|
||||
private static string GetProtocolData(string deviceId, string? redirectTicket)
|
||||
{
|
||||
var deviceInfo = new Dictionary<string, object>
|
||||
@@ -57,6 +65,9 @@ public class YnisonWebSocket : IDisposable
|
||||
|
||||
private async Task<string> ReadSocketContentAsync()
|
||||
{
|
||||
if (_socketClient == null)
|
||||
throw new InvalidOperationException("WebSocket не инициализирован");
|
||||
|
||||
var buffer = new byte[BufferSize];
|
||||
WebSocketReceiveResult result;
|
||||
do
|
||||
@@ -68,17 +79,19 @@ public class YnisonWebSocket : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Подключается к WebSocket.</summary>
|
||||
/// <param name="storage">Хранилище авторизации.</param>
|
||||
/// <param name="storage">Хранилище авторизации (для токена и deviceId).</param>
|
||||
/// <param name="url">URL WebSocket.</param>
|
||||
/// <param name="redirectTicket">Тикет перенаправления (опционально).</param>
|
||||
public async Task ConnectAsync(AuthStorage storage, string url, string? redirectTicket = null)
|
||||
{
|
||||
_socketClient = new ClientWebSocket();
|
||||
_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;
|
||||
if (_proxy != null)
|
||||
_socketClient.Options.Proxy = _proxy;
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_cancellationToken = _cancellationTokenSource.Token;
|
||||
@@ -89,7 +102,7 @@ public class YnisonWebSocket : IDisposable
|
||||
/// <summary>Начинает асинхронный приём сообщений.</summary>
|
||||
public async Task BeginReceiveAsync()
|
||||
{
|
||||
if (_socketClient.State != WebSocketState.Open)
|
||||
if (_socketClient == null || _socketClient.State != WebSocketState.Open)
|
||||
return;
|
||||
|
||||
try
|
||||
@@ -116,9 +129,11 @@ public class YnisonWebSocket : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Отправляет JSON-сообщение.</summary>
|
||||
/// <param name="json">JSON-строка.</param>
|
||||
public async ValueTask SendAsync(string json)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
throw new InvalidOperationException("WebSocket не инициализирован");
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
await _socketClient.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, _cancellationToken);
|
||||
}
|
||||
@@ -127,16 +142,14 @@ public class YnisonWebSocket : IDisposable
|
||||
public async Task StopReceiveAsync()
|
||||
{
|
||||
if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
await _cancellationTokenSource.CancelAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Освобождает ресурсы.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_socketClient.Dispose();
|
||||
_socketClient?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user