diff --git a/YandexMusic.API/API/YAlbumAPI.cs b/YandexMusic.API/API/YAlbumAPI.cs
new file mode 100644
index 0000000..5d2d11f
--- /dev/null
+++ b/YandexMusic.API/API/YAlbumAPI.cs
@@ -0,0 +1,38 @@
+using YandexMusic.API.Common;
+using YandexMusic.API.Models.Album;
+using YandexMusic.API.Models.Common;
+using YandexMusic.API.Requests.Album;
+
+namespace YandexMusic.API;
+
+///
+/// API для взаимодействия с альбомами
+///
+public class YAlbumAPI : YCommonAPI
+{
+ public YAlbumAPI(YandexMusicApi yandex) : base(yandex)
+ {
+ }
+
+ /// Получает альбом по идентификатору.
+ /// Хранилище авторизации.
+ /// Идентификатор альбома.
+ ///
+ public Task> GetAsync(AuthStorage storage, string albumId)
+ {
+ return new YGetAlbumBuilder(api, storage)
+ .Build(albumId)
+ .GetResponseAsync();
+ }
+
+ /// Получение альбомов по списку идентификаторов.
+ /// Хранилище авторизации.
+ /// Идентификаторы альбомов.
+ ///
+ public Task>> GetAsync(AuthStorage storage, IEnumerable albumIds)
+ {
+ return new YGetAlbumsBuilder(api, storage)
+ .Build(albumIds)
+ .GetResponseAsync();
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/API/YArtistAPI.cs b/YandexMusic.API/API/YArtistAPI.cs
new file mode 100644
index 0000000..f132417
--- /dev/null
+++ b/YandexMusic.API/API/YArtistAPI.cs
@@ -0,0 +1,71 @@
+using YandexMusic.API.Common;
+using YandexMusic.API.Models.Artist;
+using YandexMusic.API.Models.Common;
+using YandexMusic.API.Requests.Artist;
+
+namespace YandexMusic.API;
+
+///
+/// API для взаимодействия с исполнителями
+///
+public class YArtistAPI : YCommonAPI
+{
+ public YArtistAPI(YandexMusicApi yandex) : base(yandex)
+ {
+ }
+
+ ///
+ /// Получение исполнителя
+ ///
+ /// Хранилище
+ /// Идентификатор
+ public Task> GetAsync(AuthStorage storage, string artistId)
+ {
+ return new YGetArtistBuilder(api, storage)
+ .Build(artistId)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение исполнителей
+ ///
+ /// Хранилище
+ /// Идентификаторы
+ public Task>> GetAsync(AuthStorage storage, IEnumerable artistIds)
+ {
+ return new YGetArtistsBuilder(api, storage)
+ .Build(artistIds)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение треков исполнителя с пагинацией
+ ///
+ /// Треки поставляются по штук на страницу,
+ /// для получения всех треков необходимо использовать метод
+ ///
+ ///
+ /// Хранилище
+ /// Идентификатор исполнителя
+ /// Страница ответов
+ /// Количество треков на странице ответов
+ public Task> GetTracksAsync(AuthStorage storage, string artistId, int page = 0, int pageSize = 20)
+ {
+ return new YGetArtistTrackBuilder(api, storage)
+ .Build((artistId, page, pageSize))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение всех треков исполнителя
+ ///
+ /// Хранилище
+ /// Идентификатор исполнителя
+ public async Task> GetAllTracksAsync(AuthStorage storage, string artistId)
+ {
+ YResponse response = await GetAsync(storage, artistId);
+ return await GetTracksAsync(storage, artistId, pageSize: response.Result.Artist.Counts.Tracks);
+ }
+
+
+}
\ No newline at end of file
diff --git a/YandexMusic.API/API/YCommonAPI.cs b/YandexMusic.API/API/YCommonAPI.cs
new file mode 100644
index 0000000..d066141
--- /dev/null
+++ b/YandexMusic.API/API/YCommonAPI.cs
@@ -0,0 +1,14 @@
+namespace YandexMusic.API;
+
+///
+/// Родительский класс для ветки API
+///
+public class YCommonAPI
+{
+ protected YandexMusicApi api;
+
+ public YCommonAPI(YandexMusicApi yandex)
+ {
+ api = yandex;
+ }
+}
diff --git a/YandexMusic.API/API/YLabelAPIAsync.cs b/YandexMusic.API/API/YLabelAPIAsync.cs
new file mode 100644
index 0000000..eeac03b
--- /dev/null
+++ b/YandexMusic.API/API/YLabelAPIAsync.cs
@@ -0,0 +1,40 @@
+using YandexMusic.API.Common;
+using YandexMusic.API.Models.Common;
+using YandexMusic.API.Models.Label;
+using YandexMusic.API.Requests.Label;
+
+namespace YandexMusic.API
+{
+ public partial class YLabelAPI : YCommonAPI
+ {
+ public YLabelAPI(YandexMusicApi yandex) : base(yandex)
+ {
+ }
+
+ ///
+ /// Постраничное получение альбомов лейбла
+ ///
+ /// Хранилище
+ /// Лейбл
+ /// Страница
+ public Task> GetAlbumsByLabelAsync(AuthStorage storage, YLabel label, int page)
+ {
+ return new YGetLabelAlbumsBuilder(api, storage)
+ .Build((label, page))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Постраничное получение артистов лейбла
+ ///
+ /// Хранилище
+ /// Лейбл
+ /// Страница
+ public Task> GetArtistsByLabelAsync(AuthStorage storage, YLabel label, int page)
+ {
+ return new YGetLabelArtistsBuilder(api, storage)
+ .Build((label, page))
+ .GetResponseAsync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/API/YLandingAPIAsync.cs b/YandexMusic.API/API/YLandingAPIAsync.cs
new file mode 100644
index 0000000..8ff4fe9
--- /dev/null
+++ b/YandexMusic.API/API/YLandingAPIAsync.cs
@@ -0,0 +1,63 @@
+using YandexMusic.API.Common;
+using YandexMusic.API.Models.Common;
+using YandexMusic.API.Models.Feed;
+using YandexMusic.API.Models.Landing;
+using YandexMusic.API.Requests.Feed;
+using YandexMusic.API.Requests.Landing;
+
+namespace YandexMusic.API
+{
+ ///
+ /// API для взаимодействия с главной страницей
+ ///
+ public partial class YLandingAPI : YCommonAPI
+ {
+
+
+ public YLandingAPI(YandexMusicApi yandex) : base(yandex)
+ {
+ }
+
+ ///
+ /// Получение персональных списков
+ ///
+ /// Хранилище
+ /// Типы запрашиваемых блоков
+ ///
+ public Task> GetAsync(AuthStorage storage, params YLandingBlockType[] blocks)
+ {
+ if (blocks == null)
+ return null;
+
+ return new YGetLandingBuilder(api, storage)
+ .Build(blocks)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение ленты
+ ///
+ /// Хранилище
+ ///
+ public Task> GetFeedAsync(AuthStorage storage)
+ {
+ return new YGetFeedBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение лендинга детского раздела
+ ///
+ /// Хранилище
+ ///
+ public Task> GetChildrenLandingAsync(AuthStorage storage)
+ {
+ return new YGetChildrenLandingBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/API/YLibraryAPIAsync.cs b/YandexMusic.API/API/YLibraryAPIAsync.cs
new file mode 100644
index 0000000..46b5209
--- /dev/null
+++ b/YandexMusic.API/API/YLibraryAPIAsync.cs
@@ -0,0 +1,258 @@
+using YandexMusic.API.Common;
+using YandexMusic.API.Models.Album;
+using YandexMusic.API.Models.Artist;
+using YandexMusic.API.Models.Common;
+using YandexMusic.API.Models.Landing.Entity.Entities.Context;
+using YandexMusic.API.Models.Library;
+using YandexMusic.API.Models.Playlist;
+using YandexMusic.API.Models.Track;
+using YandexMusic.API.Requests.Library;
+
+namespace YandexMusic.API
+{
+ ///
+ /// API для взаимодействия с библиотекой
+ ///
+ public partial class YLibraryAPI : YCommonAPI
+ {
+ #region Вспомогательные функции
+
+ ///
+ /// Получение секции библиотеки
+ ///
+ /// Тип объекта библиотеки
+ /// Хранилище
+ /// Секция
+ /// Тип
+ /// Список объектов из секции
+ private Task> GetLibrarySection(AuthStorage storage, YLibrarySection section, YLibrarySectionType type = YLibrarySectionType.Likes)
+ {
+ return new YGetLibrarySectionBuilder(api, storage)
+ .Build((section, type))
+ .GetResponseAsync();
+ }
+
+ #endregion Вспомогательные функции
+
+
+
+ public YLibraryAPI(YandexMusicApi yandex) : base(yandex)
+ {
+ }
+
+ #region Лайки
+
+ ///
+ /// Получение лайкнутых треков
+ ///
+ /// Хранилище
+ ///
+ public Task> GetLikedTracksAsync(AuthStorage storage)
+ {
+ return GetLibrarySection(storage, YLibrarySection.Tracks);
+ }
+
+ ///
+ /// Получение лайкнутых альбомов
+ ///
+ /// Хранилище
+ ///
+ public Task>> GetLikedAlbumsAsync(AuthStorage storage)
+ {
+ return GetLibrarySection>(storage, YLibrarySection.Albums);
+ }
+
+ ///
+ /// Получение лайкнутых исполнителей
+ ///
+ /// Хранилище
+ ///
+ public Task>> GetLikedArtistsAsync(AuthStorage storage)
+ {
+ return GetLibrarySection>(storage, YLibrarySection.Artists);
+ }
+
+ ///
+ /// Получение лайкнутых плейлистов
+ ///
+ /// Хранилище
+ ///
+ public Task>> GetLikedPlaylistsAsync(AuthStorage storage)
+ {
+ return GetLibrarySection>(storage, YLibrarySection.Playlists);
+ }
+
+ #endregion Лайки
+
+ #region Дизлайки
+
+ ///
+ /// Получение дизлайкнутых треков
+ ///
+ /// Хранилище
+ ///
+ public Task> GetDislikedTracksAsync(AuthStorage storage)
+ {
+ return GetLibrarySection(storage, YLibrarySection.Tracks, YLibrarySectionType.Dislikes);
+ }
+
+ ///
+ /// Получение дизлайкнутых исполнителей
+ ///
+ /// Хранилище
+ ///
+ public Task>> GetDislikedArtistsAsync(AuthStorage storage)
+ {
+ return GetLibrarySection>(storage, YLibrarySection.Artists, YLibrarySectionType.Dislikes);
+ }
+
+ #endregion Дизлайки
+
+ #region Добавление в списки лайков/дизлайков
+
+ ///
+ /// Добавить трек в список лайкнутых
+ ///
+ /// Хранилище
+ /// Трек
+ ///
+ public Task> AddTrackLikeAsync(AuthStorage storage, YTrack track)
+ {
+ return new YLibraryAddBuilder(api, storage)
+ .Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Likes))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Удалить трек из списка лайкнутых
+ ///
+ /// Хранилище
+ /// Трек
+ ///
+ public Task> RemoveTrackLikeAsync(AuthStorage storage, YTrack track)
+ {
+ return new YLibraryRemoveBuilder(api, storage)
+ .Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Likes))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Добавить трек в список дизлайкнутых
+ ///
+ /// Хранилище
+ /// Трек
+ ///
+ public Task> AddTrackDislikeAsync(AuthStorage storage, YTrack track)
+ {
+ return new YLibraryAddBuilder(api, storage)
+ .Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Удалить трек из списка дизлайкнутых
+ ///
+ /// Хранилище
+ /// Трек
+ ///
+ public Task> RemoveTrackDislikeAsync(AuthStorage storage, YTrack track)
+ {
+ return new YLibraryRemoveBuilder(api, storage)
+ .Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Добавить альбом в список лайкнутых
+ ///
+ /// Хранилище
+ /// Альбом
+ ///
+ public Task> AddAlbumLikeAsync(AuthStorage storage, YAlbum album)
+ {
+ return new YLibraryAddBuilder(api, storage)
+ .Build((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Удалить альбом из списка лайкнутых
+ ///
+ /// Хранилище
+ /// Альбом
+ ///
+ public Task> RemoveAlbumLikeAsync(AuthStorage storage, YAlbum album)
+ {
+ return new YLibraryRemoveBuilder(api, storage)
+ .Build((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Добавить исполнителя в список лайкнутых
+ ///
+ /// Хранилище
+ /// Исполнитель
+ ///
+ public Task> AddArtistLikeAsync(AuthStorage storage, YArtist artist)
+ {
+ return new YLibraryAddBuilder(api, storage)
+ .Build((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Удалить исполнителя из списка лайкнутых
+ ///
+ /// Хранилище
+ /// Исполнитель
+ ///
+ public Task> RemoveArtistLikeAsync(AuthStorage storage, YArtist artist)
+ {
+ return new YLibraryRemoveBuilder(api, storage)
+ .Build((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Добавить плейлист в список лайкнутых
+ ///
+ /// Хранилище
+ /// Плейлист
+ ///
+ public Task> AddPlaylistLikeAsync(AuthStorage storage, YPlaylist playlist)
+ {
+ return new YLibraryAddBuilder(api, storage)
+ .Build((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Удалить плейлист из списка лайкнутых
+ ///
+ /// Хранилище
+ /// Плейлист
+ ///
+ public Task> RemovePlaylistLikeAsync(AuthStorage storage, YPlaylist playlist)
+ {
+ return new YLibraryRemoveBuilder(api, storage)
+ .Build((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes))
+ .GetResponseAsync();
+ }
+
+ #endregion Добавление/удаление в списки лайков/дизлайков
+
+ #region Получение списка "Вы недавно слушали"
+
+ public Task> GetRecentlyListenedAsync(AuthStorage storage, IEnumerable contextTypes, int trackCount, int contextCount)
+ {
+ return new YGetLibraryRecentlyListenedBuilder(api, storage)
+ .Build((contextTypes, trackCount, contextCount))
+ .GetResponseAsync();
+ }
+
+ #endregion Получение списка "Вы недавно слушали"
+
+
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/API/YPinsAPIAsync.cs b/YandexMusic.API/API/YPinsAPIAsync.cs
new file mode 100644
index 0000000..279e323
--- /dev/null
+++ b/YandexMusic.API/API/YPinsAPIAsync.cs
@@ -0,0 +1,33 @@
+using YandexMusic.API.Common;
+using YandexMusic.API.Models.Common;
+using YandexMusic.API.Models.Pins;
+using YandexMusic.API.Requests.Pins;
+
+namespace YandexMusic.API
+{
+ ///
+ /// API для взаимодействия с прикреплёнными объектами
+ ///
+ public partial class YPinsAPI : YCommonAPI
+ {
+
+
+ public YPinsAPI(YandexMusicApi yandex) : base(yandex)
+ {
+ }
+
+ ///
+ /// Получение списка прикреплённых объектов
+ ///
+ /// Хранилище
+ ///
+ public Task> GetAsync(AuthStorage storage)
+ {
+ return new YGetPinsBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/API/YPlaylistAPIAsync.cs b/YandexMusic.API/API/YPlaylistAPIAsync.cs
new file mode 100644
index 0000000..cf94f73
--- /dev/null
+++ b/YandexMusic.API/API/YPlaylistAPIAsync.cs
@@ -0,0 +1,335 @@
+using YandexMusic.API.Common;
+using YandexMusic.API.Models.Common;
+using YandexMusic.API.Models.Landing;
+using YandexMusic.API.Models.Landing.Entity.Entities;
+using YandexMusic.API.Models.Playlist;
+using YandexMusic.API.Models.Track;
+using YandexMusic.API.Requests.Playlist;
+
+namespace YandexMusic.API
+{
+ ///
+ /// API для взамодействия с плейлистами
+ ///
+ public partial class YPlaylistAPI : YCommonAPI
+ {
+ #region Вспомогательные функции
+
+ ///
+ /// Получение персональных плейлистов
+ ///
+ /// Хранилище
+ /// Тип
+ /// Плейлист
+ private async Task> GetPersonalPlaylist(AuthStorage storage, YGeneratedPlaylistType type)
+ {
+ List> list = await GetPersonalPlaylistsAsync(storage);
+ return list.FirstOrDefault(e => string.Equals(e.Result.GeneratedPlaylistType, type.ToString(), StringComparison.CurrentCultureIgnoreCase));
+ }
+
+ ///
+ /// Изменение плейлиста
+ ///
+ /// Хранилище
+ /// Плейлист
+ /// Список изменений
+ /// Плейлист после изменений
+ private Task> ChangePlaylist(AuthStorage storage, YPlaylist playlist, IEnumerable changes)
+ {
+ return new YPlaylistChangeBuilder(api, storage)
+ .Build((playlist, changes))
+ .GetResponseAsync();
+ }
+
+ private IEnumerable RemoveIdentical(IEnumerable tracks)
+ {
+ return tracks.Distinct();
+ }
+
+ #endregion Вспомогательные функции
+
+
+
+ public YPlaylistAPI(YandexMusicApi yandex) : base(yandex)
+ {
+ }
+
+ #region Список с главной
+
+ ///
+ /// Получение списка персональных плейлистов
+ ///
+ /// Хранилище
+ ///
+ public async Task>> GetPersonalPlaylistsAsync(AuthStorage storage)
+ {
+ YResponse landing = await api.Landing.GetAsync(storage, YLandingBlockType.PersonalPlaylists);
+
+ IEnumerable>> tasks = landing
+ .Result
+ .Blocks
+ .FirstOrDefault(b => b.Type == YLandingBlockType.PersonalPlaylists)
+ ?.Entities
+ .Select(e => api.Playlist.GetAsync(storage, ((YLandingEntityPersonalPlaylist)e).Data?.Data));
+
+ return tasks == null
+ ? new List>()
+ : new List>(await Task.WhenAll(tasks));
+ }
+
+ #endregion Список с главной
+
+ #region Стандартные плейлисты
+
+ ///
+ /// Избранное
+ ///
+ /// Хранилище
+ ///
+ public Task>> FavoritesAsync(AuthStorage storage)
+ {
+ return new YGetPlaylistFavoritesBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Плейлист дня
+ ///
+ /// Хранилище
+ ///
+ public Task> OfTheDayAsync(AuthStorage storage)
+ {
+ return GetPersonalPlaylist(storage, YGeneratedPlaylistType.PlaylistOfTheDay);
+ }
+
+ ///
+ /// Дежавю
+ ///
+ /// Хранилище
+ ///
+ public Task> DejaVuAsync(AuthStorage storage)
+ {
+ return GetPersonalPlaylist(storage, YGeneratedPlaylistType.NeverHeard);
+ }
+
+ ///
+ /// Премьера
+ ///
+ /// Хранилище
+ ///
+ public Task> PremiereAsync(AuthStorage storage)
+ {
+ return GetPersonalPlaylist(storage, YGeneratedPlaylistType.RecentTracks);
+ }
+
+ ///
+ /// Тайник
+ ///
+ /// Хранилище
+ ///
+ public Task> MissedAsync(AuthStorage storage)
+ {
+ return GetPersonalPlaylist(storage, YGeneratedPlaylistType.MissedLikes);
+ }
+
+ ///
+ /// Кинопоиск
+ ///
+ /// Хранилище
+ ///
+ public Task> KinopoiskAsync(AuthStorage storage)
+ {
+ return GetPersonalPlaylist(storage, YGeneratedPlaylistType.Kinopoisk);
+ }
+
+ #endregion Стандартные плейлисты
+
+ #region Получение плейлиста
+
+ ///
+ /// Получение плейлиста
+ ///
+ /// Хранилище
+ /// Uid пользователя-владельца плейлиста
+ /// Тип
+ ///
+ public Task> GetAsync(AuthStorage storage, string user, string kind)
+ {
+ return new YGetPlaylistBuilder(api, storage)
+ .Build((user, kind))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение плейлиста по uuid
+ ///
+ /// Хранилище
+ /// uuid
+ ///
+ public Task> GetAsync(AuthStorage storage, string uuid)
+ {
+ return new YGetPlaylistByUuidBuilder(api, storage)
+ .Build(uuid)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение плейлистов
+ ///
+ /// Хранилище
+ /// Список пар пользователь:тип
+ ///
+ public Task>> GetAsync(AuthStorage storage, IEnumerable<(string user, string kind)> ids)
+ {
+ return new YGetPlaylistsBuilder(api, storage)
+ .Build(ids)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение плейлиста
+ ///
+ /// Хранилище
+ /// Описание плейлиста, для которого будут запрошены треки
+ ///
+ public Task> GetAsync(AuthStorage storage, YPlaylist playlist)
+ {
+ return new YGetPlaylistBuilder(api, storage)
+ .Build((playlist.Owner.Uid, playlist.Kind))
+ .GetResponseAsync();
+ }
+
+ #endregion Получение плейлиста
+
+ #region Операции над плейлистами
+
+ ///
+ /// Создание
+ ///
+ /// Хранилище
+ /// Заголовок
+ ///
+ public Task> CreateAsync(AuthStorage storage, string name)
+ {
+ return new YPlaylistCreateBuilder(api, storage)
+ .Build(name)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Переименование
+ ///
+ /// Хранилище
+ /// Идентификатор плейлиста
+ /// Заголовок
+ ///
+ public Task> RenameAsync(AuthStorage storage, string kinds, string name)
+ {
+ return new YPlaylistRenameBuilder(api, storage)
+ .Build((kinds, name))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Переименование
+ ///
+ /// Хранилище
+ /// Плейлист
+ /// Заголовок
+ ///
+ public Task> RenameAsync(AuthStorage storage, YPlaylist playlist, string name)
+ {
+ return RenameAsync(storage, playlist.Kind, name);
+ }
+
+ ///
+ /// Удаление
+ ///
+ /// Хранилище
+ /// Тип
+ ///
+ public async Task DeleteAsync(AuthStorage storage, string kinds)
+ {
+ try
+ {
+ await new YPlaylistRemoveBuilder(api, storage)
+ .Build(kinds)
+ .GetResponseAsync();
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex);
+ }
+
+ return false;
+ }
+
+ ///
+ /// Удаление
+ ///
+ /// Хранилище
+ /// Плейлист
+ ///
+ public Task DeleteAsync(AuthStorage storage, YPlaylist playlist)
+ {
+ return DeleteAsync(storage, playlist.Kind);
+ }
+
+ ///
+ /// Добавление трека
+ ///
+ /// Хранилище
+ /// Плейлист
+ /// Треки для добавления
+ ///
+ public async Task> InsertTracksAsync(AuthStorage storage, YPlaylist playlist, IEnumerable tracks)
+ {
+ YResponse change = await ChangePlaylist(storage, playlist, new List {
+ new() {
+ Operation = YPlaylistChangeType.Insert,
+ At = 0,
+ Tracks = tracks.Select(t => t.GetKey())
+ }
+ });
+
+ return await GetAsync(storage, change.Result);
+ }
+
+ ///
+ /// Удаление треков
+ ///
+ /// Хранилище
+ /// Плейлист
+ /// Треки для удаления
+ ///
+ public Task> DeleteTracksAsync(AuthStorage storage, YPlaylist playlist, IEnumerable tracks)
+ {
+ List changes = RemoveIdentical(tracks)
+ .Select(t => playlist.Tracks.Select(c => c.Track).ToList().IndexOf(t))
+ .Where(i => i != -1)
+ .Select(i =>
+ {
+ YTrackContainer t = playlist.Tracks[i];
+ return new YPlaylistChange
+ {
+ Operation = YPlaylistChangeType.Delete,
+ From = i,
+ To = i + 1,
+ Tracks = new List {
+ t.Track.GetKey()
+ }
+ };
+ })
+ .ToList();
+
+ return ChangePlaylist(storage, playlist, changes);
+ }
+
+ #endregion Операции над плейлистами
+
+
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/API/YQueueAPIAsync.cs b/YandexMusic.API/API/YQueueAPIAsync.cs
new file mode 100644
index 0000000..aa380fd
--- /dev/null
+++ b/YandexMusic.API/API/YQueueAPIAsync.cs
@@ -0,0 +1,73 @@
+using YandexMusic.API.Common;
+using YandexMusic.API.Models.Common;
+using YandexMusic.API.Models.Queue;
+using YandexMusic.API.Requests.Queue;
+
+namespace YandexMusic.API
+{
+ ///
+ /// API для взаимодействия с очередями
+ ///
+ public partial class YQueueAPI : YCommonAPI
+ {
+ public YQueueAPI(YandexMusicApi yandex) : base(yandex)
+ {
+ }
+
+ ///
+ /// Получение всех очередей треков с разных устройств для синхронизации между ними
+ ///
+ /// Хранилище
+ /// Устройство
+ ///
+ public Task> ListAsync(AuthStorage storage, string device = null)
+ {
+ return new YQueuesListBuilder(api, storage)
+ .Build(device)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение очереди
+ ///
+ /// Хранилище
+ /// Идентификатор очереди
+ ///
+ public Task> GetAsync(AuthStorage storage, string queueId)
+ {
+ return new YGetQueueBuilder(api, storage)
+ .Build(queueId)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Создание новой очереди треков
+ ///
+ /// Хранилище
+ /// Очередь треков
+ /// Устройство
+ ///
+ public Task> CreateAsync(AuthStorage storage, YQueue queue, string device = null)
+ {
+ return new YQueueCreateBuilder(api, storage, device)
+ .Build(queue)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Установка текущего индекса проигрываемого трека в очереди треков
+ ///
+ /// Хранилище
+ /// Идентификатор очереди
+ /// Текущий индекс
+ /// Флаг интерактивности
+ /// Устройство
+ ///
+ public Task> UpdatePositionAsync(AuthStorage storage, string queueId, int currentIndex, bool isInteractive, string device = null)
+ {
+ return new YQueueUpdatePositionBuilder(api, storage, device)
+ .Build((queueId, currentIndex, isInteractive))
+ .GetResponseAsync();
+ }
+ }
+}
diff --git a/YandexMusic.API/API/YRadioAPIAsync.cs b/YandexMusic.API/API/YRadioAPIAsync.cs
new file mode 100644
index 0000000..169976d
--- /dev/null
+++ b/YandexMusic.API/API/YRadioAPIAsync.cs
@@ -0,0 +1,116 @@
+using YandexMusic.API.Common;
+using YandexMusic.API.Models.Common;
+using YandexMusic.API.Models.Radio;
+using YandexMusic.API.Models.Track;
+using YandexMusic.API.Requests.Radio;
+
+namespace YandexMusic.API
+{
+ ///
+ /// API для взаимодействия с радио
+ ///
+ public partial class YRadioAPI : YCommonAPI
+ {
+
+
+ public YRadioAPI(YandexMusicApi yandex) : base(yandex)
+ {
+ }
+
+ ///
+ /// Получение списка рекомендованных радиостанций
+ ///
+ /// Хранилище
+ ///
+ public Task> GetStationsDashboardAsync(AuthStorage storage)
+ {
+ return new YGetStationsDashboardBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение списка радиостанций
+ ///
+ /// Хранилище
+ ///
+ public Task>> GetStationsAsync(AuthStorage storage)
+ {
+ return new YGetStationsBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение информации о радиостанции
+ ///
+ /// Хранилище
+ /// Тип
+ /// Тэг
+ ///
+ public Task>> GetStationAsync(AuthStorage storage, string type, string tag)
+ {
+ return new YGetStationBuilder(api, storage)
+ .Build((type, tag))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение информации о радиостанции
+ ///
+ /// Хранилище
+ /// Идентификатор станции
+ ///
+ public Task>> GetStationAsync(AuthStorage storage, YStationId id)
+ {
+ return GetStationAsync(storage, id.Type, id.Tag);
+ }
+
+ ///
+ /// Получение последовательности треков радиостанции
+ ///
+ /// Хранилище
+ /// Радиостанция
+ /// Идентификатор предыдущего трека
+ ///
+ public Task> GetStationTracksAsync(AuthStorage storage, YStation station, string prevTrackId = "")
+ {
+ return new YGetStationTracksBuilder(api, storage)
+ .Build((station.Station, prevTrackId))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Установка настроек подбора треков
+ ///
+ /// Хранилище
+ /// Радиостанция
+ /// Настройки
+ ///
+ public Task> SetStationSettings2Async(AuthStorage storage, YStation station, YStationSettings2 settings)
+ {
+ return new YSetSettings2Builder(api, storage)
+ .Build((station.Station, settings))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Отправка обратной связи на действия при прослушивании радио
+ ///
+ /// Хранилище
+ /// Радиостанция
+ /// Тип обратной связи
+ /// Трек
+ /// Уникальный идентификатор партии треков. Возвращается при получении треков
+ /// Сколько было проиграно секунд трека перед действием
+ ///
+ public Task SendStationFeedBackAsync(AuthStorage storage, YStation station, YStationFeedbackType type, YTrack track = null, string batchId = "", double totalPlayedSeconds = 0)
+ {
+ return new YSetStationFeedbackBuilder(api, storage)
+ .Build((type, station, track, batchId, totalPlayedSeconds))
+ .GetResponseAsync();
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/API/YSearchAPIAsync.cs b/YandexMusic.API/API/YSearchAPIAsync.cs
new file mode 100644
index 0000000..515a27c
--- /dev/null
+++ b/YandexMusic.API/API/YSearchAPIAsync.cs
@@ -0,0 +1,141 @@
+using YandexMusic.API.Common;
+using YandexMusic.API.Models.Common;
+using YandexMusic.API.Models.Search;
+using YandexMusic.API.Requests.Search;
+
+namespace YandexMusic.API
+{
+ ///
+ /// API для поиска
+ ///
+ public partial class YSearchAPI : YCommonAPI
+ {
+
+
+ public YSearchAPI(YandexMusicApi yandex) : base(yandex)
+ {
+ }
+
+ ///
+ /// Поиск по трекам
+ ///
+ /// Хранилище
+ /// Имя трека
+ /// Номер страницы
+ /// Размер страницы
+ ///
+ public Task> TrackAsync(AuthStorage storage, string trackName, int pageNumber = 0, int pageSize = 20)
+ {
+ return SearchAsync(storage, trackName, YSearchType.Track, pageNumber, pageSize);
+ }
+
+ ///
+ /// Поиск по альбомам
+ ///
+ /// Хранилище
+ /// Имя альбома
+ /// Номер страницы
+ /// Размер страницы
+ ///
+ public Task> AlbumsAsync(AuthStorage storage, string albumName, int pageNumber = 0, int pageSize = 20)
+ {
+ return SearchAsync(storage, albumName, YSearchType.Album, pageNumber, pageSize);
+ }
+
+ ///
+ /// Поиск по артисту
+ ///
+ /// Хранилище
+ /// Имя артиста
+ /// Номер страницы
+ /// Размер страницы
+ ///
+ public Task> ArtistAsync(AuthStorage storage, string artistName, int pageNumber = 0, int pageSize = 20)
+ {
+ return SearchAsync(storage, artistName, YSearchType.Artist, pageNumber, pageSize);
+ }
+
+ ///
+ /// Поиск по плейлистам
+ ///
+ /// Хранилище
+ /// Имя плейлиста
+ /// Номер страницы
+ /// Размер страницы
+ ///
+ public Task> PlaylistAsync(AuthStorage storage, string playlistName, int pageNumber = 0, int pageSize = 20)
+ {
+ return SearchAsync(storage, playlistName, YSearchType.Playlist, pageNumber, pageSize);
+ }
+
+ ///
+ /// Поиск по плейлистам
+ ///
+ /// Хранилище
+ /// Имя подкаста
+ /// Номер страницы
+ /// Размер страницы
+ ///
+ public Task> PodcastEpisodeAsync(AuthStorage storage, string podcastName, int pageNumber = 0, int pageSize = 20)
+ {
+ return SearchAsync(storage, podcastName, YSearchType.PodcastEpisode, pageNumber, pageSize);
+ }
+
+ ///
+ /// Поиск по видео
+ ///
+ /// Хранилище
+ /// Имя видео
+ /// Номер страницы
+ /// Размер страницы
+ ///
+ public Task> VideosAsync(AuthStorage storage, string videoName, int pageNumber = 0, int pageSize = 20)
+ {
+ return SearchAsync(storage, videoName, YSearchType.Video, pageNumber, pageSize);
+ }
+
+ ///
+ /// Поиск по пользователям
+ ///
+ /// Хранилище
+ /// Имя пользователя
+ /// Номер страницы
+ /// Размер страницы
+ ///
+ public Task> UsersAsync(AuthStorage storage, string userName, int pageNumber = 0, int pageSize = 20)
+ {
+ return SearchAsync(storage, userName, YSearchType.User, pageNumber, pageSize);
+ }
+
+ ///
+ /// Поиск
+ ///
+ /// Хранилище
+ /// Поисковый запрос
+ /// Тип поиска
+ /// Страница
+ /// Размер страницы
+ ///
+ public Task> SearchAsync(AuthStorage storage, string searchText, YSearchType searchType, int page = 0, int pageSize = 20)
+ {
+ return new YSearchBuilder(api, storage)
+ .Build((searchText, searchType, page, pageSize))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Подсказка
+ ///
+ /// Хранилище
+ /// Поисковый запрос
+ ///
+ public Task> SuggestAsync(AuthStorage storage, string searchText)
+ {
+ return new YSearchSuggestBuilder(api, storage)
+ .Build(searchText)
+ .GetResponseAsync();
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/API/YTrackAPIAsync.cs b/YandexMusic.API/API/YTrackAPIAsync.cs
new file mode 100644
index 0000000..555465c
--- /dev/null
+++ b/YandexMusic.API/API/YTrackAPIAsync.cs
@@ -0,0 +1,308 @@
+using System.Security.Cryptography;
+using System.Text;
+
+using YandexMusic.API.Common;
+using YandexMusic.API.Models.Common;
+using YandexMusic.API.Models.Track;
+using YandexMusic.API.Requests.Track;
+
+namespace YandexMusic.API
+{
+ ///
+ /// API для взаимодействия с треками
+ ///
+ public partial class YTrackAPI : YCommonAPI
+ {
+ #region Вспомогательные функции
+
+ private string BuildLinkForDownload(YTrackDownloadInfo mainDownloadResponse, YStorageDownloadFile storageDownload)
+ {
+ string path = storageDownload.Path;
+ string host = storageDownload.Host;
+ string ts = storageDownload.Ts;
+ string s = storageDownload.S;
+ string codec = mainDownloadResponse.Codec;
+
+ string secret = $"XGRlBW9FXlekgbPrRHuSiA{path.Substring(1, path.Length - 1)}{s}";
+ MD5 md5 = MD5.Create();
+ byte[] md5Hash = md5.ComputeHash(Encoding.UTF8.GetBytes(secret));
+ HMACSHA1 hmacsha1 = new();
+ byte[] hmasha1Hash = hmacsha1.ComputeHash(md5Hash);
+ string sign = BitConverter.ToString(hmasha1Hash).Replace("-", "").ToLower();
+
+ string link = $"https://{host}/get-{codec}/{sign}/{ts}{path}";
+
+ return link;
+ }
+
+ #endregion Вспомогательные функции
+
+
+
+ public YTrackAPI(YandexMusicApi yandex) : base(yandex)
+ {
+ }
+
+ ///
+ /// Получение треков
+ ///
+ /// Хранилище
+ /// Идентификатор трека
+ ///
+ public Task>> GetAsync(AuthStorage storage, string trackId)
+ {
+ return new YGetTracksBuilder(api, storage)
+ .Build(new[] { trackId })
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение треков
+ ///
+ /// Хранилище
+ /// Идентификаторы треков
+ ///
+ public Task>> GetAsync(AuthStorage storage, IEnumerable trackIds)
+ {
+ return new YGetTracksBuilder(api, storage)
+ .Build(trackIds)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение метаданных для загрузки
+ ///
+ /// Хранилище
+ /// Ключ трека в формате {идентифактор трека:идентификатор альбома}
+ /// Должен ли ответ содержать прямую ссылку на загрузку
+ ///
+ public Task>> GetMetadataForDownloadAsync(AuthStorage storage, string trackKey, bool direct = false)
+ {
+ return new YTrackDownloadInfoBuilder(api, storage)
+ .Build((trackKey, direct))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение метаданных для загрузки
+ ///
+ /// Хранилище
+ /// Трек
+ /// Должен ли ответ содержать прямую ссылку на загрузку
+ ///
+ public Task>> GetMetadataForDownloadAsync(AuthStorage storage, YTrack track, bool direct = false)
+ {
+ return GetMetadataForDownloadAsync(storage, track.GetKey().ToString(), direct);
+ }
+
+ ///
+ /// Получение информации для формирования ссылки для загрузки
+ ///
+ /// Хранилище
+ /// Метаданные для загрузки
+ ///
+ public Task GetDownloadFileInfoAsync(AuthStorage storage, YTrackDownloadInfo metadataInfo)
+ {
+ return new YStorageDownloadFileBuilder(api, storage)
+ .Build(metadataInfo.DownloadInfoUrl)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение ссылки для загрузки
+ ///
+ /// Хранилище
+ /// Ключ трека в формате {идентификатор трека:идентификатор альбома}
+ ///
+ public async Task GetFileLinkAsync(AuthStorage storage, string trackKey)
+ {
+ YResponse> meta = await GetMetadataForDownloadAsync(storage, trackKey);
+ YTrackDownloadInfo info = meta.Result
+ .OrderByDescending(i => i.BitrateInKbps)
+ .First(m => m.Codec == "mp3");
+ YStorageDownloadFile storageDownload = await GetDownloadFileInfoAsync(storage, info);
+ return BuildLinkForDownload(info, storageDownload);
+ }
+
+ ///
+ /// Получение ссылки для загрузки
+ ///
+ /// Хранилище
+ /// Трек
+ ///
+ public Task GetFileLinkAsync(AuthStorage storage, YTrack track)
+ {
+ return GetFileLinkAsync(storage, track.GetKey().ToString());
+ }
+
+ ///
+ /// Отправка текущего состояния прослушиваемого трека
+ /// Хранилище
+ /// Трек
+ /// Наименования клиента, с которого происходит прослушивание
+ /// Проигрывается ли трек с кеша
+ /// Уникальный идентификатор проигрывания
+ /// Уникальный идентификатор плейлиста, если таковой прослушивается
+ /// Сколько было всего воспроизведено трека в секундах
+ /// Окончательное значение воспроизведенных секунд
+ ///
+ ///
+ public Task SendPlayTrackInfoAsync(AuthStorage storage, YTrack track, string from, bool fromCache = false, string playId = "", string playlistId = "", double totalPlayedSeconds = 0, double endPositionSeconds = 0)
+ {
+ return new YSendTrackInfoBuilder(api, storage)
+ .Build((track, from, fromCache, playId, playlistId, totalPlayedSeconds, endPositionSeconds))
+ .GetResponseAsync();
+ }
+
+ #region GetSupplement
+
+ ///
+ /// Получение дополнительной информации для трека
+ ///
+ /// Хранилище
+ /// Идентификатор трека
+ ///
+ public Task> GetSupplementAsync(AuthStorage storage, string trackId)
+ {
+ return new YGetTrackSupplementBuilder(api, storage)
+ .Build(trackId)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение дополнительной информации для трека
+ ///
+ /// Хранилище
+ /// Трек
+ ///
+ public Task> GetSupplementAsync(AuthStorage storage, YTrack track)
+ {
+ return new YGetTrackSupplementBuilder(api, storage)
+ .Build(track.GetKey().ToString())
+ .GetResponseAsync();
+ }
+
+ #endregion GetSupplement
+
+ #region GetSimilar
+
+ ///
+ /// Получение похожих треков
+ ///
+ /// Хранилище
+ /// Идентификатор трека
+ ///
+ public Task> GetSimilarAsync(AuthStorage storage, string trackId)
+ {
+ return new YGetTrackSimilarBuilder(api, storage)
+ .Build(trackId)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение похожих треков
+ ///
+ /// Хранилище
+ /// Трек
+ ///
+ public Task> GetSimilarAsync(AuthStorage storage, YTrack track)
+ {
+ return new YGetTrackSimilarBuilder(api, storage)
+ .Build(track.GetKey().ToString())
+ .GetResponseAsync();
+ }
+
+ #endregion GetSimilar
+
+ #region Получение данных трека
+
+ #region В файл
+
+ ///
+ /// Выгрузка в файл
+ ///
+ /// Хранилище
+ /// Ключ трека в формате {идентификатор трека:идентификатор альбома}
+ /// Путь для файла
+ public async Task ExtractToFileAsync(AuthStorage storage, string trackKey, string filePath)
+ {
+ string url = await GetFileLinkAsync(storage, trackKey);
+ await new DataDownloader(storage).ToFile(url, filePath);
+ }
+
+ ///
+ /// Выгрузка в файл
+ ///
+ /// Хранилище
+ /// Трек
+ /// Путь для файла
+ public Task ExtractToFileAsync(AuthStorage storage, YTrack track, string filePath)
+ {
+ return ExtractToFileAsync(storage, track.GetKey().ToString(), filePath);
+ }
+
+ #endregion В файл
+
+ #region В массив байт
+
+ ///
+ /// Получение двоичного массива данных
+ ///
+ /// Хранилище
+ /// Ключ трека в формате {идентификатор трека:идентификатор альбома}
+ ///
+ public async Task ExtractDataAsync(AuthStorage storage, string trackKey)
+ {
+ string url = await GetFileLinkAsync(storage, trackKey);
+ return await new DataDownloader(storage).AsBytes(url);
+ }
+
+ ///
+ /// Получение двоичного массива данных
+ ///
+ /// Хранилище
+ /// Трек
+ ///
+ public Task ExtractDataAsync(AuthStorage storage, YTrack track)
+ {
+ return ExtractDataAsync(storage, track.GetKey().ToString());
+ }
+
+ #endregion В массив байт
+
+ #region В поток
+
+ ///
+ /// Получение потока данных
+ ///
+ /// Хранилище
+ /// Ключ трека в формате {идентификатор трека:идентификатор альбома}
+ /// Параметры передачи управления при http запросе
+ ///
+ public async Task ExtractStreamAsync(AuthStorage storage, string trackKey,
+ HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
+ {
+ string url = await GetFileLinkAsync(storage, trackKey);
+ return await new DataDownloader(storage).AsStream(url, httpCompletionOption);
+ }
+
+ ///
+ /// Получение потока данных
+ ///
+ /// Хранилище
+ /// Трек
+ /// Параметры передачи управления при http запросе
+ ///
+ public Task ExtractStreamAsync(AuthStorage storage, YTrack track,
+ HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
+ {
+ return ExtractStreamAsync(storage, track.GetKey().ToString(), httpCompletionOption);
+ }
+
+ #endregion В поток
+
+ #endregion Получение данных трека
+
+
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/API/YUgcAPIAsync.cs b/YandexMusic.API/API/YUgcAPIAsync.cs
new file mode 100644
index 0000000..9316b28
--- /dev/null
+++ b/YandexMusic.API/API/YUgcAPIAsync.cs
@@ -0,0 +1,72 @@
+using YandexMusic.API.Common;
+using YandexMusic.API.Models.Common;
+using YandexMusic.API.Models.Playlist;
+using YandexMusic.API.Models.Ugc;
+using YandexMusic.API.Requests.Ugc;
+
+namespace YandexMusic.API
+{
+ public partial class YUgcAPI : YCommonAPI
+ {
+ public YUgcAPI(YandexMusicApi yandex) : base(yandex)
+ {
+ }
+
+ ///
+ /// Получение ссылки на загрузчик трека
+ ///
+ /// Хранилище
+ /// Плейлист, куда будет загружен трек
+ /// Название файла для загрузки
+ public Task GetUgcUploadLinkAsync(AuthStorage storage, YPlaylist playlist, string fileName)
+ {
+ return new YUgcGetUploadLinkBuilder(api, storage)
+ .Build((playlist, fileName))
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Загрузка трека из файла
+ ///
+ /// Хранилище
+ /// Ссылка на балансировщик для загрузки, можно получить из GetUgcUploadLinkAsync
+ /// Загружаемый файл
+ public Task> UploadUgcTrackAsync(AuthStorage storage, string uploadLink, string filePath)
+ {
+ if (!File.Exists(filePath))
+ throw new FileNotFoundException("Файл для загрузки не существует.", filePath);
+
+ return UploadUgcTrackAsync(storage, uploadLink, File.Open(filePath, FileMode.Open));
+ }
+
+ ///
+ /// Загрузка трека из потока
+ ///
+ /// Хранилище
+ /// Ссылка на балансировщик для загрузки, можно получить из GetUgcUploadLinkAsync
+ /// Поток с данными для загрузки
+ public Task> UploadUgcTrackAsync(AuthStorage storage, string uploadLink, Stream stream)
+ {
+ if (stream == null)
+ throw new NullReferenceException("Пустая ссылка на поток загрузки.");
+
+ using MemoryStream ms = new();
+ stream.CopyTo(ms);
+
+ return UploadUgcTrackAsync(storage, uploadLink, ms.ToArray());
+ }
+
+ ///
+ /// Загрузка трека из массива
+ ///
+ /// Хранилище
+ /// Ссылка на балансировщик для загрузки, можно получить из GetUgcUploadLinkAsync
+ /// Загружаемый трек в виде массив байтов
+ public Task> UploadUgcTrackAsync(AuthStorage storage, string uploadLink, byte[] file)
+ {
+ return new YUgcUploadBuilder(api, storage)
+ .Build((uploadLink, file))
+ .GetResponseAsync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/API/YUserAPIAsync.cs b/YandexMusic.API/API/YUserAPIAsync.cs
new file mode 100644
index 0000000..0c84ffa
--- /dev/null
+++ b/YandexMusic.API/API/YUserAPIAsync.cs
@@ -0,0 +1,303 @@
+using System.Security.Authentication;
+using System.Text.RegularExpressions;
+
+using YandexMusic.API.Common;
+using YandexMusic.API.Models.Account;
+using YandexMusic.API.Models.Common;
+using YandexMusic.API.Requests.Account;
+
+namespace YandexMusic.API
+{
+ ///
+ /// API для пользователя
+ ///
+ public partial class YUserAPI : YCommonAPI
+ {
+ #region Вспомогательные функции
+
+ private async Task GetCsrfTokenAsync(AuthStorage storage)
+ {
+ using HttpResponseMessage authMethodsResponse = await new YGetAuthMethodsBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+
+ if (!authMethodsResponse.IsSuccessStatusCode)
+ throw new HttpRequestException("Невозможно получить CFRF-токен.");
+
+ string responseString = await authMethodsResponse.Content
+ .ReadAsStringAsync();
+ Match match = Regex.Match(responseString, "\"csrf_token\" value=\"([^\"]+)\"");
+
+ if (!match.Success || match.Groups.Count < 2)
+ return false;
+
+ storage.AuthToken = new YAuthToken
+ {
+ CsfrToken = match.Groups[1].Value
+ };
+
+ return true;
+ }
+
+ private async Task LoginByCookiesAsync(AuthStorage storage)
+ {
+ if (storage.AuthToken == null)
+ throw new AuthenticationException("Невозможно инициализировать сессию входа.");
+
+ YAccessToken accessToken = await new YGetAuthCookiesBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+
+ storage.IsAuthorized = !string.IsNullOrEmpty(accessToken.AccessToken);
+
+ storage.AccessToken = accessToken;
+ storage.Token = accessToken.AccessToken;
+
+ YShortAccountInfo validateTokenResponse = await new YGetShortAccountInifoBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+
+ if (validateTokenResponse.Status != YAuthStatus.Ok)
+ throw new Exception("Вход в аккаунт не выполнен.");
+
+ storage.IsAuthorized = !string.IsNullOrWhiteSpace(validateTokenResponse.Uid);
+
+ return storage.IsAuthorized;
+ }
+
+ #endregion Вспомогательные функции
+
+
+
+ public YUserAPI(YandexMusicApi yandex) : base(yandex)
+ {
+ }
+
+ ///
+ /// Авторизация
+ ///
+ /// Хранилище
+ /// Токен авторизации
+ ///
+ public async Task AuthorizeAsync(AuthStorage storage, string token)
+ {
+ if (string.IsNullOrEmpty(token))
+ throw new Exception("Задан пустой токен авторизации.");
+
+ storage.Token = token;
+
+ // Пытаемся получить информацию о пользователе
+ YResponse authInfo = await GetUserAuthAsync(storage);
+
+ // Если не авторизован, то авторизуем
+ if (string.IsNullOrEmpty(authInfo.Result.Account.Uid))
+ throw new Exception("Пользователь незалогинен.");
+
+ // Флаг авторизации
+ storage.IsAuthorized = true;
+ storage.User = authInfo.Result.Account;
+ }
+
+ ///
+ /// Получение информации об авторизации
+ ///
+ /// Хранилище
+ ///
+ public Task> GetUserAuthAsync(AuthStorage storage)
+ {
+ return new YGetAuthInfoBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Создание сеанса и получение доступных методов авторизации
+ ///
+ /// Хранилище
+ /// Имя пользователя
+ ///
+ public async Task CreateAuthSessionAsync(AuthStorage storage, string userName)
+ {
+ if (!await GetCsrfTokenAsync(storage))
+ throw new Exception("Невозможно инициализировать сессию входа.");
+
+ YAuthTypes types = await new YGetAuthLoginUserBuilder(api, storage)
+ .Build((storage.AuthToken.CsfrToken, userName))
+ .GetResponseAsync();
+
+ storage.AuthToken.TrackId = types.TrackId;
+
+ return types;
+ }
+
+ ///
+ /// Получение ссылки на QR-код
+ ///
+ /// Хранилище
+ ///
+ public async Task GetAuthQRLinkAsync(AuthStorage storage)
+ {
+ if (!await GetCsrfTokenAsync(storage))
+ throw new Exception("Невозможно инициализировать сессию входа.");
+
+ YAuthQR result = await new YGetAuthQRBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+
+ if (result.Status != YAuthStatus.Ok)
+ return string.Empty;
+
+ storage.AuthToken = new YAuthToken
+ {
+ TrackId = result.TrackId,
+ CsfrToken = result.CsrfToken
+ };
+
+ return $"https://passport.yandex.ru/auth/magic/code/?track_id={result.TrackId}";
+ }
+
+ ///
+ /// Авторизация по QR-коду
+ ///
+ /// Хранилище
+ ///
+ public async Task AuthorizeByQRAsync(AuthStorage storage)
+ {
+ if (storage.AuthToken == null)
+ throw new Exception("Не выполнен запрос на авторизацию по QR.");
+
+ try
+ {
+ YAuthQRStatus qrStatus = await new YGetAuthLoginQRBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+ if (qrStatus.Status != YAuthStatus.Ok)
+ return qrStatus;
+
+ bool ok = await LoginByCookiesAsync(storage);
+ if (!ok)
+ throw new AuthenticationException("Ошибка авторизации по QR.");
+
+ return qrStatus;
+ }
+ catch (Exception ex)
+ {
+ throw new AuthenticationException("Ошибка авторизации по QR.", ex);
+ }
+ }
+
+ ///
+ /// Получение
+ ///
+ /// Хранилище
+ ///
+ public Task GetCaptchaAsync(AuthStorage storage)
+ {
+ if (storage.AuthToken == null || string.IsNullOrWhiteSpace(storage.AuthToken.CsfrToken))
+ throw new AuthenticationException($"Не найдена сессия входа. Выполните {nameof(CreateAuthSessionAsync)} перед использованием.");
+
+ return new YGetAuthCaptchaBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Авторизация по captcha
+ ///
+ /// Хранилище
+ /// Значение captcha
+ ///
+ public Task AuthorizeByCaptchaAsync(AuthStorage storage, string captchaValue)
+ {
+ if (storage.AuthToken == null || string.IsNullOrWhiteSpace(storage.AuthToken.CsfrToken))
+ throw new AuthenticationException($"Не найдена сессия входа. Выполните {nameof(CreateAuthSessionAsync)} перед использованием.");
+
+ return new YGetAuthLoginCaptchaBuilder(api, storage)
+ .Build(captchaValue)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Получение письма авторизации на почту пользователя
+ ///
+ /// Хранилище
+ ///
+ public Task GetAuthLetterAsync(AuthStorage storage)
+ {
+ return new YGetAuthLetterBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+ }
+
+ ///
+ /// Авторизация после подтверждения входа через письмо
+ ///
+ /// Хранилище
+ ///
+ public async Task AuthorizeByLetterAsync(AuthStorage storage)
+ {
+ YAuthLetterStatus status = await new YGetAuthLoginLetterBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+
+ if (status.Status == YAuthStatus.Ok && !status.MagicLinkConfirmed)
+ throw new Exception("Не подтвержден вход посредством e-mail.");
+
+ return await LoginByCookiesAsync(storage);
+ }
+
+ ///
+ /// Авторизация с помощью пароля из приложения Яндекс
+ ///
+ /// Хранилище
+ /// Пароль
+ ///
+ public async Task AuthorizeByAppPasswordAsync(AuthStorage storage, string password)
+ {
+ if (storage.AuthToken == null || string.IsNullOrWhiteSpace(storage.AuthToken.CsfrToken))
+ throw new AuthenticationException($"Не найдена сессия входа. Выполните {nameof(CreateAuthSessionAsync)} перед использованием.");
+
+ YAuthBase response = await new YGetAuthAppPasswordBuilder(api, storage)
+ .Build(password)
+ .GetResponseAsync();
+
+ if (response.Status == YAuthStatus.Ok)
+ {
+ bool ok = await LoginByCookiesAsync(storage);
+ if (!ok)
+ throw new AuthenticationException("Ошибка авторизации.");
+ }
+
+ return response;
+ }
+
+ ///
+ /// Получение после авторизации с помощью QR, e-mail, пароля из приложения
+ ///
+ public async Task GetAccessTokenAsync(AuthStorage storage)
+ {
+ if (storage.AuthToken == null)
+ throw new Exception("Не найдена сессия входа.");
+
+ YAccessToken accessToken = await new YGetMusicTokenBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+
+ storage.Token = accessToken.AccessToken;
+
+ return accessToken;
+ }
+
+ ///
+ /// Получение информации о пользователе через логин Яндекса
+ ///
+ public Task GetLoginInfoAsync(AuthStorage storage)
+ {
+ return new YGetLoginInfoBuilder(api, storage)
+ .Build(null)
+ .GetResponseAsync();
+ }
+
+
+ }
+}
diff --git a/YandexMusic.API/Common/AuthStorage.cs b/YandexMusic.API/Common/AuthStorage.cs
new file mode 100644
index 0000000..81ba0ee
--- /dev/null
+++ b/YandexMusic.API/Common/AuthStorage.cs
@@ -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
+{
+ ///
+ /// Хранилище данных пользователя
+ ///
+ public class AuthStorage
+ {
+ #region Свойства
+
+ ///
+ /// Http-контекст
+ ///
+ public HttpContext Context { get; }
+
+ public DebugSettings Debug { get; set; }
+
+ ///
+ /// Флаг авторизации
+ ///
+ public bool IsAuthorized { get; internal set; }
+
+ ///
+ /// Идентификатор устройства
+ ///
+ public string DeviceId { get; set; } = "csharp";
+
+ ///
+ /// Токен авторизации
+ ///
+ public string Token { get; internal set; }
+
+ ///
+ /// Аккаунт
+ ///
+ public YAccount User { get; set; }
+
+ ///
+ /// Провайдер запросов
+ ///
+ public IRequestProvider Provider { get; }
+
+ ///
+ /// Токен доступа
+ ///
+ public YAccessToken AccessToken { get; set; }
+
+ internal YAuthToken AuthToken { get; set; }
+
+ #endregion Свойства
+
+
+
+ ///
+ /// Конструктор
+ ///
+ public AuthStorage(DebugSettings settings = null)
+ {
+ User = new YAccount();
+ Context = new HttpContext();
+ Debug = settings;
+ Provider = new DefaultRequestProvider(this);
+
+ if (Debug is { ClearDirectory: true })
+ {
+ Debug.Clear();
+ }
+ }
+
+ ///
+ /// Конструктор
+ ///
+ public AuthStorage(IRequestProvider provider, DebugSettings settings = null)
+ {
+ User = new YAccount();
+ Context = new HttpContext();
+ Debug = settings;
+ Provider = provider;
+
+ if (Debug is { ClearDirectory: true })
+ {
+ Debug.Clear();
+ }
+ }
+
+ ///
+ /// Установка прокси для пользователия
+ ///
+ /// Прокси
+ public void SetProxy(IWebProxy proxy)
+ {
+ Context.WebProxy = proxy;
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Common/DataDownloader.cs b/YandexMusic.API/Common/DataDownloader.cs
new file mode 100644
index 0000000..fbec075
--- /dev/null
+++ b/YandexMusic.API/Common/DataDownloader.cs
@@ -0,0 +1,44 @@
+using System.Net;
+
+namespace YandexMusic.API.Common
+{
+ ///
+ /// Загрузчик файлов по ссылке
+ ///
+ public class DataDownloader
+ {
+ private AuthStorage authStorage;
+
+ private async Task 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 AsStream(string url, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
+ {
+ HttpContent content = await GetResponseContent(url, httpCompletionOption);
+ return await content.ReadAsStreamAsync();
+ }
+
+ public async Task 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Common/Encryptor.cs b/YandexMusic.API/Common/Encryptor.cs
new file mode 100644
index 0000000..541a57e
--- /dev/null
+++ b/YandexMusic.API/Common/Encryptor.cs
@@ -0,0 +1,75 @@
+using System.Security.Cryptography;
+using System.Text;
+
+namespace YandexMusic.API.Common
+{
+ ///
+ /// Класс для шифровки
+ ///
+ 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();
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Common/Providers/CommonRequestProvider.cs b/YandexMusic.API/Common/Providers/CommonRequestProvider.cs
new file mode 100644
index 0000000..de4ac1d
--- /dev/null
+++ b/YandexMusic.API/Common/Providers/CommonRequestProvider.cs
@@ -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 GetWebResponseAsync(HttpRequestMessage message, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
+ {
+ throw new NotImplementedException();
+ }
+
+ public virtual async Task GetDataFromResponseAsync(YandexMusicApi api, HttpResponseMessage response)
+ {
+ string result = await response.Content.ReadAsStringAsync();
+
+ if (!response.IsSuccessStatusCode)
+ {
+ YErrorResponse exception = JsonConvert.DeserializeObject(result);
+ throw exception ?? new Exception("Ошибка десериализации ответа с ошибкой.");
+ }
+
+ try
+ {
+ JsonSerializerSettings settings = new()
+ {
+ Converters = new List {
+ new YExecutionContextConverter(api, storage)
+ }
+ };
+
+ return storage.Debug != null
+ ? storage.Debug.Deserialize(response.RequestMessage?.RequestUri?.AbsolutePath, result, settings)
+ : JsonConvert.DeserializeObject(result, settings);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Ошибка десериализации {ex}");
+ }
+ }
+
+ #endregion IRequestProvider
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Common/Providers/DefaultRequestProvider.cs b/YandexMusic.API/Common/Providers/DefaultRequestProvider.cs
new file mode 100644
index 0000000..c46820f
--- /dev/null
+++ b/YandexMusic.API/Common/Providers/DefaultRequestProvider.cs
@@ -0,0 +1,69 @@
+using System.Net;
+
+using YandexMusic.API.Models.Common;
+
+namespace YandexMusic.API.Common.Providers
+{
+ ///
+ /// Стандартный провайдер запросов
+ ///
+ 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(result);
+
+ return exception ?? ex;
+ }
+
+ #endregion Вспомогательные функции
+
+
+
+ public DefaultRequestProvider(AuthStorage authStorage) : base(authStorage)
+ {
+ }
+
+
+
+ #region IRequestProvider
+
+ public override Task 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
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Common/Providers/IRequestProvider.cs b/YandexMusic.API/Common/Providers/IRequestProvider.cs
new file mode 100644
index 0000000..d48b18e
--- /dev/null
+++ b/YandexMusic.API/Common/Providers/IRequestProvider.cs
@@ -0,0 +1,25 @@
+namespace YandexMusic.API.Common.Providers
+{
+ ///
+ /// Интерфейс для провайдеров обработки запросов
+ ///
+ public interface IRequestProvider
+ {
+ ///
+ /// Функция получения ответа
+ ///
+ /// Запрос
+ /// Опция завершения запроса
+ ///
+ Task GetWebResponseAsync(HttpRequestMessage message, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead);
+
+ ///
+ /// Функция формирования ответа
+ ///
+ /// Тип объекта с ответом
+ /// API
+ /// Ответ
+ ///
+ Task GetDataFromResponseAsync(YandexMusicApi api, HttpResponseMessage response);
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Common/Providers/MockRequestProvider.cs b/YandexMusic.API/Common/Providers/MockRequestProvider.cs
new file mode 100644
index 0000000..df3937d
--- /dev/null
+++ b/YandexMusic.API/Common/Providers/MockRequestProvider.cs
@@ -0,0 +1,27 @@
+namespace YandexMusic.API.Common.Providers
+{
+ ///
+ /// Провайдер запросов данными из файла
+ ///
+ public class MockRequestProvider : CommonRequestProvider
+ {
+
+
+ public MockRequestProvider(AuthStorage authStorage) : base(authStorage)
+ {
+ storage = authStorage;
+ }
+
+
+
+ #region IRequestProvider
+
+ public override Task GetWebResponseAsync(HttpRequestMessage message,
+ HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
+ {
+ throw new NotImplementedException();
+ }
+
+ #endregion IRequestProvider
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Common/Ynison/UpperSnakeCaseNamingStrategy.cs b/YandexMusic.API/Common/Ynison/UpperSnakeCaseNamingStrategy.cs
new file mode 100644
index 0000000..752f9ba
--- /dev/null
+++ b/YandexMusic.API/Common/Ynison/UpperSnakeCaseNamingStrategy.cs
@@ -0,0 +1,7 @@
+namespace YandexMusic.API.Common.Ynison
+{
+ public class UpperSnakeCaseNamingStrategy : SnakeCaseNamingStrategy
+ {
+ protected override string ResolvePropertyName(string name) => base.ResolvePropertyName(name).ToUpper();
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Common/Ynison/YnisonPlayer.cs b/YandexMusic.API/Common/Ynison/YnisonPlayer.cs
new file mode 100644
index 0000000..713f208
--- /dev/null
+++ b/YandexMusic.API/Common/Ynison/YnisonPlayer.cs
@@ -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 {
+ 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 Свойства
+
+ ///
+ /// API
+ ///
+ public YandexMusicApi API { get; internal set; }
+
+ ///
+ /// Состояние
+ ///
+ public YYnisonState State { get; internal set; }
+
+ ///
+ /// Текущий проигрываемый трек
+ ///
+ public YTrack Current => GetCurrent();
+
+ #endregion Свойства
+
+ #region События
+
+ public class ReceiveEventArgs
+ {
+ public YYnisonState State { get; internal set; }
+ }
+
+ public delegate void OnReceiveEventHandler(YnisonPlayer player, ReceiveEventArgs args);
+
+ ///
+ /// Получение данных
+ ///
+ 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);
+
+ ///
+ /// Получение данных
+ ///
+ public event OnCloseEventHandler OnClose;
+
+ #endregion События
+
+ #region Вспомогательные функции
+
+ private string SerializeJson(object data)
+ {
+ return JsonConvert.SerializeObject(data, jsonSettings);
+ }
+
+ private T Deserialize(YYnisonMessageType messageType, string data)
+ {
+ return storage.Debug != null
+ ? storage.Debug.Deserialize($"Ynison{messageType}", data, jsonSettings)
+ : JsonConvert.DeserializeObject(data, jsonSettings);
+ }
+
+ private T DeserializeMessage(YYnisonMessageType messageType, string data)
+ {
+ JObject o = JObject.Parse(data);
+ // Сообщение с ошибкой
+ if (o.ContainsKey("error"))
+ {
+ YYnisonErrorMessage exception = Deserialize(YYnisonMessageType.Error, data);
+ throw exception ?? new Exception("Ошибка десериализации ответа с ошибкой.");
+ }
+
+ return Deserialize(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(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(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 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
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Common/Ynison/YnisonWebSocket.cs b/YandexMusic.API/Common/Ynison/YnisonWebSocket.cs
new file mode 100644
index 0000000..01363fe
--- /dev/null
+++ b/YandexMusic.API/Common/Ynison/YnisonWebSocket.cs
@@ -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 {
+ 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);
+ ///
+ /// Получение данных
+ ///
+ 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);
+ ///
+ /// Закрытие соединения
+ ///
+ public event OnCloseEventHandler OnClose;
+
+ #endregion События
+
+ #region Вспомогательные функции
+
+ private string SerializeJson(object obj)
+ {
+ return JsonConvert.SerializeObject(obj, jsonSettings);
+ }
+
+ private string GetProtocolData(string deviceId, string redirectTicket)
+ {
+ Dictionary deviceInfo = new() {
+ { "app_name", "Chrome" },
+ { "type", 1 }
+ };
+
+ Dictionary 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 ReadSocketContent()
+ {
+ byte[] buffer = new byte[size];
+ WebSocketReceiveResult result;
+
+ do
+ {
+ result = await socketClient.ReceiveAsync(new ArraySegment(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 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
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Extensions/API/YAlbumExtensions.cs b/YandexMusic.API/Extensions/API/YAlbumExtensions.cs
new file mode 100644
index 0000000..5585628
--- /dev/null
+++ b/YandexMusic.API/Extensions/API/YAlbumExtensions.cs
@@ -0,0 +1,25 @@
+using YandexMusic.API.Models.Album;
+
+namespace YandexMusic.API.Extensions.API
+{
+ ///
+ /// Методы-расширения для альбома
+ ///
+ public static partial class YAlbumExtensions
+ {
+ public static YAlbum WithTracks(this YAlbum album)
+ {
+ return WithTracksAsync(album).GetAwaiter().GetResult();
+ }
+
+ public static string AddLike(this YAlbum album)
+ {
+ return AddLikeAsync(album).GetAwaiter().GetResult();
+ }
+
+ public static string RemoveLike(this YAlbum album)
+ {
+ return RemoveLikeAsync(album).GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/YandexMusic.API/Extensions/API/YAlbumExtensionsAsync.cs b/YandexMusic.API/Extensions/API/YAlbumExtensionsAsync.cs
new file mode 100644
index 0000000..ca5243c
--- /dev/null
+++ b/YandexMusic.API/Extensions/API/YAlbumExtensionsAsync.cs
@@ -0,0 +1,30 @@
+using YandexMusic.API.Models.Album;
+
+namespace YandexMusic.API.Extensions.API
+{
+ ///
+ /// Методы-расширения для альбома
+ ///
+ public static partial class YAlbumExtensions
+ {
+ public static async Task WithTracksAsync(this YAlbum album)
+ {
+ return album.Volumes != null
+ ? album
+ : (await album.Context.API.Album.GetAsync(album.Context.Storage, album.Id))
+ .Result;
+ }
+
+ public static async Task AddLikeAsync(this YAlbum album)
+ {
+ return (await album.Context.API.Library.AddAlbumLikeAsync(album.Context.Storage, album))
+ .Result;
+ }
+
+ public static async Task RemoveLikeAsync(this YAlbum album)
+ {
+ return (await album.Context.API.Library.RemoveAlbumLikeAsync(album.Context.Storage, album))
+ .Result;
+ }
+ }
+}
diff --git a/YandexMusic.API/Extensions/API/YArtistExtensions.cs b/YandexMusic.API/Extensions/API/YArtistExtensions.cs
new file mode 100644
index 0000000..e9c4f2e
--- /dev/null
+++ b/YandexMusic.API/Extensions/API/YArtistExtensions.cs
@@ -0,0 +1,36 @@
+using YandexMusic.API.Models.Artist;
+using YandexMusic.API.Models.Track;
+
+namespace YandexMusic.API.Extensions.API
+{
+ ///
+ /// Методы-расширения для исполнителя
+ ///
+ public static partial class YArtistExtensions
+ {
+ public static YArtistBriefInfo BriefInfo(this YArtist artist)
+ {
+ return BriefInfoAsync(artist).GetAwaiter().GetResult();
+ }
+
+ public static YTracksPage GetTracks(this YArtist artist, int page = 0, int pageSize = 20)
+ {
+ return GetTracksAsync(artist, page, pageSize).GetAwaiter().GetResult();
+ }
+
+ public static List GetAllTracks(this YArtist artist)
+ {
+ return GetAllTracksAsync(artist).GetAwaiter().GetResult();
+ }
+
+ public static string AddLike(this YArtist artist)
+ {
+ return AddLikeAsync(artist).GetAwaiter().GetResult();
+ }
+
+ public static string RemoveLike(this YArtist artist)
+ {
+ return RemoveLikeAsync(artist).GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/YandexMusic.API/Extensions/API/YArtistExtensionsAsync.cs b/YandexMusic.API/Extensions/API/YArtistExtensionsAsync.cs
new file mode 100644
index 0000000..6812591
--- /dev/null
+++ b/YandexMusic.API/Extensions/API/YArtistExtensionsAsync.cs
@@ -0,0 +1,41 @@
+using YandexMusic.API.Models.Artist;
+using YandexMusic.API.Models.Track;
+
+namespace YandexMusic.API.Extensions.API
+{
+ ///
+ /// Методы-расширения для исполнителя
+ ///
+ public static partial class YArtistExtensions
+ {
+ public static async Task BriefInfoAsync(this YArtist artist)
+ {
+ return (await artist.Context.API.Artist.GetAsync(artist.Context.Storage, artist.Id))
+ .Result;
+ }
+
+ public static async Task GetTracksAsync(this YArtist artist, int page = 0, int pageSize = 20)
+ {
+ return (await artist.Context.API.Artist.GetTracksAsync(artist.Context.Storage, artist.Id, page, pageSize))
+ .Result;
+ }
+
+ public static async Task> GetAllTracksAsync(this YArtist artist)
+ {
+ return (await artist.Context.API.Artist.GetAllTracksAsync(artist.Context.Storage, artist.Id))
+ .Result.Tracks;
+ }
+
+ public static async Task AddLikeAsync(this YArtist artist)
+ {
+ return (await artist.Context.API.Library.AddArtistLikeAsync(artist.Context.Storage, artist))
+ .Result;
+ }
+
+ public static async Task RemoveLikeAsync(this YArtist artist)
+ {
+ return (await artist.Context.API.Library.RemoveArtistLikeAsync(artist.Context.Storage, artist))
+ .Result;
+ }
+ }
+}
diff --git a/YandexMusic.API/Extensions/API/YPlaylistExtensions.cs b/YandexMusic.API/Extensions/API/YPlaylistExtensions.cs
new file mode 100644
index 0000000..ef3b46f
--- /dev/null
+++ b/YandexMusic.API/Extensions/API/YPlaylistExtensions.cs
@@ -0,0 +1,56 @@
+using YandexMusic.API.Models.Playlist;
+using YandexMusic.API.Models.Track;
+
+namespace YandexMusic.API.Extensions.API
+{
+ ///
+ /// Методы-расширения для плейлиста
+ ///
+ public static partial class YPlaylistExtensions
+ {
+ private static bool CheckUser(YPlaylist playlist)
+ {
+ return playlist.Owner.Uid == playlist.Context.Storage.User.Uid;
+ }
+
+ public static YPlaylist WithTracks(this YPlaylist playlist)
+ {
+ return WithTracksAsync(playlist).GetAwaiter().GetResult();
+ }
+
+ public static string AddLike(this YPlaylist playlist)
+ {
+ return AddLikeAsync(playlist).GetAwaiter().GetResult();
+ }
+
+ public static string RemoveLike(this YPlaylist playlist)
+ {
+ return RemoveLikeAsync(playlist).GetAwaiter().GetResult();
+ }
+
+ public static YPlaylist Rename(this YPlaylist playlist, string newName)
+ {
+ return RenameAsync(playlist, newName).GetAwaiter().GetResult();
+ }
+
+ public static bool Delete(this YPlaylist playlist)
+ {
+ return DeleteAsync(playlist).GetAwaiter().GetResult();
+ }
+
+ public static YPlaylist InsertTracks(this YPlaylist playlist, params YTrack[] tracks)
+ {
+ return InsertTracksAsync(playlist, tracks).GetAwaiter().GetResult();
+ }
+
+ public static YPlaylist RemoveTracks(this YPlaylist playlist, params YTrack[] tracks)
+ {
+ return RemoveTracksAsync(playlist, tracks).GetAwaiter().GetResult();
+ }
+
+ public static bool UploadTracks(this YPlaylist playlist, string filePath, string fileName)
+ {
+ return UploadTracksAsync(playlist, filePath, fileName).GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/YandexMusic.API/Extensions/API/YPlaylistExtensionsAsync.cs b/YandexMusic.API/Extensions/API/YPlaylistExtensionsAsync.cs
new file mode 100644
index 0000000..e1338d3
--- /dev/null
+++ b/YandexMusic.API/Extensions/API/YPlaylistExtensionsAsync.cs
@@ -0,0 +1,72 @@
+using YandexMusic.API.Models.Playlist;
+using YandexMusic.API.Models.Track;
+
+namespace YandexMusic.API.Extensions.API
+{
+ ///
+ /// Методы-расширения для плейлиста
+ ///
+ public static partial class YPlaylistExtensions
+ {
+ public static async Task WithTracksAsync(this YPlaylist playlist)
+ {
+ return playlist.Tracks != null
+ ? playlist
+ : (await playlist.Context.API.Playlist.GetAsync(playlist.Context.Storage, playlist))
+ .Result;
+ }
+
+ public static async Task AddLikeAsync(this YPlaylist playlist)
+ {
+ return (await playlist.Context.API.Library.AddPlaylistLikeAsync(playlist.Context.Storage, playlist))
+ .Result;
+ }
+
+ public static async Task RemoveLikeAsync(this YPlaylist playlist)
+ {
+ return (await playlist.Context.API.Library.RemovePlaylistLikeAsync(playlist.Context.Storage, playlist))
+ .Result;
+ }
+
+ public static async Task RenameAsync(this YPlaylist playlist, string newName)
+ {
+ return CheckUser(playlist)
+ ? (await playlist.Context.API.Playlist.RenameAsync(playlist.Context.Storage, playlist, newName))
+ .Result
+ : playlist;
+ }
+
+ public static async Task DeleteAsync(this YPlaylist playlist)
+ {
+ return CheckUser(playlist) && await playlist.Context.API.Playlist.DeleteAsync(playlist.Context.Storage, playlist);
+ }
+
+ public static async Task InsertTracksAsync(this YPlaylist playlist, params YTrack[] tracks)
+ {
+ return CheckUser(playlist)
+ ? (await playlist.Context.API.Playlist.InsertTracksAsync(playlist.Context.Storage, playlist, tracks))
+ .Result
+ : playlist;
+ }
+
+ public static async Task RemoveTracksAsync(this YPlaylist playlist, params YTrack[] tracks)
+ {
+ return CheckUser(playlist)
+ ? (await playlist.Context.API.Playlist.DeleteTracksAsync(playlist.Context.Storage, playlist, tracks))
+ .Result
+ : playlist;
+ }
+
+ public static async Task UploadTracksAsync(this YPlaylist playlist, string filePath, string fileName)
+ {
+ if (!CheckUser(playlist))
+ return false;
+
+ string target = (await playlist.Context.API.UserGeneratedContent.GetUgcUploadLinkAsync(playlist.Context.Storage, playlist, fileName))
+ .PostTarget;
+
+ return (await playlist.Context.API.UserGeneratedContent.UploadUgcTrackAsync(playlist.Context.Storage, target, filePath))
+ .Result == "CREATED";
+ }
+ }
+}
diff --git a/YandexMusic.API/Extensions/API/YStationResultExtensions.cs b/YandexMusic.API/Extensions/API/YStationResultExtensions.cs
new file mode 100644
index 0000000..39e11a0
--- /dev/null
+++ b/YandexMusic.API/Extensions/API/YStationResultExtensions.cs
@@ -0,0 +1,26 @@
+using YandexMusic.API.Models.Radio;
+using YandexMusic.API.Models.Track;
+
+namespace YandexMusic.API.Extensions.API
+{
+ ///
+ /// Методы-расширения для радиостанции
+ ///
+ public static partial class YStationResultExtensions
+ {
+ public static List GetTracks(this YStation station, string prevTrackId = "")
+ {
+ return GetTracksAsync(station, prevTrackId).GetAwaiter().GetResult();
+ }
+
+ public static string SetSettings2(this YStation station, YStationSettings2 settings)
+ {
+ return SetSettings2Async(station, settings).GetAwaiter().GetResult();
+ }
+
+ public static string SendFeedBack(this YStation station, YStationFeedbackType type, YTrack track = null, string batchId = "", double totalPlayedSeconds = 0)
+ {
+ return SendFeedBackAsync(station, type, track, batchId, totalPlayedSeconds).GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/YandexMusic.API/Extensions/API/YStationResultExtensionsAsync.cs b/YandexMusic.API/Extensions/API/YStationResultExtensionsAsync.cs
new file mode 100644
index 0000000..764e007
--- /dev/null
+++ b/YandexMusic.API/Extensions/API/YStationResultExtensionsAsync.cs
@@ -0,0 +1,28 @@
+using YandexMusic.API.Models.Radio;
+using YandexMusic.API.Models.Track;
+
+namespace YandexMusic.API.Extensions.API
+{
+ ///
+ /// Методы-расширения для радиостанции
+ ///
+ public static partial class YStationResultExtensions
+ {
+ public static async Task> GetTracksAsync(this YStation station, string prevTrackId = "")
+ {
+ return (await station.Context.API.Radio.GetStationTracksAsync(station.Context.Storage, station, prevTrackId))
+ .Result.Sequence;
+ }
+
+ public static async Task SetSettings2Async(this YStation station, YStationSettings2 settings)
+ {
+ return (await station.Context.API.Radio.SetStationSettings2Async(station.Context.Storage, station, settings))
+ .Result;
+ }
+
+ public static Task SendFeedBackAsync(this YStation station, YStationFeedbackType type, YTrack track = null, string batchId = "", double totalPlayedSeconds = 0)
+ {
+ return station.Context.API.Radio.SendStationFeedBackAsync(station.Context.Storage, station, type, track, batchId, totalPlayedSeconds);
+ }
+ }
+}
diff --git a/YandexMusic.API/Extensions/API/YTrackExtensions.cs b/YandexMusic.API/Extensions/API/YTrackExtensions.cs
new file mode 100644
index 0000000..052e621
--- /dev/null
+++ b/YandexMusic.API/Extensions/API/YTrackExtensions.cs
@@ -0,0 +1,55 @@
+using YandexMusic.API.Models.Track;
+
+namespace YandexMusic.API.Extensions.API
+{
+ ///
+ /// Методы-расширения для трека
+ ///
+ public static partial class YTrackExtensions
+ {
+ public static string GetLink(this YTrack track)
+ {
+ return GetLinkAsync(track).GetAwaiter().GetResult();
+ }
+
+ public static void Save(this YTrack track, string filePath)
+ {
+ SaveAsync(track, filePath).GetAwaiter().GetResult();
+ }
+
+ public static int AddLike(this YTrack track)
+ {
+ return AddLikeAsync(track).GetAwaiter().GetResult();
+ }
+
+ public static int RemoveLike(this YTrack track)
+ {
+ return RemoveLikeAsync(track).GetAwaiter().GetResult();
+ }
+
+ public static int AddDislike(this YTrack track)
+ {
+ return AddDislikeAsync(track).GetAwaiter().GetResult();
+ }
+
+ public static int RemoveDislike(this YTrack track)
+ {
+ return RemoveDislikeAsync(track).GetAwaiter().GetResult();
+ }
+
+ public static string SendPlayTrackInfo(this YTrack track, string from, bool fromCache = false, string playId = "", string playlistId = "", double totalPlayedSeconds = 0, double endPositionSeconds = 0)
+ {
+ return SendPlayTrackInfoAsync(track, from, fromCache, playId, playlistId, totalPlayedSeconds).GetAwaiter().GetResult();
+ }
+
+ public static YTrackSupplement Supplement(this YTrack track)
+ {
+ return SupplementAsync(track).GetAwaiter().GetResult();
+ }
+
+ public static YTrackSimilar Similar(this YTrack track)
+ {
+ return SimilarAsync(track).GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/YandexMusic.API/Extensions/API/YTrackExtensionsAsync.cs b/YandexMusic.API/Extensions/API/YTrackExtensionsAsync.cs
new file mode 100644
index 0000000..cbfba1f
--- /dev/null
+++ b/YandexMusic.API/Extensions/API/YTrackExtensionsAsync.cs
@@ -0,0 +1,61 @@
+using YandexMusic.API.Models.Track;
+
+namespace YandexMusic.API.Extensions.API
+{
+ ///
+ /// Методы-расширения для трека
+ ///
+ public static partial class YTrackExtensions
+ {
+ public static Task GetLinkAsync(this YTrack track)
+ {
+ return track.Context.API.Track.GetFileLinkAsync(track.Context.Storage, track);
+ }
+
+ public static Task SaveAsync(this YTrack track, string filePath)
+ {
+ return track.Context.API.Track.ExtractToFileAsync(track.Context.Storage, track, filePath);
+ }
+
+ public static async Task AddLikeAsync(this YTrack track)
+ {
+ return (await track.Context.API.Library.AddTrackLikeAsync(track.Context.Storage, track))
+ .Result.Revision;
+ }
+
+ public static async Task RemoveLikeAsync(this YTrack track)
+ {
+ return (await track.Context.API.Library.RemoveTrackLikeAsync(track.Context.Storage, track))
+ .Result.Revision;
+ }
+
+ public static async Task AddDislikeAsync(this YTrack track)
+ {
+ return (await track.Context.API.Library.AddTrackDislikeAsync(track.Context.Storage, track))
+ .Result.Revision;
+ }
+
+ public static async Task RemoveDislikeAsync(this YTrack track)
+ {
+ return (await track.Context.API.Library.RemoveTrackDislikeAsync(track.Context.Storage, track))
+ ?.Result.Revision ?? -1;
+ }
+
+ public static Task SendPlayTrackInfoAsync(this YTrack track, string from, bool fromCache = false, string playId = "", string playlistId = "", double totalPlayedSeconds = 0, double endPositionSeconds = 0)
+ {
+ return track.Context.API.Track.SendPlayTrackInfoAsync(track.Context.Storage, track, from, fromCache, playId, playlistId, totalPlayedSeconds);
+ }
+
+ public static async Task SupplementAsync(this YTrack track)
+ {
+ return (await track.Context.API.Track.GetSupplementAsync(track.Context.Storage, track))
+ .Result;
+ }
+
+ public static async Task SimilarAsync(this YTrack track)
+ {
+ return (await track.Context.API.Track.GetSimilarAsync(track.Context.Storage, track))
+ .Result;
+ }
+ }
+}
diff --git a/YandexMusic.API/Extensions/HttpRequestHeaderExtensions.cs b/YandexMusic.API/Extensions/HttpRequestHeaderExtensions.cs
new file mode 100644
index 0000000..8d6c8e3
--- /dev/null
+++ b/YandexMusic.API/Extensions/HttpRequestHeaderExtensions.cs
@@ -0,0 +1,12 @@
+using System.Net;
+
+namespace YandexMusic.API.Extensions
+{
+ public static class HttpRequestHeaderExtensions
+ {
+ public static string GetName(this HttpRequestHeader header)
+ {
+ return header.ToString().SplitByCapitalLetter("-");
+ }
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Extensions/StringExtensions.cs b/YandexMusic.API/Extensions/StringExtensions.cs
new file mode 100644
index 0000000..e4911f2
--- /dev/null
+++ b/YandexMusic.API/Extensions/StringExtensions.cs
@@ -0,0 +1,50 @@
+using System.Text.RegularExpressions;
+
+namespace YandexMusic.API.Extensions
+{
+ public static class StringExtensions
+ {
+ public static string ReplaceRegex(this string str, string regExpr, string replStr, RegexOptions options = RegexOptions.IgnoreCase)
+ {
+ return str == null
+ ? string.Empty
+ : Regex.Replace(str, regExpr, replStr);
+ }
+
+ public static string SplitByCapitalLetter(this string str, string delimiter)
+ {
+ return string.Join(delimiter, Regex.Matches(str, @"([A-Z]+)(?=([A-Z][a-z]|$)) | [A-Z][a-z].+?(?=([A-Z]|$))", RegexOptions.IgnorePatternWhitespace)
+ .Cast()
+ .Select(m => m.ToString()));
+ }
+
+ ///
+ /// Проверяет соответствие регулярному выражению
+ ///
+ public static bool IsMatch(this string str, string pattern, RegexOptions options)
+ {
+ return Regex.IsMatch(str, pattern, options);
+ }
+
+ ///
+ /// Проверяет соответствие регулярному выражению
+ ///
+ public static bool IsMatch(this string str, string pattern)
+ {
+ return IsMatch(str, pattern, RegexOptions.IgnoreCase);
+ }
+
+ ///
+ /// Возвращает совпадения для регулярного выражения
+ ///
+ public static string[] GetMatches(this string str, string pattern, RegexOptions options = RegexOptions.IgnoreCase)
+ {
+ return str.IsMatch(pattern, options)
+ ? Regex.Matches(str, pattern, options)
+ .Cast()
+ .Select(m => m.Value)
+ .ToArray()
+ : new string[] { };
+ }
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Models/Account/YAccessToken.cs b/YandexMusic.API/Models/Account/YAccessToken.cs
new file mode 100644
index 0000000..75dbbb3
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAccessToken.cs
@@ -0,0 +1,15 @@
+namespace YandexMusic.API.Models.Account
+{
+ public class YAccessToken
+ {
+ [JsonProperty("status")]
+ public string Status { get; set; }
+ [JsonProperty("access_token")]
+ public string AccessToken { get; set; }
+ [JsonProperty("expires_in")]
+ public string Expires { get; set; }
+ [JsonProperty("token_type")]
+ public string TokenType { get; set; }
+ public string Uid { get; set; }
+ }
+}
diff --git a/YandexMusic.API/Models/Account/YAccount.cs b/YandexMusic.API/Models/Account/YAccount.cs
new file mode 100644
index 0000000..c533034
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAccount.cs
@@ -0,0 +1,25 @@
+using YandexMusic.API.Models.Common;
+
+namespace YandexMusic.API.Models.Account
+{
+ public class YAccount
+ {
+ public bool Child { get; set; }
+ public string Birthday { get; set; }
+ public string DisplayName { get; set; }
+ public string FirstName { get; set; }
+ public string FullName { get; set; }
+ public bool HostedUser { get; set; }
+ public string Login { get; set; }
+ public bool NonOwnerFamilyMember { get; set; }
+ public DateTime Now { get; set; }
+ [JsonProperty("passport-phones")]
+ public List PassportPhones { get; set; }
+ public int Region { get; set; }
+ public string RegionCode { get; set; }
+ public DateTime RegisteredAt { get; set; }
+ public string SecondName { get; set; }
+ public bool ServiceAvailable { get; set; }
+ public string Uid { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Models/Account/YAccountResult.cs b/YandexMusic.API/Models/Account/YAccountResult.cs
new file mode 100644
index 0000000..dbeb5ba
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAccountResult.cs
@@ -0,0 +1,26 @@
+using YandexMusic.API.Models.Common;
+
+namespace YandexMusic.API.Models.Account
+{
+ public class YAccountResult
+ {
+ public YAccount Account { get; set; }
+ public string DefaultEmail { get; set; }
+ public List HasOptions { get; set; }
+ public YMasterHub MasterHub { get; set; }
+ public YPermissions Permissions { get; set; }
+ public YPlus Plus { get; set; }
+ public bool PretrialActive { get; set; }
+ public bool SubEditor { get; set; }
+ public int SubEditorLevel { get; set; }
+ public YSubscription Subscription { get; set; }
+ public YBar BarBelow { get; set; }
+ // Повторяющееся свойство с другим названием
+ [JsonProperty("bar-below")]
+ private YBar BarBelow2
+ {
+ set => BarBelow = value;
+ }
+ public string Userhash { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Models/Account/YAuthBase.cs b/YandexMusic.API/Models/Account/YAuthBase.cs
new file mode 100644
index 0000000..8c02c0b
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAuthBase.cs
@@ -0,0 +1,12 @@
+namespace YandexMusic.API.Models.Account
+{
+ public class YAuthBase
+ {
+ public YAuthStatus Status { get; set; }
+
+ [JsonProperty("redirect_url")]
+ public string RedirectUrl { get; set; }
+
+ public List Errors { get; set; }
+ }
+}
diff --git a/YandexMusic.API/Models/Account/YAuthCaptcha.cs b/YandexMusic.API/Models/Account/YAuthCaptcha.cs
new file mode 100644
index 0000000..01becec
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAuthCaptcha.cs
@@ -0,0 +1,28 @@
+namespace YandexMusic.API.Models.Account
+{
+ public class YAuthCaptcha : YAuthBase
+ {
+ public string Id { get; set; }
+
+ public string Name { get; set; }
+
+ public string Label { get; set; }
+
+ public string Mode { get; set; }
+
+ public List Error { get; set; }
+
+ public bool CountryFromAudioWhiteList { get; set; }
+
+ public YAuthCaptchaOptions Options { get; set; }
+
+ public YAuthCaptchaVoice Voice { get; set; }
+
+ [JsonProperty("image_url")]
+ public string ImageUrl { get; set; }
+
+ public string Key { get; set; }
+
+ public string Static { get; set; }
+ }
+}
diff --git a/YandexMusic.API/Models/Account/YAuthCaptchaError.cs b/YandexMusic.API/Models/Account/YAuthCaptchaError.cs
new file mode 100644
index 0000000..17385f6
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAuthCaptchaError.cs
@@ -0,0 +1,8 @@
+namespace YandexMusic.API.Models.Account
+{
+ public class YAuthCaptchaError
+ {
+ public string Message { get; set; }
+ public YAuthCaptchaErrorCode Code { get; set; }
+ }
+}
diff --git a/YandexMusic.API/Models/Account/YAuthCaptchaErrorCode.cs b/YandexMusic.API/Models/Account/YAuthCaptchaErrorCode.cs
new file mode 100644
index 0000000..5faf756
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAuthCaptchaErrorCode.cs
@@ -0,0 +1,9 @@
+namespace YandexMusic.API.Models.Account
+{
+ public enum YAuthCaptchaErrorCode
+ {
+ MissingValue,
+ CaptchaLocate,
+ Incorrect
+ }
+}
diff --git a/YandexMusic.API/Models/Account/YAuthCaptchaOptions.cs b/YandexMusic.API/Models/Account/YAuthCaptchaOptions.cs
new file mode 100644
index 0000000..812e7d5
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAuthCaptchaOptions.cs
@@ -0,0 +1,7 @@
+namespace YandexMusic.API.Models.Account
+{
+ public class YAuthCaptchaOptions
+ {
+ public bool AsyncCheck { get; set; }
+ }
+}
diff --git a/YandexMusic.API/Models/Account/YAuthCaptchaVoice.cs b/YandexMusic.API/Models/Account/YAuthCaptchaVoice.cs
new file mode 100644
index 0000000..cdfb7b8
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAuthCaptchaVoice.cs
@@ -0,0 +1,10 @@
+namespace YandexMusic.API.Models.Account
+{
+ public class YAuthCaptchaVoice
+ {
+ public string Url { get; set; }
+
+ [JsonProperty("intro_url")]
+ public string IntroUrl { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Models/Account/YAuthError.cs b/YandexMusic.API/Models/Account/YAuthError.cs
new file mode 100644
index 0000000..1b12e4e
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAuthError.cs
@@ -0,0 +1,22 @@
+using System.Runtime.Serialization;
+
+namespace YandexMusic.API.Models.Account
+{
+ public enum YAuthError
+ {
+ [EnumMember(Value = "authorization.invalid")]
+ AuthorizationInvalid,
+ [EnumMember(Value = "sessionid.invalid")]
+ SessionIdInvalid,
+ [EnumMember(Value = "password.not_matched")]
+ PasswordNotMatched,
+ [EnumMember(Value = "password.empty")]
+ PasswordEmpty,
+ [EnumMember(Value = "captcha.required")]
+ CaptchaRequired,
+ [EnumMember(Value = "captcha.not_matched")]
+ CaptchaNotMatched,
+ [EnumMember(Value = "oauth_token.invalid")]
+ OAuthTokenInvalid
+ }
+}
diff --git a/YandexMusic.API/Models/Account/YAuthLetter.cs b/YandexMusic.API/Models/Account/YAuthLetter.cs
new file mode 100644
index 0000000..7e13d3a
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAuthLetter.cs
@@ -0,0 +1,9 @@
+namespace YandexMusic.API.Models.Account
+{
+ public class YAuthLetter : YAuthBase
+ {
+ public List Code { get; set; }
+
+ public string Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Models/Account/YAuthLetterStatus.cs b/YandexMusic.API/Models/Account/YAuthLetterStatus.cs
new file mode 100644
index 0000000..e367ed0
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAuthLetterStatus.cs
@@ -0,0 +1,11 @@
+namespace YandexMusic.API.Models.Account
+{
+ public class YAuthLetterStatus : YAuthBase
+ {
+ [JsonProperty("magic_link_confirmed")]
+ public bool MagicLinkConfirmed { get; set; }
+
+ [JsonProperty("track_id")]
+ public string TrackId { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Models/Account/YAuthMethod.cs b/YandexMusic.API/Models/Account/YAuthMethod.cs
new file mode 100644
index 0000000..fbc77a2
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAuthMethod.cs
@@ -0,0 +1,22 @@
+using System.Runtime.Serialization;
+
+namespace YandexMusic.API.Models.Account
+{
+ public enum YAuthMethod
+ {
+ Password,
+ [EnumMember(Value = "magic_x_token")]
+ MagicToken,
+ [EnumMember(Value = "magic_x_token_with_pictures")]
+ MagicTokenWithPictures,
+ [EnumMember(Value = "magic_link")]
+ MagicLink,
+ Magic,
+ Otp,
+ [EnumMember(Value = "social_gg")]
+ Social,
+ WebAuthN,
+ [EnumMember(Value = "sms_code")]
+ SmsCode
+ }
+}
diff --git a/YandexMusic.API/Models/Account/YAuthQr.cs b/YandexMusic.API/Models/Account/YAuthQr.cs
new file mode 100644
index 0000000..48c4ac1
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAuthQr.cs
@@ -0,0 +1,11 @@
+namespace YandexMusic.API.Models.Account
+{
+ public class YAuthQR : YAuthBase
+ {
+ [JsonProperty("track_id")]
+ public string TrackId { get; set; }
+
+ [JsonProperty("csrf_token")]
+ public string CsrfToken { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Models/Account/YAuthQrStatus.cs b/YandexMusic.API/Models/Account/YAuthQrStatus.cs
new file mode 100644
index 0000000..707986b
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAuthQrStatus.cs
@@ -0,0 +1,19 @@
+namespace YandexMusic.API.Models.Account
+{
+ public class YAuthQRStatus : YAuthBase
+ {
+ [JsonProperty("default_uid")]
+ public int DefaultUid { get; set; }
+
+ public string RetPath { get; set; }
+
+ [JsonProperty("track_id")]
+ public string TrackId { get; set; }
+
+ public string Id { get; set; }
+
+ public string State { get; set; }
+
+ public YAuthCaptcha Captcha { get; set; }
+ }
+}
diff --git a/YandexMusic.API/Models/Account/YAuthStatus.cs b/YandexMusic.API/Models/Account/YAuthStatus.cs
new file mode 100644
index 0000000..6742cc7
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAuthStatus.cs
@@ -0,0 +1,8 @@
+namespace YandexMusic.API.Models.Account
+{
+ public enum YAuthStatus
+ {
+ Ok,
+ Error
+ }
+}
diff --git a/YandexMusic.API/Models/Account/YAuthToken.cs b/YandexMusic.API/Models/Account/YAuthToken.cs
new file mode 100644
index 0000000..d1aeaa7
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAuthToken.cs
@@ -0,0 +1,11 @@
+namespace YandexMusic.API.Models.Account
+{
+ public class YAuthToken
+ {
+ [JsonProperty("csfr_token")]
+ public string CsfrToken { get; set; }
+
+ [JsonProperty("track_id")]
+ public string TrackId { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Models/Account/YAuthTypes.cs b/YandexMusic.API/Models/Account/YAuthTypes.cs
new file mode 100644
index 0000000..378f392
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YAuthTypes.cs
@@ -0,0 +1,47 @@
+namespace YandexMusic.API.Models.Account
+{
+ public class YAuthTypes : YAuthBase
+ {
+ [JsonProperty("primary_alias_type")]
+ public string PrimaryAliasType { get; set; }
+
+ [JsonProperty("csrf_token")]
+ public string CsrfToken { get; set; }
+
+ public string LocCsrf { get; set; }
+
+ [JsonProperty("use_new_suggest_by_phone")]
+ public bool UseNewSuggestByPhone { get; set; }
+
+ [JsonProperty("is_rfc_2fa_enabled")]
+ public bool IsRfc2faEnabled { get; set; }
+
+ [JsonProperty("track_id")]
+ public string TrackId { get; set; }
+
+ [JsonProperty("can_authorize")]
+ public string CanAuthorize { get; set; }
+
+ [JsonProperty("preferred_auth_method")]
+ public YAuthMethod PreferredAuthMethod { get; set; }
+
+ [JsonProperty("auth_methods")]
+ public List AuthMethods { get; set; }
+
+ [JsonProperty("can_register")]
+ public bool CanRegister { get; set; }
+
+ [JsonProperty("location_id")]
+ public string LocationId { get; set; }
+
+ public string Country { get; set; }
+
+ [JsonProperty("phone_number")]
+ public YPhoneNumber PhoneNumberNumber { get; set; }
+
+ [JsonProperty("magic_link_email")]
+ public string MagicLinkEmail { get; set; }
+
+ public string TractorTargetLocationHost { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Models/Account/YBar.cs b/YandexMusic.API/Models/Account/YBar.cs
new file mode 100644
index 0000000..770bcc5
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YBar.cs
@@ -0,0 +1,14 @@
+using YandexMusic.API.Models.Common;
+
+namespace YandexMusic.API.Models.Account
+{
+ public class YBar : YStyle
+ {
+ public string AlertId { get; set; }
+ public string Text { get; set; }
+ public string AlertType { get; set; }
+ public YButton Button { get; set; }
+ public bool CloseButton { get; set; }
+ public YCloseButtonStyles CloseButtonStyles { get; set; }
+ }
+}
diff --git a/YandexMusic.API/Models/Account/YLoginInfo.cs b/YandexMusic.API/Models/Account/YLoginInfo.cs
new file mode 100644
index 0000000..829518b
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YLoginInfo.cs
@@ -0,0 +1,31 @@
+namespace YandexMusic.API.Models.Account
+{
+ public class YLoginInfo
+ {
+ public string Id { get; set; }
+ public string Login { get; set; }
+ [JsonProperty("client_id")]
+ public string ClientId { get; set; }
+ [JsonProperty("display_name")]
+ public string DisplayName { get; set; }
+ [JsonProperty("real_name")]
+ public string RealName { get; set; }
+ [JsonProperty("first_name")]
+ public string FirstName { get; set; }
+ [JsonProperty("last_name")]
+ public string LastName { get; set; }
+ public string Sex { get; set; }
+ [JsonProperty("default_email")]
+ public string DefaultEmail { get; set; }
+ public List Emails { get; set; }
+ public string Birthday { get; set; }
+ [JsonProperty("default_avatar_id")]
+ public string DefaultAvatarId { get; set; }
+
+ public string AvatarUrl => $"https://avatars.mds.yandex.net/get-yapic/{DefaultAvatarId}/islands-200";
+ [JsonProperty("is_avatar_empty")]
+ public bool IsAvatarEmpty { get; set; }
+ public string PsuId { get; set; }
+
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Models/Account/YPhoneNumber.cs b/YandexMusic.API/Models/Account/YPhoneNumber.cs
new file mode 100644
index 0000000..70c9f2e
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YPhoneNumber.cs
@@ -0,0 +1,20 @@
+namespace YandexMusic.API.Models.Account
+{
+ public class YPhoneNumber
+ {
+ [JsonProperty("masked_e164")]
+ public string MaskedE164 { get; set; }
+
+ public string E164 { get; set; }
+
+ public string International { get; set; }
+
+ [JsonProperty("masked_original")]
+ public string MaskedOriginal { get; set; }
+
+ public string Original { get; set; }
+
+ [JsonProperty("masked_international")]
+ public string MaskedInternational { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Models/Account/YShortAccountInfo.cs b/YandexMusic.API/Models/Account/YShortAccountInfo.cs
new file mode 100644
index 0000000..a1768da
--- /dev/null
+++ b/YandexMusic.API/Models/Account/YShortAccountInfo.cs
@@ -0,0 +1,81 @@
+namespace YandexMusic.API.Models.Account
+{
+ public class YShortAccountInfo : YAuthBase
+ {
+ [JsonProperty("public_id")]
+ public string PublicId { get; set; }
+
+ public string Uid { get; set; }
+
+ public string FirstName { get; set; }
+
+ public string LastName { get; set; }
+
+ public string Birthday { get; set; }
+
+ [JsonProperty("has_password")]
+ public bool HasPassword { get; set; }
+
+ public List Partitions { get; set; }
+
+ [JsonProperty("primary_alias_type")]
+ public int PrimaryAliasType { get; set; }
+
+ [JsonProperty("display_name")]
+ public string DisplayName { get; set; }
+
+ [JsonProperty("normalized_display_login")]
+ public string NormalizedDisplayLogin { get; set; }
+
+ [JsonProperty("x_token_issued_at")]
+ public int XTokenIssuedAt { get; set; }
+
+ [JsonProperty("display_login")]
+ public string DisplayLogin { get; set; }
+
+ [JsonProperty("public_name")]
+ public string PublicName { get; set; }
+
+ [JsonProperty("avatar_url")]
+ public string AvatarUrl { get; set; }
+
+ [JsonProperty("native_default_email")]
+ public string NativeDefaultEmail { get; set; }
+
+ [JsonProperty("has_plus")]
+ public bool HasPlus { get; set; }
+
+ [JsonProperty("location_id")]
+ public int LocationId { get; set; }
+
+ [JsonProperty("gender")]
+ public string Gender { get; set; }
+
+ [JsonProperty("is_avatar_empty")]
+ public bool IsAvatarEmpty { get; set; }
+
+ [JsonProperty("machine_readable_login")]
+ public string MachineReadableLogin { get; set; }
+
+ [JsonProperty("has_cards")]
+ public bool HasCards { get; set; }
+
+ [JsonProperty("has_family")]
+ public bool HasFamily { get; set; }
+
+ [JsonProperty("picture_login_forbidden")]
+ public bool PictureLoginForbidden { get; set; }
+
+ [JsonProperty("can_account_join_master")]
+ public bool CanAccountJoinMaster { get; set; }
+
+ [JsonProperty("secure_phone_number")]
+ public string SecurePhoneNumber { get; set; }
+
+ [JsonProperty("x_token_client_id")]
+ public string XTokenClientId { get; set; }
+
+ [JsonProperty("x_token_need_reset")]
+ public bool XTokenNeedReset { get; set; }
+ }
+}
diff --git a/YandexMusic.API/Models/Album/YAlbum.cs b/YandexMusic.API/Models/Album/YAlbum.cs
new file mode 100644
index 0000000..80055c3
--- /dev/null
+++ b/YandexMusic.API/Models/Album/YAlbum.cs
@@ -0,0 +1,98 @@
+using YandexMusic.API.Models.Artist;
+using YandexMusic.API.Models.Common;
+using YandexMusic.API.Models.Common.Cover;
+using YandexMusic.API.Models.Track;
+
+namespace YandexMusic.API.Models.Album
+{
+ public sealed class YLabelConverter : JsonConverter
+ {
+ public override bool CanConvert(Type objectType)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ if (reader.TokenType == JsonToken.Null)
+ return null;
+
+ JArray jArray = JArray.Load(reader);
+ JTokenType tokenType = jArray.FirstOrDefault()?.Type ?? JTokenType.String;
+ object label;
+
+ try
+ {
+ label = tokenType switch
+ {
+ JTokenType.Object => jArray.ToObject>(),
+ _ => jArray.ToObject>()
+ };
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Ошибка десериализации типа \"{objectType.Name}\".", ex);
+ }
+
+ return label;
+ }
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ JArray array = JArray.FromObject(value);
+
+ array.WriteTo(writer);
+ }
+ }
+
+ public class YAlbum : YBaseModel
+ {
+ public YButton ActionButton { get; set; }
+ public List Artists { get; set; }
+ public bool Available { get; set; }
+ public bool AvailableForMobile { get; set; }
+ public List AvailableForOptions { get; set; }
+ public bool AvailableForPremiumUsers { get; set; }
+ public bool AvailablePartially { get; set; }
+ public string BackgroundImageUrl { get; set; }
+ public string BackgroundVideoUrl { get; set; }
+ public List Bests { get; set; }
+ public List Buy { get; set; }
+ public bool ChildContent { get; set; }
+ public string ContentWarning { get; set; }
+ public string CoverUri { get; set; }
+ [JsonConverter(typeof(YCoverConverter))]
+ public YCover Cover { get; set; }
+ public YCustomWave CustomWave { get; set; }
+ public YDerivedColors DerivedColors { get; set; }
+ public string Description { get; set; }
+ public List Disclaimers { get; set; }
+ public List Duplicates { get; set; }
+ public bool HasTrailer { get; set; }
+ public string Genre { get; set; }
+ public string Id { get; set; }
+ [JsonConverter(typeof(YLabelConverter))]
+ public dynamic Labels { get; set; }
+ public int LikesCount { get; set; }
+ public bool ListeningFinished { get; set; }
+ public string MetaTagId { get; set; }
+ public YMetaType MetaType { get; set; }
+ public string OgImage { get; set; }
+ public YPager Pager { get; set; }
+ public List Prerolls { get; set; }
+ public bool Recent { get; set; }
+ public DateTime ReleaseDate { get; set; }
+ public string ShortDescription { get; set; }
+ public YSortOrder SortOrder { get; set; }
+ public string StorageDir { get; set; }
+ public string Title { get; set; }
+ public int TrackCount { get; set; }
+ public YTrackPosition TrackPosition { get; set; }
+ public YTrailer Trailer { get; set; }
+ public string Type { get; set; }
+ public string Version { get; set; }
+ public bool VeryImportant { get; set; }
+ public List> Volumes { get; set; }
+ public int Year { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/YandexMusic.API/Models/Artist/YArtist.cs b/YandexMusic.API/Models/Artist/YArtist.cs
new file mode 100644
index 0000000..c4c36fa
--- /dev/null
+++ b/YandexMusic.API/Models/Artist/YArtist.cs
@@ -0,0 +1,41 @@
+using YandexMusic.API.Models.Common;
+using YandexMusic.API.Models.Common.Cover;
+
+namespace YandexMusic.API.Models.Artist
+{
+ public class YArtist : YBaseModel
+ {
+ public YButton ActionButton { get; set; }
+ public bool Available { get; set; }
+ public bool Composer { get; set; }
+ public List Countries { get; set; }
+ public YArtistCounts Counts { get; set; }
+ [JsonConverter(typeof(YCoverConverter))]
+ public YCover Cover { get; set; }
+ public List DbAliases { get; set; }
+#warning Непонятная коллекция с содержимым разных типов
+ public List