Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6f78da9c8 | ||
|
|
0bbaac5689 | ||
|
|
a7caf829d3 | ||
|
|
add7f08215 | ||
|
|
36e28ce3fe | ||
|
|
5541d0ad27 | ||
|
|
ea9f392896 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -361,4 +361,4 @@ MigrationBackup/
|
|||||||
|
|
||||||
# Fody - auto-generated XML schema
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
/YaMusicCli/YaMusicCli.csproj
|
YaMusicCli/
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
using YandexMusic.API.Extensions.API;
|
|
||||||
|
|
||||||
internal class Program
|
|
||||||
{
|
|
||||||
private async static Task Main(string[] args)
|
|
||||||
{
|
|
||||||
var client = new YandexMusic.YandexMusicClient();
|
|
||||||
var type = await client.Authorize("y0__xDy2budARje-AYg7rmliBc11LbYoMeUiwiO6f6mSCAMDYVIKg");
|
|
||||||
var playlists = (await client.GetFavoritesAsync()).Where(t => t.Owner.Uid == client.Account.Uid).ToList();
|
|
||||||
var playlist = await client.GetPlaylistAsync("97ae0768-8a40-8485-9fa4-b6c856bc6b21");
|
|
||||||
var tracks = playlist.Tracks.Where(t => t.Id == "21696942").Select(t => t.Track).ToArray();
|
|
||||||
var x = await playlist.RemoveTracksAsync(tracks);
|
|
||||||
Console.WriteLine($"TC: {playlist.TrackCount}; {playlist.Title}");
|
|
||||||
Console.ReadKey();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Album;
|
using YandexMusic.API.Models.Album;
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Requests.Album;
|
using YandexMusic.API.Requests.Album;
|
||||||
|
|
||||||
namespace YandexMusic.API;
|
namespace YandexMusic.API;
|
||||||
@@ -8,21 +6,13 @@ namespace YandexMusic.API;
|
|||||||
/// <summary>API для работы с альбомами.</summary>
|
/// <summary>API для работы с альбомами.</summary>
|
||||||
public class YAlbumAPI : YCommonAPI
|
public class YAlbumAPI : YCommonAPI
|
||||||
{
|
{
|
||||||
/// <summary>Инициализирует новый экземпляр API альбомов.</summary>
|
public YAlbumAPI(YandexMusicApi api) : base(api) { }
|
||||||
/// <param name="yandex">Экземпляр основного API.</param>
|
|
||||||
public YAlbumAPI(YandexMusicApi yandex) : base(yandex) { }
|
|
||||||
|
|
||||||
/// <summary>Получает альбом по идентификатору.</summary>
|
/// <summary>Получает альбом по идентификатору.</summary>
|
||||||
/// <param name="storage">Хранилище данных авторизации.</param>
|
public Task<YAlbum?> GetAsync(string albumId)
|
||||||
/// <param name="albumId">Идентификатор альбома.</param>
|
=> new YGetAlbumBuilder(Api).ExecuteAsync(albumId);
|
||||||
/// <returns>Ответ API с моделью альбома.</returns>
|
|
||||||
public Task<YResponse<YAlbum>> GetAsync(AuthStorage storage, string albumId)
|
|
||||||
=> new YGetAlbumBuilder(api, storage).Build(albumId).GetResponseAsync();
|
|
||||||
|
|
||||||
/// <summary>Получает несколько альбомов по списку идентификаторов.</summary>
|
/// <summary>Получает несколько альбомов по списку идентификаторов.</summary>
|
||||||
/// <param name="storage">Хранилище данных авторизации.</param>
|
public Task<List<YAlbum>?> GetAsync(IEnumerable<string> albumIds)
|
||||||
/// <param name="albumIds">Список идентификаторов альбомов.</param>
|
=> new YGetAlbumsBuilder(Api).ExecuteAsync(albumIds);
|
||||||
/// <returns>Ответ API со списком альбомов.</returns>
|
|
||||||
public Task<YResponse<List<YAlbum>>> GetAsync(AuthStorage storage, IEnumerable<string> albumIds)
|
|
||||||
=> new YGetAlbumsBuilder(api, storage).Build(albumIds).GetResponseAsync();
|
|
||||||
}
|
}
|
||||||
@@ -1,71 +1,27 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Artist;
|
using YandexMusic.API.Models.Artist;
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Requests.Artist;
|
using YandexMusic.API.Requests.Artist;
|
||||||
|
|
||||||
namespace YandexMusic.API;
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>API для работы с исполнителями.</summary>
|
||||||
/// API для взаимодействия с исполнителями
|
|
||||||
/// </summary>
|
|
||||||
public class YArtistAPI : YCommonAPI
|
public class YArtistAPI : YCommonAPI
|
||||||
{
|
{
|
||||||
public YArtistAPI(YandexMusicApi yandex) : base(yandex)
|
public YArtistAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
public Task<YArtistBriefInfo?> GetAsync(string artistId)
|
||||||
|
=> new YGetArtistBuilder(Api).ExecuteAsync(artistId);
|
||||||
|
|
||||||
|
public Task<List<YArtist>?> GetAsync(IEnumerable<string> artistIds)
|
||||||
|
=> new YGetArtistsBuilder(Api).ExecuteAsync(artistIds);
|
||||||
|
|
||||||
|
public Task<YTracksPage?> GetTracksAsync(string artistId, int page = 0, int pageSize = 20)
|
||||||
|
=> new YGetArtistTrackBuilder(Api).ExecuteAsync((artistId, page, pageSize));
|
||||||
|
|
||||||
|
public async Task<YTracksPage?> GetAllTracksAsync(string artistId)
|
||||||
{
|
{
|
||||||
|
var info = await GetAsync(artistId);
|
||||||
|
if (info?.Artist?.Counts?.Tracks == null)
|
||||||
|
return null;
|
||||||
|
return await GetTracksAsync(artistId, pageSize: info.Artist.Counts.Tracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение исполнителя
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="artistId">Идентификатор</param>
|
|
||||||
public Task<YResponse<YArtistBriefInfo>> GetAsync(AuthStorage storage, string artistId)
|
|
||||||
{
|
|
||||||
return new YGetArtistBuilder(api, storage)
|
|
||||||
.Build(artistId)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение исполнителей
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="artistIds">Идентификаторы</param>
|
|
||||||
public Task<YResponse<List<YArtist>>> GetAsync(AuthStorage storage, IEnumerable<string> artistIds)
|
|
||||||
{
|
|
||||||
return new YGetArtistsBuilder(api, storage)
|
|
||||||
.Build(artistIds)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение треков исполнителя с пагинацией
|
|
||||||
/// <remarks>
|
|
||||||
/// Треки поставляются по <paramref name="pageSize"/> штук на страницу,
|
|
||||||
/// для получения всех треков необходимо использовать метод <see cref="GetAllTracksAsync"/>
|
|
||||||
/// </remarks>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="artistId">Идентификатор исполнителя</param>
|
|
||||||
/// <param name="page">Страница ответов</param>
|
|
||||||
/// <param name="pageSize">Количество треков на странице ответов</param>
|
|
||||||
public Task<YResponse<YTracksPage>> GetTracksAsync(AuthStorage storage, string artistId, int page = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return new YGetArtistTrackBuilder(api, storage)
|
|
||||||
.Build((artistId, page, pageSize))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение всех треков исполнителя
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="artistId">Идентификатор исполнителя</param>
|
|
||||||
public async Task<YResponse<YTracksPage>> GetAllTracksAsync(AuthStorage storage, string artistId)
|
|
||||||
{
|
|
||||||
YResponse<YArtistBriefInfo> response = await GetAsync(storage, artistId);
|
|
||||||
return await GetTracksAsync(storage, artistId, pageSize: response.Result.Artist.Counts.Tracks);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
namespace YandexMusic.API;
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
/// <summary>Родительский класс для всех веток API.</summary>
|
/// <summary>Базовый класс для всех веток API.</summary>
|
||||||
public abstract class YCommonAPI
|
public abstract class YCommonAPI
|
||||||
{
|
{
|
||||||
/// <summary>Основной экземпляр API.</summary>
|
/// <summary>Основной экземпляр API.</summary>
|
||||||
protected readonly YandexMusicApi api;
|
protected YandexMusicApi Api { get; }
|
||||||
|
|
||||||
/// <summary>Инициализирует новый экземпляр.</summary>
|
protected YCommonAPI(YandexMusicApi api) => Api = api;
|
||||||
/// <param name="yandex">Экземпляр основного API.</param>
|
|
||||||
protected YCommonAPI(YandexMusicApi yandex) => api = yandex;
|
|
||||||
}
|
}
|
||||||
@@ -1,39 +1,19 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
using YandexMusic.API.Models.Common;
|
||||||
using YandexMusic.API.Models.Label;
|
using YandexMusic.API.Models.Label;
|
||||||
using YandexMusic.API.Requests.Label;
|
using YandexMusic.API.Requests.Label;
|
||||||
|
|
||||||
namespace YandexMusic.API;
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
public partial class YLabelAPI : YCommonAPI
|
/// <summary>API для работы с лейблами.</summary>
|
||||||
|
public class YLabelAPI : YCommonAPI
|
||||||
{
|
{
|
||||||
public YLabelAPI(YandexMusicApi yandex) : base(yandex)
|
public YLabelAPI(YandexMusicApi api) : base(api) { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Получает альбомы лейбла с пагинацией.</summary>
|
||||||
/// Постраничное получение альбомов лейбла
|
public Task<YLabelAlbums?> GetAlbumsByLabelAsync(YLabel label, int page = 0)
|
||||||
/// </summary>
|
=> new YGetLabelAlbumsBuilder(Api).ExecuteAsync((label, page));
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="label">Лейбл</param>
|
|
||||||
/// <param name="page">Страница</param>
|
|
||||||
public Task<YResponse<YLabelAlbums>> GetAlbumsByLabelAsync(AuthStorage storage, YLabel label, int page)
|
|
||||||
{
|
|
||||||
return new YGetLabelAlbumsBuilder(api, storage)
|
|
||||||
.Build((label, page))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Получает артистов лейбла с пагинацией.</summary>
|
||||||
/// Постраничное получение артистов лейбла
|
public Task<YLabelArtists?> GetArtistsByLabelAsync(YLabel label, int page = 0)
|
||||||
/// </summary>
|
=> new YGetLabelArtistsBuilder(Api).ExecuteAsync((label, page));
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="label">Лейбл</param>
|
|
||||||
/// <param name="page">Страница</param>
|
|
||||||
public Task<YResponse<YLabelArtists>> GetArtistsByLabelAsync(AuthStorage storage, YLabel label, int page)
|
|
||||||
{
|
|
||||||
return new YGetLabelArtistsBuilder(api, storage)
|
|
||||||
.Build((label, page))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,43 +1,21 @@
|
|||||||
using YandexMusic.API.Common;
|
using YandexMusic.API.Models.Feed;
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Feed;
|
|
||||||
using YandexMusic.API.Models.Landing;
|
using YandexMusic.API.Models.Landing;
|
||||||
using YandexMusic.API.Requests.Feed;
|
using YandexMusic.API.Requests.Feed;
|
||||||
using YandexMusic.API.Requests.Landing;
|
using YandexMusic.API.Requests.Landing;
|
||||||
|
|
||||||
namespace YandexMusic.API;
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
/// <summary>API для взаимодействия с главной страницей (лендингом).</summary>
|
/// <summary>API для работы с главной страницей (лендингом).</summary>
|
||||||
public class YLandingAPI : YCommonAPI
|
public class YLandingAPI : YCommonAPI
|
||||||
{
|
{
|
||||||
/// <summary>Инициализирует новый экземпляр API лендинга.</summary>
|
public YLandingAPI(YandexMusicApi api) : base(api) { }
|
||||||
/// <param name="yandex">Экземпляр основного API.</param>
|
|
||||||
public YLandingAPI(YandexMusicApi yandex) : base(yandex) { }
|
|
||||||
|
|
||||||
/// <summary>Получает персональные блоки лендинга.</summary>
|
public Task<YLanding?> GetAsync(params YLandingBlockType[] blocks)
|
||||||
/// <param name="storage">Хранилище авторизации.</param>
|
=> new YGetLandingBuilder(Api).ExecuteAsync(blocks);
|
||||||
/// <param name="blocks">Типы запрашиваемых блоков.</param>
|
|
||||||
/// <returns>Ответ API с лендингом.</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">Если массив blocks равен null.</exception>
|
|
||||||
public Task<YResponse<YLanding>> GetAsync(AuthStorage storage, params YLandingBlockType[] blocks)
|
|
||||||
{
|
|
||||||
if (blocks == null)
|
|
||||||
throw new ArgumentNullException(nameof(blocks), "Массив блоков не может быть null");
|
|
||||||
|
|
||||||
return new YGetLandingBuilder(api, storage)
|
public Task<YFeed?> GetFeedAsync()
|
||||||
.Build(blocks)
|
=> new YGetFeedBuilder(Api).ExecuteAsync(null!);
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Получает ленту событий (фид).</summary>
|
public Task<YChildrenLanding?> GetChildrenLandingAsync()
|
||||||
/// <param name="storage">Хранилище авторизации.</param>
|
=> new YGetChildrenLandingBuilder(Api).ExecuteAsync(null!);
|
||||||
/// <returns>Ответ API с лентой.</returns>
|
|
||||||
public Task<YResponse<YFeed>> GetFeedAsync(AuthStorage storage)
|
|
||||||
=> new YGetFeedBuilder(api, storage).Build(null!).GetResponseAsync();
|
|
||||||
|
|
||||||
/// <summary>Получает лендинг детского раздела.</summary>
|
|
||||||
/// <param name="storage">Хранилище авторизации.</param>
|
|
||||||
/// <returns>Ответ API с детским лендингом.</returns>
|
|
||||||
public Task<YResponse<YChildrenLanding>> GetChildrenLandingAsync(AuthStorage storage)
|
|
||||||
=> new YGetChildrenLandingBuilder(api, storage).Build(null!).GetResponseAsync();
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Album;
|
using YandexMusic.API.Models.Album;
|
||||||
using YandexMusic.API.Models.Artist;
|
using YandexMusic.API.Models.Artist;
|
||||||
using YandexMusic.API.Models.Common;
|
using YandexMusic.API.Models.Common;
|
||||||
@@ -10,244 +9,82 @@ using YandexMusic.API.Requests.Library;
|
|||||||
|
|
||||||
namespace YandexMusic.API;
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>API для работы с библиотекой (лайки, дизлайки, недавно прослушанное).</summary>
|
||||||
/// API для взаимодействия с библиотекой
|
public class YLibraryAPI : YCommonAPI
|
||||||
/// </summary>
|
|
||||||
public partial class YLibraryAPI : YCommonAPI
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
public YLibraryAPI(YandexMusicApi api) : base(api) { }
|
||||||
/// Получение секции библиотеки
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Тип объекта библиотеки</typeparam>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="section">Секция</param>
|
|
||||||
/// <param name="type">Тип</param>
|
|
||||||
/// <returns>Список объектов из секции</returns>
|
|
||||||
private Task<YResponse<T>> GetLibrarySection<T>(AuthStorage storage, YLibrarySection section, YLibrarySectionType type = YLibrarySectionType.Likes)
|
|
||||||
{
|
|
||||||
return new YGetLibrarySectionBuilder<T>(api, storage)
|
|
||||||
.Build((section, type))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public YLibraryAPI(YandexMusicApi yandex) : base(yandex)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Лайки
|
#region Лайки
|
||||||
|
|
||||||
/// <summary>
|
public Task<YLibraryTracks?> GetLikedTracksAsync()
|
||||||
/// Получение лайкнутых треков
|
=> new YGetLibrarySectionBuilder<YLibraryTracks>(Api).ExecuteAsync((YLibrarySection.Tracks, YLibrarySectionType.Likes));
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YLibraryTracks>> GetLikedTracksAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetLibrarySection<YLibraryTracks>(storage, YLibrarySection.Tracks);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<List<YLibraryAlbum>?> GetLikedAlbumsAsync()
|
||||||
/// Получение лайкнутых альбомов
|
=> new YGetLibrarySectionBuilder<List<YLibraryAlbum>>(Api).ExecuteAsync((YLibrarySection.Albums, YLibrarySectionType.Likes));
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YLibraryAlbum>>> GetLikedAlbumsAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetLibrarySection<List<YLibraryAlbum>>(storage, YLibrarySection.Albums);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<List<YArtist>?> GetLikedArtistsAsync()
|
||||||
/// Получение лайкнутых исполнителей
|
=> new YGetLibrarySectionBuilder<List<YArtist>>(Api).ExecuteAsync((YLibrarySection.Artists, YLibrarySectionType.Likes));
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YArtist>>> GetLikedArtistsAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetLibrarySection<List<YArtist>>(storage, YLibrarySection.Artists);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<List<YLibraryPlaylists>?> GetLikedPlaylistsAsync()
|
||||||
/// Получение лайкнутых плейлистов
|
=> new YGetLibrarySectionBuilder<List<YLibraryPlaylists>>(Api).ExecuteAsync((YLibrarySection.Playlists, YLibrarySectionType.Likes));
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YLibraryPlaylists>>> GetLikedPlaylistsAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetLibrarySection<List<YLibraryPlaylists>>(storage, YLibrarySection.Playlists);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Лайки
|
#endregion
|
||||||
|
|
||||||
#region Дизлайки
|
#region Дизлайки
|
||||||
|
|
||||||
/// <summary>
|
public Task<YLibraryTracks?> GetDislikedTracksAsync()
|
||||||
/// Получение дизлайкнутых треков
|
=> new YGetLibrarySectionBuilder<YLibraryTracks>(Api).ExecuteAsync((YLibrarySection.Tracks, YLibrarySectionType.Dislikes));
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YLibraryTracks>> GetDislikedTracksAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetLibrarySection<YLibraryTracks>(storage, YLibrarySection.Tracks, YLibrarySectionType.Dislikes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<List<YArtist>?> GetDislikedArtistsAsync()
|
||||||
/// Получение дизлайкнутых исполнителей
|
=> new YGetLibrarySectionBuilder<List<YArtist>>(Api).ExecuteAsync((YLibrarySection.Artists, YLibrarySectionType.Dislikes));
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YArtist>>> GetDislikedArtistsAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return GetLibrarySection<List<YArtist>>(storage, YLibrarySection.Artists, YLibrarySectionType.Dislikes);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Дизлайки
|
#endregion
|
||||||
|
|
||||||
#region Добавление в списки лайков/дизлайков
|
#region Добавление/удаление
|
||||||
|
|
||||||
/// <summary>
|
public Task<int?> AddTrackLikeAsync(YTrack track)
|
||||||
/// Добавить трек в список лайкнутых
|
=> new YLibraryAddBuilder<YRevision>(Api).ExecuteAsync((track.Id, YLibrarySection.Tracks, YLibrarySectionType.Likes))
|
||||||
/// </summary>
|
.ContinueWith(t => t.Result?.Revision);
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YPlaylist>> AddTrackLikeAsync(AuthStorage storage, YTrack track)
|
|
||||||
{
|
|
||||||
return new YLibraryAddBuilder<YPlaylist>(api, storage)
|
|
||||||
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Likes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<int?> RemoveTrackLikeAsync(YTrack track)
|
||||||
/// Удалить трек из списка лайкнутых
|
=> new YLibraryRemoveBuilder<YRevision>(Api).ExecuteAsync((track.Id, YLibrarySection.Tracks, YLibrarySectionType.Likes))
|
||||||
/// </summary>
|
.ContinueWith(t => t.Result?.Revision);
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YRevision>> RemoveTrackLikeAsync(AuthStorage storage, YTrack track)
|
|
||||||
{
|
|
||||||
return new YLibraryRemoveBuilder<YRevision>(api, storage)
|
|
||||||
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Likes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<int?> AddTrackDislikeAsync(YTrack track)
|
||||||
/// Добавить трек в список дизлайкнутых
|
=> new YLibraryAddBuilder<YRevision>(Api).ExecuteAsync((track.Id, YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
|
||||||
/// </summary>
|
.ContinueWith(t => t.Result?.Revision);
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YRevision>> AddTrackDislikeAsync(AuthStorage storage, YTrack track)
|
|
||||||
{
|
|
||||||
return new YLibraryAddBuilder<YRevision>(api, storage)
|
|
||||||
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<int?> RemoveTrackDislikeAsync(YTrack track)
|
||||||
/// Удалить трек из списка дизлайкнутых
|
=> new YLibraryRemoveBuilder<YRevision>(Api).ExecuteAsync((track.Id, YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
|
||||||
/// </summary>
|
.ContinueWith(t => t.Result?.Revision);
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YRevision>> RemoveTrackDislikeAsync(AuthStorage storage, YTrack track)
|
|
||||||
{
|
|
||||||
return new YLibraryRemoveBuilder<YRevision>(api, storage)
|
|
||||||
.Build((track.GetKey().ToString(), YLibrarySection.Tracks, YLibrarySectionType.Dislikes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<string?> AddAlbumLikeAsync(YAlbum album)
|
||||||
/// Добавить альбом в список лайкнутых
|
=> new YLibraryAddBuilder<string>(Api).ExecuteAsync((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes));
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="album">Альбом</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<string>> AddAlbumLikeAsync(AuthStorage storage, YAlbum album)
|
|
||||||
{
|
|
||||||
return new YLibraryAddBuilder<string>(api, storage)
|
|
||||||
.Build((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<string?> RemoveAlbumLikeAsync(YAlbum album)
|
||||||
/// Удалить альбом из списка лайкнутых
|
=> new YLibraryRemoveBuilder<string>(Api).ExecuteAsync((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes));
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="album">Альбом</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<string>> RemoveAlbumLikeAsync(AuthStorage storage, YAlbum album)
|
|
||||||
{
|
|
||||||
return new YLibraryRemoveBuilder<string>(api, storage)
|
|
||||||
.Build((album.Id, YLibrarySection.Albums, YLibrarySectionType.Likes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<string?> AddArtistLikeAsync(YArtist artist)
|
||||||
/// Добавить исполнителя в список лайкнутых
|
=> new YLibraryAddBuilder<string>(Api).ExecuteAsync((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes));
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="artist">Исполнитель</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<string>> AddArtistLikeAsync(AuthStorage storage, YArtist artist)
|
|
||||||
{
|
|
||||||
return new YLibraryAddBuilder<string>(api, storage)
|
|
||||||
.Build((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<string?> RemoveArtistLikeAsync(YArtist artist)
|
||||||
/// Удалить исполнителя из списка лайкнутых
|
=> new YLibraryRemoveBuilder<string>(Api).ExecuteAsync((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes));
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="artist">Исполнитель</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<string>> RemoveArtistLikeAsync(AuthStorage storage, YArtist artist)
|
|
||||||
{
|
|
||||||
return new YLibraryRemoveBuilder<string>(api, storage)
|
|
||||||
.Build((artist.Id, YLibrarySection.Artists, YLibrarySectionType.Likes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<string?> AddPlaylistLikeAsync(YPlaylist playlist)
|
||||||
/// Добавить плейлист в список лайкнутых
|
=> new YLibraryAddBuilder<string>(Api).ExecuteAsync((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes));
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="playlist">Плейлист</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<string>> AddPlaylistLikeAsync(AuthStorage storage, YPlaylist playlist)
|
|
||||||
{
|
|
||||||
return new YLibraryAddBuilder<string>(api, storage)
|
|
||||||
.Build((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<string?> RemovePlaylistLikeAsync(YPlaylist playlist)
|
||||||
/// Удалить плейлист из списка лайкнутых
|
=> new YLibraryRemoveBuilder<string>(Api).ExecuteAsync((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes));
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="playlist">Плейлист</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<string>> RemovePlaylistLikeAsync(AuthStorage storage, YPlaylist playlist)
|
|
||||||
{
|
|
||||||
return new YLibraryRemoveBuilder<string>(api, storage)
|
|
||||||
.Build((playlist.GetKey().ToString(), YLibrarySection.Playlists, YLibrarySectionType.Likes))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Добавление/удаление в списки лайков/дизлайков
|
#endregion
|
||||||
|
|
||||||
#region Получение списка "Вы недавно слушали"
|
#region Недавно прослушанное
|
||||||
|
|
||||||
public Task<YResponse<YRecentlyListenedContext>> GetRecentlyListenedAsync(AuthStorage storage, IEnumerable<YPlayContextType> contextTypes, int trackCount, int contextCount)
|
|
||||||
{
|
|
||||||
return new YGetLibraryRecentlyListenedBuilder(api, storage)
|
|
||||||
.Build((contextTypes, trackCount, contextCount))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Получение списка "Вы недавно слушали"
|
|
||||||
|
|
||||||
|
public Task<YRecentlyListenedContext?> GetRecentlyListenedAsync(
|
||||||
|
IEnumerable<YPlayContextType> contextTypes,
|
||||||
|
int trackCount = 50,
|
||||||
|
int contextCount = 10)
|
||||||
|
=> new YGetLibraryRecentlyListenedBuilder(Api).ExecuteAsync((contextTypes, trackCount, contextCount));
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,13 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Pins;
|
using YandexMusic.API.Models.Pins;
|
||||||
using YandexMusic.API.Requests.Pins;
|
using YandexMusic.API.Requests.Pins;
|
||||||
|
|
||||||
namespace YandexMusic.API;
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
/// <summary>API для взаимодействия с закреплёнными объектами (пинами).</summary>
|
/// <summary>API для работы с закреплёнными объектами (пинами).</summary>
|
||||||
public class YPinsAPI : YCommonAPI
|
public class YPinsAPI : YCommonAPI
|
||||||
{
|
{
|
||||||
/// <summary>Инициализирует новый экземпляр API пинов.</summary>
|
public YPinsAPI(YandexMusicApi api) : base(api) { }
|
||||||
/// <param name="yandex">Экземпляр основного API.</param>
|
|
||||||
public YPinsAPI(YandexMusicApi yandex) : base(yandex) { }
|
|
||||||
|
|
||||||
/// <summary>Получает список закреплённых объектов.</summary>
|
public Task<YPins?> GetAsync()
|
||||||
/// <param name="storage">Хранилище авторизации.</param>
|
=> new YGetPinsBuilder(Api).ExecuteAsync(null!);
|
||||||
/// <returns>Ответ API со списком пинов.</returns>
|
|
||||||
public Task<YResponse<YPins>> GetAsync(AuthStorage storage)
|
|
||||||
=> new YGetPinsBuilder(api, storage).Build(null!).GetResponseAsync();
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Landing;
|
using YandexMusic.API.Models.Landing;
|
||||||
using YandexMusic.API.Models.Landing.Entity.Entities;
|
using YandexMusic.API.Models.Landing.Entity.Entities;
|
||||||
using YandexMusic.API.Models.Playlist;
|
using YandexMusic.API.Models.Playlist;
|
||||||
@@ -8,143 +6,114 @@ using YandexMusic.API.Requests.Playlist;
|
|||||||
|
|
||||||
namespace YandexMusic.API;
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
/// <summary>API для взаимодействия с плейлистами.</summary>
|
/// <summary>API для работы с плейлистами.</summary>
|
||||||
public class YPlaylistAPI : YCommonAPI
|
public class YPlaylistAPI : YCommonAPI
|
||||||
{
|
{
|
||||||
/// <summary>Инициализирует новый экземпляр API плейлистов.</summary>
|
public YPlaylistAPI(YandexMusicApi api) : base(api) { }
|
||||||
/// <param name="yandex">Экземпляр основного API.</param>
|
|
||||||
public YPlaylistAPI(YandexMusicApi yandex) : base(yandex) { }
|
|
||||||
|
|
||||||
/// <summary>Получает список персональных плейлистов с главной страницы.</summary>
|
public async Task<List<YPlaylist>> GetPersonalPlaylistsAsync()
|
||||||
/// <param name="storage">Хранилище авторизации.</param>
|
|
||||||
/// <returns>Список ответов с плейлистами.</returns>
|
|
||||||
public async Task<List<YResponse<YPlaylist>>> GetPersonalPlaylistsAsync(AuthStorage storage)
|
|
||||||
{
|
{
|
||||||
var landing = await api.Landing.GetAsync(storage, YLandingBlockType.PersonalPlaylists);
|
var landing = await Api.Landing.GetAsync(YLandingBlockType.PersonalPlaylists);
|
||||||
var block = landing.Result?.Blocks?.FirstOrDefault(b => b.Type == YLandingBlockType.PersonalPlaylists);
|
var block = landing?.Blocks?.FirstOrDefault(b => b.Type == YLandingBlockType.PersonalPlaylists);
|
||||||
if (block?.Entities == null)
|
if (block?.Entities == null)
|
||||||
return new List<YResponse<YPlaylist>>();
|
return new List<YPlaylist>();
|
||||||
|
|
||||||
var tasks = block.Entities
|
var tasks = block.Entities
|
||||||
.OfType<YLandingEntityPersonalPlaylist>()
|
.OfType<YLandingEntityPersonalPlaylist>()
|
||||||
.Select(e => api.Playlist.GetAsync(storage, e.Data?.Data));
|
.Select(e => GetAsync(e.Data?.Data?.Owner?.Uid ?? Api.Storage.User.Uid, e.Data?.Data?.Kind ?? ""))
|
||||||
|
.Where(t => t != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
return new List<YResponse<YPlaylist>>(await Task.WhenAll(tasks));
|
var results = await Task.WhenAll(tasks);
|
||||||
|
return results.Where(p => p != null).ToList()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Получает избранные плейлисты.</summary>
|
public Task<YPlaylist?> GetAsync(string user, string kind)
|
||||||
public Task<YResponse<List<YPlaylist>>> FavoritesAsync(AuthStorage storage)
|
=> new YGetPlaylistBuilder(Api).ExecuteAsync((user, kind));
|
||||||
=> new YGetPlaylistFavoritesBuilder(api, storage).Build(null!).GetResponseAsync();
|
|
||||||
|
|
||||||
/// <summary>Получает плейлист дня.</summary>
|
public Task<YPlaylist?> GetAsync(string uuid)
|
||||||
public Task<YResponse<YPlaylist>> OfTheDayAsync(AuthStorage storage)
|
=> new YGetPlaylistByUuidBuilder(Api).ExecuteAsync(uuid);
|
||||||
=> GetPersonalPlaylistAsync(storage, YGeneratedPlaylistType.PlaylistOfTheDay);
|
|
||||||
|
|
||||||
/// <summary>Получает плейлист «Дежавю».</summary>
|
public Task<YPlaylist?> GetAsync(YPlaylist playlist)
|
||||||
public Task<YResponse<YPlaylist>> DejaVuAsync(AuthStorage storage)
|
=> GetAsync(playlist.Owner.Uid, playlist.Kind);
|
||||||
=> GetPersonalPlaylistAsync(storage, YGeneratedPlaylistType.NeverHeard);
|
|
||||||
|
|
||||||
/// <summary>Получает плейлист «Премьера».</summary>
|
public Task<List<YPlaylist>?> GetAsync(IEnumerable<(string user, string kind)> ids)
|
||||||
public Task<YResponse<YPlaylist>> PremiereAsync(AuthStorage storage)
|
=> new YGetPlaylistsBuilder(Api).ExecuteAsync(ids);
|
||||||
=> GetPersonalPlaylistAsync(storage, YGeneratedPlaylistType.RecentTracks);
|
|
||||||
|
|
||||||
/// <summary>Получает плейлист «Тайник».</summary>
|
public Task<List<YPlaylist>?> FavoritesAsync()
|
||||||
public Task<YResponse<YPlaylist>> MissedAsync(AuthStorage storage)
|
=> new YGetPlaylistFavoritesBuilder(Api).ExecuteAsync(null!);
|
||||||
=> GetPersonalPlaylistAsync(storage, YGeneratedPlaylistType.MissedLikes);
|
|
||||||
|
|
||||||
/// <summary>Получает плейлист «Кинопоиск».</summary>
|
public async Task<YPlaylist?> OfTheDayAsync()
|
||||||
public Task<YResponse<YPlaylist>> KinopoiskAsync(AuthStorage storage)
|
=> (await GetPersonalPlaylistsAsync()).FirstOrDefault(p => p?.GeneratedPlaylistType == YGeneratedPlaylistType.PlaylistOfTheDay.ToString());
|
||||||
=> GetPersonalPlaylistAsync(storage, YGeneratedPlaylistType.Kinopoisk);
|
|
||||||
|
|
||||||
private async Task<YResponse<YPlaylist>> GetPersonalPlaylistAsync(AuthStorage storage, YGeneratedPlaylistType type)
|
public async Task<YPlaylist?> DejaVuAsync()
|
||||||
{
|
=> (await GetPersonalPlaylistsAsync()).FirstOrDefault(p => p?.GeneratedPlaylistType == YGeneratedPlaylistType.NeverHeard.ToString());
|
||||||
var list = await GetPersonalPlaylistsAsync(storage);
|
|
||||||
return list.FirstOrDefault(e => string.Equals(e.Result?.GeneratedPlaylistType, type.ToString(), StringComparison.CurrentCultureIgnoreCase))
|
|
||||||
?? throw new Exception($"Плейлист типа {type} не найден.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Получает плейлист по идентификатору пользователя и типа.</summary>
|
public async Task<YPlaylist?> PremiereAsync()
|
||||||
public Task<YResponse<YPlaylist>> GetAsync(AuthStorage storage, string user, string kind)
|
=> (await GetPersonalPlaylistsAsync()).FirstOrDefault(p => p?.GeneratedPlaylistType == YGeneratedPlaylistType.RecentTracks.ToString());
|
||||||
=> new YGetPlaylistBuilder(api, storage).Build((user, kind)).GetResponseAsync();
|
|
||||||
|
|
||||||
/// <summary>Получает плейлист по UUID.</summary>
|
public async Task<YPlaylist?> MissedAsync()
|
||||||
public Task<YResponse<YPlaylist>> GetAsync(AuthStorage storage, string uuid)
|
=> (await GetPersonalPlaylistsAsync()).FirstOrDefault(p => p?.GeneratedPlaylistType == YGeneratedPlaylistType.MissedLikes.ToString());
|
||||||
=> new YGetPlaylistByUuidBuilder(api, storage).Build(uuid).GetResponseAsync();
|
|
||||||
|
|
||||||
/// <summary>Получает несколько плейлистов по списку пар (пользователь, тип).</summary>
|
public async Task<YPlaylist?> KinopoiskAsync()
|
||||||
public Task<YResponse<List<YPlaylist>>> GetAsync(AuthStorage storage, IEnumerable<(string user, string kind)> ids)
|
=> (await GetPersonalPlaylistsAsync()).FirstOrDefault(p => p?.GeneratedPlaylistType == YGeneratedPlaylistType.Kinopoisk.ToString());
|
||||||
=> new YGetPlaylistsBuilder(api, storage).Build(ids).GetResponseAsync();
|
|
||||||
|
|
||||||
/// <summary>Получает плейлист по объекту плейлиста (обновляет его треки).</summary>
|
public Task<YPlaylist?> CreateAsync(string name)
|
||||||
public Task<YResponse<YPlaylist>> GetAsync(AuthStorage storage, YPlaylist playlist)
|
=> new YPlaylistCreateBuilder(Api).ExecuteAsync(name);
|
||||||
=> new YGetPlaylistBuilder(api, storage).Build((playlist.Owner.Uid, playlist.Kind)).GetResponseAsync();
|
|
||||||
|
|
||||||
/// <summary>Создаёт новый плейлист с заданным именем.</summary>
|
public Task<YPlaylist?> RenameAsync(string kind, string name)
|
||||||
public Task<YResponse<YPlaylist>> CreateAsync(AuthStorage storage, string name)
|
=> new YPlaylistRenameBuilder(Api).ExecuteAsync((kind, name));
|
||||||
=> new YPlaylistCreateBuilder(api, storage).Build(name).GetResponseAsync();
|
|
||||||
|
|
||||||
/// <summary>Переименовывает плейлист.</summary>
|
public Task<YPlaylist?> RenameAsync(YPlaylist playlist, string name)
|
||||||
public Task<YResponse<YPlaylist>> RenameAsync(AuthStorage storage, string kind, string name)
|
=> RenameAsync(playlist.Kind, name);
|
||||||
=> new YPlaylistRenameBuilder(api, storage).Build((kind, name)).GetResponseAsync();
|
|
||||||
|
|
||||||
/// <summary>Переименовывает плейлист.</summary>
|
public async Task<bool> DeleteAsync(string kind)
|
||||||
public Task<YResponse<YPlaylist>> RenameAsync(AuthStorage storage, YPlaylist playlist, string name)
|
|
||||||
=> RenameAsync(storage, playlist.Kind, name);
|
|
||||||
|
|
||||||
/// <summary>Удаляет плейлист.</summary>
|
|
||||||
public async Task<bool> DeleteAsync(AuthStorage storage, string kind)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await new YPlaylistRemoveBuilder(api, storage).Build(kind).GetResponseAsync();
|
await new YPlaylistRemoveBuilder(Api).ExecuteAsync(kind);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch
|
||||||
{
|
{
|
||||||
// Логирование ошибки можно добавить через ILogger
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Удаляет плейлист.</summary>
|
public Task<bool> DeleteAsync(YPlaylist playlist)
|
||||||
public Task<bool> DeleteAsync(AuthStorage storage, YPlaylist playlist)
|
=> DeleteAsync(playlist.Kind);
|
||||||
=> DeleteAsync(storage, playlist.Kind);
|
|
||||||
|
|
||||||
/// <summary>Добавляет треки в начало плейлиста.</summary>
|
public async Task<YPlaylist?> InsertTracksAsync(YPlaylist playlist, IEnumerable<YTrack> tracks)
|
||||||
public async Task<YResponse<YPlaylist>> InsertTracksAsync(AuthStorage storage, YPlaylist playlist, IEnumerable<YTrack> tracks)
|
|
||||||
{
|
{
|
||||||
var change = await ChangePlaylistAsync(storage, playlist, new List<YPlaylistChange>
|
var change = await new YPlaylistChangeBuilder(Api).ExecuteAsync((playlist, new[]
|
||||||
{
|
{
|
||||||
new()
|
new YPlaylistChange
|
||||||
{
|
{
|
||||||
Operation = YPlaylistChangeType.Insert,
|
Operation = YPlaylistChangeType.Insert,
|
||||||
At = 0,
|
At = 0,
|
||||||
Tracks = tracks.Select(t => t.GetKey())
|
Tracks = tracks.Select(t => t.GetKey())
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
return await GetAsync(storage, change.Result);
|
return change != null ? await GetAsync(change) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Удаляет треки из плейлиста.</summary>
|
public async Task<YPlaylist?> DeleteTracksAsync(YPlaylist playlist, IEnumerable<YTrack> tracks)
|
||||||
public Task<YResponse<YPlaylist>> DeleteTracksAsync(AuthStorage storage, YPlaylist playlist, IEnumerable<YTrack> tracks)
|
|
||||||
{
|
{
|
||||||
var distinctTracks = tracks.Distinct().ToList();
|
var distinctTracks = tracks.Distinct().ToList();
|
||||||
|
var indices = distinctTracks
|
||||||
var changes = distinctTracks
|
|
||||||
.Select(t => playlist.Tracks?.FindIndex(ct => ct.Track?.GetKey() == t.GetKey()) ?? -1)
|
.Select(t => playlist.Tracks?.FindIndex(ct => ct.Track?.GetKey() == t.GetKey()) ?? -1)
|
||||||
.Where(i => i != -1)
|
.Where(i => i != -1)
|
||||||
.Select(i => new YPlaylistChange
|
|
||||||
{
|
|
||||||
Operation = YPlaylistChangeType.Delete,
|
|
||||||
From = i,
|
|
||||||
To = i + 1,
|
|
||||||
Tracks = new List<YTrackAlbumPair> { playlist.Tracks![i].Track!.GetKey() }
|
|
||||||
})
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return ChangePlaylistAsync(storage, playlist, changes);
|
var changes = indices.Select(i => new YPlaylistChange
|
||||||
}
|
{
|
||||||
|
Operation = YPlaylistChangeType.Delete,
|
||||||
|
From = i,
|
||||||
|
To = i + 1,
|
||||||
|
Tracks = new[] { playlist.Tracks![i].Track!.GetKey() }
|
||||||
|
});
|
||||||
|
|
||||||
private Task<YResponse<YPlaylist>> ChangePlaylistAsync(AuthStorage storage, YPlaylist playlist, IEnumerable<YPlaylistChange> changes)
|
var change = await new YPlaylistChangeBuilder(Api).ExecuteAsync((playlist, changes));
|
||||||
=> new YPlaylistChangeBuilder(api, storage).Build((playlist, changes)).GetResponseAsync();
|
return change != null ? await GetAsync(change) : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,72 +1,22 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Queue;
|
using YandexMusic.API.Models.Queue;
|
||||||
using YandexMusic.API.Requests.Queue;
|
using YandexMusic.API.Requests.Queue;
|
||||||
|
|
||||||
namespace YandexMusic.API;
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>API для работы с очередями воспроизведения.</summary>
|
||||||
/// API для взаимодействия с очередями
|
public class YQueueAPI : YCommonAPI
|
||||||
/// </summary>
|
|
||||||
public partial class YQueueAPI : YCommonAPI
|
|
||||||
{
|
{
|
||||||
public YQueueAPI(YandexMusicApi yandex) : base(yandex)
|
public YQueueAPI(YandexMusicApi api) : base(api) { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<YQueueItemsContainer?> ListAsync(string? device = null)
|
||||||
/// Получение всех очередей треков с разных устройств для синхронизации между ними
|
=> new YQueuesListBuilder(Api, device).ExecuteAsync(null!);
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="device">Устройство</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YQueueItemsContainer>> ListAsync(AuthStorage storage, string? device = null)
|
|
||||||
{
|
|
||||||
return new YQueuesListBuilder(api, storage)
|
|
||||||
.Build(device)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<YQueue?> GetAsync(string queueId)
|
||||||
/// Получение очереди
|
=> new YGetQueueBuilder(Api).ExecuteAsync(queueId);
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="queueId">Идентификатор очереди</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YQueue>> GetAsync(AuthStorage storage, string queueId)
|
|
||||||
{
|
|
||||||
return new YGetQueueBuilder(api, storage)
|
|
||||||
.Build(queueId)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<YNewQueue?> CreateAsync(YQueue queue, string? device = null)
|
||||||
/// Создание новой очереди треков
|
=> new YQueueCreateBuilder(Api, device).ExecuteAsync(queue);
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="queue">Очередь треков</param>
|
|
||||||
/// <param name="device">Устройство</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YNewQueue>> CreateAsync(AuthStorage storage, YQueue queue, string? device = null)
|
|
||||||
{
|
|
||||||
return new YQueueCreateBuilder(api, storage, device)
|
|
||||||
.Build(queue)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<YUpdatedQueue?> UpdatePositionAsync(string queueId, int currentIndex, bool isInteractive, string? device = null)
|
||||||
/// Установка текущего индекса проигрываемого трека в очереди треков
|
=> new YQueueUpdatePositionBuilder(Api, device).ExecuteAsync((queueId, currentIndex, isInteractive));
|
||||||
/// </summary>
|
}
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="queueId">Идентификатор очереди</param>
|
|
||||||
/// <param name="currentIndex">Текущий индекс</param>
|
|
||||||
/// <param name="isInteractive">Флаг интерактивности</param>
|
|
||||||
/// <param name="device">Устройство</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YUpdatedQueue>> UpdatePositionAsync(AuthStorage storage, string queueId, int currentIndex, bool isInteractive, string device = null)
|
|
||||||
{
|
|
||||||
return new YQueueUpdatePositionBuilder(api, storage, device)
|
|
||||||
.Build((queueId, currentIndex, isInteractive))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,113 +1,37 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Radio;
|
using YandexMusic.API.Models.Radio;
|
||||||
using YandexMusic.API.Models.Track;
|
using YandexMusic.API.Models.Track;
|
||||||
using YandexMusic.API.Requests.Radio;
|
using YandexMusic.API.Requests.Radio;
|
||||||
|
|
||||||
namespace YandexMusic.API;
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>API для работы с радио.</summary>
|
||||||
/// API для взаимодействия с радио
|
public class YRadioAPI : YCommonAPI
|
||||||
/// </summary>
|
|
||||||
public partial class YRadioAPI : YCommonAPI
|
|
||||||
{
|
{
|
||||||
public YRadioAPI(YandexMusicApi yandex) : base(yandex)
|
public YRadioAPI(YandexMusicApi api) : base(api) { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<YStationsDashboard?> GetStationsDashboardAsync()
|
||||||
/// Получение списка рекомендованных радиостанций
|
=> new YGetStationsDashboardBuilder(Api).ExecuteAsync(null!);
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YStationsDashboard>> GetStationsDashboardAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return new YGetStationsDashboardBuilder(api, storage)
|
|
||||||
.Build(null)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<List<YStation>?> GetStationsAsync()
|
||||||
/// Получение списка радиостанций
|
=> new YGetStationsBuilder(Api).ExecuteAsync(null!);
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YStation>>> GetStationsAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return new YGetStationsBuilder(api, storage)
|
|
||||||
.Build(null)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<List<YStation>?> GetStationAsync(string type, string tag)
|
||||||
/// Получение информации о радиостанции
|
=> new YGetStationBuilder(Api).ExecuteAsync((type, tag));
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="type">Тип</param>
|
|
||||||
/// <param name="tag">Тэг</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YStation>>> GetStationAsync(AuthStorage storage, string type, string tag)
|
|
||||||
{
|
|
||||||
return new YGetStationBuilder(api, storage)
|
|
||||||
.Build((type, tag))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<List<YStation>?> GetStationAsync(YStationId id)
|
||||||
/// Получение информации о радиостанции
|
=> GetStationAsync(id.Type, id.Tag);
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="id">Идентификатор станции</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YStation>>> GetStationAsync(AuthStorage storage, YStationId id)
|
|
||||||
{
|
|
||||||
return GetStationAsync(storage, id.Type, id.Tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<YStationSequence?> GetStationTracksAsync(YStation station, string prevTrackId = "")
|
||||||
/// Получение последовательности треков радиостанции
|
=> new YGetStationTracksBuilder(Api).ExecuteAsync((station.Station, prevTrackId));
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="station">Радиостанция</param>
|
|
||||||
/// <param name="prevTrackId">Идентификатор предыдущего трека</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YStationSequence>> GetStationTracksAsync(AuthStorage storage, YStation station, string prevTrackId = "")
|
|
||||||
{
|
|
||||||
return new YGetStationTracksBuilder(api, storage)
|
|
||||||
.Build((station.Station, prevTrackId))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Установка настроек подбора треков
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="station">Радиостанция</param>
|
|
||||||
/// <param name="settings">Настройки</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<string>> SetStationSettings2Async(AuthStorage storage, YStation station, YStationSettings2 settings)
|
|
||||||
{
|
|
||||||
return new YSetSettings2Builder(api, storage)
|
|
||||||
.Build((station.Station, settings))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отправка обратной связи на действия при прослушивании радио
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="station">Радиостанция</param>
|
|
||||||
/// <param name="type">Тип обратной связи</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <param name="batchId">Уникальный идентификатор партии треков. Возвращается при получении треков</param>
|
|
||||||
/// <param name="totalPlayedSeconds">Сколько было проиграно секунд трека перед действием</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<string> 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public Task<string?> SetStationSettings2Async(YStation station, YStationSettings2 settings)
|
||||||
|
=> new YSetSettings2Builder(Api).ExecuteAsync((station.Station, settings));
|
||||||
|
|
||||||
|
public Task<string?> SendStationFeedbackAsync(
|
||||||
|
YStation station,
|
||||||
|
YStationFeedbackType type,
|
||||||
|
YTrack? track = null,
|
||||||
|
string batchId = "",
|
||||||
|
double totalPlayedSeconds = 0)
|
||||||
|
=> new YSetStationFeedbackBuilder(Api).ExecuteAsync((type, station, track, batchId, totalPlayedSeconds));
|
||||||
}
|
}
|
||||||
@@ -1,139 +1,38 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
using YandexMusic.API.Models.Common;
|
||||||
using YandexMusic.API.Models.Search;
|
using YandexMusic.API.Models.Search;
|
||||||
using YandexMusic.API.Requests.Search;
|
using YandexMusic.API.Requests.Search;
|
||||||
|
|
||||||
namespace YandexMusic.API;
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>API для поиска.</summary>
|
||||||
/// API для поиска
|
public class YSearchAPI : YCommonAPI
|
||||||
/// </summary>
|
|
||||||
public partial class YSearchAPI : YCommonAPI
|
|
||||||
{
|
{
|
||||||
|
public YSearchAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
public YSearchAPI(YandexMusicApi yandex) : base(yandex)
|
public Task<YSearch?> TrackAsync(string trackName, int page = 0, int pageSize = 20)
|
||||||
{
|
=> SearchAsync(trackName, YSearchType.Track, page, pageSize);
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<YSearch?> AlbumsAsync(string albumName, int page = 0, int pageSize = 20)
|
||||||
/// Поиск по трекам
|
=> SearchAsync(albumName, YSearchType.Album, page, pageSize);
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="trackName">Имя трека</param>
|
|
||||||
/// <param name="pageNumber">Номер страницы</param>
|
|
||||||
/// <param name="pageSize">Размер страницы</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearch>> TrackAsync(AuthStorage storage, string trackName, int pageNumber = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return SearchAsync(storage, trackName, YSearchType.Track, pageNumber, pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<YSearch?> ArtistAsync(string artistName, int page = 0, int pageSize = 20)
|
||||||
/// Поиск по альбомам
|
=> SearchAsync(artistName, YSearchType.Artist, page, pageSize);
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="albumName">Имя альбома</param>
|
|
||||||
/// <param name="pageNumber">Номер страницы</param>
|
|
||||||
/// <param name="pageSize">Размер страницы</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearch>> AlbumsAsync(AuthStorage storage, string albumName, int pageNumber = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return SearchAsync(storage, albumName, YSearchType.Album, pageNumber, pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<YSearch?> PlaylistAsync(string playlistName, int page = 0, int pageSize = 20)
|
||||||
/// Поиск по артисту
|
=> SearchAsync(playlistName, YSearchType.Playlist, page, pageSize);
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="artistName">Имя артиста</param>
|
|
||||||
/// <param name="pageNumber">Номер страницы</param>
|
|
||||||
/// <param name="pageSize">Размер страницы</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearch>> ArtistAsync(AuthStorage storage, string artistName, int pageNumber = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return SearchAsync(storage, artistName, YSearchType.Artist, pageNumber, pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<YSearch?> PodcastEpisodeAsync(string podcastName, int page = 0, int pageSize = 20)
|
||||||
/// Поиск по плейлистам
|
=> SearchAsync(podcastName, YSearchType.PodcastEpisode, page, pageSize);
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="playlistName">Имя плейлиста</param>
|
|
||||||
/// <param name="pageNumber">Номер страницы</param>
|
|
||||||
/// <param name="pageSize">Размер страницы</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearch>> PlaylistAsync(AuthStorage storage, string playlistName, int pageNumber = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return SearchAsync(storage, playlistName, YSearchType.Playlist, pageNumber, pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<YSearch?> VideosAsync(string videoName, int page = 0, int pageSize = 20)
|
||||||
/// Поиск по плейлистам
|
=> SearchAsync(videoName, YSearchType.Video, page, pageSize);
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="podcastName">Имя подкаста</param>
|
|
||||||
/// <param name="pageNumber">Номер страницы</param>
|
|
||||||
/// <param name="pageSize">Размер страницы</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearch>> PodcastEpisodeAsync(AuthStorage storage, string podcastName, int pageNumber = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return SearchAsync(storage, podcastName, YSearchType.PodcastEpisode, pageNumber, pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<YSearch?> UsersAsync(string userName, int page = 0, int pageSize = 20)
|
||||||
/// Поиск по видео
|
=> SearchAsync(userName, YSearchType.User, page, pageSize);
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="videoName">Имя видео</param>
|
|
||||||
/// <param name="pageNumber">Номер страницы</param>
|
|
||||||
/// <param name="pageSize">Размер страницы</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearch>> VideosAsync(AuthStorage storage, string videoName, int pageNumber = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return SearchAsync(storage, videoName, YSearchType.Video, pageNumber, pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Поиск по пользователям
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="userName">Имя пользователя</param>
|
|
||||||
/// <param name="pageNumber">Номер страницы</param>
|
|
||||||
/// <param name="pageSize">Размер страницы</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearch>> UsersAsync(AuthStorage storage, string userName, int pageNumber = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return SearchAsync(storage, userName, YSearchType.User, pageNumber, pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Поиск
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="searchText">Поисковый запрос</param>
|
|
||||||
/// <param name="searchType">Тип поиска</param>
|
|
||||||
/// <param name="page">Страница</param>
|
|
||||||
/// <param name="pageSize">Размер страницы</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearch>> SearchAsync(AuthStorage storage, string searchText, YSearchType searchType, int page = 0, int pageSize = 20)
|
|
||||||
{
|
|
||||||
return new YSearchBuilder(api, storage)
|
|
||||||
.Build((searchText, searchType, page, pageSize))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Подсказка
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="searchText">Поисковый запрос</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YSearchSuggest>> SuggestAsync(AuthStorage storage, string searchText)
|
|
||||||
{
|
|
||||||
return new YSearchSuggestBuilder(api, storage)
|
|
||||||
.Build(searchText)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public Task<YSearch?> SearchAsync(string searchText, YSearchType searchType, int page = 0, int pageSize = 20)
|
||||||
|
=> new YSearchBuilder(Api).ExecuteAsync((searchText, searchType, page, pageSize));
|
||||||
|
|
||||||
|
public Task<YSearchSuggest?> GetSearchSuggestionsAsync(string searchText)
|
||||||
|
=> new YSearchSuggestBuilder(Api).ExecuteAsync(searchText);
|
||||||
}
|
}
|
||||||
@@ -1,307 +1,110 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
using YandexMusic.API.Models.Common;
|
||||||
using YandexMusic.API.Models.Track;
|
using YandexMusic.API.Models.Track;
|
||||||
using YandexMusic.API.Requests.Track;
|
using YandexMusic.API.Requests.Track;
|
||||||
|
|
||||||
namespace YandexMusic.API;
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>API для работы с треками (получение, загрузка, метаданные).</summary>
|
||||||
/// API для взаимодействия с треками
|
public class YTrackAPI : YCommonAPI
|
||||||
/// </summary>
|
|
||||||
public partial class YTrackAPI : YCommonAPI
|
|
||||||
{
|
{
|
||||||
#region Вспомогательные функции
|
public YTrackAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
private string BuildLinkForDownload(YTrackDownloadInfo mainDownloadResponse, YStorageDownloadFile storageDownload)
|
private static string BuildDownloadLink(YTrackDownloadInfo info, YStorageDownloadFile storageDownload)
|
||||||
{
|
{
|
||||||
string path = storageDownload.Path;
|
var path = storageDownload.Path;
|
||||||
string host = storageDownload.Host;
|
var host = storageDownload.Host;
|
||||||
string ts = storageDownload.Ts;
|
var ts = storageDownload.Ts;
|
||||||
string s = storageDownload.S;
|
var s = storageDownload.S;
|
||||||
string codec = mainDownloadResponse.Codec;
|
var codec = info.Codec;
|
||||||
|
|
||||||
string secret = $"XGRlBW9FXlekgbPrRHuSiA{path.Substring(1, path.Length - 1)}{s}";
|
var secret = $"XGRlBW9FXlekgbPrRHuSiA{path[1..]}{s}";
|
||||||
MD5 md5 = MD5.Create();
|
var md5Hash = MD5.HashData(Encoding.UTF8.GetBytes(secret));
|
||||||
byte[] md5Hash = md5.ComputeHash(Encoding.UTF8.GetBytes(secret));
|
var hmacsha1 = new HMACSHA1(md5Hash);
|
||||||
HMACSHA1 hmacsha1 = new();
|
var sign = BitConverter.ToString(hmacsha1.ComputeHash(md5Hash)).Replace("-", "").ToLower();
|
||||||
byte[] hmasha1Hash = hmacsha1.ComputeHash(md5Hash);
|
return $"https://{host}/get-{codec}/{sign}/{ts}{path}";
|
||||||
string sign = BitConverter.ToString(hmasha1Hash).Replace("-", "").ToLower();
|
|
||||||
|
|
||||||
string link = $"https://{host}/get-{codec}/{sign}/{ts}{path}";
|
|
||||||
|
|
||||||
return link;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Вспомогательные функции
|
public async Task<YTrack?> GetAsync(string trackId)
|
||||||
|
=> (await GetAsync([trackId]))?.FirstOrDefault();
|
||||||
|
|
||||||
|
public Task<List<YTrack>?> GetAsync(IEnumerable<string> trackIds)
|
||||||
|
=> new YGetTracksBuilder(Api).ExecuteAsync(trackIds);
|
||||||
|
public Task<List<YTrackDownloadInfo>?> GetMetadataForDownloadAsync(string trackKey, bool direct = false)
|
||||||
|
=> new YTrackDownloadInfoBuilder(Api).ExecuteAsync((trackKey, direct));
|
||||||
|
|
||||||
|
public Task<List<YTrackDownloadInfo>?> GetMetadataForDownloadAsync(YTrack track, bool direct = false)
|
||||||
|
=> GetMetadataForDownloadAsync(track.GetKey().ToString(), direct);
|
||||||
|
|
||||||
public YTrackAPI(YandexMusicApi yandex) : base(yandex)
|
public Task<YStorageDownloadFile?> GetDownloadFileInfoAsync(YTrackDownloadInfo metadataInfo)
|
||||||
|
=> new YStorageDownloadFileBuilder(Api).ExecuteAsync(metadataInfo.DownloadInfoUrl);
|
||||||
|
|
||||||
|
public async Task<string?> GetFileLinkAsync(string trackKey)
|
||||||
{
|
{
|
||||||
|
var meta = await GetMetadataForDownloadAsync(trackKey);
|
||||||
|
var info = meta?.OrderByDescending(i => i.BitrateInKbps).FirstOrDefault(m => m.Codec == "mp3");
|
||||||
|
if (info == null) return null;
|
||||||
|
var storageDownload = await GetDownloadFileInfoAsync(info);
|
||||||
|
if (storageDownload == null) return null;
|
||||||
|
return BuildDownloadLink(info, storageDownload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public Task<string?> GetFileLinkAsync(YTrack track)
|
||||||
/// Получение треков
|
=> GetFileLinkAsync(track.GetKey().ToString());
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
public async Task ExtractToFileAsync(string trackKey, string filePath)
|
||||||
/// <param name="trackId">Идентификатор трека</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YTrack>>> GetAsync(AuthStorage storage, string trackId)
|
|
||||||
{
|
{
|
||||||
return new YGetTracksBuilder(api, storage)
|
var url = await GetFileLinkAsync(trackKey);
|
||||||
.Build(new[] { trackId })
|
if (string.IsNullOrEmpty(url)) throw new Exception("Не удалось получить ссылку на трек");
|
||||||
.GetResponseAsync();
|
using var response = await Api.HttpClient.GetAsync(url);
|
||||||
|
await using var fs = File.Create(filePath);
|
||||||
|
await response.Content.CopyToAsync(fs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public Task ExtractToFileAsync(YTrack track, string filePath)
|
||||||
/// Получение треков
|
=> ExtractToFileAsync(track.GetKey().ToString(), filePath);
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
public async Task<byte[]> ExtractDataAsync(string trackKey)
|
||||||
/// <param name="trackIds">Идентификаторы треков</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YTrack>>> GetAsync(AuthStorage storage, IEnumerable<string> trackIds)
|
|
||||||
{
|
{
|
||||||
return new YGetTracksBuilder(api, storage)
|
var url = await GetFileLinkAsync(trackKey);
|
||||||
.Build(trackIds)
|
if (string.IsNullOrEmpty(url)) throw new Exception("Не удалось получить ссылку на трек");
|
||||||
.GetResponseAsync();
|
return await Api.HttpClient.GetByteArrayAsync(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public Task<byte[]> ExtractDataAsync(YTrack track)
|
||||||
/// Получение метаданных для загрузки
|
=> ExtractDataAsync(track.GetKey().ToString());
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
public async Task<Stream> ExtractStreamAsync(string trackKey, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
||||||
/// <param name="trackKey">Ключ трека в формате {идентифактор трека:идентификатор альбома}</param>
|
|
||||||
/// <param name="direct">Должен ли ответ содержать прямую ссылку на загрузку</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YTrackDownloadInfo>>> GetMetadataForDownloadAsync(AuthStorage storage, string trackKey, bool direct = false)
|
|
||||||
{
|
{
|
||||||
return new YTrackDownloadInfoBuilder(api, storage)
|
var url = await GetFileLinkAsync(trackKey);
|
||||||
.Build((trackKey, direct))
|
if (string.IsNullOrEmpty(url)) throw new Exception("Не удалось получить ссылку на трек");
|
||||||
.GetResponseAsync();
|
var response = await Api.HttpClient.GetAsync(url, completionOption);
|
||||||
|
return await response.Content.ReadAsStreamAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public Task<Stream> ExtractStreamAsync(YTrack track, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
||||||
/// Получение метаданных для загрузки
|
=> ExtractStreamAsync(track.GetKey().ToString(), completionOption);
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <param name="direct">Должен ли ответ содержать прямую ссылку на загрузку</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<List<YTrackDownloadInfo>>> GetMetadataForDownloadAsync(AuthStorage storage, YTrack track, bool direct = false)
|
|
||||||
{
|
|
||||||
return GetMetadataForDownloadAsync(storage, track.GetKey().ToString(), direct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<string?> SendPlayTrackInfoAsync(
|
||||||
/// Получение информации для формирования ссылки для загрузки
|
YTrack track,
|
||||||
/// </summary>
|
string from,
|
||||||
/// <param name="storage">Хранилище</param>
|
bool fromCache = false,
|
||||||
/// <param name="metadataInfo">Метаданные для загрузки</param>
|
string playId = "",
|
||||||
/// <returns></returns>
|
string playlistId = "",
|
||||||
public Task<YStorageDownloadFile> GetDownloadFileInfoAsync(AuthStorage storage, YTrackDownloadInfo metadataInfo)
|
double totalPlayedSeconds = 0,
|
||||||
{
|
double endPositionSeconds = 0)
|
||||||
return new YStorageDownloadFileBuilder(api, storage)
|
=> new YSendTrackInfoBuilder(Api).ExecuteAsync((track, from, fromCache, playId, playlistId, totalPlayedSeconds, endPositionSeconds));
|
||||||
.Build(metadataInfo.DownloadInfoUrl)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<YTrackSupplement?> GetSupplementAsync(string trackId)
|
||||||
/// Получение ссылки для загрузки
|
=> new YGetTrackSupplementBuilder(Api).ExecuteAsync(trackId);
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="trackKey">Ключ трека в формате {идентификатор трека:идентификатор альбома}</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<string> GetFileLinkAsync(AuthStorage storage, string trackKey)
|
|
||||||
{
|
|
||||||
YResponse<List<YTrackDownloadInfo>> 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<YTrackSupplement?> GetSupplementAsync(YTrack track)
|
||||||
/// Получение ссылки для загрузки
|
=> GetSupplementAsync(track.GetKey().ToString());
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<string> GetFileLinkAsync(AuthStorage storage, YTrack track)
|
|
||||||
{
|
|
||||||
return GetFileLinkAsync(storage, track.GetKey().ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отправка текущего состояния прослушиваемого трека
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <param name="from">Наименования клиента, с которого происходит прослушивание</param>
|
|
||||||
/// <param name="fromCache">Проигрывается ли трек с кеша</param>
|
|
||||||
/// <param name="playId">Уникальный идентификатор проигрывания</param>
|
|
||||||
/// <param name="playlistId">Уникальный идентификатор плейлиста, если таковой прослушивается</param>
|
|
||||||
/// <param name="totalPlayedSeconds">Сколько было всего воспроизведено трека в секундах</param>
|
|
||||||
/// <param name="endPositionSeconds">Окончательное значение воспроизведенных секунд</param>
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<string> 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
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение дополнительной информации для трека
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="trackId">Идентификатор трека</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YTrackSupplement>> GetSupplementAsync(AuthStorage storage, string trackId)
|
|
||||||
{
|
|
||||||
return new YGetTrackSupplementBuilder(api, storage)
|
|
||||||
.Build(trackId)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение дополнительной информации для трека
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YTrackSupplement>> GetSupplementAsync(AuthStorage storage, YTrack track)
|
|
||||||
{
|
|
||||||
return new YGetTrackSupplementBuilder(api, storage)
|
|
||||||
.Build(track.GetKey().ToString())
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion GetSupplement
|
|
||||||
|
|
||||||
#region GetSimilar
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение похожих треков
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="trackId">Идентификатор трека</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YTrackSimilar>> GetSimilarAsync(AuthStorage storage, string trackId)
|
|
||||||
{
|
|
||||||
return new YGetTrackSimilarBuilder(api, storage)
|
|
||||||
.Build(trackId)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение похожих треков
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YTrackSimilar>> GetSimilarAsync(AuthStorage storage, YTrack track)
|
|
||||||
{
|
|
||||||
return new YGetTrackSimilarBuilder(api, storage)
|
|
||||||
.Build(track.GetKey().ToString())
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion GetSimilar
|
|
||||||
|
|
||||||
#region Получение данных трека
|
|
||||||
|
|
||||||
#region В файл
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Выгрузка в файл
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="trackKey">Ключ трека в формате {идентификатор трека:идентификатор альбома}</param>
|
|
||||||
/// <param name="filePath">Путь для файла</param>
|
|
||||||
public async Task ExtractToFileAsync(AuthStorage storage, string trackKey, string filePath)
|
|
||||||
{
|
|
||||||
string url = await GetFileLinkAsync(storage, trackKey);
|
|
||||||
await new DataDownloader(storage).ToFile(url, filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Выгрузка в файл
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <param name="filePath">Путь для файла</param>
|
|
||||||
public Task ExtractToFileAsync(AuthStorage storage, YTrack track, string filePath)
|
|
||||||
{
|
|
||||||
return ExtractToFileAsync(storage, track.GetKey().ToString(), filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion В файл
|
|
||||||
|
|
||||||
#region В массив байт
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение двоичного массива данных
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="trackKey">Ключ трека в формате {идентификатор трека:идентификатор альбома}</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<byte[]> ExtractDataAsync(AuthStorage storage, string trackKey)
|
|
||||||
{
|
|
||||||
string url = await GetFileLinkAsync(storage, trackKey);
|
|
||||||
return await new DataDownloader(storage).AsBytes(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение двоичного массива данных
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<byte[]> ExtractDataAsync(AuthStorage storage, YTrack track)
|
|
||||||
{
|
|
||||||
return ExtractDataAsync(storage, track.GetKey().ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion В массив байт
|
|
||||||
|
|
||||||
#region В поток
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение потока данных
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="trackKey">Ключ трека в формате {идентификатор трека:идентификатор альбома}</param>
|
|
||||||
/// <param name="httpCompletionOption">Параметры передачи управления при http запросе</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<Stream> ExtractStreamAsync(AuthStorage storage, string trackKey,
|
|
||||||
HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
|
||||||
{
|
|
||||||
string url = await GetFileLinkAsync(storage, trackKey);
|
|
||||||
return await new DataDownloader(storage).AsStream(url, httpCompletionOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение потока данных
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="track">Трек</param>
|
|
||||||
/// <param name="httpCompletionOption">Параметры передачи управления при http запросе</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<Stream> ExtractStreamAsync(AuthStorage storage, YTrack track,
|
|
||||||
HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
|
||||||
{
|
|
||||||
return ExtractStreamAsync(storage, track.GetKey().ToString(), httpCompletionOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion В поток
|
|
||||||
|
|
||||||
#endregion Получение данных трека
|
|
||||||
|
|
||||||
|
public Task<YTrackSimilar?> GetSimilarAsync(string trackId)
|
||||||
|
=> new YGetTrackSimilarBuilder(Api).ExecuteAsync(trackId);
|
||||||
|
|
||||||
|
public Task<YTrackSimilar?> GetSimilarAsync(YTrack track)
|
||||||
|
=> GetSimilarAsync(track.GetKey().ToString());
|
||||||
}
|
}
|
||||||
@@ -1,71 +1,36 @@
|
|||||||
using YandexMusic.API.Common;
|
using YandexMusic.API.Models.Playlist;
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Playlist;
|
|
||||||
using YandexMusic.API.Models.Ugc;
|
using YandexMusic.API.Models.Ugc;
|
||||||
using YandexMusic.API.Requests.Ugc;
|
using YandexMusic.API.Requests.Ugc;
|
||||||
|
|
||||||
namespace YandexMusic.API;
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
public partial class YUgcAPI : YCommonAPI
|
/// <summary>API для загрузки пользовательского контента (UGC).</summary>
|
||||||
|
public class YUgcAPI : YCommonAPI
|
||||||
{
|
{
|
||||||
public YUgcAPI(YandexMusicApi yandex) : base(yandex)
|
public YUgcAPI(YandexMusicApi api) : base(api) { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public Task<YUgcUpload?> GetUgcUploadLinkAsync(YPlaylist playlist, string fileName)
|
||||||
/// Получение ссылки на загрузчик трека
|
=> new YUgcGetUploadLinkBuilder(Api).ExecuteAsync((playlist, fileName));
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="playlist">Плейлист, куда будет загружен трек</param>
|
|
||||||
/// <param name="fileName">Название файла для загрузки</param>
|
|
||||||
public Task<YUgcUpload> GetUgcUploadLinkAsync(AuthStorage storage, YPlaylist playlist, string fileName)
|
|
||||||
{
|
|
||||||
return new YUgcGetUploadLinkBuilder(api, storage)
|
|
||||||
.Build((playlist, fileName))
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public async Task<string?> UploadTrackToPlaylistAsync(YPlaylist playlist, string fileName, string filePath)
|
||||||
/// Загрузка трека из файла
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="uploadLink">Ссылка на балансировщик для загрузки, можно получить из GetUgcUploadLinkAsync</param>
|
|
||||||
/// <param name="filePath">Загружаемый файл</param>
|
|
||||||
public Task<YResponse<string>> UploadUgcTrackAsync(AuthStorage storage, string uploadLink, string filePath)
|
|
||||||
{
|
{
|
||||||
if (!File.Exists(filePath))
|
if (!File.Exists(filePath))
|
||||||
throw new FileNotFoundException("Файл для загрузки не существует.", filePath);
|
throw new FileNotFoundException("Файл не найден", filePath);
|
||||||
|
return await UploadTrackToPlaylistAsync(playlist, fileName, await File.ReadAllBytesAsync(filePath));
|
||||||
return UploadUgcTrackAsync(storage, uploadLink, File.Open(filePath, FileMode.Open));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public async Task<string?> UploadTrackToPlaylistAsync(YPlaylist playlist, string fileName, Stream stream)
|
||||||
/// Загрузка трека из потока
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="uploadLink">Ссылка на балансировщик для загрузки, можно получить из GetUgcUploadLinkAsync</param>
|
|
||||||
/// <param name="stream">Поток с данными для загрузки</param>
|
|
||||||
public Task<YResponse<string>> UploadUgcTrackAsync(AuthStorage storage, string uploadLink, Stream stream)
|
|
||||||
{
|
{
|
||||||
if (stream == null)
|
using var ms = new MemoryStream();
|
||||||
throw new NullReferenceException("Пустая ссылка на поток загрузки.");
|
await stream.CopyToAsync(ms);
|
||||||
|
return await UploadTrackToPlaylistAsync(playlist, fileName, ms.ToArray());
|
||||||
using MemoryStream ms = new();
|
|
||||||
stream.CopyTo(ms);
|
|
||||||
|
|
||||||
return UploadUgcTrackAsync(storage, uploadLink, ms.ToArray());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public async Task<string?> UploadTrackToPlaylistAsync(YPlaylist playlist, string fileName, byte[] file)
|
||||||
/// Загрузка трека из массива
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="uploadLink">Ссылка на балансировщик для загрузки, можно получить из GetUgcUploadLinkAsync</param>
|
|
||||||
/// <param name="file">Загружаемый трек в виде массив байтов</param>
|
|
||||||
public Task<YResponse<string>> UploadUgcTrackAsync(AuthStorage storage, string uploadLink, byte[] file)
|
|
||||||
{
|
{
|
||||||
return new YUgcUploadBuilder(api, storage)
|
var uploadLink = await GetUgcUploadLinkAsync(playlist, fileName);
|
||||||
.Build((uploadLink, file))
|
if (uploadLink?.PostTarget == null) return null;
|
||||||
.GetResponseAsync();
|
var result = await new YUgcUploadBuilder(Api).ExecuteAsync((uploadLink.PostTarget, file));
|
||||||
|
return result?.Result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,300 +1,175 @@
|
|||||||
using System.Security.Authentication;
|
using System.Security.Authentication;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Account;
|
using YandexMusic.API.Models.Account;
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Requests.Account;
|
using YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
namespace YandexMusic.API;
|
namespace YandexMusic.API;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>API для работы с пользователем и авторизации.</summary>
|
||||||
/// API для пользователя
|
public class YUserAPI : YCommonAPI
|
||||||
/// </summary>
|
|
||||||
public partial class YUserAPI : YCommonAPI
|
|
||||||
{
|
{
|
||||||
#region Вспомогательные функции
|
public YUserAPI(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
private async Task<bool> GetCsrfTokenAsync(AuthStorage storage)
|
private async Task<bool> GetCsrfTokenAsync()
|
||||||
{
|
{
|
||||||
using HttpResponseMessage authMethodsResponse = await new YGetAuthMethodsBuilder(api, storage)
|
using var response = await new YGetAuthMethodsBuilder(Api).ExecuteRawAsync(null!);
|
||||||
.Build(null)
|
if (response == null || !response.IsSuccessStatusCode)
|
||||||
.GetResponseAsync();
|
throw new HttpRequestException("Не удалось получить CSRF-токен");
|
||||||
|
|
||||||
if (!authMethodsResponse.IsSuccessStatusCode)
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
throw new HttpRequestException("Невозможно получить CFRF-токен.");
|
var csrfMatch = Regex.Match(content, @"window\.__CSRF__\s*=\s*""([^""]+)""");
|
||||||
|
var processMatch = Regex.Match(content, @"'process_uuid'\s*:\s*'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'");
|
||||||
|
|
||||||
string responseString = await authMethodsResponse.Content
|
if (!csrfMatch.Success || !processMatch.Success)
|
||||||
.ReadAsStringAsync();
|
|
||||||
Match match = Regex.Match(responseString, "\"csrf_token\" value=\"([^\"]+)\"");
|
|
||||||
|
|
||||||
if (!match.Success || match.Groups.Count < 2)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
storage.AuthToken = new YAuthToken
|
Api.Storage.HeaderToken = new YAuthToken
|
||||||
{
|
{
|
||||||
CsfrToken = match.Groups[1].Value
|
CsfrToken = csrfMatch.Groups[1].Value,
|
||||||
|
ProcessUuid = processMatch.Groups[1].Value
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await new YPostAuthStats(Api).ExecuteAsync(null!);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> LoginByCookiesAsync()
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new AuthenticationException("Сессия входа не инициализирована");
|
||||||
|
|
||||||
|
var accessToken = await new YGetAuthCookiesBuilder(Api).ExecuteAsync(null!);
|
||||||
|
if (accessToken == null || string.IsNullOrEmpty(accessToken.AccessToken))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Api.Storage.AccessToken = accessToken;
|
||||||
|
Api.Storage.Token = accessToken.AccessToken;
|
||||||
|
|
||||||
|
await AuthorizeAsync(accessToken.AccessToken);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> LoginByCookiesAsync(AuthStorage storage)
|
public async Task AuthorizeAsync(string token)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Авторизация
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="token">Токен авторизации</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task AuthorizeAsync(AuthStorage storage, string token)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(token))
|
if (string.IsNullOrEmpty(token))
|
||||||
throw new Exception("Задан пустой токен авторизации.");
|
throw new Exception("Токен не может быть пустым");
|
||||||
|
|
||||||
storage.Token = token;
|
Api.Storage.Token = token;
|
||||||
|
var authInfo = await new YGetAuthInfoBuilder(Api).ExecuteAsync(null!);
|
||||||
|
if (authInfo?.Account?.Uid == null)
|
||||||
|
throw new Exception("Пользователь не авторизован");
|
||||||
|
|
||||||
// Пытаемся получить информацию о пользователе
|
Api.Storage.SetAuthorized(authInfo.Account, token);
|
||||||
YResponse<YAccountResult> authInfo = await GetUserAuthAsync(storage);
|
|
||||||
|
|
||||||
// Если не авторизован, то авторизуем
|
|
||||||
if (string.IsNullOrEmpty(authInfo.Result.Account.Uid))
|
|
||||||
throw new Exception("Пользователь незалогинен.");
|
|
||||||
|
|
||||||
// Флаг авторизации
|
|
||||||
storage.IsAuthorized = true;
|
|
||||||
storage.User = authInfo.Result.Account;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public Task<YAccountResult?> GetUserAuthAsync()
|
||||||
/// Получение информации об авторизации
|
=> new YGetAuthInfoBuilder(Api).ExecuteAsync(null!);
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
public async Task<YAuthTypes?> CreateAuthSessionAsync(string userName)
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YResponse<YAccountResult>> GetUserAuthAsync(AuthStorage storage)
|
|
||||||
{
|
{
|
||||||
return new YGetAuthInfoBuilder(api, storage)
|
if (!await GetCsrfTokenAsync())
|
||||||
.Build(null)
|
throw new Exception("Не удалось инициализировать сессию");
|
||||||
.GetResponseAsync();
|
|
||||||
|
var result = await new YGetAuthLoginUserBuilder(Api).ExecuteAsync((Api.Storage.AuthToken.CsfrToken, userName));
|
||||||
|
if (result?.TrackId != null)
|
||||||
|
Api.Storage.AuthToken.TrackId = result.TrackId;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public async Task<string?> GetAuthQRLinkAsync()
|
||||||
/// Создание сеанса и получение доступных методов авторизации
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="userName">Имя пользователя</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<YAuthTypes> CreateAuthSessionAsync(AuthStorage storage, string userName)
|
|
||||||
{
|
{
|
||||||
if (!await GetCsrfTokenAsync(storage))
|
if (!await GetCsrfTokenAsync())
|
||||||
throw new Exception("Невозможно инициализировать сессию входа.");
|
throw new Exception("Не удалось инициализировать сессию");
|
||||||
|
|
||||||
YAuthTypes types = await new YGetAuthLoginUserBuilder(api, storage)
|
var qr = await new YGetAuthQRBuilder(Api).ExecuteAsync(null!);
|
||||||
.Build((storage.AuthToken.CsfrToken, userName))
|
if (qr?.Status != YAuthStatus.Ok || string.IsNullOrEmpty(qr.TrackId))
|
||||||
.GetResponseAsync();
|
return null;
|
||||||
|
|
||||||
storage.AuthToken.TrackId = types.TrackId;
|
Api.Storage.AuthToken = new YAuthToken
|
||||||
|
|
||||||
return types;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение ссылки на QR-код
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<string> 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,
|
TrackId = qr.TrackId,
|
||||||
CsfrToken = result.CsrfToken
|
CsfrToken = qr.CsrfToken
|
||||||
};
|
};
|
||||||
|
return $"https://passport.yandex.ru/auth/magic/code/?track_id={qr.TrackId}";
|
||||||
return $"https://passport.yandex.ru/auth/magic/code/?track_id={result.TrackId}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public async Task<YAuthQRStatus?> CheckQRStatusAsync()
|
||||||
/// Авторизация по QR-коду
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<YAuthQRStatus> AuthorizeByQRAsync(AuthStorage storage)
|
|
||||||
{
|
{
|
||||||
if (storage.AuthToken == null)
|
if (Api.Storage.AuthToken == null)
|
||||||
throw new Exception("Не выполнен запрос на авторизацию по QR.");
|
throw new Exception("Сессия не инициализирована");
|
||||||
|
|
||||||
try
|
var status = await new YPostQrStatus(Api).ExecuteAsync(null!);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(status?.TrackId))
|
||||||
{
|
{
|
||||||
YAuthQRStatus qrStatus = await new YGetAuthLoginQRBuilder(api, storage)
|
Api.Storage.AuthToken.SessionTrackId = status.TrackId;
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение <see cref="YAuthCaptcha"/>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YAuthCaptcha> 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Авторизация по captcha
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="captchaValue">Значение captcha</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YAuthBase> 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получение письма авторизации на почту пользователя
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<YAuthLetter> GetAuthLetterAsync(AuthStorage storage)
|
|
||||||
{
|
|
||||||
return new YGetAuthLetterBuilder(api, storage)
|
|
||||||
.Build(null)
|
|
||||||
.GetResponseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Авторизация после подтверждения входа через письмо
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<bool> 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Авторизация с помощью пароля из приложения Яндекс
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="storage">Хранилище</param>
|
|
||||||
/// <param name="password">Пароль</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<YAuthBase> 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;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public async Task<YAuthQRSession?> AuthorizeByQRAsync()
|
||||||
/// Получение <see cref="YAccessToken"/> после авторизации с помощью QR, e-mail, пароля из приложения
|
|
||||||
/// </summary>
|
|
||||||
public async Task<YAccessToken> GetAccessTokenAsync(AuthStorage storage)
|
|
||||||
{
|
{
|
||||||
if (storage.AuthToken == null)
|
if (Api.Storage.AuthToken == null)
|
||||||
throw new Exception("Не найдена сессия входа.");
|
throw new Exception("Сессия не инициализирована");
|
||||||
|
|
||||||
YAccessToken accessToken = await new YGetMusicTokenBuilder(api, storage)
|
if (string.IsNullOrWhiteSpace(Api.Storage.AuthToken.SessionTrackId))
|
||||||
.Build(null)
|
throw new Exception("Токен сессии не инициализирован");
|
||||||
.GetResponseAsync();
|
|
||||||
|
|
||||||
storage.Token = accessToken.AccessToken;
|
var status = await new YGetAuthLoginQRBuilder(Api).ExecuteAsync(null!);
|
||||||
|
if (status != null && status.DefaultUid != 0 && await LoginByCookiesAsync())
|
||||||
return accessToken;
|
return status;
|
||||||
|
throw new AuthenticationException("Ошибка авторизации по QR");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public Task<YAuthCaptcha?> GetCaptchaAsync()
|
||||||
/// Получение информации о пользователе через логин Яндекса
|
|
||||||
/// </summary>
|
|
||||||
public Task<YLoginInfo> GetLoginInfoAsync(AuthStorage storage)
|
|
||||||
{
|
{
|
||||||
return new YGetLoginInfoBuilder(api, storage)
|
if (Api.Storage.AuthToken == null)
|
||||||
.Build(null)
|
throw new AuthenticationException("Выполните CreateAuthSessionAsync перед использованием");
|
||||||
.GetResponseAsync();
|
return new YGetAuthCaptchaBuilder(Api).ExecuteAsync(null!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<YAuthBase?> AuthorizeByCaptchaAsync(string captchaValue)
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new AuthenticationException("Выполните CreateAuthSessionAsync перед использованием");
|
||||||
|
return new YGetAuthLoginCaptchaBuilder(Api).ExecuteAsync(captchaValue);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
public Task<YAuthLetter?> GetAuthLetterAsync()
|
||||||
|
=> new YGetAuthLetterBuilder(Api).ExecuteAsync(null!);
|
||||||
|
|
||||||
|
public async Task<bool> AuthorizeByLetterAsync()
|
||||||
|
{
|
||||||
|
var status = await new YGetAuthLoginLetterBuilder(Api).ExecuteAsync(null!);
|
||||||
|
if (status?.Status != YAuthStatus.Ok || !status.MagicLinkConfirmed)
|
||||||
|
throw new Exception("Письмо не подтверждено");
|
||||||
|
return await LoginByCookiesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YAuthBase?> AuthorizeByAppPasswordAsync(string password)
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new AuthenticationException("Выполните CreateAuthSessionAsync перед использованием");
|
||||||
|
|
||||||
|
var result = await new YGetAuthAppPasswordBuilder(Api).ExecuteAsync(password);
|
||||||
|
if (result?.Status == YAuthStatus.Ok && await LoginByCookiesAsync())
|
||||||
|
return result;
|
||||||
|
throw new AuthenticationException("Ошибка авторизации по паролю");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YAccessToken?> GetAccessTokenAsync()
|
||||||
|
{
|
||||||
|
if (Api.Storage.AuthToken == null)
|
||||||
|
throw new Exception("Сессия не инициализована");
|
||||||
|
|
||||||
|
var token = await new YGetMusicTokenBuilder(Api).ExecuteAsync(null!);
|
||||||
|
if (token?.AccessToken != null)
|
||||||
|
Api.Storage.Token = token.AccessToken;
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<YLoginInfo?> GetLoginInfoAsync()
|
||||||
|
=> new YGetLoginInfoBuilder(Api).ExecuteAsync(null!);
|
||||||
|
}
|
||||||
@@ -1,21 +1,16 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Common.Ynison;
|
using YandexMusic.API.Common.Ynison;
|
||||||
|
|
||||||
namespace YandexMusic.API;
|
namespace YandexMusic.API;
|
||||||
/// <summary>
|
|
||||||
/// API Ynison
|
/// <summary>API для работы с Ynison (WebSocket-плеер).</summary>
|
||||||
/// </summary>
|
public class YYnisonAPI : YCommonAPI
|
||||||
public partial class YYnisonAPI : YCommonAPI
|
|
||||||
{
|
{
|
||||||
public YYnisonAPI(YandexMusicApi yandex) : base(yandex)
|
public YYnisonAPI(YandexMusicApi api) : base(api) { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public YnisonPlayer GetPlayer(AuthStorage storage)
|
public YnisonPlayer GetPlayer()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(storage.Token))
|
if (string.IsNullOrEmpty(Api.Storage.Token))
|
||||||
throw new Exception("Токен пользователя не задан.");
|
throw new Exception("Токен пользователя не задан");
|
||||||
|
return new YnisonPlayer(Api, Api.Storage);
|
||||||
return new(api, storage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,80 +1,76 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using YandexMusic.API.Common.Providers;
|
|
||||||
using YandexMusic.API.Models.Account;
|
using YandexMusic.API.Models.Account;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Common;
|
namespace YandexMusic.API.Common;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Хранилище данных пользователя
|
/// Хранилище данных авторизации. Не содержит HTTP-зависимостей.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AuthStorage
|
public class AuthStorage
|
||||||
{
|
{
|
||||||
/// <summary>
|
private CookieContainer _cookieContainer;
|
||||||
/// Http-контекст
|
|
||||||
/// </summary>
|
public AuthStorage(CookieContainer cookieContainer)
|
||||||
public HttpContext Context { get; }
|
{
|
||||||
|
_cookieContainer = cookieContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CookieContainer CookieContainer => _cookieContainer;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Флаг авторизации
|
/// Флаг, указывающий, авторизован ли пользователь.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsAuthorized { get; internal set; }
|
public bool IsAuthorized { get; internal set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Идентификатор устройства
|
/// Идентификатор устройства (используется в заголовках).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DeviceId { get; set; } = "csharp";
|
public string DeviceId { get; set; } = "csharp";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Токен авторизации
|
/// OAuth-токен для доступа к API.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Token { get; internal set; }
|
public string Token { get; internal set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Аккаунт
|
/// Информация об аккаунте пользователя.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public YAccount User { get; set; }
|
public YAccount User { get; internal set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Провайдер запросов
|
/// Временный токен доступа (используется в некоторых сценариях авторизации).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IRequestProvider Provider { get; }
|
public YAccessToken AccessToken { get; internal set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Токен доступа
|
/// Внутренние данные авторизации (CSRF, track_id и т.д.).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public YAccessToken AccessToken { get; set; }
|
public YAuthToken HeaderToken { get; set; } = new();
|
||||||
|
|
||||||
internal YAuthToken AuthToken { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Конструктор
|
/// Внутренние данные авторизации (CSRF, track_id и т.д.).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AuthStorage(IRequestProvider provider)
|
public YAuthToken AuthToken { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Устанавливает флаг авторизации и сохраняет информацию об аккаунте.
|
||||||
|
/// </summary>
|
||||||
|
internal void SetAuthorized(YAccount user, string token)
|
||||||
|
{
|
||||||
|
User = user ?? throw new ArgumentNullException(nameof(user));
|
||||||
|
Token = token ?? throw new ArgumentNullException(nameof(token));
|
||||||
|
IsAuthorized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сбрасывает состояние авторизации.
|
||||||
|
/// </summary>
|
||||||
|
internal void ResetAuthorization()
|
||||||
{
|
{
|
||||||
User = new YAccount();
|
User = new YAccount();
|
||||||
Context = new HttpContext();
|
Token = string.Empty;
|
||||||
Provider = provider;
|
AccessToken = new YAccessToken();
|
||||||
|
AuthToken = new YAuthToken();
|
||||||
|
IsAuthorized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Конструктор
|
|
||||||
/// </summary>
|
|
||||||
public AuthStorage()
|
|
||||||
{
|
|
||||||
User = new YAccount();
|
|
||||||
Context = new HttpContext();
|
|
||||||
Provider = new DefaultRequestProvider(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Установка прокси для пользователия
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="proxy">Прокси</param>
|
|
||||||
public void SetProxy(IWebProxy proxy)
|
|
||||||
{
|
|
||||||
Context.WebProxy = proxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Common;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Загрузчик файлов по ссылке
|
|
||||||
/// </summary>
|
|
||||||
public class DataDownloader
|
|
||||||
{
|
|
||||||
private AuthStorage authStorage;
|
|
||||||
|
|
||||||
private async Task<HttpContent> GetResponseContent(string url, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
|
||||||
{
|
|
||||||
HttpRequestMessage message = new(new HttpMethod(WebRequestMethods.Http.Get), url);
|
|
||||||
|
|
||||||
HttpResponseMessage response = await authStorage.Provider.GetWebResponseAsync(message, httpCompletionOption);
|
|
||||||
return response.Content;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Stream> AsStream(string url, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
|
||||||
{
|
|
||||||
HttpContent content = await GetResponseContent(url, httpCompletionOption);
|
|
||||||
return await content.ReadAsStreamAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<byte[]> AsBytes(string url)
|
|
||||||
{
|
|
||||||
HttpContent content = await GetResponseContent(url);
|
|
||||||
return await content.ReadAsByteArrayAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ToFile(string url, string fileName)
|
|
||||||
{
|
|
||||||
using Stream stream = await AsStream(url);
|
|
||||||
using FileStream fs = File.Create(fileName);
|
|
||||||
await stream.CopyToAsync(fs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DataDownloader(AuthStorage storage)
|
|
||||||
{
|
|
||||||
authStorage = storage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using YandexMusic.API.Converters;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Common.Providers;
|
|
||||||
|
|
||||||
/// <summary>Базовый провайдер HTTP-запросов с общей логикой десериализации.</summary>
|
|
||||||
public abstract class CommonRequestProvider : IRequestProvider
|
|
||||||
{
|
|
||||||
/// <summary>Хранилище данных авторизации.</summary>
|
|
||||||
protected readonly AuthStorage storage;
|
|
||||||
|
|
||||||
/// <summary>Инициализирует новый экземпляр провайдера.</summary>
|
|
||||||
/// <param name="authStorage">Хранилище авторизации.</param>
|
|
||||||
protected CommonRequestProvider(AuthStorage authStorage)
|
|
||||||
{
|
|
||||||
storage = authStorage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Выполняет HTTP-запрос и возвращает ответ.</summary>
|
|
||||||
public abstract Task<HttpResponseMessage> GetWebResponseAsync(
|
|
||||||
HttpRequestMessage message,
|
|
||||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead);
|
|
||||||
|
|
||||||
/// <summary>Преобразует HTTP-ответ в объект типа T.</summary>
|
|
||||||
public virtual async Task<T> GetDataFromResponseAsync<T>(
|
|
||||||
YandexMusicApi api,
|
|
||||||
HttpResponseMessage response)
|
|
||||||
{
|
|
||||||
var json = await response.Content.ReadAsStringAsync();
|
|
||||||
|
|
||||||
JsonSerializerOptions JsonOptions = new()
|
|
||||||
{
|
|
||||||
PropertyNameCaseInsensitive = true,
|
|
||||||
Converters = {
|
|
||||||
new JsonStringEnumConverter(JsonNamingPolicy.KebabCaseLower),
|
|
||||||
new IntToStringConverter(),
|
|
||||||
new YExecutionContextConverter(api, storage),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var error = JsonSerializer.Deserialize<YErrorResponse>(json, JsonOptions);
|
|
||||||
throw error ?? new Exception("Ошибка десериализации ответа с ошибкой.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Если нужен контекст выполнения, он добавляется через кастомный конвертер
|
|
||||||
return JsonSerializer.Deserialize<T>(json, JsonOptions)
|
|
||||||
?? throw new JsonException("Десериализация вернула null");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new Exception($"Ошибка десериализации: {ex.Message}", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Common.Providers;
|
|
||||||
|
|
||||||
/// <summary>Стандартный провайдер HTTP-запросов с использованием HttpClient.</summary>
|
|
||||||
public class DefaultRequestProvider : CommonRequestProvider
|
|
||||||
{
|
|
||||||
/// <summary>Инициализирует новый экземпляр провайдера.</summary>
|
|
||||||
/// <param name="authStorage">Хранилище авторизации.</param>
|
|
||||||
public DefaultRequestProvider(AuthStorage authStorage) : base(authStorage) { }
|
|
||||||
|
|
||||||
/// <summary>Выполняет HTTP-запрос и возвращает ответ.</summary>
|
|
||||||
/// <param name="message">HTTP-запрос.</param>
|
|
||||||
/// <param name="completionOption">Опция завершения запроса.</param>
|
|
||||||
/// <returns>HTTP-ответ.</returns>
|
|
||||||
public override async Task<HttpResponseMessage> GetWebResponseAsync(
|
|
||||||
HttpRequestMessage message,
|
|
||||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
|
||||||
{
|
|
||||||
using var handler = new SocketsHttpHandler
|
|
||||||
{
|
|
||||||
Proxy = storage.Context.WebProxy,
|
|
||||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
|
|
||||||
UseCookies = true,
|
|
||||||
CookieContainer = storage.Context.Cookies,
|
|
||||||
AllowAutoRedirect = true,
|
|
||||||
MaxAutomaticRedirections = 10
|
|
||||||
};
|
|
||||||
|
|
||||||
using var client = new HttpClient(handler);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await client.SendAsync(message, completionOption);
|
|
||||||
}
|
|
||||||
catch (HttpRequestException ex)
|
|
||||||
{
|
|
||||||
// Пытаемся извлечь тело ошибки, если оно доступно
|
|
||||||
if (ex.InnerException == null)
|
|
||||||
throw;
|
|
||||||
|
|
||||||
throw new Exception($"Ошибка HTTP-запроса: {ex.Message}", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
namespace YandexMusic.API.Common.Providers
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Интерфейс для провайдеров обработки запросов
|
|
||||||
/// </summary>
|
|
||||||
public interface IRequestProvider
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Функция получения ответа
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">Запрос</param>
|
|
||||||
/// <param name="completionOption">Опция завершения запроса</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
Task<HttpResponseMessage> GetWebResponseAsync(HttpRequestMessage message, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Функция формирования ответа
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Тип объекта с ответом</typeparam>
|
|
||||||
/// <param name="api">API</param>
|
|
||||||
/// <param name="response">Ответ</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
Task<T> GetDataFromResponseAsync<T>(YandexMusicApi api, HttpResponseMessage response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
47
YandexMusic.API/Common/YandexMusicHttpClientFactory.cs
Normal file
47
YandexMusic.API/Common/YandexMusicHttpClientFactory.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Фабрика для создания стандартного HttpClient с поддержкой кук, прокси и автоматической декомпрессией.
|
||||||
|
/// </summary>
|
||||||
|
public static class YandexMusicHttpClientFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт стандартный HttpClient с автоматическим управлением куками.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="proxy">Прокси-сервер (опционально).</param>
|
||||||
|
/// <param name="timeout">Таймаут запросов (по умолчанию 30 секунд).</param>
|
||||||
|
/// <param name="userAgent">User-Agent (по умолчанию как у браузера Chrome).</param>
|
||||||
|
/// <returns>Настроенный HttpClient.</returns>
|
||||||
|
public static HttpClient CreateDefault(
|
||||||
|
CookieContainer? cookieContainer = null,
|
||||||
|
IWebProxy? proxy = null,
|
||||||
|
TimeSpan? timeout = null,
|
||||||
|
string? userAgent = null)
|
||||||
|
{
|
||||||
|
var handler = new HttpClientHandler
|
||||||
|
{
|
||||||
|
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
|
||||||
|
UseCookies = true,
|
||||||
|
CookieContainer = cookieContainer ?? new CookieContainer(),
|
||||||
|
AllowAutoRedirect = true,
|
||||||
|
MaxAutomaticRedirections = 10,
|
||||||
|
Proxy = proxy,
|
||||||
|
UseProxy = proxy != null
|
||||||
|
};
|
||||||
|
|
||||||
|
var client = new HttpClient(handler, disposeHandler: true)
|
||||||
|
{
|
||||||
|
Timeout = timeout ?? TimeSpan.FromSeconds(30)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Стандартный User-Agent, похожий на браузерный
|
||||||
|
client.DefaultRequestHeaders.Add("User-Agent",
|
||||||
|
userAgent ?? "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
||||||
|
client.DefaultRequestHeaders.Add("Accept", "*/*");
|
||||||
|
client.DefaultRequestHeaders.Add("Accept-Language", "ru-RU,ru;q=0.9,en;q=0.8");
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Net;
|
||||||
|
using System.Net.WebSockets;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using YandexMusic.API.Models.Track;
|
using YandexMusic.API.Models.Track;
|
||||||
@@ -12,6 +13,7 @@ public class YnisonPlayer : IDisposable
|
|||||||
{
|
{
|
||||||
private readonly JsonSerializerOptions _jsonOptions;
|
private readonly JsonSerializerOptions _jsonOptions;
|
||||||
private readonly AuthStorage _storage;
|
private readonly AuthStorage _storage;
|
||||||
|
private readonly IWebProxy? _proxy;
|
||||||
private YnisonWebSocket? _redirector;
|
private YnisonWebSocket? _redirector;
|
||||||
private YnisonWebSocket? _state;
|
private YnisonWebSocket? _state;
|
||||||
|
|
||||||
@@ -33,40 +35,36 @@ public class YnisonPlayer : IDisposable
|
|||||||
/// <summary>Аргументы события получения состояния.</summary>
|
/// <summary>Аргументы события получения состояния.</summary>
|
||||||
public class ReceiveEventArgs : EventArgs
|
public class ReceiveEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
/// <summary>Состояние плеера.</summary>
|
|
||||||
public YYnisonState State { get; init; } = null!;
|
public YYnisonState State { get; init; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Аргументы события закрытия соединения.</summary>
|
/// <summary>Аргументы события закрытия соединения.</summary>
|
||||||
public class CloseEventArgs : EventArgs
|
public class CloseEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
/// <summary>Статус закрытия.</summary>
|
|
||||||
public WebSocketCloseStatus? Status { get; init; }
|
public WebSocketCloseStatus? Status { get; init; }
|
||||||
/// <summary>Описание причины закрытия.</summary>
|
|
||||||
public string? Description { get; init; }
|
public string? Description { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal YnisonPlayer(YandexMusicApi api, AuthStorage authStorage)
|
internal YnisonPlayer(YandexMusicApi api, AuthStorage authStorage, IWebProxy? proxy = null)
|
||||||
{
|
{
|
||||||
API = api;
|
API = api;
|
||||||
_storage = authStorage;
|
_storage = authStorage;
|
||||||
|
_proxy = proxy;
|
||||||
_jsonOptions = new JsonSerializerOptions
|
_jsonOptions = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
Converters = { new JsonStringEnumConverter(new UpperSnakeCaseNamingPolicy(), false) }
|
Converters = { new JsonStringEnumConverter(new UpperSnakeCaseNamingPolicy(), false) }
|
||||||
};
|
};
|
||||||
_redirector = new YnisonWebSocket();
|
_redirector = new YnisonWebSocket(_proxy);
|
||||||
_state = new YnisonWebSocket();
|
_state = new YnisonWebSocket(_proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions);
|
private string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions);
|
||||||
|
|
||||||
private T Deserialize<T>(YYnisonMessageType messageType, string data)
|
private T Deserialize<T>(YYnisonMessageType messageType, string data)
|
||||||
{
|
=> JsonSerializer.Deserialize<T>(data, _jsonOptions)
|
||||||
return JsonSerializer.Deserialize<T>(data, _jsonOptions)
|
|
||||||
?? throw new JsonException("Десериализация вернула null");
|
?? throw new JsonException("Десериализация вернула null");
|
||||||
}
|
|
||||||
|
|
||||||
private T DeserializeMessage<T>(YYnisonMessageType messageType, string data)
|
private T DeserializeMessage<T>(YYnisonMessageType messageType, string data)
|
||||||
{
|
{
|
||||||
@@ -120,8 +118,8 @@ public class YnisonPlayer : IDisposable
|
|||||||
if (index < 0 || index >= State.PlayerState.PlayerQueue.PlayableList.Count)
|
if (index < 0 || index >= State.PlayerState.PlayerQueue.PlayableList.Count)
|
||||||
return null;
|
return null;
|
||||||
var item = State.PlayerState.PlayerQueue.PlayableList[index];
|
var item = State.PlayerState.PlayerQueue.PlayableList[index];
|
||||||
var response = await API.Track.GetAsync(_storage, item.PlayableId);
|
var response = await API.Track.GetAsync(item.PlayableId);
|
||||||
return response?.Result?.FirstOrDefault();
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateStateAsync()
|
private async Task UpdateStateAsync()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Net;
|
||||||
|
using System.Net.WebSockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
@@ -7,14 +8,15 @@ namespace YandexMusic.API.Common.Ynison;
|
|||||||
/// <summary>WebSocket-клиент для взаимодействия с протоколом Ynison.</summary>
|
/// <summary>WebSocket-клиент для взаимодействия с протоколом Ynison.</summary>
|
||||||
public class YnisonWebSocket : IDisposable
|
public class YnisonWebSocket : IDisposable
|
||||||
{
|
{
|
||||||
private readonly ClientWebSocket _socketClient = new();
|
private ClientWebSocket? _socketClient;
|
||||||
private CancellationTokenSource? _cancellationTokenSource;
|
private CancellationTokenSource? _cancellationTokenSource;
|
||||||
private CancellationToken _cancellationToken;
|
private CancellationToken _cancellationToken;
|
||||||
private readonly StringBuilder _data = new();
|
private readonly StringBuilder _data = new();
|
||||||
private const int BufferSize = 4096;
|
private const int BufferSize = 4096;
|
||||||
|
private readonly IWebProxy? _proxy;
|
||||||
|
|
||||||
/// <summary>Флаг, указывает, открыто ли соединение.</summary>
|
/// <summary>Флаг, указывает, открыто ли соединение.</summary>
|
||||||
public bool IsConnected => _socketClient.State == WebSocketState.Open;
|
public bool IsConnected => _socketClient?.State == WebSocketState.Open;
|
||||||
|
|
||||||
/// <summary>Событие получения сообщения.</summary>
|
/// <summary>Событие получения сообщения.</summary>
|
||||||
public event EventHandler<ReceiveEventArgs>? OnReceive;
|
public event EventHandler<ReceiveEventArgs>? OnReceive;
|
||||||
@@ -25,19 +27,25 @@ public class YnisonWebSocket : IDisposable
|
|||||||
/// <summary>Аргументы события получения данных.</summary>
|
/// <summary>Аргументы события получения данных.</summary>
|
||||||
public class ReceiveEventArgs : EventArgs
|
public class ReceiveEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
/// <summary>Полученные данные (JSON-строка).</summary>
|
|
||||||
public string Data { get; init; } = null!;
|
public string Data { get; init; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Аргументы события закрытия соединения.</summary>
|
/// <summary>Аргументы события закрытия соединения.</summary>
|
||||||
public class CloseEventArgs : EventArgs
|
public class CloseEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
/// <summary>Статус закрытия.</summary>
|
|
||||||
public WebSocketCloseStatus? Status { get; init; }
|
public WebSocketCloseStatus? Status { get; init; }
|
||||||
/// <summary>Описание причины закрытия.</summary>
|
|
||||||
public string? Description { get; init; }
|
public string? Description { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр WebSocket-клиента.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="proxy">Прокси-сервер (опционально).</param>
|
||||||
|
public YnisonWebSocket(IWebProxy? proxy = null)
|
||||||
|
{
|
||||||
|
_proxy = proxy;
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetProtocolData(string deviceId, string? redirectTicket)
|
private static string GetProtocolData(string deviceId, string? redirectTicket)
|
||||||
{
|
{
|
||||||
var deviceInfo = new Dictionary<string, object>
|
var deviceInfo = new Dictionary<string, object>
|
||||||
@@ -57,6 +65,9 @@ public class YnisonWebSocket : IDisposable
|
|||||||
|
|
||||||
private async Task<string> ReadSocketContentAsync()
|
private async Task<string> ReadSocketContentAsync()
|
||||||
{
|
{
|
||||||
|
if (_socketClient == null)
|
||||||
|
throw new InvalidOperationException("WebSocket не инициализирован");
|
||||||
|
|
||||||
var buffer = new byte[BufferSize];
|
var buffer = new byte[BufferSize];
|
||||||
WebSocketReceiveResult result;
|
WebSocketReceiveResult result;
|
||||||
do
|
do
|
||||||
@@ -68,17 +79,19 @@ public class YnisonWebSocket : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Подключается к WebSocket.</summary>
|
/// <summary>Подключается к WebSocket.</summary>
|
||||||
/// <param name="storage">Хранилище авторизации.</param>
|
/// <param name="storage">Хранилище авторизации (для токена и deviceId).</param>
|
||||||
/// <param name="url">URL WebSocket.</param>
|
/// <param name="url">URL WebSocket.</param>
|
||||||
/// <param name="redirectTicket">Тикет перенаправления (опционально).</param>
|
/// <param name="redirectTicket">Тикет перенаправления (опционально).</param>
|
||||||
public async Task ConnectAsync(AuthStorage storage, string url, string? redirectTicket = null)
|
public async Task ConnectAsync(AuthStorage storage, string url, string? redirectTicket = null)
|
||||||
{
|
{
|
||||||
|
_socketClient = new ClientWebSocket();
|
||||||
_socketClient.Options.AddSubProtocol("Bearer");
|
_socketClient.Options.AddSubProtocol("Bearer");
|
||||||
var protocolData = GetProtocolData(storage.DeviceId, redirectTicket);
|
var protocolData = GetProtocolData(storage.DeviceId, redirectTicket);
|
||||||
_socketClient.Options.SetRequestHeader("Sec-WebSocket-Protocol", $"Bearer, v2, {protocolData}");
|
_socketClient.Options.SetRequestHeader("Sec-WebSocket-Protocol", $"Bearer, v2, {protocolData}");
|
||||||
_socketClient.Options.SetRequestHeader("Origin", "https://music.yandex.ru");
|
_socketClient.Options.SetRequestHeader("Origin", "https://music.yandex.ru");
|
||||||
_socketClient.Options.SetRequestHeader("Authorization", $"OAuth {storage.Token}");
|
_socketClient.Options.SetRequestHeader("Authorization", $"OAuth {storage.Token}");
|
||||||
_socketClient.Options.Proxy = storage.Context.WebProxy;
|
if (_proxy != null)
|
||||||
|
_socketClient.Options.Proxy = _proxy;
|
||||||
|
|
||||||
_cancellationTokenSource = new CancellationTokenSource();
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
_cancellationToken = _cancellationTokenSource.Token;
|
_cancellationToken = _cancellationTokenSource.Token;
|
||||||
@@ -89,7 +102,7 @@ public class YnisonWebSocket : IDisposable
|
|||||||
/// <summary>Начинает асинхронный приём сообщений.</summary>
|
/// <summary>Начинает асинхронный приём сообщений.</summary>
|
||||||
public async Task BeginReceiveAsync()
|
public async Task BeginReceiveAsync()
|
||||||
{
|
{
|
||||||
if (_socketClient.State != WebSocketState.Open)
|
if (_socketClient == null || _socketClient.State != WebSocketState.Open)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -116,9 +129,11 @@ public class YnisonWebSocket : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Отправляет JSON-сообщение.</summary>
|
/// <summary>Отправляет JSON-сообщение.</summary>
|
||||||
/// <param name="json">JSON-строка.</param>
|
|
||||||
public async ValueTask SendAsync(string json)
|
public async ValueTask SendAsync(string json)
|
||||||
{
|
{
|
||||||
|
if (_socketClient == null)
|
||||||
|
throw new InvalidOperationException("WebSocket не инициализирован");
|
||||||
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(json);
|
var bytes = Encoding.UTF8.GetBytes(json);
|
||||||
await _socketClient.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, _cancellationToken);
|
await _socketClient.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, _cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -127,16 +142,14 @@ public class YnisonWebSocket : IDisposable
|
|||||||
public async Task StopReceiveAsync()
|
public async Task StopReceiveAsync()
|
||||||
{
|
{
|
||||||
if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
|
if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
|
||||||
{
|
|
||||||
await _cancellationTokenSource.CancelAsync();
|
await _cancellationTokenSource.CancelAsync();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Освобождает ресурсы.</summary>
|
/// <summary>Освобождает ресурсы.</summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_cancellationTokenSource?.Dispose();
|
_cancellationTokenSource?.Dispose();
|
||||||
_socketClient.Dispose();
|
_socketClient?.Dispose();
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace YandexMusic.API.Converters;
|
namespace YandexMusic.API.Converters;
|
||||||
|
|
||||||
public class IntToStringConverter : JsonConverter<string>
|
internal class IntToStringConverter : JsonConverter<string>
|
||||||
{
|
{
|
||||||
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
|
|||||||
42
YandexMusic.API/Converters/StringToIntConverter.cs
Normal file
42
YandexMusic.API/Converters/StringToIntConverter.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Converters;
|
||||||
|
|
||||||
|
internal class StringToIntConverter : JsonConverter<int>
|
||||||
|
{
|
||||||
|
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
// Если текущий токен — строка
|
||||||
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
|
{
|
||||||
|
string? stringValue = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(stringValue))
|
||||||
|
{
|
||||||
|
throw new JsonException("Строка не может быть пустой или null для преобразования в int.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пробуем распарсить с учётом возможных пробелов и инвариантной культуры
|
||||||
|
if (int.TryParse(stringValue.Trim(), out int result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new JsonException($"Невозможно преобразовать строку \"{stringValue}\" в int.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если токен — число (стандартное поведение)
|
||||||
|
if (reader.TokenType == JsonTokenType.Number)
|
||||||
|
{
|
||||||
|
return reader.GetInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new JsonException($"Ожидалась строка или число, получен {reader.TokenType}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
// Записываем число как обычное JSON-число
|
||||||
|
writer.WriteNumberValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,27 +3,31 @@ using YandexMusic.API.Models.Album;
|
|||||||
namespace YandexMusic.API.Extensions.API;
|
namespace YandexMusic.API.Extensions.API;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Методы-расширения для альбома
|
/// Методы-расширения для альбома.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static partial class YAlbumExtensions
|
public static class YAlbumExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает полную информацию об альбоме вместе с треками (если ещё не загружены).
|
||||||
|
/// </summary>
|
||||||
public static async Task<YAlbum> WithTracksAsync(this YAlbum album)
|
public static async Task<YAlbum> WithTracksAsync(this YAlbum album)
|
||||||
{
|
{
|
||||||
return album.Volumes != null
|
if (album.Volumes != null)
|
||||||
? album
|
return album;
|
||||||
: (await album.Context.API.Album.GetAsync(album.Context.Storage, album.Id))
|
|
||||||
.Result;
|
var result = await album.Context.API.Album.GetAsync(album.Id);
|
||||||
|
return result ?? album;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<string> AddLikeAsync(this YAlbum album)
|
/// <summary>
|
||||||
{
|
/// Добавляет альбом в список лайкнутых.
|
||||||
return (await album.Context.API.Library.AddAlbumLikeAsync(album.Context.Storage, album))
|
/// </summary>
|
||||||
.Result;
|
public static async Task<string?> AddLikeAsync(this YAlbum album)
|
||||||
}
|
=> await album.Context.API.Library.AddAlbumLikeAsync(album);
|
||||||
|
|
||||||
public static async Task<string> RemoveLikeAsync(this YAlbum album)
|
/// <summary>
|
||||||
{
|
/// Удаляет альбом из списка лайкнутых.
|
||||||
return (await album.Context.API.Library.RemoveAlbumLikeAsync(album.Context.Storage, album))
|
/// </summary>
|
||||||
.Result;
|
public static async Task<string?> RemoveLikeAsync(this YAlbum album)
|
||||||
}
|
=> await album.Context.API.Library.RemoveAlbumLikeAsync(album);
|
||||||
}
|
}
|
||||||
@@ -4,37 +4,37 @@ using YandexMusic.API.Models.Track;
|
|||||||
namespace YandexMusic.API.Extensions.API;
|
namespace YandexMusic.API.Extensions.API;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Методы-расширения для исполнителя
|
/// Методы-расширения для исполнителя.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static partial class YArtistExtensions
|
public static class YArtistExtensions
|
||||||
{
|
{
|
||||||
public static async Task<YArtistBriefInfo> BriefInfoAsync(this YArtist artist)
|
/// <summary>
|
||||||
{
|
/// Получает расширенную информацию об исполнителе.
|
||||||
return (await artist.Context.API.Artist.GetAsync(artist.Context.Storage, artist.Id))
|
/// </summary>
|
||||||
.Result;
|
public static async Task<YArtistBriefInfo?> BriefInfoAsync(this YArtist artist)
|
||||||
}
|
=> await artist.Context.API.Artist.GetAsync(artist.Id);
|
||||||
|
|
||||||
public static async Task<YTracksPage> GetTracksAsync(this YArtist artist, int page = 0, int pageSize = 20)
|
/// <summary>
|
||||||
{
|
/// Получает страницу треков исполнителя.
|
||||||
return (await artist.Context.API.Artist.GetTracksAsync(artist.Context.Storage, artist.Id, page, pageSize))
|
/// </summary>
|
||||||
.Result;
|
public static async Task<YTracksPage?> GetTracksAsync(this YArtist artist, int page = 0, int pageSize = 20)
|
||||||
}
|
=> await artist.Context.API.Artist.GetTracksAsync(artist.Id, page, pageSize);
|
||||||
|
|
||||||
public static async Task<List<YTrack>> GetAllTracksAsync(this YArtist artist)
|
/// <summary>
|
||||||
{
|
/// Получает все треки исполнителя.
|
||||||
return (await artist.Context.API.Artist.GetAllTracksAsync(artist.Context.Storage, artist.Id))
|
/// </summary>
|
||||||
.Result.Tracks;
|
public static async Task<List<YTrack>?> GetAllTracksAsync(this YArtist artist)
|
||||||
}
|
=> (await artist.Context.API.Artist.GetAllTracksAsync(artist.Id))?.Tracks;
|
||||||
|
|
||||||
public static async Task<string> AddLikeAsync(this YArtist artist)
|
/// <summary>
|
||||||
{
|
/// Добавляет исполнителя в список лайкнутых.
|
||||||
return (await artist.Context.API.Library.AddArtistLikeAsync(artist.Context.Storage, artist))
|
/// </summary>
|
||||||
.Result;
|
public static async Task<string?> AddLikeAsync(this YArtist artist)
|
||||||
}
|
=> await artist.Context.API.Library.AddArtistLikeAsync(artist);
|
||||||
|
|
||||||
public static async Task<string> RemoveLikeAsync(this YArtist artist)
|
/// <summary>
|
||||||
{
|
/// Удаляет исполнителя из списка лайкнутых.
|
||||||
return (await artist.Context.API.Library.RemoveArtistLikeAsync(artist.Context.Storage, artist))
|
/// </summary>
|
||||||
.Result;
|
public static async Task<string?> RemoveLikeAsync(this YArtist artist)
|
||||||
}
|
=> await artist.Context.API.Library.RemoveArtistLikeAsync(artist);
|
||||||
}
|
}
|
||||||
@@ -4,73 +4,66 @@ using YandexMusic.API.Models.Track;
|
|||||||
namespace YandexMusic.API.Extensions.API;
|
namespace YandexMusic.API.Extensions.API;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Методы-расширения для плейлиста
|
/// Методы-расширения для плейлиста.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static partial class YPlaylistExtensions
|
public static class YPlaylistExtensions
|
||||||
{
|
{
|
||||||
private static bool CheckUser(YPlaylist playlist)
|
private static bool IsOwner(YPlaylist playlist)
|
||||||
|
=> playlist.Owner.Uid == playlist.Context.Storage.User.Uid;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает полную информацию о плейлисте вместе с треками.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<YPlaylist?> WithTracksAsync(this YPlaylist playlist)
|
||||||
{
|
{
|
||||||
return playlist.Owner.Uid == playlist.Context.Storage.User.Uid;
|
if (playlist.Tracks != null)
|
||||||
|
return playlist;
|
||||||
|
return await playlist.Context.API.Playlist.GetAsync(playlist);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<YPlaylist> WithTracksAsync(this YPlaylist playlist)
|
/// <summary>
|
||||||
{
|
/// Добавляет плейлист в список лайкнутых.
|
||||||
return playlist.Tracks != null
|
/// </summary>
|
||||||
? playlist
|
public static async Task<string?> AddLikeAsync(this YPlaylist playlist)
|
||||||
: (await playlist.Context.API.Playlist.GetAsync(playlist.Context.Storage, playlist))
|
=> await playlist.Context.API.Library.AddPlaylistLikeAsync(playlist);
|
||||||
.Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<string> AddLikeAsync(this YPlaylist playlist)
|
/// <summary>
|
||||||
{
|
/// Удаляет плейлист из списка лайкнутых.
|
||||||
return (await playlist.Context.API.Library.AddPlaylistLikeAsync(playlist.Context.Storage, playlist))
|
/// </summary>
|
||||||
.Result;
|
public static async Task<string?> RemoveLikeAsync(this YPlaylist playlist)
|
||||||
}
|
=> await playlist.Context.API.Library.RemovePlaylistLikeAsync(playlist);
|
||||||
|
|
||||||
public static async Task<string> RemoveLikeAsync(this YPlaylist playlist)
|
/// <summary>
|
||||||
{
|
/// Переименовывает плейлист (только для владельца).
|
||||||
return (await playlist.Context.API.Library.RemovePlaylistLikeAsync(playlist.Context.Storage, playlist))
|
/// </summary>
|
||||||
.Result;
|
public static async Task<YPlaylist?> RenameAsync(this YPlaylist playlist, string newName)
|
||||||
}
|
=> IsOwner(playlist) ? await playlist.Context.API.Playlist.RenameAsync(playlist, newName) : playlist;
|
||||||
|
|
||||||
public static async Task<YPlaylist> RenameAsync(this YPlaylist playlist, string newName)
|
|
||||||
{
|
|
||||||
return CheckUser(playlist)
|
|
||||||
? (await playlist.Context.API.Playlist.RenameAsync(playlist.Context.Storage, playlist, newName))
|
|
||||||
.Result
|
|
||||||
: playlist;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Удаляет плейлист (только для владельца).
|
||||||
|
/// </summary>
|
||||||
public static async Task<bool> DeleteAsync(this YPlaylist playlist)
|
public static async Task<bool> DeleteAsync(this YPlaylist playlist)
|
||||||
|
=> IsOwner(playlist) && await playlist.Context.API.Playlist.DeleteAsync(playlist);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вставляет треки в начало плейлиста (только для владельца).
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<YPlaylist?> InsertTracksAsync(this YPlaylist playlist, params YTrack[] tracks)
|
||||||
|
=> IsOwner(playlist) ? await playlist.Context.API.Playlist.InsertTracksAsync(playlist, tracks) : playlist;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Удаляет треки из плейлиста (только для владельца).
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<YPlaylist?> RemoveTracksAsync(this YPlaylist playlist, params YTrack[] tracks)
|
||||||
|
=> IsOwner(playlist) ? await playlist.Context.API.Playlist.DeleteTracksAsync(playlist, tracks) : playlist;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Загружает трек в плейлист (только для владельца).
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<bool> UploadTrackAsync(this YPlaylist playlist, string filePath, string fileName)
|
||||||
{
|
{
|
||||||
return CheckUser(playlist) && await playlist.Context.API.Playlist.DeleteAsync(playlist.Context.Storage, playlist);
|
if (!IsOwner(playlist)) return false;
|
||||||
|
var result = await playlist.Context.API.UserGeneratedContent.UploadTrackToPlaylistAsync(playlist, fileName, filePath);
|
||||||
|
return result == "CREATED";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public static async Task<YPlaylist> 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<YPlaylist> 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<bool> 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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,24 +4,25 @@ using YandexMusic.API.Models.Track;
|
|||||||
namespace YandexMusic.API.Extensions.API;
|
namespace YandexMusic.API.Extensions.API;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Методы-расширения для радиостанции
|
/// Методы-расширения для радиостанции.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static partial class YStationResultExtensions
|
public static class YStationResultExtensions
|
||||||
{
|
{
|
||||||
public static async Task<List<YSequenceItem>> GetTracksAsync(this YStation station, string prevTrackId = "")
|
/// <summary>
|
||||||
{
|
/// Получает список треков для радиостанции.
|
||||||
return (await station.Context.API.Radio.GetStationTracksAsync(station.Context.Storage, station, prevTrackId))
|
/// </summary>
|
||||||
.Result.Sequence;
|
public static async Task<List<YSequenceItem>?> GetTracksAsync(this YStation station, string prevTrackId = "")
|
||||||
}
|
=> (await station.Context.API.Radio.GetStationTracksAsync(station, prevTrackId))?.Sequence;
|
||||||
|
|
||||||
public static async Task<string> SetSettings2Async(this YStation station, YStationSettings2 settings)
|
/// <summary>
|
||||||
{
|
/// Устанавливает настройки станции.
|
||||||
return (await station.Context.API.Radio.SetStationSettings2Async(station.Context.Storage, station, settings))
|
/// </summary>
|
||||||
.Result;
|
public static async Task<string?> SetSettings2Async(this YStation station, YStationSettings2 settings)
|
||||||
}
|
=> await station.Context.API.Radio.SetStationSettings2Async(station, settings);
|
||||||
|
|
||||||
public static Task<string> SendFeedBackAsync(this YStation station, YStationFeedbackType type, YTrack track = null, string batchId = "", double totalPlayedSeconds = 0)
|
/// <summary>
|
||||||
{
|
/// Отправляет обратную связь о прослушивании.
|
||||||
return station.Context.API.Radio.SendStationFeedBackAsync(station.Context.Storage, station, type, track, batchId, totalPlayedSeconds);
|
/// </summary>
|
||||||
}
|
public static Task<string?> SendFeedbackAsync(this YStation station, YStationFeedbackType type, YTrack? track = null, string batchId = "", double totalPlayedSeconds = 0)
|
||||||
}
|
=> station.Context.API.Radio.SendStationFeedbackAsync(station, type, track, batchId, totalPlayedSeconds);
|
||||||
|
}
|
||||||
@@ -3,58 +3,61 @@ using YandexMusic.API.Models.Track;
|
|||||||
namespace YandexMusic.API.Extensions.API;
|
namespace YandexMusic.API.Extensions.API;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Методы-расширения для трека
|
/// Методы-расширения для трека.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static partial class YTrackExtensions
|
public static class YTrackExtensions
|
||||||
{
|
{
|
||||||
public static Task<string> GetLinkAsync(this YTrack track)
|
/// <summary>
|
||||||
{
|
/// Получает прямую ссылку на скачивание трека.
|
||||||
return track.Context.API.Track.GetFileLinkAsync(track.Context.Storage, track);
|
/// </summary>
|
||||||
}
|
public static Task<string?> GetLinkAsync(this YTrack track)
|
||||||
|
=> track.Context.API.Track.GetFileLinkAsync(track);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сохраняет трек в файл.
|
||||||
|
/// </summary>
|
||||||
public static Task SaveAsync(this YTrack track, string filePath)
|
public static Task SaveAsync(this YTrack track, string filePath)
|
||||||
{
|
=> track.Context.API.Track.ExtractToFileAsync(track, filePath);
|
||||||
return track.Context.API.Track.ExtractToFileAsync(track.Context.Storage, track, filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<int> AddLikeAsync(this YTrack track)
|
/// <summary>
|
||||||
{
|
/// Добавляет трек в список лайкнутых.
|
||||||
return (await track.Context.API.Library.AddTrackLikeAsync(track.Context.Storage, track))
|
/// </summary>
|
||||||
.Result.Revision;
|
public static async Task<int?> AddLikeAsync(this YTrack track)
|
||||||
}
|
=> await track.Context.API.Library.AddTrackLikeAsync(track);
|
||||||
|
|
||||||
public static async Task<int> RemoveLikeAsync(this YTrack track)
|
/// <summary>
|
||||||
{
|
/// Удаляет трек из списка лайкнутых.
|
||||||
return (await track.Context.API.Library.RemoveTrackLikeAsync(track.Context.Storage, track))
|
/// </summary>
|
||||||
.Result.Revision;
|
public static async Task<int?> RemoveLikeAsync(this YTrack track)
|
||||||
}
|
=> await track.Context.API.Library.RemoveTrackLikeAsync(track);
|
||||||
|
|
||||||
public static async Task<int> AddDislikeAsync(this YTrack track)
|
/// <summary>
|
||||||
{
|
/// Добавляет трек в список дизлайкнутых.
|
||||||
return (await track.Context.API.Library.AddTrackDislikeAsync(track.Context.Storage, track))
|
/// </summary>
|
||||||
.Result.Revision;
|
public static async Task<int?> AddDislikeAsync(this YTrack track)
|
||||||
}
|
=> await track.Context.API.Library.AddTrackDislikeAsync(track);
|
||||||
|
|
||||||
public static async Task<int> RemoveDislikeAsync(this YTrack track)
|
/// <summary>
|
||||||
{
|
/// Удаляет трек из списка дизлайкнутых.
|
||||||
return (await track.Context.API.Library.RemoveTrackDislikeAsync(track.Context.Storage, track))
|
/// </summary>
|
||||||
?.Result.Revision ?? -1;
|
public static async Task<int?> RemoveDislikeAsync(this YTrack track)
|
||||||
}
|
=> await track.Context.API.Library.RemoveTrackDislikeAsync(track);
|
||||||
|
|
||||||
public static Task<string> SendPlayTrackInfoAsync(this YTrack track, string from, bool fromCache = false, string playId = "", string playlistId = "", double totalPlayedSeconds = 0, double endPositionSeconds = 0)
|
/// <summary>
|
||||||
{
|
/// Отправляет информацию о воспроизведении трека.
|
||||||
return track.Context.API.Track.SendPlayTrackInfoAsync(track.Context.Storage, track, from, fromCache, playId, playlistId, totalPlayedSeconds);
|
/// </summary>
|
||||||
}
|
public static Task<string?> SendPlayTrackInfoAsync(this YTrack track, string from, bool fromCache = false, string playId = "", string playlistId = "", double totalPlayedSeconds = 0, double endPositionSeconds = 0)
|
||||||
|
=> track.Context.API.Track.SendPlayTrackInfoAsync(track, from, fromCache, playId, playlistId, totalPlayedSeconds, endPositionSeconds);
|
||||||
|
|
||||||
public static async Task<YTrackSupplement> SupplementAsync(this YTrack track)
|
/// <summary>
|
||||||
{
|
/// Получает дополнительную информацию о треке.
|
||||||
return (await track.Context.API.Track.GetSupplementAsync(track.Context.Storage, track))
|
/// </summary>
|
||||||
.Result;
|
public static async Task<YTrackSupplement?> SupplementAsync(this YTrack track)
|
||||||
}
|
=> await track.Context.API.Track.GetSupplementAsync(track);
|
||||||
|
|
||||||
public static async Task<YTrackSimilar> SimilarAsync(this YTrack track)
|
/// <summary>
|
||||||
{
|
/// Получает похожие треки.
|
||||||
return (await track.Context.API.Track.GetSimilarAsync(track.Context.Storage, track))
|
/// </summary>
|
||||||
.Result;
|
public static async Task<YTrackSimilar?> SimilarAsync(this YTrack track)
|
||||||
}
|
=> await track.Context.API.Track.GetSimilarAsync(track);
|
||||||
}
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Extensions;
|
|
||||||
|
|
||||||
public static class HttpRequestHeaderExtensions
|
|
||||||
{
|
|
||||||
public static string GetName(this HttpRequestHeader header)
|
|
||||||
{
|
|
||||||
return header.ToString().SplitByCapitalLetter("-");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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<Match>()
|
|
||||||
.Select(m => m.ToString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Проверяет соответствие регулярному выражению
|
|
||||||
/// </summary>
|
|
||||||
public static bool IsMatch(this string str, string pattern, RegexOptions options)
|
|
||||||
{
|
|
||||||
return Regex.IsMatch(str, pattern, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Проверяет соответствие регулярному выражению
|
|
||||||
/// </summary>
|
|
||||||
public static bool IsMatch(this string str, string pattern)
|
|
||||||
{
|
|
||||||
return IsMatch(str, pattern, RegexOptions.IgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Возвращает совпадения для регулярного выражения
|
|
||||||
/// </summary>
|
|
||||||
public static string[] GetMatches(this string str, string pattern, RegexOptions options = RegexOptions.IgnoreCase)
|
|
||||||
{
|
|
||||||
return str.IsMatch(pattern, options)
|
|
||||||
? Regex.Matches(str, pattern, options)
|
|
||||||
.Cast<Match>()
|
|
||||||
.Select(m => m.Value)
|
|
||||||
.ToArray()
|
|
||||||
: new string[] { };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
5
YandexMusic.API/Models/Account/YAuthEmpty.cs
Normal file
5
YandexMusic.API/Models/Account/YAuthEmpty.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public class YAuthEmpty
|
||||||
|
{
|
||||||
|
}
|
||||||
21
YandexMusic.API/Models/Account/YAuthQRSession.cs
Normal file
21
YandexMusic.API/Models/Account/YAuthQRSession.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public class YAuthQRSession
|
||||||
|
{
|
||||||
|
[JsonPropertyName("default_uid")]
|
||||||
|
public int DefaultUid { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("retpath")]
|
||||||
|
public string RetPath { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("track_id")]
|
||||||
|
public string TrackId { get; set; }
|
||||||
|
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
public string State { get; set; }
|
||||||
|
|
||||||
|
public YAuthCaptcha Captcha { get; set; }
|
||||||
|
}
|
||||||
@@ -9,4 +9,4 @@ public class YAuthQR : YAuthBase
|
|||||||
|
|
||||||
[JsonPropertyName("csrf_token")]
|
[JsonPropertyName("csrf_token")]
|
||||||
public string CsrfToken { get; set; }
|
public string CsrfToken { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
9
YandexMusic.API/Models/Account/YAuthQrState.cs
Normal file
9
YandexMusic.API/Models/Account/YAuthQrState.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
public enum YAuthQrState
|
||||||
|
{
|
||||||
|
[EnumMember(Value = "otp_auth_finished")]
|
||||||
|
OtpAuthFinished,
|
||||||
|
}
|
||||||
@@ -2,19 +2,11 @@
|
|||||||
|
|
||||||
namespace YandexMusic.API.Models.Account;
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
public class YAuthQRStatus : YAuthBase
|
public class YAuthQRStatus
|
||||||
{
|
{
|
||||||
[JsonPropertyName("default_uid")]
|
[JsonPropertyName("state")]
|
||||||
public int DefaultUid { get; set; }
|
public string? State { get; set; } = null;
|
||||||
|
|
||||||
public string RetPath { get; set; }
|
[JsonPropertyName("trackId")]
|
||||||
|
public string TrackId { get; set; } = string.Empty;
|
||||||
[JsonPropertyName("track_id")]
|
|
||||||
public string TrackId { get; set; }
|
|
||||||
|
|
||||||
public string Id { get; set; }
|
|
||||||
|
|
||||||
public string State { get; set; }
|
|
||||||
|
|
||||||
public YAuthCaptcha Captcha { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
using System.Text.Json.Serialization;
|
namespace YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
namespace YandexMusic.API.Models.Account;
|
|
||||||
|
|
||||||
public class YAuthToken
|
public class YAuthToken
|
||||||
{
|
{
|
||||||
[JsonPropertyName("csfr_token")]
|
|
||||||
public string CsfrToken { get; set; }
|
public string CsfrToken { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("track_id")]
|
|
||||||
public string TrackId { get; set; }
|
public string TrackId { get; set; }
|
||||||
|
|
||||||
|
public string SessionTrackId { get; set; }
|
||||||
|
|
||||||
|
public string ProcessUuid { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<string, string> Cookie { get; set; } = new();
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
using YandexMusic.API.Models.Album;
|
using YandexMusic.API.Models.Album;
|
||||||
using YandexMusic.API.Models.Common;
|
using YandexMusic.API.Models.Common;
|
||||||
using YandexMusic.API.Models.Common.Cover;
|
using YandexMusic.API.Models.Common.Cover;
|
||||||
@@ -11,7 +10,6 @@ public class YArtistBriefInfo
|
|||||||
{
|
{
|
||||||
public YButton ActionButton { get; set; }
|
public YButton ActionButton { get; set; }
|
||||||
public List<YAlbum> Albums { get; set; }
|
public List<YAlbum> Albums { get; set; }
|
||||||
[JsonConverter(typeof(YCoverConverter))]
|
|
||||||
public List<YCover> AllCovers { get; set; }
|
public List<YCover> AllCovers { get; set; }
|
||||||
public List<YAlbum> AlsoAlbums { get; set; }
|
public List<YAlbum> AlsoAlbums { get; set; }
|
||||||
public YArtist Artist { get; set; }
|
public YArtist Artist { get; set; }
|
||||||
|
|||||||
@@ -1,33 +1,7 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace YandexMusic.API.Models.Common.Cover;
|
namespace YandexMusic.API.Models.Common.Cover;
|
||||||
|
|
||||||
public class YCoverConverter : JsonConverter<YCover>
|
|
||||||
{
|
|
||||||
public override YCover? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType != JsonTokenType.StartObject) return null;
|
|
||||||
using var doc = JsonDocument.ParseValue(ref reader);
|
|
||||||
var root = doc.RootElement;
|
|
||||||
var type = root.TryGetProperty("type", out var t) ? t.GetString() : null;
|
|
||||||
if (root.TryGetProperty("error", out _)) type = "error";
|
|
||||||
|
|
||||||
return type switch
|
|
||||||
{
|
|
||||||
"color" => JsonSerializer.Deserialize<YCoverColor>(root.GetRawText(), options),
|
|
||||||
"error" => JsonSerializer.Deserialize<YCoverError>(root.GetRawText(), options),
|
|
||||||
"from-artist-photos" or "from-album-cover" => JsonSerializer.Deserialize<YCoverImage>(root.GetRawText(), options),
|
|
||||||
"pic" => JsonSerializer.Deserialize<YCoverPic>(root.GetRawText(), options),
|
|
||||||
"mosaic" => JsonSerializer.Deserialize<YCoverMosaic>(root.GetRawText(), options),
|
|
||||||
_ => new YCover() { Type = YCoverType.Error }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, YCover value, JsonSerializerOptions options)
|
|
||||||
=> JsonSerializer.Serialize(writer, value, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonConverter(typeof(YCoverConverter))]
|
[JsonConverter(typeof(YCoverConverter))]
|
||||||
public class YCover
|
public class YCover
|
||||||
{
|
{
|
||||||
|
|||||||
29
YandexMusic.API/Models/Common/Cover/YCoverConverter.cs
Normal file
29
YandexMusic.API/Models/Common/Cover/YCoverConverter.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Models.Common.Cover;
|
||||||
|
|
||||||
|
public class YCoverConverter : JsonConverter<YCover>
|
||||||
|
{
|
||||||
|
public override YCover? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.StartObject) return null;
|
||||||
|
using var doc = JsonDocument.ParseValue(ref reader);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
var type = root.TryGetProperty("type", out var t) ? t.GetString() : null;
|
||||||
|
if (root.TryGetProperty("error", out _)) type = "error";
|
||||||
|
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
"color" => JsonSerializer.Deserialize<YCoverColor>(root.GetRawText(), options),
|
||||||
|
"error" => JsonSerializer.Deserialize<YCoverError>(root.GetRawText(), options),
|
||||||
|
"from-artist-photos" or "from-album-cover" => JsonSerializer.Deserialize<YCoverImage>(root.GetRawText(), options),
|
||||||
|
"pic" => JsonSerializer.Deserialize<YCoverPic>(root.GetRawText(), options),
|
||||||
|
"mosaic" => JsonSerializer.Deserialize<YCoverMosaic>(root.GetRawText(), options),
|
||||||
|
_ => new YCover() { Type = YCoverType.Error }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, YCover value, JsonSerializerOptions options)
|
||||||
|
=> JsonSerializer.Serialize(writer, value, options);
|
||||||
|
}
|
||||||
@@ -2,11 +2,15 @@ using YandexMusic.API.Common;
|
|||||||
|
|
||||||
namespace YandexMusic.API.Models.Common;
|
namespace YandexMusic.API.Models.Common;
|
||||||
|
|
||||||
/// <summary>Контекст выполнения, содержащий ссылки на API и хранилище.</summary>
|
/// <summary>
|
||||||
|
/// Контекст выполнения, содержащий ссылки на API и хранилище.
|
||||||
|
/// Используется в моделях для вызова методов расширения.
|
||||||
|
/// </summary>
|
||||||
public class YExecutionContext
|
public class YExecutionContext
|
||||||
{
|
{
|
||||||
/// <summary>Экземпляр основного API.</summary>
|
/// <summary>Экземпляр основного API.</summary>
|
||||||
public YandexMusicApi API { get; internal set; } = null!;
|
public YandexMusicApi API { get; internal set; } = null!;
|
||||||
|
|
||||||
/// <summary>Хранилище данных авторизации.</summary>
|
/// <summary>Хранилище данных авторизации.</summary>
|
||||||
public AuthStorage Storage { get; internal set; } = null!;
|
public AuthStorage Storage { get; internal set; } = null!;
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,34 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using YandexMusic.API.Common;
|
using YandexMusic.API.Common;
|
||||||
|
using YandexMusic.API.Models.Common;
|
||||||
|
|
||||||
namespace YandexMusic.API.Models.Common;
|
namespace YandexMusic.API.Converters;
|
||||||
|
|
||||||
/// <summary>Конвертер для внедрения контекста выполнения (API и хранилище) в модели.</summary>
|
/// <summary>
|
||||||
|
/// Конвертер для внедрения контекста выполнения (API и хранилище) в модели, наследуемые от YBaseModel.
|
||||||
|
/// </summary>
|
||||||
public class YExecutionContextConverter : JsonConverter<object>
|
public class YExecutionContextConverter : JsonConverter<object>
|
||||||
{
|
{
|
||||||
private readonly YandexMusicApi _api;
|
private readonly YandexMusicApi _api;
|
||||||
private readonly AuthStorage _storage;
|
private readonly AuthStorage _storage;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр конвертера.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="api">Экземпляр основного API.</param>
|
||||||
|
/// <param name="storage">Хранилище авторизации.</param>
|
||||||
public YExecutionContextConverter(YandexMusicApi api, AuthStorage storage)
|
public YExecutionContextConverter(YandexMusicApi api, AuthStorage storage)
|
||||||
{
|
{
|
||||||
_api = api;
|
_api = api;
|
||||||
_storage = storage;
|
_storage = storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override bool CanConvert(Type typeToConvert) =>
|
public override bool CanConvert(Type typeToConvert) =>
|
||||||
typeof(YBaseModel).IsAssignableFrom(typeToConvert);
|
typeof(YBaseModel).IsAssignableFrom(typeToConvert);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
// Убираем этот конвертер из опций, чтобы избежать рекурсии
|
// Убираем этот конвертер из опций, чтобы избежать рекурсии
|
||||||
@@ -33,6 +43,7 @@ public class YExecutionContextConverter : JsonConverter<object>
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
var innerOptions = new JsonSerializerOptions(options);
|
var innerOptions = new JsonSerializerOptions(options);
|
||||||
|
|||||||
@@ -1,26 +1,19 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Account;
|
using YandexMusic.API.Models.Account;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Account;
|
namespace YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
[YPassportRequest(WebRequestMethods.Http.Post, "registration-validations/auth/multi_step/commit_password")]
|
internal class YGetAuthAppPasswordBuilder : YAuthRequestBuilder<YAuthBase?, string>
|
||||||
internal class YGetAuthAppPasswordBuilder : YRequestBuilder<YAuthBase, string>
|
|
||||||
{
|
{
|
||||||
public YGetAuthAppPasswordBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetAuthAppPasswordBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string PathTemplate => "registration-validations/auth/multi_step/commit_password";
|
||||||
|
protected override HttpContent? GetContent(string password)
|
||||||
protected override HttpContent GetContent(string tuple)
|
=> new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
{ "csrf_token", Api.Storage.AuthToken.CsfrToken },
|
||||||
{ "csrf_token", storage.AuthToken.CsfrToken },
|
{ "track_id", Api.Storage.AuthToken.TrackId },
|
||||||
{ "track_id", storage.AuthToken.TrackId },
|
{ "password", password },
|
||||||
{ "password", tuple },
|
|
||||||
{ "retpath", "https://passport.yandex.ru/am/finish?status=ok&from=Login" }
|
{ "retpath", "https://passport.yandex.ru/am/finish?status=ok&from=Login" }
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,13 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Account;
|
namespace YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
[YPassportRequest(WebRequestMethods.Http.Post, "registration-validations/textcaptcha")]
|
internal class YGetAuthCaptchaBuilder : YAuthRequestBuilder<YAuthCaptcha?, object>
|
||||||
internal class YGetAuthCaptchaBuilder : YRequestBuilder<Models.Account.YAuthCaptcha, string>
|
|
||||||
{
|
{
|
||||||
public YGetAuthCaptchaBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetAuthCaptchaBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string PathTemplate => "registration-validations/textcaptcha";
|
||||||
|
protected override HttpContent? GetContent(object _)
|
||||||
protected override HttpContent GetContent(string tuple)
|
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "csrf_token", Api.Storage.AuthToken.CsfrToken }, { "track_id", Api.Storage.AuthToken.TrackId } });
|
||||||
{
|
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
|
||||||
{ "csrf_token", storage.AuthToken.CsfrToken },
|
|
||||||
{ "track_id", storage.AuthToken.TrackId },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
|
||||||
{
|
|
||||||
headers.Add("X-Requested-With", "XMLHttpRequest");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,36 +1,55 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Account;
|
using YandexMusic.API.Models.Account;
|
||||||
using YandexMusic.API.Requests.Common;
|
using YandexMusic.API.Requests.Common;
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Account;
|
namespace YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
[YMobileProxyRequest(WebRequestMethods.Http.Post, "1/bundle/oauth/token_by_sessionid")]
|
internal class YGetAuthCookiesBuilder : YAuthRequestBuilder<YAccessToken?, object>
|
||||||
internal class YGetAuthCookiesBuilder : YRequestBuilder<YAccessToken, string>
|
|
||||||
{
|
{
|
||||||
public YGetAuthCookiesBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetAuthCookiesBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string BaseUrl => YConstants.Endpoints.MobilePassportUrl;
|
||||||
|
protected override string PathTemplate => "1/bundle/oauth/token_by_sessionid";
|
||||||
|
protected override HttpContent? GetContent(object _)
|
||||||
|
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "client_id", YConstants.XClientId }, { "client_secret", YConstants.XClientSecret } });
|
||||||
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
||||||
{
|
{
|
||||||
CookieCollection cookieCollection = new() {
|
base.SetCustomHeaders(headers);
|
||||||
storage.Context.Cookies.GetCookies(new Uri("https://yandex.ru/")),
|
headers.Add("ya-client-host", "passport.yandex.ru");
|
||||||
storage.Context.Cookies.GetCookies(new Uri("https://passport.yandex.ru/"))
|
|
||||||
|
var cookieString = GetCookieString();
|
||||||
|
if (!string.IsNullOrEmpty(cookieString))
|
||||||
|
headers.Add("Ya-Client-Cookie", cookieString);
|
||||||
|
}
|
||||||
|
private string GetCookieString()
|
||||||
|
{
|
||||||
|
var container = Storage.CookieContainer;
|
||||||
|
if (container == null) return string.Empty;
|
||||||
|
|
||||||
|
var uris = new[]
|
||||||
|
{
|
||||||
|
new Uri("https://yandex.ru"),
|
||||||
|
new Uri("https://passport.yandex.ru"),
|
||||||
|
new Uri("https://mobileproxy.passport.yandex.net")
|
||||||
};
|
};
|
||||||
|
|
||||||
headers.Add("Ya-Client-Cookie", string.Join(";", cookieCollection.Select(c => $"{c.Name}={c.Value}")));
|
var cookies = new List<string>();
|
||||||
headers.Add("Ya-Client-Host", "passport.yandex.ru");
|
foreach (var uri in uris)
|
||||||
}
|
{
|
||||||
|
var cookieCollection = container.GetCookies(uri);
|
||||||
|
foreach (Cookie cookie in cookieCollection)
|
||||||
|
{
|
||||||
|
cookies.Add($"{cookie.Name}={cookie.Value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override HttpContent GetContent(string tuple)
|
var distinct = cookies
|
||||||
{
|
.Select(c => c.Split('=')[0])
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
.Distinct()
|
||||||
{ "client_id", YConstants.XClientId },
|
.Select(name => cookies.First(c => c.StartsWith(name + "=")))
|
||||||
{ "client_secret", YConstants.XClientSecret }
|
.ToList();
|
||||||
});
|
|
||||||
|
return string.Join("; ", distinct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Account;
|
using YandexMusic.API.Models.Account;
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Account;
|
namespace YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Get, "account/status")]
|
internal class YGetAuthInfoBuilder : YMusicRequestBuilder<YAccountResult?, object>
|
||||||
public class YGetAuthInfoBuilder : YRequestBuilder<YResponse<YAccountResult>, object>
|
|
||||||
{
|
{
|
||||||
public YGetAuthInfoBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetAuthInfoBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
}
|
protected override string PathTemplate => "account/status";
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,13 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using YandexMusic.API.Models.Account;
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Account;
|
namespace YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
[YPassportRequest(WebRequestMethods.Http.Post, "registration-validations/auth/send_magic_letter")]
|
internal class YGetAuthLetterBuilder : YAuthRequestBuilder<YAuthLetter?, object>
|
||||||
internal class YGetAuthLetterBuilder : YRequestBuilder<Models.Account.YAuthLetter, string>
|
|
||||||
{
|
{
|
||||||
public YGetAuthLetterBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetAuthLetterBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string PathTemplate => "registration-validations/auth/send_magic_letter";
|
||||||
|
protected override HttpContent? GetContent(object _)
|
||||||
protected override HttpContent GetContent(string tuple)
|
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "csrf_token", Api.Storage.AuthToken.CsfrToken }, { "track_id", Api.Storage.AuthToken.TrackId } });
|
||||||
{
|
|
||||||
if (storage.AuthToken == null)
|
|
||||||
{
|
|
||||||
throw new Exception("Не найдена сессия входа.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "csrf_token", storage.AuthToken.CsfrToken },
|
|
||||||
{ "track_id", storage.AuthToken.TrackId },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,13 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Account;
|
using YandexMusic.API.Models.Account;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Account;
|
namespace YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
[YPassportRequest(WebRequestMethods.Http.Post, "registration-validations/checkHuman")]
|
internal class YGetAuthLoginCaptchaBuilder : YAuthRequestBuilder<YAuthBase?, string>
|
||||||
internal class YGetAuthLoginCaptchaBuilder : YRequestBuilder<YAuthBase, string>
|
|
||||||
{
|
{
|
||||||
public YGetAuthLoginCaptchaBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetAuthLoginCaptchaBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string PathTemplate => "registration-validations/checkHuman";
|
||||||
|
protected override HttpContent? GetContent(string captchaAnswer)
|
||||||
protected override HttpContent GetContent(string tuple)
|
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "csrf_token", Api.Storage.AuthToken.CsfrToken }, { "track_id", Api.Storage.AuthToken.TrackId }, { "answer", captchaAnswer } });
|
||||||
{
|
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
|
||||||
{ "csrf_token", storage.AuthToken.CsfrToken },
|
|
||||||
{ "track_id", storage.AuthToken.TrackId },
|
|
||||||
{ "answer", tuple }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
|
||||||
{
|
|
||||||
headers.Add("X-Requested-With", "XMLHttpRequest");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,13 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Account;
|
using YandexMusic.API.Models.Account;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Account;
|
namespace YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
[YPassportRequest(WebRequestMethods.Http.Post, "auth/letter/status/")]
|
internal class YGetAuthLoginLetterBuilder : YAuthRequestBuilder<YAuthLetterStatus?, object>
|
||||||
internal class YGetAuthLoginLetterBuilder : YRequestBuilder<YAuthLetterStatus, string>
|
|
||||||
{
|
{
|
||||||
public YGetAuthLoginLetterBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetAuthLoginLetterBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string PathTemplate => "auth/letter/status/";
|
||||||
|
protected override HttpContent? GetContent(object _)
|
||||||
protected override HttpContent GetContent(string tuple)
|
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "csrf_token", Api.Storage.AuthToken.CsfrToken }, { "track_id", Api.Storage.AuthToken.TrackId } });
|
||||||
{
|
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
|
||||||
{ "csrf_token", storage.AuthToken.CsfrToken },
|
|
||||||
{ "track_id", storage.AuthToken.TrackId },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,28 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Account;
|
using YandexMusic.API.Models.Account;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Account;
|
namespace YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
[YPassportRequest(WebRequestMethods.Http.Post, "auth/new/magic/status/")]
|
internal class YGetAuthLoginQRBuilder : YAuthRequestBuilder<YAuthQRSession, string>
|
||||||
internal class YGetAuthLoginQRBuilder : YRequestBuilder<YAuthQRStatus, string>
|
|
||||||
{
|
{
|
||||||
public YGetAuthLoginQRBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetAuthLoginQRBuilder(YandexMusicApi yandex) : base(yandex)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
|
|
||||||
|
protected override string PathTemplate => "pwl-yandex/api/passport/sessions/get_session";
|
||||||
|
|
||||||
protected override HttpContent GetContent(string tuple)
|
protected override HttpContent GetContent(string tuple)
|
||||||
{
|
{
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
||||||
{ "csrf_token", storage.AuthToken.CsfrToken },
|
{ "track_id", Api.Storage.AuthToken.SessionTrackId }
|
||||||
{ "track_id", storage.AuthToken.TrackId }
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
||||||
|
{
|
||||||
|
headers.Add("X-Csrf-Token", Api.Storage.HeaderToken.CsfrToken);
|
||||||
|
headers.Add("Process-Uuid", Api.Storage.HeaderToken.ProcessUuid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,13 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Account;
|
using YandexMusic.API.Models.Account;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Account;
|
namespace YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
[YPassportRequest(WebRequestMethods.Http.Post, "registration-validations/auth/multi_step/start")]
|
internal class YGetAuthLoginUserBuilder : YAuthRequestBuilder<YAuthTypes?, (string token, string login)>
|
||||||
internal class YGetAuthLoginUserBuilder : YRequestBuilder<YAuthTypes, (string token, string login)>
|
|
||||||
{
|
{
|
||||||
public YGetAuthLoginUserBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetAuthLoginUserBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string PathTemplate => "registration-validations/auth/multi_step/start";
|
||||||
|
protected override HttpContent? GetContent((string token, string login) tuple)
|
||||||
protected override HttpContent GetContent((string token, string login) tuple)
|
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "csrf_token", tuple.token }, { "login", tuple.login } });
|
||||||
{
|
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
|
||||||
{ "csrf_token", tuple.token },
|
|
||||||
{ "login", tuple.login }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,13 @@
|
|||||||
using System.Collections.Specialized;
|
using System.Net;
|
||||||
using System.Net;
|
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Requests.Common;
|
using YandexMusic.API.Requests.Common;
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Account;
|
namespace YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
[YPassportRequest(WebRequestMethods.Http.Get, "am")]
|
internal class YGetAuthMethodsBuilder : YRequestBuilder<object>
|
||||||
internal class YGetAuthMethodsBuilder : YRequestBuilder<HttpResponseMessage, string>
|
|
||||||
{
|
{
|
||||||
public YGetAuthMethodsBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetAuthMethodsBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
}
|
protected override string PathTemplate => "am";
|
||||||
|
|
||||||
protected override NameValueCollection GetQueryParams(string tuple)
|
protected override string BaseUrl => YConstants.Endpoints.PassportUrl;
|
||||||
{
|
|
||||||
return new NameValueCollection {
|
|
||||||
{ "app_platform", "android" }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,19 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Account;
|
using YandexMusic.API.Models.Account;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Account;
|
namespace YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
[YPassportRequest(WebRequestMethods.Http.Post, "registration-validations/auth/password/submit")]
|
internal class YGetAuthQRBuilder : YAuthRequestBuilder<YAuthQR?, object>
|
||||||
internal class YGetAuthQRBuilder : YRequestBuilder<YAuthQR, string>
|
|
||||||
{
|
{
|
||||||
public YGetAuthQRBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetAuthQRBuilder(YandexMusicApi api) : base(api) { }
|
||||||
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
|
protected override string PathTemplate => "pwl-yandex/api/passport/auth/password/submit";
|
||||||
|
protected override HttpContent? GetContent(object _)
|
||||||
|
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "retpath", "" } });
|
||||||
|
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
||||||
{
|
{
|
||||||
}
|
headers.Add("X-Csrf-Token", Api.Storage.HeaderToken.CsfrToken);
|
||||||
|
headers.Add("Process-Uuid", Api.Storage.HeaderToken.ProcessUuid);
|
||||||
protected override HttpContent GetContent(string tuple)
|
|
||||||
{
|
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
|
||||||
{ "csrf_token", storage.AuthToken.CsfrToken },
|
|
||||||
{ "retpath", "https://passport.yandex.ru/profile" },
|
|
||||||
{ "with_code", "1" },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,11 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Account;
|
using YandexMusic.API.Models.Account;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Account;
|
namespace YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
[YLoginRequest(WebRequestMethods.Http.Get, "info")]
|
internal class YGetLoginInfoBuilder : YAuthRequestBuilder<YLoginInfo?, object>
|
||||||
public class YGetLoginInfoBuilder : YRequestBuilder<YLoginInfo, object>
|
|
||||||
{
|
{
|
||||||
public YGetLoginInfoBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetLoginInfoBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
}
|
protected override string PathTemplate => "info";
|
||||||
}
|
}
|
||||||
@@ -1,35 +1,25 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Account;
|
using YandexMusic.API.Models.Account;
|
||||||
using YandexMusic.API.Requests.Common;
|
using YandexMusic.API.Requests.Common;
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Account;
|
namespace YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
[YOAuthMobile(WebRequestMethods.Http.Post, "/1/token")]
|
internal class YGetMusicTokenBuilder : YAuthRequestBuilder<YAccessToken?, object>
|
||||||
internal class YGetMusicTokenBuilder : YRequestBuilder<YAccessToken, string>
|
|
||||||
{
|
{
|
||||||
public YGetMusicTokenBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetMusicTokenBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string PathTemplate => "/1/token";
|
||||||
|
protected override HttpContent? GetContent(object _)
|
||||||
protected override HttpContent GetContent(string tuple)
|
=> new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
{
|
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string>
|
|
||||||
{
|
{
|
||||||
{ "client_id", YConstants.ClientId },
|
{ "client_id", YConstants.ClientId },
|
||||||
{ "client_secret", YConstants.ClientSecret },
|
{ "client_secret", YConstants.ClientSecret },
|
||||||
{ "grant_type", "x-token" },
|
{ "grant_type", "x-token" },
|
||||||
{ "access_token", storage.AccessToken.AccessToken }
|
{ "access_token", Api.Storage.AccessToken.AccessToken }
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
||||||
{
|
{
|
||||||
headers.Remove("Authorization");
|
headers.Remove("Authorization");
|
||||||
|
|
||||||
base.SetCustomHeaders(headers);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,30 +1,19 @@
|
|||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Account;
|
using YandexMusic.API.Models.Account;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Account;
|
namespace YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
[YMobileProxyRequest(WebRequestMethods.Http.Get, "/1/bundle/account/short_info/")]
|
internal class YGetShortAccountInfoBuilder : YAuthRequestBuilder<YShortAccountInfo?, object>
|
||||||
internal class YGetShortAccountInifoBuilder : YRequestBuilder<YShortAccountInfo, object>
|
|
||||||
{
|
{
|
||||||
public YGetShortAccountInifoBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetShortAccountInfoBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
}
|
protected override string PathTemplate => "1/bundle/account/short_info/";
|
||||||
|
protected override NameValueCollection GetQueryParams(object _)
|
||||||
protected override NameValueCollection GetQueryParams(object tuple)
|
=> new() { { "avatar_size", "islands-300" } };
|
||||||
{
|
|
||||||
return new NameValueCollection {
|
|
||||||
{ "avatar_size", "islands-300" }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
||||||
{
|
{
|
||||||
headers.Add("Ya-Consumer-Authorization", $"OAuth {storage.AccessToken.AccessToken}");
|
headers.Add("Ya-Consumer-Authorization", $"OAuth {Api.Storage.AccessToken.AccessToken}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
19
YandexMusic.API/Requests/Account/YPostAuthStats.cs
Normal file
19
YandexMusic.API/Requests/Account/YPostAuthStats.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
|
internal class YPostAuthStats : YAuthRequestBuilder<YAuthEmpty?, object>
|
||||||
|
{
|
||||||
|
public YPostAuthStats(YandexMusicApi api) : base(api) { }
|
||||||
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
|
protected override string PathTemplate => "pwl-yandex/api/passport/stats";
|
||||||
|
protected override HttpContent? GetContent(object _)
|
||||||
|
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "messageType", "CLIENT_READY" } });
|
||||||
|
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
||||||
|
{
|
||||||
|
headers.Add("X-Csrf-Token", Api.Storage.HeaderToken.CsfrToken);
|
||||||
|
headers.Add("Process-Uuid", Api.Storage.HeaderToken.ProcessUuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
YandexMusic.API/Requests/Account/YPostQrStatus.cs
Normal file
23
YandexMusic.API/Requests/Account/YPostQrStatus.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using YandexMusic.API.Models.Account;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Requests.Account;
|
||||||
|
|
||||||
|
internal class YPostQrStatus : YAuthRequestBuilder<YAuthQRStatus?, object>
|
||||||
|
{
|
||||||
|
public YPostQrStatus(YandexMusicApi api) : base(api) { }
|
||||||
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
|
protected override string PathTemplate => "pwl-yandex/api/passport/auth/magic/code/status";
|
||||||
|
protected override HttpContent? GetContent(object _)
|
||||||
|
=> new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["csrf_token"] = Api.Storage.AuthToken.CsfrToken,
|
||||||
|
["track_id"] = Api.Storage.AuthToken.TrackId,
|
||||||
|
});
|
||||||
|
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
||||||
|
{
|
||||||
|
headers.Add("X-Csrf-Token", Api.Storage.HeaderToken.CsfrToken);
|
||||||
|
headers.Add("Process-Uuid", Api.Storage.HeaderToken.ProcessUuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,13 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Album;
|
using YandexMusic.API.Models.Album;
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Album;
|
namespace YandexMusic.API.Requests.Album;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Get, "albums/{albumId}/with-tracks")]
|
internal class YGetAlbumBuilder : YMusicRequestBuilder<YAlbum?, string>
|
||||||
public class YGetAlbumBuilder : YRequestBuilder<YResponse<YAlbum>, string>
|
|
||||||
{
|
{
|
||||||
public YGetAlbumBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetAlbumBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
}
|
protected override string PathTemplate => "albums/{albumId}/with-tracks";
|
||||||
|
|
||||||
protected override Dictionary<string, string> GetSubstitutions(string albumId)
|
protected override Dictionary<string, string> GetSubstitutions(string albumId)
|
||||||
{
|
=> new() { { "albumId", albumId } };
|
||||||
return new Dictionary<string, string> {
|
|
||||||
{ "albumId", albumId }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,13 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Album;
|
using YandexMusic.API.Models.Album;
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Album;
|
namespace YandexMusic.API.Requests.Album;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Post, "albums")]
|
internal class YGetAlbumsBuilder : YMusicRequestBuilder<List<YAlbum>?, IEnumerable<string>>
|
||||||
public class YGetAlbumsBuilder : YRequestBuilder<YResponse<List<YAlbum>>, IEnumerable<string>>
|
|
||||||
{
|
{
|
||||||
public YGetAlbumsBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetAlbumsBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string PathTemplate => "albums";
|
||||||
|
protected override HttpContent? GetContent(IEnumerable<string> albumIds)
|
||||||
protected override HttpContent GetContent(IEnumerable<string> albumIds)
|
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "album-ids", string.Join(",", albumIds) } });
|
||||||
{
|
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
|
||||||
{ "album-ids", string.Join(",", albumIds) }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,13 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Artist;
|
using YandexMusic.API.Models.Artist;
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Artist;
|
namespace YandexMusic.API.Requests.Artist;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Get, "artists/{artistId}/brief-info")]
|
internal class YGetArtistBuilder : YMusicRequestBuilder<YArtistBriefInfo?, string>
|
||||||
public class YGetArtistBuilder : YRequestBuilder<YResponse<YArtistBriefInfo>, string>
|
|
||||||
{
|
{
|
||||||
public YGetArtistBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetArtistBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
}
|
protected override string PathTemplate => "artists/{artistId}/brief-info";
|
||||||
|
|
||||||
protected override Dictionary<string, string> GetSubstitutions(string artistId)
|
protected override Dictionary<string, string> GetSubstitutions(string artistId)
|
||||||
{
|
=> new() { { "artistId", artistId } };
|
||||||
return new Dictionary<string, string> {
|
|
||||||
{ "artistId", artistId }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,16 @@
|
|||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Artist;
|
using YandexMusic.API.Models.Artist;
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Artist;
|
namespace YandexMusic.API.Requests.Artist;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Get, "artists/{artistId}/tracks")]
|
internal class YGetArtistTrackBuilder : YMusicRequestBuilder<YTracksPage?, (string id, int page, int pageSize)>
|
||||||
public class YGetArtistTrackBuilder : YRequestBuilder<YResponse<YTracksPage>, (string id, int page, int pageSize)>
|
|
||||||
{
|
{
|
||||||
public YGetArtistTrackBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth) { }
|
public YGetArtistTrackBuilder(YandexMusicApi api) : base(api) { }
|
||||||
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
|
protected override string PathTemplate => "artists/{artistId}/tracks";
|
||||||
protected override Dictionary<string, string> GetSubstitutions((string id, int page, int pageSize) tuple)
|
protected override Dictionary<string, string> GetSubstitutions((string id, int page, int pageSize) tuple)
|
||||||
{
|
=> new() { { "artistId", tuple.id } };
|
||||||
return new Dictionary<string, string> {
|
|
||||||
{ "artistId", tuple.id },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override NameValueCollection GetQueryParams((string id, int page, int pageSize) tuple)
|
protected override NameValueCollection GetQueryParams((string id, int page, int pageSize) tuple)
|
||||||
{
|
=> new() { { "page", tuple.page.ToString() }, { "pageSize", tuple.pageSize.ToString() } };
|
||||||
return new NameValueCollection {
|
|
||||||
{ "page", tuple.page.ToString() },
|
|
||||||
{ "pageSize", tuple.pageSize.ToString() },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,13 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Artist;
|
using YandexMusic.API.Models.Artist;
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Artist;
|
namespace YandexMusic.API.Requests.Artist;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Post, "artists")]
|
internal class YGetArtistsBuilder : YMusicRequestBuilder<List<YArtist>?, IEnumerable<string>>
|
||||||
public class YGetArtistsBuilder : YRequestBuilder<YResponse<List<YArtist>>, IEnumerable<string>>
|
|
||||||
{
|
{
|
||||||
public YGetArtistsBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetArtistsBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string PathTemplate => "artists";
|
||||||
|
protected override HttpContent? GetContent(IEnumerable<string> artistIds)
|
||||||
protected override HttpContent GetContent(IEnumerable<string> artistIds)
|
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "artist-Ids", string.Join(",", artistIds) } });
|
||||||
{
|
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
|
||||||
{ "artist-Ids", string.Join(",", artistIds) }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
public class YApiRequestAttribute : YBasePathRequestAttribute
|
|
||||||
{
|
|
||||||
public YApiRequestAttribute(string method, string url) : base(method, url)
|
|
||||||
{
|
|
||||||
basePath = "https://api.music.yandex.net";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
namespace YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Атрибут запроса относительно базового адреса
|
|
||||||
/// </summary>
|
|
||||||
public class YBasePathRequestAttribute : YRequestAttribute
|
|
||||||
{
|
|
||||||
#region Поля
|
|
||||||
|
|
||||||
protected string basePath;
|
|
||||||
|
|
||||||
#endregion Поля
|
|
||||||
|
|
||||||
#region Свойства
|
|
||||||
public override string Url => GetFullUrl();
|
|
||||||
|
|
||||||
#endregion Свойства
|
|
||||||
|
|
||||||
#region Вспомогательные функции
|
|
||||||
|
|
||||||
private string GetFullUrl()
|
|
||||||
{
|
|
||||||
return $"{basePath.TrimEnd('/')}/{path.TrimStart('/')}";
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Вспомогательные функции
|
|
||||||
|
|
||||||
public YBasePathRequestAttribute(string method, string url) : base(method, url)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
public class YLoginRequestAttribute : YBasePathRequestAttribute
|
|
||||||
{
|
|
||||||
public YLoginRequestAttribute(string method, string url) : base(method, url)
|
|
||||||
{
|
|
||||||
basePath = "https://login.yandex.ru";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
public class YMobileProxyRequestAttribute : YBasePathRequestAttribute
|
|
||||||
{
|
|
||||||
public YMobileProxyRequestAttribute(string method, string url) : base(method, url)
|
|
||||||
{
|
|
||||||
basePath = "https://mobileproxy.passport.yandex.net";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
public class YOAuthMobileAttribute : YBasePathRequestAttribute
|
|
||||||
{
|
|
||||||
public YOAuthMobileAttribute(string method, string url) : base(method, url)
|
|
||||||
{
|
|
||||||
basePath = "https://oauth.mobile.yandex.net";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
public class YOAuthRequestAttribute : YBasePathRequestAttribute
|
|
||||||
{
|
|
||||||
public YOAuthRequestAttribute(string method, string url) : base(method, url)
|
|
||||||
{
|
|
||||||
basePath = "https://oauth.yandex.ru";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
public class YPassportRequestAttribute : YBasePathRequestAttribute
|
|
||||||
{
|
|
||||||
public YPassportRequestAttribute(string method, string url) : base(method, url)
|
|
||||||
{
|
|
||||||
basePath = "https://passport.yandex.ru";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
namespace YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Атрибут запроса без привязки к базовому адресу
|
|
||||||
/// </summary>
|
|
||||||
public class YRequestAttribute : Attribute
|
|
||||||
{
|
|
||||||
#region Поля
|
|
||||||
|
|
||||||
protected string path;
|
|
||||||
|
|
||||||
#endregion Поля
|
|
||||||
|
|
||||||
#region Свойства
|
|
||||||
|
|
||||||
public string Method { get; }
|
|
||||||
public virtual string Url => path;
|
|
||||||
|
|
||||||
#endregion Свойства
|
|
||||||
|
|
||||||
public YRequestAttribute(string method, string url)
|
|
||||||
{
|
|
||||||
Method = method;
|
|
||||||
path = url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
public class YWebApiRequestAttribute : YBasePathRequestAttribute
|
|
||||||
{
|
|
||||||
public YWebApiRequestAttribute(string method, string url) : base(method, url)
|
|
||||||
{
|
|
||||||
basePath = "https://music.yandex.ru";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Common;
|
|
||||||
|
|
||||||
public class HttpContext
|
|
||||||
{
|
|
||||||
public CookieContainer Cookies;
|
|
||||||
|
|
||||||
public HttpContext()
|
|
||||||
{
|
|
||||||
Cookies = new CookieContainer();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IWebProxy WebProxy { get; set; }
|
|
||||||
|
|
||||||
public long GetTimeInterval()
|
|
||||||
{
|
|
||||||
DateTime dt = TimeZoneInfo.ConvertTimeToUtc(DateTime.Now);
|
|
||||||
DateTime dt1970 = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
TimeSpan tsInterval = dt.Subtract(dt1970);
|
|
||||||
long iMilliseconds = Convert.ToInt64(tsInterval.TotalMilliseconds);
|
|
||||||
|
|
||||||
return iMilliseconds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
18
YandexMusic.API/Requests/Common/YAuthRequestBuilder.cs
Normal file
18
YandexMusic.API/Requests/Common/YAuthRequestBuilder.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using YandexMusic.API.Requests.Common;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Requests;
|
||||||
|
|
||||||
|
/// <summary>Базовый класс для запросов к Passport (passport.yandex.ru).</summary>
|
||||||
|
internal abstract class YAuthRequestBuilder<TResponse, TParams> : YJsonRequestBuilder<TResponse, TParams>
|
||||||
|
{
|
||||||
|
protected override string BaseUrl => YConstants.Endpoints.PassportUrl;
|
||||||
|
|
||||||
|
protected YAuthRequestBuilder(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
||||||
|
{
|
||||||
|
base.SetCustomHeaders(headers);
|
||||||
|
headers.Add("X-Requested-With", "XMLHttpRequest");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,4 +7,11 @@ internal class YConstants
|
|||||||
|
|
||||||
public const string XClientId = "c0ebe342af7d48fbbbfcf2d2eedb8f9e";
|
public const string XClientId = "c0ebe342af7d48fbbbfcf2d2eedb8f9e";
|
||||||
public const string XClientSecret = "ad0a908f0aa341a182a37ecd75bc319e";
|
public const string XClientSecret = "ad0a908f0aa341a182a37ecd75bc319e";
|
||||||
|
|
||||||
|
internal static class Endpoints
|
||||||
|
{
|
||||||
|
public const string MusicUrl = "https://api.music.yandex.net";
|
||||||
|
public const string PassportUrl = "https://passport.yandex.ru/";
|
||||||
|
public const string MobilePassportUrl = "https://mobileproxy.passport.yandex.net";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
54
YandexMusic.API/Requests/Common/YJsonRequestBuilder.cs
Normal file
54
YandexMusic.API/Requests/Common/YJsonRequestBuilder.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using YandexMusic.API.Converters;
|
||||||
|
using YandexMusic.API.Models.Common;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Requests.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Строитель запросов с десериализацией JSON-ответа в TResponse.
|
||||||
|
/// </summary>
|
||||||
|
internal abstract class YJsonRequestBuilder<TResponse, TParams> : YRequestBuilder<TParams>
|
||||||
|
{
|
||||||
|
protected YJsonRequestBuilder(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
protected virtual async Task<TResponse?> DeserializeAsync(HttpResponseMessage response)
|
||||||
|
{
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
Converters = {
|
||||||
|
new JsonStringEnumConverter(JsonNamingPolicy.KebabCaseLower),
|
||||||
|
new IntToStringConverter(),
|
||||||
|
new StringToIntConverter(),
|
||||||
|
new YExecutionContextConverter(Api, Storage),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = JsonSerializer.Deserialize<YErrorResponse>(json, options);
|
||||||
|
throw error ?? new Exception($"Ошибка HTTP {response.StatusCode}: {json}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<TResponse>(json, options);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Ошибка десериализации: {ex.Message}\nJSON: {json}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Выполняет запрос и возвращает десериализованный объект типа TResponse.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<TResponse?> ExecuteAsync(TParams parameters)
|
||||||
|
{
|
||||||
|
using var response = await ExecuteRawAsync(parameters);
|
||||||
|
return await DeserializeAsync(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
YandexMusic.API/Requests/Common/YMusicRequestBuilder.cs
Normal file
55
YandexMusic.API/Requests/Common/YMusicRequestBuilder.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using YandexMusic.API.Converters;
|
||||||
|
using YandexMusic.API.Models.Common;
|
||||||
|
using YandexMusic.API.Requests.Common;
|
||||||
|
|
||||||
|
namespace YandexMusic.API.Requests;
|
||||||
|
|
||||||
|
/// <summary>Базовый класс для запросов к API Яндекс Музыки (api.music.yandex.net).</summary>
|
||||||
|
internal abstract class YMusicRequestBuilder<TResponse, TParams> : YJsonRequestBuilder<TResponse, TParams>
|
||||||
|
{
|
||||||
|
protected override string BaseUrl => YConstants.Endpoints.MusicUrl;
|
||||||
|
|
||||||
|
protected YMusicRequestBuilder(YandexMusicApi api) : base(api) { }
|
||||||
|
|
||||||
|
protected override void SetCustomHeaders(HttpRequestHeaders headers)
|
||||||
|
{
|
||||||
|
base.SetCustomHeaders(headers);
|
||||||
|
headers.Add("X-Yandex-Music-Client", Storage.DeviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<TResponse?> DeserializeAsync(HttpResponseMessage response)
|
||||||
|
{
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
Converters = {
|
||||||
|
new JsonStringEnumConverter(JsonNamingPolicy.KebabCaseLower),
|
||||||
|
new IntToStringConverter(),
|
||||||
|
new StringToIntConverter(),
|
||||||
|
new YExecutionContextConverter(Api, Storage),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = JsonSerializer.Deserialize<YErrorResponse>(json, options);
|
||||||
|
throw error ?? new Exception($"Ошибка HTTP {response.StatusCode}: {json}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var uResponse = JsonSerializer.Deserialize<YResponse<TResponse>>(json, options);
|
||||||
|
if (uResponse == null) return default;
|
||||||
|
return uResponse.Result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Ошибка десериализации: {ex.Message}\nJSON: {json}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Common.Providers;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Common;
|
|
||||||
|
|
||||||
internal class YRequest<T>
|
|
||||||
{
|
|
||||||
private HttpRequestMessage msg;
|
|
||||||
private IRequestProvider provider;
|
|
||||||
|
|
||||||
protected YandexMusicApi api;
|
|
||||||
|
|
||||||
public YRequest(HttpRequestMessage message, YandexMusicApi yandex, AuthStorage auth)
|
|
||||||
{
|
|
||||||
msg = message;
|
|
||||||
api = yandex;
|
|
||||||
provider = auth.Provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<T> GetResponseAsync()
|
|
||||||
{
|
|
||||||
if (msg == null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
HttpResponseMessage response = await provider.GetWebResponseAsync(msg);
|
|
||||||
|
|
||||||
if (typeof(T) == typeof(HttpResponseMessage))
|
|
||||||
return (T)(object)response;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await provider.GetDataFromResponseAsync<T>(api, response);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
response.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,38 +1,39 @@
|
|||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Reflection;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
using YandexMusic.API.Common;
|
using YandexMusic.API.Common;
|
||||||
using YandexMusic.API.Extensions;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Common;
|
namespace YandexMusic.API.Requests.Common;
|
||||||
|
|
||||||
/// <summary>Базовый строитель HTTP-запросов к API Яндекс.Музыки.</summary>
|
/// <summary>Базовый строитель HTTP-запросов.</summary>
|
||||||
/// <typeparam name="TResponse">Тип ответа.</typeparam>
|
|
||||||
/// <typeparam name="TParams">Тип параметров запроса.</typeparam>
|
/// <typeparam name="TParams">Тип параметров запроса.</typeparam>
|
||||||
public abstract class YRequestBuilder<TResponse, TParams>
|
internal abstract class YRequestBuilder<TParams>
|
||||||
{
|
{
|
||||||
private readonly YRequestAttribute _requestInfo;
|
/// <summary>HTTP-метод (GET, POST и т.д.).</summary>
|
||||||
private Dictionary<string, string> _substitutions = null!;
|
protected abstract string Method { get; }
|
||||||
|
|
||||||
|
/// <summary>Базовый URL (например, "https://api.music.yandex.net").</summary>
|
||||||
|
protected abstract string BaseUrl { get; }
|
||||||
|
|
||||||
|
/// <summary>Шаблон пути (может содержать плейсхолдеры вида {id}).</summary>
|
||||||
|
protected abstract string PathTemplate { get; }
|
||||||
|
|
||||||
private readonly JsonSerializerOptions _jsonOptions;
|
private readonly JsonSerializerOptions _jsonOptions;
|
||||||
|
|
||||||
protected readonly YandexMusicApi api;
|
/// <summary>Основной экземпляр API.</summary>
|
||||||
protected readonly AuthStorage storage;
|
protected YandexMusicApi Api { get; }
|
||||||
protected string device;
|
|
||||||
|
|
||||||
protected YRequestBuilder(YandexMusicApi yandex, AuthStorage auth)
|
/// <summary>Хранилище авторизации (сокращение для Api.Storage).</summary>
|
||||||
|
protected AuthStorage Storage => Api.Storage;
|
||||||
|
|
||||||
|
protected YRequestBuilder(YandexMusicApi api)
|
||||||
{
|
{
|
||||||
_requestInfo = GetType().GetCustomAttribute<YRequestAttribute>()
|
Api = api;
|
||||||
?? throw new NotImplementedException($"Отсутствует атрибут {nameof(YRequestAttribute)}");
|
|
||||||
api = yandex;
|
|
||||||
storage = auth;
|
|
||||||
device = $"os=CSharp; os_version=; manufacturer=FrigaT; model=Yandex Music API; clid=; device_id={storage.DeviceId}; uuid=random";
|
|
||||||
|
|
||||||
_jsonOptions = new JsonSerializerOptions
|
_jsonOptions = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
@@ -41,57 +42,91 @@ public abstract class YRequestBuilder<TResponse, TParams>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Uri BuildUri(TParams tuple)
|
private string FullUrl => $"{BaseUrl.TrimEnd('/')}/{PathTemplate.TrimStart('/')}";
|
||||||
|
|
||||||
|
private Uri BuildUri(TParams parameters, Dictionary<string, string> substitutions)
|
||||||
{
|
{
|
||||||
var queryParams = GetQueryParams(tuple);
|
var queryParams = GetQueryParams(parameters);
|
||||||
var modifiedParams = HttpUtility.ParseQueryString(string.Empty);
|
var modifiedParams = HttpUtility.ParseQueryString(string.Empty);
|
||||||
foreach (string? key in queryParams)
|
foreach (string? key in queryParams)
|
||||||
if (key != null)
|
if (key != null)
|
||||||
modifiedParams[key] = ReplaceSubs(queryParams[key]!);
|
modifiedParams[key] = ReplaceSubs(queryParams[key]!, substitutions);
|
||||||
var endpoint = ReplaceSubs(_requestInfo.Url);
|
var endpoint = ReplaceSubs(FullUrl, substitutions);
|
||||||
var builder = new UriBuilder(endpoint) { Query = modifiedParams.ToString() ?? string.Empty };
|
var builder = new UriBuilder(endpoint) { Query = modifiedParams.ToString() ?? string.Empty };
|
||||||
return builder.Uri;
|
return builder.Uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpRequestMessage CreateMessage(TParams tuple)
|
private HttpRequestMessage CreateMessage(TParams parameters, Dictionary<string, string> substitutions)
|
||||||
{
|
{
|
||||||
var msg = new HttpRequestMessage
|
var msg = new HttpRequestMessage
|
||||||
{
|
{
|
||||||
RequestUri = BuildUri(tuple),
|
RequestUri = BuildUri(parameters, substitutions),
|
||||||
Method = new HttpMethod(_requestInfo.Method),
|
Method = new HttpMethod(Method),
|
||||||
Content = GetContent(tuple)
|
Content = GetContent(parameters)
|
||||||
};
|
};
|
||||||
msg.Headers.TryAddWithoutValidation(HttpRequestHeader.AcceptCharset.GetName(), Encoding.UTF8.WebName);
|
msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.AcceptCharset), Encoding.UTF8.WebName);
|
||||||
msg.Headers.TryAddWithoutValidation(HttpRequestHeader.AcceptEncoding.GetName(), "gzip");
|
msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.AcceptEncoding), "gzip");
|
||||||
if (!string.IsNullOrEmpty(storage.Token))
|
if (!string.IsNullOrEmpty(Storage.Token))
|
||||||
msg.Headers.TryAddWithoutValidation(HttpRequestHeader.Authorization.GetName(), $"OAuth {storage.Token}");
|
msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.Authorization), $"OAuth {Storage.Token}");
|
||||||
SetCustomHeaders(msg.Headers);
|
SetCustomHeaders(msg.Headers);
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected string ReplaceSubs(string str)
|
// Вспомогательный метод: преобразование HttpRequestHeader в строку (как было в HttpRequestHeaderExtensions)
|
||||||
|
private static string GetHeaderName(HttpRequestHeader header)
|
||||||
{
|
{
|
||||||
var subs = str.GetMatches(@"\{.+?\}");
|
return SplitByCapitalLetter(header.ToString(), "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вспомогательный метод: разбиение строки по заглавным буквам (из StringExtensions)
|
||||||
|
private static string SplitByCapitalLetter(string str, string delimiter)
|
||||||
|
{
|
||||||
|
var matches = Regex.Matches(str, @"([A-Z]+)(?=([A-Z][a-z]|$)) | [A-Z][a-z].+?(?=([A-Z]|$))", RegexOptions.IgnorePatternWhitespace);
|
||||||
|
return string.Join(delimiter, matches.Cast<Match>().Select(m => m.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вспомогательный метод: замена всех вхождений регулярного выражения (из StringExtensions)
|
||||||
|
private static string ReplaceRegex(string input, string pattern, string replacement, RegexOptions options = RegexOptions.IgnoreCase)
|
||||||
|
{
|
||||||
|
return string.IsNullOrEmpty(input) ? string.Empty : Regex.Replace(input, pattern, replacement, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вспомогательный метод: получение совпадений по регулярному выражению (из StringExtensions)
|
||||||
|
private static string[] GetMatches(string input, string pattern, RegexOptions options = RegexOptions.IgnoreCase)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input) || !Regex.IsMatch(input, pattern, options))
|
||||||
|
return Array.Empty<string>();
|
||||||
|
return Regex.Matches(input, pattern, options)
|
||||||
|
.Cast<Match>()
|
||||||
|
.Select(m => m.Value)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ReplaceSubs(string str, Dictionary<string, string> substitutions)
|
||||||
|
{
|
||||||
|
var subs = GetMatches(str, @"\{.+?\}");
|
||||||
foreach (var s in subs)
|
foreach (var s in subs)
|
||||||
{
|
{
|
||||||
var key = s.ReplaceRegex(@"[\{\}]", string.Empty);
|
var key = ReplaceRegex(s, @"[\{\}]", string.Empty);
|
||||||
if (!_substitutions.TryGetValue(key, out var value))
|
if (!substitutions.TryGetValue(key, out var value))
|
||||||
throw new Exception($"Не найдена подстановка {s}");
|
throw new Exception($"Не найдена подстановка {s}");
|
||||||
str = str.Replace(s, value);
|
str = str.Replace(s, value);
|
||||||
}
|
}
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual Dictionary<string, string> GetSubstitutions(TParams tuple) => [];
|
protected virtual Dictionary<string, string> GetSubstitutions(TParams parameters) => [];
|
||||||
protected virtual NameValueCollection GetQueryParams(TParams tuple) => [];
|
protected virtual NameValueCollection GetQueryParams(TParams parameters) => [];
|
||||||
protected virtual HttpContent? GetContent(TParams tuple) => null;
|
protected virtual HttpContent? GetContent(TParams parameters) => null;
|
||||||
protected virtual void SetCustomHeaders(HttpRequestHeaders headers) { }
|
protected virtual void SetCustomHeaders(HttpRequestHeaders headers) { }
|
||||||
|
|
||||||
protected string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions);
|
protected string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions);
|
||||||
|
|
||||||
internal YRequest<TResponse> Build(TParams tuple)
|
/// <summary>Выполняет запрос и возвращает десериализованный ответ.</summary>
|
||||||
|
public async Task<HttpResponseMessage?> ExecuteRawAsync(TParams parameters)
|
||||||
{
|
{
|
||||||
_substitutions = GetSubstitutions(tuple);
|
var substitutions = GetSubstitutions(parameters);
|
||||||
var msg = CreateMessage(tuple);
|
using var msg = CreateMessage(parameters, substitutions);
|
||||||
return new YRequest<TResponse>(msg, api, storage);
|
return await Api.HttpClient.SendAsync(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Feed;
|
using YandexMusic.API.Models.Feed;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Feed;
|
namespace YandexMusic.API.Requests.Feed;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Get, "feed")]
|
internal class YGetFeedBuilder : YMusicRequestBuilder<YFeed?, object>
|
||||||
public class YGetFeedBuilder : YRequestBuilder<YResponse<YFeed>, object>
|
|
||||||
{
|
{
|
||||||
public YGetFeedBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetFeedBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
}
|
protected override string PathTemplate => "feed";
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,17 @@
|
|||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
using YandexMusic.API.Models.Common;
|
||||||
using YandexMusic.API.Models.Label;
|
using YandexMusic.API.Models.Label;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Label;
|
namespace YandexMusic.API.Requests.Label;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Get, "labels/{labelId}/albums")]
|
internal class YGetLabelAlbumsBuilder : YMusicRequestBuilder<YLabelAlbums?, (YLabel label, int pageNumber)>
|
||||||
public class YGetLabelAlbumsBuilder : YRequestBuilder<YResponse<YLabelAlbums>, (YLabel label, int pageNumber)>
|
|
||||||
{
|
{
|
||||||
public YGetLabelAlbumsBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetLabelAlbumsBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
}
|
protected override string PathTemplate => "labels/{labelId}/albums";
|
||||||
|
|
||||||
protected override NameValueCollection GetQueryParams((YLabel label, int pageNumber) tuple)
|
|
||||||
{
|
|
||||||
return new NameValueCollection {
|
|
||||||
{ "page", tuple.pageNumber.ToString() }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Dictionary<string, string> GetSubstitutions((YLabel label, int pageNumber) tuple)
|
protected override Dictionary<string, string> GetSubstitutions((YLabel label, int pageNumber) tuple)
|
||||||
{
|
=> new() { { "labelId", tuple.label.Id } };
|
||||||
return new Dictionary<string, string> {
|
protected override NameValueCollection GetQueryParams((YLabel label, int pageNumber) tuple)
|
||||||
{ "labelId", tuple.label.Id }
|
=> new() { { "page", tuple.pageNumber.ToString() } };
|
||||||
};
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,17 @@
|
|||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
using YandexMusic.API.Models.Common;
|
||||||
using YandexMusic.API.Models.Label;
|
using YandexMusic.API.Models.Label;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Label;
|
namespace YandexMusic.API.Requests.Label;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Get, "labels/{labelId}/artists")]
|
internal class YGetLabelArtistsBuilder : YMusicRequestBuilder<YLabelArtists?, (YLabel label, int pageNumber)>
|
||||||
public class YGetLabelArtistsBuilder : YRequestBuilder<YResponse<YLabelArtists>, (YLabel label, int pageNumber)>
|
|
||||||
{
|
{
|
||||||
public YGetLabelArtistsBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetLabelArtistsBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
}
|
protected override string PathTemplate => "labels/{labelId}/artists";
|
||||||
|
|
||||||
protected override NameValueCollection GetQueryParams((YLabel label, int pageNumber) tuple)
|
|
||||||
{
|
|
||||||
return new NameValueCollection {
|
|
||||||
{ "page", tuple.pageNumber.ToString() }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Dictionary<string, string> GetSubstitutions((YLabel label, int pageNumber) tuple)
|
protected override Dictionary<string, string> GetSubstitutions((YLabel label, int pageNumber) tuple)
|
||||||
{
|
=> new() { { "labelId", tuple.label.Id } };
|
||||||
return new Dictionary<string, string> {
|
protected override NameValueCollection GetQueryParams((YLabel label, int pageNumber) tuple)
|
||||||
{ "labelId", tuple.label.Id }
|
=> new() { { "page", tuple.pageNumber.ToString() } };
|
||||||
};
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Landing;
|
using YandexMusic.API.Models.Landing;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Landing;
|
namespace YandexMusic.API.Requests.Landing;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Get, "children-landing/catalogue")]
|
internal class YGetChildrenLandingBuilder : YMusicRequestBuilder<YChildrenLanding?, object>
|
||||||
public class YGetChildrenLandingBuilder : YRequestBuilder<YResponse<YChildrenLanding>, object>
|
|
||||||
{
|
{
|
||||||
public YGetChildrenLandingBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetChildrenLandingBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
}
|
protected override string PathTemplate => "children-landing/catalogue";
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,17 @@
|
|||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Landing;
|
using YandexMusic.API.Models.Landing;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Landing;
|
namespace YandexMusic.API.Requests.Landing;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Get, "landing3")]
|
internal class YGetLandingBuilder : YMusicRequestBuilder<YLanding?, YLandingBlockType[]>
|
||||||
public class YGetLandingBuilder : YRequestBuilder<YResponse<YLanding>, YLandingBlockType[]>
|
|
||||||
{
|
{
|
||||||
public YGetLandingBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetLandingBuilder(YandexMusicApi api) : base(api) { }
|
||||||
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
|
protected override string PathTemplate => "landing3";
|
||||||
|
protected override NameValueCollection GetQueryParams(YLandingBlockType[] blocks)
|
||||||
{
|
{
|
||||||
}
|
string blocksStr = string.Join(",", blocks.Select(b => SerializeJson(b).Replace("\"", "")));
|
||||||
|
return new NameValueCollection { { "blocks", blocksStr } };
|
||||||
protected override NameValueCollection GetQueryParams(YLandingBlockType[] tuple)
|
|
||||||
{
|
|
||||||
string blocks = string.Join(",", tuple
|
|
||||||
.Select(b => SerializeJson(b).Replace("\"", string.Empty)));
|
|
||||||
|
|
||||||
return new NameValueCollection {
|
|
||||||
{ "blocks", blocks }
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,36 +1,22 @@
|
|||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Landing.Entity.Entities.Context;
|
using YandexMusic.API.Models.Landing.Entity.Entities.Context;
|
||||||
using YandexMusic.API.Models.Library;
|
using YandexMusic.API.Models.Library;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Library;
|
namespace YandexMusic.API.Requests.Library;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Get, "/users/{uid}/contexts")]
|
internal class YGetLibraryRecentlyListenedBuilder : YMusicRequestBuilder<YRecentlyListenedContext?, (IEnumerable<YPlayContextType> contextTypes, int trackCount, int contextCount)>
|
||||||
public class YGetLibraryRecentlyListenedBuilder : YRequestBuilder<YResponse<YRecentlyListenedContext>,
|
|
||||||
(IEnumerable<YPlayContextType> contextTypes, int trackCount, int contextCount)>
|
|
||||||
{
|
{
|
||||||
public YGetLibraryRecentlyListenedBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetLibraryRecentlyListenedBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
}
|
protected override string PathTemplate => "users/{uid}/contexts";
|
||||||
|
protected override Dictionary<string, string> GetSubstitutions((IEnumerable<YPlayContextType> contextTypes, int trackCount, int contextCount) tuple)
|
||||||
|
=> new() { { "uid", Api.Storage.User.Uid } };
|
||||||
protected override NameValueCollection GetQueryParams((IEnumerable<YPlayContextType> contextTypes, int trackCount, int contextCount) tuple)
|
protected override NameValueCollection GetQueryParams((IEnumerable<YPlayContextType> contextTypes, int trackCount, int contextCount) tuple)
|
||||||
{
|
=> new()
|
||||||
return new NameValueCollection {
|
{
|
||||||
{ "trackCount", tuple.trackCount.ToString() },
|
{ "trackCount", tuple.trackCount.ToString() },
|
||||||
{ "contextCount", tuple.contextCount.ToString() },
|
{ "contextCount", tuple.contextCount.ToString() },
|
||||||
{ "types", string.Join(",", tuple.contextTypes.Select(x => x.ToString().ToLowerInvariant())) }
|
{ "types", string.Join(",", tuple.contextTypes.Select(x => x.ToString().ToLowerInvariant())) }
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
protected override Dictionary<string, string> GetSubstitutions((IEnumerable<YPlayContextType> contextTypes, int trackCount, int contextCount) tuple)
|
|
||||||
{
|
|
||||||
return new Dictionary<string, string> {
|
|
||||||
{ "uid", storage.User.Uid }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,18 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Library;
|
using YandexMusic.API.Models.Library;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Library;
|
namespace YandexMusic.API.Requests.Library;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Get, "users/{uid}/{type}/{section}")]
|
internal class YGetLibrarySectionBuilder<T> : YMusicRequestBuilder<T?, (YLibrarySection section, YLibrarySectionType type)>
|
||||||
public class YGetLibrarySectionBuilder<T> : YRequestBuilder<YResponse<T>, (YLibrarySection section, YLibrarySectionType type)>
|
|
||||||
{
|
{
|
||||||
public YGetLibrarySectionBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetLibrarySectionBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
}
|
protected override string PathTemplate => "users/{uid}/{type}/{section}";
|
||||||
|
|
||||||
protected override Dictionary<string, string> GetSubstitutions((YLibrarySection section, YLibrarySectionType type) tuple)
|
protected override Dictionary<string, string> GetSubstitutions((YLibrarySection section, YLibrarySectionType type) tuple)
|
||||||
{
|
=> new()
|
||||||
return new Dictionary<string, string> {
|
{
|
||||||
{ "uid", storage.User.Uid },
|
{ "uid", Api.Storage.User.Uid },
|
||||||
{ "type", tuple.type.ToString().ToLower() },
|
{ "type", tuple.type.ToString().ToLower() },
|
||||||
{ "section", tuple.section.ToString().ToLower() },
|
{ "section", tuple.section.ToString().ToLower() }
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,33 +1,20 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Library;
|
using YandexMusic.API.Models.Library;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Library;
|
namespace YandexMusic.API.Requests.Library;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Post, "users/{uid}/{type}/{section}/add-multiple")]
|
internal class YLibraryAddBuilder<T> : YMusicRequestBuilder<T?, (string id, YLibrarySection section, YLibrarySectionType type)>
|
||||||
public class YLibraryAddBuilder<T> : YRequestBuilder<YResponse<T>, (string id, YLibrarySection section, YLibrarySectionType type)>
|
|
||||||
{
|
{
|
||||||
public YLibraryAddBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YLibraryAddBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string PathTemplate => "users/{uid}/{type}/{section}/add-multiple";
|
||||||
|
|
||||||
protected override Dictionary<string, string> GetSubstitutions((string id, YLibrarySection section, YLibrarySectionType type) tuple)
|
protected override Dictionary<string, string> GetSubstitutions((string id, YLibrarySection section, YLibrarySectionType type) tuple)
|
||||||
{
|
=> new()
|
||||||
return new Dictionary<string, string> {
|
{
|
||||||
{ "uid", storage.User.Uid },
|
{ "uid", Api.Storage.User.Uid },
|
||||||
{ "type", tuple.type.ToString().ToLower() },
|
{ "type", tuple.type.ToString().ToLower() },
|
||||||
{ "section", tuple.section.ToString().ToLower() },
|
{ "section", tuple.section.ToString().ToLower() }
|
||||||
};
|
};
|
||||||
}
|
protected override HttpContent? GetContent((string id, YLibrarySection section, YLibrarySectionType type) tuple)
|
||||||
|
=> new FormUrlEncodedContent(new Dictionary<string, string> { { $"{tuple.section.ToString().ToLower().TrimEnd('s')}-ids", tuple.id } });
|
||||||
protected override HttpContent GetContent((string id, YLibrarySection section, YLibrarySectionType type) tuple)
|
|
||||||
{
|
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
|
||||||
{ $"{tuple.section.ToString().ToLower().TrimEnd('s')}-ids", tuple.id }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,33 +1,20 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Library;
|
using YandexMusic.API.Models.Library;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Library;
|
namespace YandexMusic.API.Requests.Library;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Post, "users/{uid}/{type}/{section}/remove")]
|
internal class YLibraryRemoveBuilder<T> : YMusicRequestBuilder<T?, (string id, YLibrarySection section, YLibrarySectionType type)>
|
||||||
public class YLibraryRemoveBuilder<T> : YRequestBuilder<YResponse<T>, (string id, YLibrarySection section, YLibrarySectionType type)>
|
|
||||||
{
|
{
|
||||||
public YLibraryRemoveBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YLibraryRemoveBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string PathTemplate => "users/{uid}/{type}/{section}/remove";
|
||||||
|
|
||||||
protected override Dictionary<string, string> GetSubstitutions((string id, YLibrarySection section, YLibrarySectionType type) tuple)
|
protected override Dictionary<string, string> GetSubstitutions((string id, YLibrarySection section, YLibrarySectionType type) tuple)
|
||||||
{
|
=> new()
|
||||||
return new Dictionary<string, string> {
|
{
|
||||||
{ "uid", storage.User.Uid },
|
{ "uid", Api.Storage.User.Uid },
|
||||||
{ "type", tuple.type.ToString().ToLower() },
|
{ "type", tuple.type.ToString().ToLower() },
|
||||||
{ "section", tuple.section.ToString().ToLower() },
|
{ "section", tuple.section.ToString().ToLower() }
|
||||||
};
|
};
|
||||||
}
|
protected override HttpContent? GetContent((string id, YLibrarySection section, YLibrarySectionType type) tuple)
|
||||||
|
=> new FormUrlEncodedContent(new Dictionary<string, string> { { $"{tuple.section.ToString().ToLower().TrimEnd('s')}-ids", tuple.id } });
|
||||||
protected override HttpContent GetContent((string id, YLibrarySection section, YLibrarySectionType type) tuple)
|
|
||||||
{
|
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
|
||||||
{ $"{tuple.section.ToString().ToLower().TrimEnd('s')}-ids", tuple.id }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Pins;
|
using YandexMusic.API.Models.Pins;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Pins;
|
namespace YandexMusic.API.Requests.Pins;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Get, "pins")]
|
internal class YGetPinsBuilder : YMusicRequestBuilder<YPins?, object>
|
||||||
public class YGetPinsBuilder : YRequestBuilder<YResponse<YPins>, object>
|
|
||||||
{
|
{
|
||||||
public YGetPinsBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetPinsBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
}
|
protected override string PathTemplate => "pins";
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,13 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Playlist;
|
using YandexMusic.API.Models.Playlist;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Playlist;
|
namespace YandexMusic.API.Requests.Playlist;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Get, "users/{user}/playlists/{kind}")]
|
internal class YGetPlaylistBuilder : YMusicRequestBuilder<YPlaylist?, (string user, string kind)>
|
||||||
public class YGetPlaylistBuilder : YRequestBuilder<YResponse<YPlaylist>, (string user, string kind)>
|
|
||||||
{
|
{
|
||||||
public YGetPlaylistBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetPlaylistBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
}
|
protected override string PathTemplate => "users/{user}/playlists/{kind}";
|
||||||
|
|
||||||
protected override Dictionary<string, string> GetSubstitutions((string user, string kind) tuple)
|
protected override Dictionary<string, string> GetSubstitutions((string user, string kind) tuple)
|
||||||
{
|
=> new() { { "user", tuple.user }, { "kind", tuple.kind } };
|
||||||
return new Dictionary<string, string> {
|
|
||||||
{ "user", tuple.user },
|
|
||||||
{ "kind", tuple.kind },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,13 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Playlist;
|
using YandexMusic.API.Models.Playlist;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Playlist;
|
namespace YandexMusic.API.Requests.Playlist;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Get, "playlist/{uuid}")]
|
internal class YGetPlaylistByUuidBuilder : YMusicRequestBuilder<YPlaylist?, string>
|
||||||
public class YGetPlaylistByUuidBuilder : YRequestBuilder<YResponse<YPlaylist>, string>
|
|
||||||
{
|
{
|
||||||
public YGetPlaylistByUuidBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetPlaylistByUuidBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
}
|
protected override string PathTemplate => "playlist/{uuid}";
|
||||||
|
|
||||||
protected override Dictionary<string, string> GetSubstitutions(string uuid)
|
protected override Dictionary<string, string> GetSubstitutions(string uuid)
|
||||||
{
|
=> new() { { "uuid", uuid } };
|
||||||
return new Dictionary<string, string> {
|
|
||||||
{ "uuid", uuid },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,13 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Playlist;
|
using YandexMusic.API.Models.Playlist;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Playlist;
|
namespace YandexMusic.API.Requests.Playlist;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Get, "users/{uid}/playlists/list")]
|
internal class YGetPlaylistFavoritesBuilder : YMusicRequestBuilder<List<YPlaylist>?, object>
|
||||||
public class YGetPlaylistFavoritesBuilder : YRequestBuilder<YResponse<List<YPlaylist>>, object>
|
|
||||||
{
|
{
|
||||||
public YGetPlaylistFavoritesBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetPlaylistFavoritesBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Get;
|
||||||
}
|
protected override string PathTemplate => "users/{uid}/playlists/list";
|
||||||
|
protected override Dictionary<string, string> GetSubstitutions(object _)
|
||||||
protected override Dictionary<string, string> GetSubstitutions(object tuple)
|
=> new() { { "uid", Api.Storage.User.Uid } };
|
||||||
{
|
|
||||||
return new Dictionary<string, string> {
|
|
||||||
{ "uid", storage.User.Uid }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,16 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Playlist;
|
using YandexMusic.API.Models.Playlist;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Playlist;
|
namespace YandexMusic.API.Requests.Playlist;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Post, "playlists/list")]
|
internal class YGetPlaylistsBuilder : YMusicRequestBuilder<List<YPlaylist>?, IEnumerable<(string User, string Kind)>>
|
||||||
public class YGetPlaylistsBuilder : YRequestBuilder<YResponse<List<YPlaylist>>, IEnumerable<(string User, string Kind)>>
|
|
||||||
{
|
{
|
||||||
public YGetPlaylistsBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YGetPlaylistsBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string PathTemplate => "playlists/list";
|
||||||
|
protected override HttpContent? GetContent(IEnumerable<(string User, string Kind)> playlistIds)
|
||||||
protected override HttpContent GetContent(IEnumerable<(string User, string Kind)> playlistIds)
|
=> new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
|
||||||
{ "playlist-Ids", string.Join(",", playlistIds.Select(t => $"{t.User}:{t.Kind}")) }
|
{ "playlist-Ids", string.Join(",", playlistIds.Select(t => $"{t.User}:{t.Kind}")) }
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,34 +1,20 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Playlist;
|
using YandexMusic.API.Models.Playlist;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Playlist;
|
namespace YandexMusic.API.Requests.Playlist;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Post, "users/{uid}/playlists/{kind}/change")]
|
internal class YPlaylistChangeBuilder : YMusicRequestBuilder<YPlaylist?, (YPlaylist playlist, IEnumerable<YPlaylistChange> changes)>
|
||||||
public class YPlaylistChangeBuilder : YRequestBuilder<YResponse<YPlaylist>, (YPlaylist playlist, IEnumerable<YPlaylistChange> changes)>
|
|
||||||
{
|
{
|
||||||
public YPlaylistChangeBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YPlaylistChangeBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string PathTemplate => "users/{uid}/playlists/{kind}/change";
|
||||||
|
|
||||||
protected override Dictionary<string, string> GetSubstitutions((YPlaylist playlist, IEnumerable<YPlaylistChange> changes) tuple)
|
protected override Dictionary<string, string> GetSubstitutions((YPlaylist playlist, IEnumerable<YPlaylistChange> changes) tuple)
|
||||||
{
|
=> new() { { "uid", Api.Storage.User.Uid }, { "kind", tuple.playlist.Kind } };
|
||||||
return new Dictionary<string, string> {
|
protected override HttpContent? GetContent((YPlaylist playlist, IEnumerable<YPlaylistChange> changes) tuple)
|
||||||
{ "uid", storage.User.Uid },
|
=> new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
{ "kind", tuple.playlist.Kind }
|
{
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HttpContent GetContent((YPlaylist playlist, IEnumerable<YPlaylistChange> changes) tuple)
|
|
||||||
{
|
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
|
||||||
{ "kind", tuple.playlist.Kind },
|
{ "kind", tuple.playlist.Kind },
|
||||||
{ "revision", tuple.playlist.Revision.ToString() },
|
{ "revision", tuple.playlist.Revision.ToString() },
|
||||||
{ "diff", SerializeJson(tuple.changes) }
|
{ "diff", SerializeJson(tuple.changes) }
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,15 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Playlist;
|
using YandexMusic.API.Models.Playlist;
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Playlist;
|
namespace YandexMusic.API.Requests.Playlist;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Post, "users/{uid}/playlists/create")]
|
internal class YPlaylistCreateBuilder : YMusicRequestBuilder<YPlaylist?, string>
|
||||||
public class YPlaylistCreateBuilder : YRequestBuilder<YResponse<YPlaylist>, string>
|
|
||||||
{
|
{
|
||||||
public YPlaylistCreateBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YPlaylistCreateBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string PathTemplate => "users/{uid}/playlists/create";
|
||||||
|
|
||||||
protected override Dictionary<string, string> GetSubstitutions(string name)
|
protected override Dictionary<string, string> GetSubstitutions(string name)
|
||||||
{
|
=> new() { { "uid", Api.Storage.User.Uid } };
|
||||||
return new Dictionary<string, string> {
|
protected override HttpContent? GetContent(string name)
|
||||||
{ "uid", storage.User.Uid }
|
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "title", name }, { "visibility", "public" } });
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HttpContent GetContent(string name)
|
|
||||||
{
|
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
|
||||||
{ "title", name },
|
|
||||||
{ "visibility", "public" }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,12 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Models.Common;
|
|
||||||
using YandexMusic.API.Models.Playlist;
|
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Playlist;
|
namespace YandexMusic.API.Requests.Playlist;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Post, "users/{uid}/playlists/{kind}/name")]
|
internal class YPlaylistRemoveBuilder : YMusicRequestBuilder<HttpResponseMessage, string>
|
||||||
public class YPlaylistRenameBuilder : YRequestBuilder<YResponse<YPlaylist>, (string kind, string name)>
|
|
||||||
{
|
{
|
||||||
public YPlaylistRenameBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YPlaylistRemoveBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string PathTemplate => "users/{uid}/playlists/{kind}/delete";
|
||||||
|
protected override Dictionary<string, string> GetSubstitutions(string kind)
|
||||||
protected override Dictionary<string, string> GetSubstitutions((string kind, string name) tuple)
|
=> new() { { "uid", Api.Storage.User.Uid }, { "kind", kind } };
|
||||||
{
|
|
||||||
return new Dictionary<string, string> {
|
|
||||||
{ "uid", storage.User.Uid },
|
|
||||||
{ "kind", tuple.kind }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HttpContent GetContent((string kind, string name) tuple)
|
|
||||||
{
|
|
||||||
return new FormUrlEncodedContent(new Dictionary<string, string> {
|
|
||||||
{ "value", tuple.name }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,15 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using YandexMusic.API.Models.Playlist;
|
||||||
using YandexMusic.API.Common;
|
|
||||||
using YandexMusic.API.Requests.Common;
|
|
||||||
using YandexMusic.API.Requests.Common.Attributes;
|
|
||||||
|
|
||||||
namespace YandexMusic.API.Requests.Playlist;
|
namespace YandexMusic.API.Requests.Playlist;
|
||||||
|
|
||||||
[YApiRequest(WebRequestMethods.Http.Post, "users/{uid}/playlists/{kind}/delete")]
|
internal class YPlaylistRenameBuilder : YMusicRequestBuilder<YPlaylist?, (string kind, string name)>
|
||||||
public class YPlaylistRemoveBuilder : YRequestBuilder<HttpResponseMessage, string>
|
|
||||||
{
|
{
|
||||||
public YPlaylistRemoveBuilder(YandexMusicApi yandex, AuthStorage auth) : base(yandex, auth)
|
public YPlaylistRenameBuilder(YandexMusicApi api) : base(api) { }
|
||||||
{
|
protected override string Method => WebRequestMethods.Http.Post;
|
||||||
}
|
protected override string PathTemplate => "users/{uid}/playlists/{kind}/name";
|
||||||
|
protected override Dictionary<string, string> GetSubstitutions((string kind, string name) tuple)
|
||||||
protected override Dictionary<string, string> GetSubstitutions(string kind)
|
=> new() { { "uid", Api.Storage.User.Uid }, { "kind", tuple.kind } };
|
||||||
{
|
protected override HttpContent? GetContent((string kind, string name) tuple)
|
||||||
return new Dictionary<string, string> {
|
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "value", tuple.name } });
|
||||||
{ "uid", storage.User.Uid },
|
|
||||||
{ "kind", kind }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user