Добавьте файлы проекта.
This commit is contained in:
103
YandexMusic.API/Common/AuthStorage.cs
Normal file
103
YandexMusic.API/Common/AuthStorage.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Хранилище данных пользователя
|
||||
/// </summary>
|
||||
public class AuthStorage
|
||||
{
|
||||
#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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
44
YandexMusic.API/Common/DataDownloader.cs
Normal file
44
YandexMusic.API/Common/DataDownloader.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
YandexMusic.API/Common/Encryptor.cs
Normal file
75
YandexMusic.API/Common/Encryptor.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace YandexMusic.API.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Класс для шифровки
|
||||
/// </summary>
|
||||
public class Encryptor
|
||||
{
|
||||
#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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
60
YandexMusic.API/Common/Providers/CommonRequestProvider.cs
Normal file
60
YandexMusic.API/Common/Providers/CommonRequestProvider.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using YandexMusic.API.Models.Common;
|
||||
|
||||
namespace YandexMusic.API.Common.Providers
|
||||
{
|
||||
public class CommonRequestProvider : IRequestProvider
|
||||
{
|
||||
#region Поля
|
||||
|
||||
protected AuthStorage storage;
|
||||
|
||||
#endregion Поля
|
||||
|
||||
|
||||
|
||||
public CommonRequestProvider(AuthStorage authStorage)
|
||||
{
|
||||
storage = authStorage;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#region IRequestProvider
|
||||
|
||||
public virtual Task<HttpResponseMessage> GetWebResponseAsync(HttpRequestMessage message, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public virtual async Task<T> GetDataFromResponseAsync<T>(YandexMusicApi api, HttpResponseMessage response)
|
||||
{
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion IRequestProvider
|
||||
}
|
||||
}
|
||||
69
YandexMusic.API/Common/Providers/DefaultRequestProvider.cs
Normal file
69
YandexMusic.API/Common/Providers/DefaultRequestProvider.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Net;
|
||||
|
||||
using YandexMusic.API.Models.Common;
|
||||
|
||||
namespace YandexMusic.API.Common.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Стандартный провайдер запросов
|
||||
/// </summary>
|
||||
public class DefaultRequestProvider : CommonRequestProvider
|
||||
{
|
||||
#region Вспомогательные функции
|
||||
|
||||
private Exception ProcessException(Exception ex)
|
||||
{
|
||||
if (ex is not WebException webException)
|
||||
return ex;
|
||||
|
||||
if (webException.Response is null)
|
||||
return ex;
|
||||
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
#region IRequestProvider
|
||||
|
||||
public override Task<HttpResponseMessage> GetWebResponseAsync(HttpRequestMessage message,
|
||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpClient client = new(new SocketsHttpHandler
|
||||
{
|
||||
Proxy = storage.Context.WebProxy,
|
||||
AutomaticDecompression = DecompressionMethods.GZip,
|
||||
UseCookies = true,
|
||||
CookieContainer = storage.Context.Cookies,
|
||||
});
|
||||
|
||||
return client.SendAsync(message, completionOption);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw ProcessException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion IRequestProvider
|
||||
}
|
||||
}
|
||||
25
YandexMusic.API/Common/Providers/IRequestProvider.cs
Normal file
25
YandexMusic.API/Common/Providers/IRequestProvider.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
27
YandexMusic.API/Common/Providers/MockRequestProvider.cs
Normal file
27
YandexMusic.API/Common/Providers/MockRequestProvider.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace YandexMusic.API.Common.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Провайдер запросов данными из файла
|
||||
/// </summary>
|
||||
public class MockRequestProvider : CommonRequestProvider
|
||||
{
|
||||
|
||||
|
||||
public MockRequestProvider(AuthStorage authStorage) : base(authStorage)
|
||||
{
|
||||
storage = authStorage;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#region IRequestProvider
|
||||
|
||||
public override Task<HttpResponseMessage> GetWebResponseAsync(HttpRequestMessage message,
|
||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#endregion IRequestProvider
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace YandexMusic.API.Common.Ynison
|
||||
{
|
||||
public class UpperSnakeCaseNamingStrategy : SnakeCaseNamingStrategy
|
||||
{
|
||||
protected override string ResolvePropertyName(string name) => base.ResolvePropertyName(name).ToUpper();
|
||||
}
|
||||
}
|
||||
315
YandexMusic.API/Common/Ynison/YnisonPlayer.cs
Normal file
315
YandexMusic.API/Common/Ynison/YnisonPlayer.cs
Normal file
@@ -0,0 +1,315 @@
|
||||
using System.Net.WebSockets;
|
||||
using YandexMusic.API.Models.Track;
|
||||
using YandexMusic.API.Models.Ynison;
|
||||
using YandexMusic.API.Models.Ynison.Messages;
|
||||
|
||||
namespace YandexMusic.API.Common.Ynison
|
||||
{
|
||||
public class YnisonPlayer : IDisposable
|
||||
{
|
||||
#region Поля
|
||||
|
||||
private readonly JsonSerializerSettings jsonSettings = new()
|
||||
{
|
||||
Converters = new List<JsonConverter> {
|
||||
new StringEnumConverter(new UpperSnakeCaseNamingStrategy())
|
||||
},
|
||||
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
// Важно! Унисон отдаёт данные в SnakeCase
|
||||
NamingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
};
|
||||
|
||||
private AuthStorage storage;
|
||||
private YnisonWebSocket redirector;
|
||||
private YnisonWebSocket state;
|
||||
|
||||
#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
|
||||
{
|
||||
public YYnisonState State { get; internal set; }
|
||||
}
|
||||
|
||||
public delegate void OnReceiveEventHandler(YnisonPlayer player, ReceiveEventArgs args);
|
||||
|
||||
/// <summary>
|
||||
/// Получение данных
|
||||
/// </summary>
|
||||
public event OnReceiveEventHandler OnReceive;
|
||||
|
||||
|
||||
public class CloseEventArgs
|
||||
{
|
||||
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"))
|
||||
{
|
||||
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"
|
||||
};
|
||||
|
||||
YYnisonUpdateFullStateMessage fullState = new()
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return SerializeJson(fullState);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
179
YandexMusic.API/Common/Ynison/YnisonWebSocket.cs
Normal file
179
YandexMusic.API/Common/Ynison/YnisonWebSocket.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
|
||||
namespace YandexMusic.API.Common.Ynison
|
||||
{
|
||||
public class YnisonWebSocket : IDisposable
|
||||
{
|
||||
#region Поля
|
||||
|
||||
private readonly JsonSerializerSettings jsonSettings = new()
|
||||
{
|
||||
Converters = new List<JsonConverter> {
|
||||
new StringEnumConverter {
|
||||
NamingStrategy = new CamelCaseNamingStrategy()
|
||||
}
|
||||
},
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
|
||||
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
|
||||
{
|
||||
public string Data { get; internal set; }
|
||||
}
|
||||
|
||||
public delegate void OnReceiveEventHandler(YnisonWebSocket socket, ReceiveEventArgs args);
|
||||
/// <summary>
|
||||
/// Получение данных
|
||||
/// </summary>
|
||||
public event OnReceiveEventHandler OnReceive;
|
||||
|
||||
public class CloseEventArgs
|
||||
{
|
||||
public WebSocketCloseStatus? Status { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
|
||||
public delegate void OnCloseEventHandler(YnisonWebSocket socket, CloseEventArgs args);
|
||||
/// <summary>
|
||||
/// Закрытие соединения
|
||||
/// </summary>
|
||||
public event OnCloseEventHandler OnClose;
|
||||
|
||||
#endregion События
|
||||
|
||||
#region Вспомогательные функции
|
||||
|
||||
private string SerializeJson(object obj)
|
||||
{
|
||||
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
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
#endregion Вспомогательные функции
|
||||
|
||||
|
||||
|
||||
public bool Connect(AuthStorage storage, string url, string redirectTicket = null)
|
||||
{
|
||||
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()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
public ValueTask Send(string json)
|
||||
{
|
||||
ReadOnlyMemory<byte> message = new(Encoding.UTF8.GetBytes(json));
|
||||
return socketClient.SendAsync(message, WebSocketMessageType.Text, false, CancellationToken.None);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user