6 Commits

Author SHA1 Message Date
526353d679 Переделано воспроизведение аудио
All checks were successful
Release / pack-and-publish (release) Successful in 36s
2026-04-21 11:14:36 +03:00
eb1eba0162 Полный рефакторинг api. Вынесено отдельно api passport 2026-04-20 23:30:01 +03:00
FrigaT
34261d02a9 Авторизация через паспорт
All checks were successful
Release / pack-and-publish (release) Successful in 31s
2026-04-20 15:58:27 +03:00
FrigaT
5f761d4fe8 Добавлена авторизация через паспорт 2026-04-20 15:56:38 +03:00
FrigaT
b6f78da9c8 dispose
All checks were successful
Release / pack-and-publish (release) Successful in 37s
2026-04-20 14:47:43 +03:00
FrigaT
0bbaac5689 Переделан способ авторизации по qr
All checks were successful
Release / pack-and-publish (release) Successful in 1m5s
2026-04-20 14:31:47 +03:00
72 changed files with 1320 additions and 673 deletions

View File

@@ -0,0 +1,111 @@
using System.Security.Authentication;
using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Account;
namespace YandexMusic.API;
/// <summary>API для работы с пользователем и авторизации.</summary>
public class YAuthAPI : YCommonAPI
{
public YAuthAPI(YandexMusicApi api) : base(api) { }
/// <summary>Авторизация по готовому музыкальному токену (OAuth).</summary>
public async Task AuthorizeAsync(string musicToken)
{
if (string.IsNullOrEmpty(musicToken))
throw new ArgumentException("Токен не может быть пустым", nameof(musicToken));
Api.Storage.Token = musicToken;
var authInfo = await new YGetAuthInfoBuilder(Api).ExecuteAsync(null!);
if (authInfo?.Account?.Uid == null)
throw new Exception("Пользователь не авторизован");
Api.Storage.SetAuthorized(authInfo.Account, musicToken);
}
/// <summary>Авторизация по паспортному токену (полученному, например, после QR-входа).</summary>
public async Task AuthorizeByPassportTokenAsync(string passportToken)
{
if (string.IsNullOrEmpty(passportToken))
throw new ArgumentException("Паспортный токен не может быть пустым", nameof(passportToken));
var musicToken = await Api.Passport.GetMusicTokenByPassportTokenAsync(passportToken);
if (musicToken?.AccessToken == null)
throw new Exception("Не удалось обменять паспортный токен на музыкальный");
await AuthorizeAsync(musicToken.AccessToken);
}
/// <summary>Получение информации о текущем аккаунте.</summary>
public Task<YAccountResult?> GetUserAuthAsync()
=> new YGetAuthInfoBuilder(Api).ExecuteAsync(null!);
/// <summary>Создание сессии авторизации (получение доступных методов входа).</summary>
public async Task<YAuthTypes?> CreateAuthSessionAsync(string userName)
{
if (!await Api.Passport.GetCsrfTokenAsync()) // вместо InitSessionAsync
throw new Exception("Не удалось инициализировать сессию");
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>Получение капчи.</summary>
public Task<YAuthCaptcha?> GetCaptchaAsync()
{
if (Api.Storage.AuthToken == null)
throw new AuthenticationException("Выполните CreateAuthSessionAsync перед использованием");
return new YGetAuthCaptchaBuilder(Api).ExecuteAsync(null!);
}
/// <summary>Авторизация по капче.</summary>
public Task<YAuthBase?> AuthorizeByCaptchaAsync(string captchaValue)
{
if (Api.Storage.AuthToken == null)
throw new AuthenticationException("Выполните CreateAuthSessionAsync перед использованием");
return new YGetAuthLoginCaptchaBuilder(Api).ExecuteAsync(captchaValue);
}
/// <summary>Отправить письмо для авторизации.</summary>
public Task<YAuthLetter?> GetAuthLetterAsync()
=> new YGetAuthLetterBuilder(Api).ExecuteAsync(null!);
/// <summary>Подтверждение входа по письму и получение музыкального токена.</summary>
public async Task<bool> AuthorizeByLetterAsync()
{
var status = await new YGetAuthLoginLetterBuilder(Api).ExecuteAsync(null!);
if (status?.Status != YAuthStatus.Ok || !status.MagicLinkConfirmed)
throw new Exception("Письмо не подтверждено");
var musicToken = await Api.Passport.GetMusicTokenByCookiesAsync();
if (musicToken?.AccessToken == null)
throw new Exception("Не удалось получить музыкальный токен после подтверждения письма");
await AuthorizeAsync(musicToken.AccessToken);
return true;
}
/// <summary>Авторизация по паролю приложения Яндекс.</summary>
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)
throw new AuthenticationException("Ошибка авторизации по паролю");
var musicToken = await Api.Passport.GetMusicTokenByCookiesAsync();
if (musicToken?.AccessToken == null)
throw new Exception("Не удалось получить музыкальный токен после ввода пароля");
await AuthorizeAsync(musicToken.AccessToken);
return result;
}
/// <summary>Получение информации о пользователе через логин.</summary>
public Task<YLoginInfo?> GetLoginInfoAsync()
=> new YGetLoginInfoBuilder(Api).ExecuteAsync(null!);
}

View File

@@ -0,0 +1,230 @@
using System.Security.Authentication;
using System.Text.RegularExpressions;
using YandexMusic.API.Models.Account;
using YandexMusic.API.Models.Passport;
using YandexMusic.API.Requests.Account;
using YandexMusic.API.Requests.Passport;
namespace YandexMusic.API;
/// <summary>API для работы с яндекс паспортом</summary>
public class YPassportAPI : YCommonAPI
{
public YPassportAPI(YandexMusicApi api) : base(api) { }
public async Task<YAccessToken?> GetMusicTokenByCookiesAsync()
{
if (string.IsNullOrEmpty(Api.Storage.AuthToken.TrackId))
{
if (!await GetCsrfTokenAsync())
throw new Exception("Не удалось инициализировать сессию");
await CreateTrackAsync(); // ваш приватный метод создания track_id
}
return await new YGetAuthCookiesBuilder(Api).ExecuteAsync(null!);
}
public async Task<YAccessToken?> GetMusicTokenByPassportTokenAsync(string passportToken)
=> await GetAccessTokenAsync(passportToken);
public async Task<string?> GetAuthQRLinkAsync()
{
if (!await GetCsrfTokenAsync())
throw new Exception("Не удалось инициализировать сессию");
await CreateTrackAsync();
return $"https://passport.yandex.ru/auth/magic/code/?track_id={Api.Storage.AuthToken.TrackId}";
}
public async Task<YAuthQRStatus?> CheckQRStatusAsync()
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
var status = await new YGetQrStatus(Api).ExecuteAsync(null!);
if (!string.IsNullOrWhiteSpace(status?.TrackId))
{
Api.Storage.AuthToken.SessionTrackId = status.TrackId;
}
return status;
}
public async Task<YAuthQrSession?> AuthorizeByQRAsync()
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
if (string.IsNullOrWhiteSpace(Api.Storage.AuthToken.SessionTrackId))
throw new Exception("Токен сессии не инициализирован");
var status = await new YGetAuthLoginQRBuilder(Api).ExecuteAsync(null!);
if (status != null && status.DefaultUid != 0 && await LoginByCookiesAsync())
return status;
throw new AuthenticationException("Ошибка авторизации по QR");
}
/// <summary>Многоступенчатая авторизация: начало (передача логина).</summary>
public async Task<YMultistepStart?> MultistepStartAsync(string login)
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована. Вызовите GetAuthQRLinkAsync или CreateTrackAsync");
return await new YMultistepStartBuilder(Api).ExecuteAsync(login);
}
/// <summary>Многоступенчатая авторизация: ввод пароля.</summary>
public async Task<YPassportUser?> MultistepPasswordAsync(string password)
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YMultiStepPasswordBuilder(Api).ExecuteAsync(password);
}
/// <summary>Авторизация с помощью RFC OTP.</summary>
public async Task<YPassportUser?> RfcOtpPasswordAsync(string rfcOtp)
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YRfcOtpBuilder(Api).ExecuteAsync(rfcOtp);
}
/// <summary>Создание сессии пользователя.</summary>
public async Task<YPassportSession?> CreateUserSessionAsync()
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YGetSessionBuilder(Api).ExecuteAsync(null!);
}
/// <summary>Проверка состояния сессии.</summary>
public async Task<YPassportSessionStatus?> GetSessionStateAsync()
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YCheckSessionBuilder(Api).ExecuteAsync(null!);
}
/// <summary>Проверка номера телефона (валидация).</summary>
public async Task<YValidatePhoneNumberResult?> ValidatePhoneNumberAsync(string phone)
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YValidatePhoneNumberBuilder(Api).ExecuteAsync(phone);
}
/// <summary>Проверка доступности номера.</summary>
public async Task<YCheckAvailabilityResult?> CheckPhoneAvailabilityAsync(string phone)
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YCheckPhoneAvailabilityBuilder(Api).ExecuteAsync(phone);
}
/// <summary>Запрос на отправку push-уведомления.</summary>
public async Task<YSendPushResult?> SuggestSendPushAsync(string phone)
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YSendPushBuilder(Api).ExecuteAsync(phone);
}
/// <summary>Проверка push-кода.</summary>
public async Task CheckPushCodeAsync(string code)
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
await new YCheckPushCodeBuilder(Api).ExecuteAsync(code);
}
/// <summary>Проверка на "сквоттера" (захват номера).</summary>
public async Task<YValidateSquatter?> ValidateSquatterAsync(string phone)
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YValidateSquatterBuilder(Api).ExecuteAsync(phone);
}
/// <summary>Получение списка аккаунтов по номеру телефона.</summary>
public async Task<YSuggestByPhoneResult?> SuggestByPhoneAsync()
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
return await new YSuggestByPhoneBuilder(Api).ExecuteAsync(null!);
}
/// <summary>Вход по паролю (упрощённый, если не нужна многоступенчатость).</summary>
public async Task<YPassportUser?> LoginByPasswordAsync(string password)
{
// Сначала запускаем мультистеп (если track_id уже есть)
if (string.IsNullOrEmpty(Api.Storage.AuthToken.TrackId))
throw new Exception("TrackId не найден. Вызовите MultistepStartAsync сначала.");
return await MultistepPasswordAsync(password);
}
private async Task CreateTrackAsync()
{
if (!await GetCsrfTokenAsync())
throw new Exception("Невозможно инициализировать сессию входа.");
var track = await new YCreateTrackBuilder(Api).ExecuteAsync(null!);
if (string.IsNullOrWhiteSpace(track?.TrackId) || string.IsNullOrWhiteSpace(track?.CsrfToken))
throw new Exception("Не удалось создать трек паспорта.");
Api.Storage.AuthToken.TrackId = track.TrackId;
Api.Storage.AuthToken.CsfrToken = track.CsrfToken;
}
internal async Task<bool> GetCsrfTokenAsync()
{
using var response = await new YGetAuthMethodsBuilder(Api).ExecuteRawAsync(null!);
if (response == null || !response.IsSuccessStatusCode)
throw new HttpRequestException("Не удалось получить CSRF-токен");
var content = await response.Content.ReadAsStringAsync();
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})'");
if (!csrfMatch.Success || !processMatch.Success)
return false;
Api.Storage.HeaderToken = new YAuthToken
{
CsfrToken = csrfMatch.Groups[1].Value,
ProcessUuid = processMatch.Groups[1].Value
};
return true;
}
internal async Task<YAccessToken?> GetAccessTokenAsync(string passportToken)
{
if (string.IsNullOrEmpty(passportToken))
throw new Exception("Сессия не инициализована");
var token = await new YGetMusicTokenBuilder(Api).ExecuteAsync(passportToken);
if (token?.AccessToken != null)
Api.Storage.Token = token.AccessToken;
return token;
}
internal 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;
await GetAccessTokenAsync(accessToken.AccessToken);
return true;
}
}

View File

@@ -26,8 +26,8 @@ public class YTrackAPI : YCommonAPI
return $"https://{host}/get-{codec}/{sign}/{ts}{path}"; return $"https://{host}/get-{codec}/{sign}/{ts}{path}";
} }
public Task<YTrack?> GetAsync(string trackId) public async Task<YTrack?> GetAsync(string trackId)
=> GetAsync(trackId); => (await GetAsync([trackId]))?.FirstOrDefault();
public Task<List<YTrack>?> GetAsync(IEnumerable<string> trackIds) public Task<List<YTrack>?> GetAsync(IEnumerable<string> trackIds)
=> new YGetTracksBuilder(Api).ExecuteAsync(trackIds); => new YGetTracksBuilder(Api).ExecuteAsync(trackIds);

View File

@@ -1,159 +0,0 @@
using System.Security.Authentication;
using System.Text.RegularExpressions;
using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Account;
namespace YandexMusic.API;
/// <summary>API для работы с пользователем и авторизации.</summary>
public class YUserAPI : YCommonAPI
{
public YUserAPI(YandexMusicApi api) : base(api) { }
private async Task<bool> GetCsrfTokenAsync()
{
using var response = await new YGetAuthMethodsBuilder(Api).ExecuteRawAsync(null!);
if (response == null || !response.IsSuccessStatusCode)
throw new HttpRequestException("Не удалось получить CSRF-токен");
var content = await response.Content.ReadAsStringAsync();
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})'");
if (!csrfMatch.Success || !processMatch.Success)
return false;
Api.Storage.AuthToken = new YAuthToken
{
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;
var shortInfo = await new YGetShortAccountInfoBuilder(Api).ExecuteAsync(null!);
if (shortInfo?.Status != YAuthStatus.Ok || string.IsNullOrWhiteSpace(shortInfo.Uid))
throw new Exception("Не удалось подтвердить авторизацию");
return true;
}
public async Task AuthorizeAsync(string token)
{
if (string.IsNullOrEmpty(token))
throw new Exception("Токен не может быть пустым");
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);
}
public Task<YAccountResult?> GetUserAuthAsync()
=> new YGetAuthInfoBuilder(Api).ExecuteAsync(null!);
public async Task<YAuthTypes?> CreateAuthSessionAsync(string userName)
{
if (!await GetCsrfTokenAsync())
throw new Exception("Не удалось инициализировать сессию");
var result = await new YGetAuthLoginUserBuilder(Api).ExecuteAsync((Api.Storage.AuthToken.CsfrToken, userName));
if (result?.TrackId != null)
Api.Storage.AuthToken.TrackId = result.TrackId;
return result;
}
public async Task<string?> GetAuthQRLinkAsync()
{
if (!await GetCsrfTokenAsync())
throw new Exception("Не удалось инициализировать сессию");
var qr = await new YGetAuthQRBuilder(Api).ExecuteAsync(null!);
if (qr?.Status != YAuthStatus.Ok || string.IsNullOrEmpty(qr.TrackId))
return null;
Api.Storage.AuthToken = new YAuthToken
{
TrackId = qr.TrackId,
CsfrToken = qr.CsrfToken
};
return $"https://passport.yandex.ru/auth/magic/code/?track_id={qr.TrackId}";
}
public async Task<YAuthQRStatus?> AuthorizeByQRAsync()
{
if (Api.Storage.AuthToken == null)
throw new Exception("Сессия не инициализирована");
var status = await new YGetAuthLoginQRBuilder(Api).ExecuteAsync(null!);
if (status?.Status == YAuthStatus.Ok && await LoginByCookiesAsync())
return status;
throw new AuthenticationException("Ошибка авторизации по QR");
}
public Task<YAuthCaptcha?> GetCaptchaAsync()
{
if (Api.Storage.AuthToken == null)
throw new AuthenticationException("Выполните CreateAuthSessionAsync перед использованием");
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!);
}

View File

@@ -1,3 +1,4 @@
using System.Net;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Common; namespace YandexMusic.API.Common;
@@ -7,6 +8,15 @@ namespace YandexMusic.API.Common;
/// </summary> /// </summary>
public class AuthStorage public class AuthStorage
{ {
private CookieContainer _cookieContainer;
public AuthStorage(CookieContainer cookieContainer)
{
_cookieContainer = cookieContainer;
}
public CookieContainer CookieContainer => _cookieContainer;
/// <summary> /// <summary>
/// Флаг, указывающий, авторизован ли пользователь. /// Флаг, указывающий, авторизован ли пользователь.
/// </summary> /// </summary>
@@ -35,7 +45,17 @@ public class AuthStorage
/// <summary> /// <summary>
/// Внутренние данные авторизации (CSRF, track_id и т.д.). /// Внутренние данные авторизации (CSRF, track_id и т.д.).
/// </summary> /// </summary>
public YAuthToken AuthToken { get; set; } = new(); public YAuthToken? HeaderToken { get; set; } = new();
/// <summary>
/// Внутренние данные авторизации (CSRF, track_id и т.д.).
/// </summary>
public YAuthToken? AuthToken { get; set; } = new();
/// <summary>
/// Страна, используемая для авторизации (по умолчанию "ru"). Может влиять на язык интерфейса и доступные методы авторизации.
/// </summary>
public object Country { get; set; } = "ru";
/// <summary> /// <summary>
/// Устанавливает флаг авторизации и сохраняет информацию об аккаунте. /// Устанавливает флаг авторизации и сохраняет информацию об аккаунте.

View File

@@ -1,6 +1,6 @@
using YandexMusic.API.Models.Album; using YandexMusic.API.Models.Album;
namespace YandexMusic.API.Extensions.API; namespace YandexMusic.API;
/// <summary> /// <summary>
/// Методы-расширения для альбома. /// Методы-расширения для альбома.
@@ -15,7 +15,7 @@ public static class YAlbumExtensions
if (album.Volumes != null) if (album.Volumes != null)
return album; return album;
var result = await album.Context.API.Album.GetAsync(album.Id); var result = await album.Context.Api.Album.GetAsync(album.Id);
return result ?? album; return result ?? album;
} }
@@ -23,11 +23,11 @@ public static class YAlbumExtensions
/// Добавляет альбом в список лайкнутых. /// Добавляет альбом в список лайкнутых.
/// </summary> /// </summary>
public static async Task<string?> AddLikeAsync(this YAlbum album) public static async Task<string?> AddLikeAsync(this YAlbum album)
=> await album.Context.API.Library.AddAlbumLikeAsync(album); => await album.Context.Api.Library.AddAlbumLikeAsync(album);
/// <summary> /// <summary>
/// Удаляет альбом из списка лайкнутых. /// Удаляет альбом из списка лайкнутых.
/// </summary> /// </summary>
public static async Task<string?> RemoveLikeAsync(this YAlbum album) public static async Task<string?> RemoveLikeAsync(this YAlbum album)
=> await album.Context.API.Library.RemoveAlbumLikeAsync(album); => await album.Context.Api.Library.RemoveAlbumLikeAsync(album);
} }

View File

@@ -1,7 +1,7 @@
using YandexMusic.API.Models.Artist; using YandexMusic.API.Models.Artist;
using YandexMusic.API.Models.Track; using YandexMusic.API.Models.Track;
namespace YandexMusic.API.Extensions.API; namespace YandexMusic.API;
/// <summary> /// <summary>
/// Методы-расширения для исполнителя. /// Методы-расширения для исполнителя.
@@ -12,29 +12,29 @@ public static class YArtistExtensions
/// Получает расширенную информацию об исполнителе. /// Получает расширенную информацию об исполнителе.
/// </summary> /// </summary>
public static async Task<YArtistBriefInfo?> BriefInfoAsync(this YArtist artist) public static async Task<YArtistBriefInfo?> BriefInfoAsync(this YArtist artist)
=> await artist.Context.API.Artist.GetAsync(artist.Id); => await artist.Context.Api.Artist.GetAsync(artist.Id);
/// <summary> /// <summary>
/// Получает страницу треков исполнителя. /// Получает страницу треков исполнителя.
/// </summary> /// </summary>
public static async Task<YTracksPage?> GetTracksAsync(this YArtist artist, int page = 0, int pageSize = 20) 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); => await artist.Context.Api.Artist.GetTracksAsync(artist.Id, page, pageSize);
/// <summary> /// <summary>
/// Получает все треки исполнителя. /// Получает все треки исполнителя.
/// </summary> /// </summary>
public static async Task<List<YTrack>?> GetAllTracksAsync(this YArtist artist) public static async Task<List<YTrack>?> GetAllTracksAsync(this YArtist artist)
=> (await artist.Context.API.Artist.GetAllTracksAsync(artist.Id))?.Tracks; => (await artist.Context.Api.Artist.GetAllTracksAsync(artist.Id))?.Tracks;
/// <summary> /// <summary>
/// Добавляет исполнителя в список лайкнутых. /// Добавляет исполнителя в список лайкнутых.
/// </summary> /// </summary>
public static async Task<string?> AddLikeAsync(this YArtist artist) public static async Task<string?> AddLikeAsync(this YArtist artist)
=> await artist.Context.API.Library.AddArtistLikeAsync(artist); => await artist.Context.Api.Library.AddArtistLikeAsync(artist);
/// <summary> /// <summary>
/// Удаляет исполнителя из списка лайкнутых. /// Удаляет исполнителя из списка лайкнутых.
/// </summary> /// </summary>
public static async Task<string?> RemoveLikeAsync(this YArtist artist) public static async Task<string?> RemoveLikeAsync(this YArtist artist)
=> await artist.Context.API.Library.RemoveArtistLikeAsync(artist); => await artist.Context.Api.Library.RemoveArtistLikeAsync(artist);
} }

View File

@@ -1,7 +1,7 @@
using YandexMusic.API.Models.Playlist; using YandexMusic.API.Models.Playlist;
using YandexMusic.API.Models.Track; using YandexMusic.API.Models.Track;
namespace YandexMusic.API.Extensions.API; namespace YandexMusic.API;
/// <summary> /// <summary>
/// Методы-расширения для плейлиста. /// Методы-расширения для плейлиста.
@@ -18,44 +18,44 @@ public static class YPlaylistExtensions
{ {
if (playlist.Tracks != null) if (playlist.Tracks != null)
return playlist; return playlist;
return await playlist.Context.API.Playlist.GetAsync(playlist); return await playlist.Context.Api.Playlist.GetAsync(playlist);
} }
/// <summary> /// <summary>
/// Добавляет плейлист в список лайкнутых. /// Добавляет плейлист в список лайкнутых.
/// </summary> /// </summary>
public static async Task<string?> AddLikeAsync(this YPlaylist playlist) public static async Task<string?> AddLikeAsync(this YPlaylist playlist)
=> await playlist.Context.API.Library.AddPlaylistLikeAsync(playlist); => await playlist.Context.Api.Library.AddPlaylistLikeAsync(playlist);
/// <summary> /// <summary>
/// Удаляет плейлист из списка лайкнутых. /// Удаляет плейлист из списка лайкнутых.
/// </summary> /// </summary>
public static async Task<string?> RemoveLikeAsync(this YPlaylist playlist) public static async Task<string?> RemoveLikeAsync(this YPlaylist playlist)
=> await playlist.Context.API.Library.RemovePlaylistLikeAsync(playlist); => await playlist.Context.Api.Library.RemovePlaylistLikeAsync(playlist);
/// <summary> /// <summary>
/// Переименовывает плейлист (только для владельца). /// Переименовывает плейлист (только для владельца).
/// </summary> /// </summary>
public static async Task<YPlaylist?> RenameAsync(this YPlaylist playlist, string newName) public static async Task<YPlaylist?> RenameAsync(this YPlaylist playlist, string newName)
=> IsOwner(playlist) ? await playlist.Context.API.Playlist.RenameAsync(playlist, newName) : playlist; => IsOwner(playlist) ? await playlist.Context.Api.Playlist.RenameAsync(playlist, newName) : playlist;
/// <summary> /// <summary>
/// Удаляет плейлист (только для владельца). /// Удаляет плейлист (только для владельца).
/// </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); => IsOwner(playlist) && await playlist.Context.Api.Playlist.DeleteAsync(playlist);
/// <summary> /// <summary>
/// Вставляет треки в начало плейлиста (только для владельца). /// Вставляет треки в начало плейлиста (только для владельца).
/// </summary> /// </summary>
public static async Task<YPlaylist?> InsertTracksAsync(this YPlaylist playlist, params YTrack[] tracks) public static async Task<YPlaylist?> InsertTracksAsync(this YPlaylist playlist, params YTrack[] tracks)
=> IsOwner(playlist) ? await playlist.Context.API.Playlist.InsertTracksAsync(playlist, tracks) : playlist; => IsOwner(playlist) ? await playlist.Context.Api.Playlist.InsertTracksAsync(playlist, tracks) : playlist;
/// <summary> /// <summary>
/// Удаляет треки из плейлиста (только для владельца). /// Удаляет треки из плейлиста (только для владельца).
/// </summary> /// </summary>
public static async Task<YPlaylist?> RemoveTracksAsync(this YPlaylist playlist, params YTrack[] tracks) public static async Task<YPlaylist?> RemoveTracksAsync(this YPlaylist playlist, params YTrack[] tracks)
=> IsOwner(playlist) ? await playlist.Context.API.Playlist.DeleteTracksAsync(playlist, tracks) : playlist; => IsOwner(playlist) ? await playlist.Context.Api.Playlist.DeleteTracksAsync(playlist, tracks) : playlist;
/// <summary> /// <summary>
/// Загружает трек в плейлист (только для владельца). /// Загружает трек в плейлист (только для владельца).
@@ -63,7 +63,7 @@ public static class YPlaylistExtensions
public static async Task<bool> UploadTrackAsync(this YPlaylist playlist, string filePath, string fileName) public static async Task<bool> UploadTrackAsync(this YPlaylist playlist, string filePath, string fileName)
{ {
if (!IsOwner(playlist)) return false; if (!IsOwner(playlist)) return false;
var result = await playlist.Context.API.UserGeneratedContent.UploadTrackToPlaylistAsync(playlist, fileName, filePath); var result = await playlist.Context.Api.UserGeneratedContent.UploadTrackToPlaylistAsync(playlist, fileName, filePath);
return result == "CREATED"; return result == "CREATED";
} }
} }

View File

@@ -1,7 +1,7 @@
using YandexMusic.API.Models.Radio; using YandexMusic.API.Models.Radio;
using YandexMusic.API.Models.Track; using YandexMusic.API.Models.Track;
namespace YandexMusic.API.Extensions.API; namespace YandexMusic.API;
/// <summary> /// <summary>
/// Методы-расширения для радиостанции. /// Методы-расширения для радиостанции.
@@ -12,17 +12,17 @@ public static class YStationResultExtensions
/// Получает список треков для радиостанции. /// Получает список треков для радиостанции.
/// </summary> /// </summary>
public static async Task<List<YSequenceItem>?> GetTracksAsync(this YStation station, string prevTrackId = "") public static async Task<List<YSequenceItem>?> GetTracksAsync(this YStation station, string prevTrackId = "")
=> (await station.Context.API.Radio.GetStationTracksAsync(station, prevTrackId))?.Sequence; => (await station.Context.Api.Radio.GetStationTracksAsync(station, prevTrackId))?.Sequence;
/// <summary> /// <summary>
/// Устанавливает настройки станции. /// Устанавливает настройки станции.
/// </summary> /// </summary>
public static async Task<string?> SetSettings2Async(this YStation station, YStationSettings2 settings) public static async Task<string?> SetSettings2Async(this YStation station, YStationSettings2 settings)
=> await station.Context.API.Radio.SetStationSettings2Async(station, settings); => await station.Context.Api.Radio.SetStationSettings2Async(station, settings);
/// <summary> /// <summary>
/// Отправляет обратную связь о прослушивании. /// Отправляет обратную связь о прослушивании.
/// </summary> /// </summary>
public static Task<string?> SendFeedbackAsync(this YStation station, YStationFeedbackType type, YTrack? track = null, string batchId = "", double totalPlayedSeconds = 0) 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); => station.Context.Api.Radio.SendStationFeedbackAsync(station, type, track, batchId, totalPlayedSeconds);
} }

View File

@@ -1,6 +1,6 @@
using YandexMusic.API.Models.Track; using YandexMusic.API.Models.Track;
namespace YandexMusic.API.Extensions.API; namespace YandexMusic.API;
/// <summary> /// <summary>
/// Методы-расширения для трека. /// Методы-расширения для трека.
@@ -11,53 +11,53 @@ public static class YTrackExtensions
/// Получает прямую ссылку на скачивание трека. /// Получает прямую ссылку на скачивание трека.
/// </summary> /// </summary>
public static Task<string?> GetLinkAsync(this YTrack track) public static Task<string?> GetLinkAsync(this YTrack track)
=> track.Context.API.Track.GetFileLinkAsync(track); => track.Context.Api.Track.GetFileLinkAsync(track);
/// <summary> /// <summary>
/// Сохраняет трек в файл. /// Сохраняет трек в файл.
/// </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); => track.Context.Api.Track.ExtractToFileAsync(track, filePath);
/// <summary> /// <summary>
/// Добавляет трек в список лайкнутых. /// Добавляет трек в список лайкнутых.
/// </summary> /// </summary>
public static async Task<int?> AddLikeAsync(this YTrack track) public static async Task<int?> AddLikeAsync(this YTrack track)
=> await track.Context.API.Library.AddTrackLikeAsync(track); => await track.Context.Api.Library.AddTrackLikeAsync(track);
/// <summary> /// <summary>
/// Удаляет трек из списка лайкнутых. /// Удаляет трек из списка лайкнутых.
/// </summary> /// </summary>
public static async Task<int?> RemoveLikeAsync(this YTrack track) public static async Task<int?> RemoveLikeAsync(this YTrack track)
=> await track.Context.API.Library.RemoveTrackLikeAsync(track); => await track.Context.Api.Library.RemoveTrackLikeAsync(track);
/// <summary> /// <summary>
/// Добавляет трек в список дизлайкнутых. /// Добавляет трек в список дизлайкнутых.
/// </summary> /// </summary>
public static async Task<int?> AddDislikeAsync(this YTrack track) public static async Task<int?> AddDislikeAsync(this YTrack track)
=> await track.Context.API.Library.AddTrackDislikeAsync(track); => await track.Context.Api.Library.AddTrackDislikeAsync(track);
/// <summary> /// <summary>
/// Удаляет трек из списка дизлайкнутых. /// Удаляет трек из списка дизлайкнутых.
/// </summary> /// </summary>
public static async Task<int?> RemoveDislikeAsync(this YTrack track) public static async Task<int?> RemoveDislikeAsync(this YTrack track)
=> await track.Context.API.Library.RemoveTrackDislikeAsync(track); => await track.Context.Api.Library.RemoveTrackDislikeAsync(track);
/// <summary> /// <summary>
/// Отправляет информацию о воспроизведении трека. /// Отправляет информацию о воспроизведении трека.
/// </summary> /// </summary>
public static Task<string?> SendPlayTrackInfoAsync(this YTrack track, string from, bool fromCache = false, string playId = "", string playlistId = "", double totalPlayedSeconds = 0, double endPositionSeconds = 0) 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); => track.Context.Api.Track.SendPlayTrackInfoAsync(track, from, fromCache, playId, playlistId, totalPlayedSeconds, endPositionSeconds);
/// <summary> /// <summary>
/// Получает дополнительную информацию о треке. /// Получает дополнительную информацию о треке.
/// </summary> /// </summary>
public static async Task<YTrackSupplement?> SupplementAsync(this YTrack track) public static async Task<YTrackSupplement?> SupplementAsync(this YTrack track)
=> await track.Context.API.Track.GetSupplementAsync(track); => await track.Context.Api.Track.GetSupplementAsync(track);
/// <summary> /// <summary>
/// Получает похожие треки. /// Получает похожие треки.
/// </summary> /// </summary>
public static async Task<YTrackSimilar?> SimilarAsync(this YTrack track) public static async Task<YTrackSimilar?> SimilarAsync(this YTrack track)
=> await track.Context.API.Track.GetSimilarAsync(track); => await track.Context.Api.Track.GetSimilarAsync(track);
} }

View File

@@ -2,4 +2,4 @@
public class YAuthEmpty public class YAuthEmpty
{ {
} }

View File

@@ -0,0 +1,9 @@
using System.Runtime.Serialization;
namespace YandexMusic.API.Models.Account;
public enum YAuthQrState
{
[EnumMember(Value = "otp_auth_finished")]
OtpAuthFinished,
}

View File

@@ -1,16 +1,13 @@
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; }
[JsonPropertyName("process_uuid")] public string SessionTrackId { get; set; }
public string ProcessUuid { get; set; } public string ProcessUuid { get; set; }
public Dictionary<string, string> Cookie { get; set; } = new(); public Dictionary<string, string> Cookie { get; set; } = new();

View File

@@ -9,7 +9,7 @@ namespace YandexMusic.API.Models.Common;
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!;

View File

@@ -38,7 +38,7 @@ public class YExecutionContextConverter : JsonConverter<object>
var obj = JsonSerializer.Deserialize(ref reader, typeToConvert, innerOptions); var obj = JsonSerializer.Deserialize(ref reader, typeToConvert, innerOptions);
if (obj is YBaseModel baseModel) if (obj is YBaseModel baseModel)
{ {
baseModel.Context = new YExecutionContext { API = _api, Storage = _storage }; baseModel.Context = new YExecutionContext { Api = _api, Storage = _storage };
} }
return obj; return obj;
} }

View File

@@ -1,9 +1,16 @@
using System.Xml.Serialization;
namespace YandexMusic.API.Models.Common; namespace YandexMusic.API.Models.Common;
[XmlRoot("download-info")]
public class YStorageDownloadFile public class YStorageDownloadFile
{ {
[XmlElement("host")]
public string Host { get; set; } public string Host { get; set; }
[XmlElement("path")]
public string Path { get; set; } public string Path { get; set; }
[XmlElement("s")]
public string S { get; set; } public string S { get; set; }
[XmlElement("ts")]
public string Ts { get; set; } public string Ts { get; set; }
} }

View File

@@ -1,8 +1,9 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Models.Account; namespace YandexMusic.API.Models.Passport;
public class YAuthQR : YAuthBase public class YPassportTrack : YAuthBase
{ {
[JsonPropertyName("track_id")] [JsonPropertyName("track_id")]
public string TrackId { get; set; } public string TrackId { get; set; }

View File

@@ -1,12 +1,14 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Models.Account; namespace YandexMusic.API.Models.Passport;
public class YAuthQRStatus : YAuthBase public class YAuthQrSession
{ {
[JsonPropertyName("default_uid")] [JsonPropertyName("default_uid")]
public int DefaultUid { get; set; } public int DefaultUid { get; set; }
[JsonPropertyName("retpath")]
public string RetPath { get; set; } public string RetPath { get; set; }
[JsonPropertyName("track_id")] [JsonPropertyName("track_id")]

View File

@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YAuthQRStatus
{
[JsonPropertyName("state")]
public string? State { get; set; } = null;
[JsonPropertyName("trackId")]
public string TrackId { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YCheckAvailabilityResult
{
[JsonPropertyName("antifraudScore")]
public string AntifraudScore { get; set; } = string.Empty;
[JsonPropertyName("hasAvailableAccounts")]
public bool HasAvailableAccounts { get; set; }
[JsonPropertyName("flnFlowRequired")]
public bool FlnFlowRequired { get; set; }
[JsonPropertyName("can_use_push")]
public bool CanUsePush { get; set; }
[JsonPropertyName("can_use_webauthn")]
public bool CanUseWebauthn { get; set; }
[JsonPropertyName("has_master")]
public bool HasMaster { get; set; }
[JsonPropertyName("is_session_mastered")]
public bool IsSessionMastered { get; set; }
[JsonPropertyName("does_master_have_free_slots")]
public bool DoesMasterHaveFreeSlots { get; set; }
[JsonPropertyName("allowed_registration_flows")]
public List<object> AllowedRegistrationFlows { get; set; } = new();
[JsonPropertyName("SuggestBy")]
public string SuggestBy { get; set; } = string.Empty;
[JsonPropertyName("master_info")]
public YMasterInfo? MasterInfo { get; set; }
}

View File

@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YMasterInfo
{
[JsonPropertyName("firstname")]
public string FirstName { get; set; } = string.Empty;
[JsonPropertyName("lastname")]
public string LastName { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,42 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YMultistepStart
{
[JsonPropertyName("track_id")]
public string TrackId { get; set; } = string.Empty;
[JsonPropertyName("can_authorize")]
public bool CanAuthorize { get; set; }
[JsonPropertyName("can_register")]
public bool CanRegister { get; set; }
[JsonPropertyName("is_rfc_2fa_enabled")]
public bool IsRfc2faEnabled { get; set; }
[JsonPropertyName("allowed_account_types")]
public List<string> AllowedAccountTypes { get; set; } = new();
[JsonPropertyName("location_id")]
public string LocationId { get; set; } = string.Empty;
[JsonPropertyName("primary_alias_type")]
public int PrimaryAliasType { get; set; }
[JsonPropertyName("auth_methods")]
public List<string> AuthMethods { get; set; } = new();
[JsonPropertyName("preferred_auth_method")]
public string PreferredAuthMethod { get; set; } = string.Empty;
[JsonPropertyName("csrf_token")]
public string CsrfToken { get; set; } = string.Empty;
[JsonPropertyName("error")]
public string? Error { get; set; }
[JsonPropertyName("errors")]
public List<string>? Errors { get; set; }
}

View File

@@ -0,0 +1,48 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YPassportAccount
{
[JsonPropertyName("avatar_url")]
public string AvatarUrl { get; set; } = string.Empty;
[JsonPropertyName("display_login")]
public string DisplayLogin { get; set; } = string.Empty;
[JsonPropertyName("display_name")]
public YPassportName? DisplayName { get; set; }
[JsonPropertyName("has_master")]
public bool HasMaster { get; set; }
[JsonPropertyName("has_plus")]
public bool HasPlus { get; set; }
[JsonPropertyName("has_secure_phone")]
public bool HasSecurePhone { get; set; }
[JsonPropertyName("is_2fa_enabled")]
public bool Is2faEnabled { get; set; }
[JsonPropertyName("is_rfc_2fa_enabled")]
public bool IsRfc2faEnabled { get; set; }
[JsonPropertyName("is_sms_2fa_enabled")]
public bool IsSms2faEnabled { get; set; }
[JsonPropertyName("is_workspace_user")]
public bool IsWorkspaceUser { get; set; }
[JsonPropertyName("is_yandexoid")]
public bool IsYandexoid { get; set; }
public bool Login { get; set; }
public YPassportPerson? Person { get; set; }
[JsonPropertyName("secure_phone_id")]
public int SecurePhoneId { get; set; }
public int Uid { get; set; }
}

View File

@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YPassportName
{
[JsonPropertyName("default_avatar")]
public string DefaultAvatar { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,11 @@
namespace YandexMusic.API.Models.Passport;
public class YPassportPerson
{
public string Birthday { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public int Gender { get; set; }
public string Language { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YPassportSession
{
[JsonPropertyName("track_id")]
public string TrackId { get; set; } = string.Empty;
[JsonPropertyName("default_uid")]
public string DefaultUid { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YPassportSessionStatus
{
[JsonPropertyName("session_is_correct")]
public bool SessionIsCorrect { get; set; }
[JsonPropertyName("session_has_users")]
public bool SessionHasUsers { get; set; }
}

View File

@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YPassportUser
{
[JsonPropertyName("track_id")]
public string TrackId { get; set; } = string.Empty;
[JsonPropertyName("state")]
public string State { get; set; } = string.Empty;
[JsonPropertyName("account")]
public YPassportAccount? Account { get; set; }
[JsonPropertyName("error")]
public string? Error { get; set; }
[JsonPropertyName("errors")]
public List<string>? Errors { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace YandexMusic.API.Models.Passport;
public class YPushApp
{
public string App { get; set; } = string.Empty;
public string Platform { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YSendPushResult
{
[JsonPropertyName("pushes_devices_list")]
public List<object> PushesDevicesList { get; set; } = new();
[JsonPropertyName("deny_resend_until")]
public int DenyResendUntil { get; set; }
[JsonPropertyName("is_push_silent")]
public bool IsPushSilent { get; set; }
[JsonPropertyName("apps_for_bright_push")]
public List<YPushApp> AppsForBrightPush { get; set; } = new();
}

View File

@@ -0,0 +1,54 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YSuggestAccount
{
[JsonPropertyName("allowed_auth_flows")]
public List<string> AllowedAuthFlows { get; set; } = new();
[JsonPropertyName("avatar_url")]
public string AvatarUrl { get; set; } = string.Empty;
[JsonPropertyName("default_avatar")]
public string DefaultAvatar { get; set; } = string.Empty;
[JsonPropertyName("display_name")]
public YPassportName? DisplayName { get; set; }
[JsonPropertyName("has_bank_card")]
public bool HasBankCard { get; set; }
[JsonPropertyName("has_family")]
public bool HasFamily { get; set; }
[JsonPropertyName("has_master")]
public bool HasMaster { get; set; }
[JsonPropertyName("has_plus")]
public bool HasPlus { get; set; }
[JsonPropertyName("is_communal")]
public bool IsCommunal { get; set; }
[JsonPropertyName("location_id")]
public string LocationId { get; set; } = string.Empty;
[JsonPropertyName("login")]
public string Login { get; set; } = string.Empty;
[JsonPropertyName("primary_alias_type")]
public int PrimaryAliasType { get; set; }
[JsonPropertyName("priority")]
public int Priority { get; set; }
[JsonPropertyName("uid")]
public long Uid { get; set; }
[JsonPropertyName("shields")]
public List<string> Shields { get; set; } = new();
[JsonPropertyName("require_additional_sms_to_login")]
public bool RequireAdditionalSmsToLogin { get; set; }
}

View File

@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YSuggestByPhoneResult
{
[JsonPropertyName("accounts")]
public List<YSuggestAccount> Accounts { get; set; } = new();
[JsonPropertyName("allowed_registration_flows")]
public List<string> AllowedRegistrationFlows { get; set; } = new();
[JsonPropertyName("uid_from_bb")]
public string UidFromBb { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Models.Passport;
public class YValidatePhoneNumberResult
{
[JsonPropertyName("phone_number")]
public YPhoneNumber? PhoneNumber { get; set; }
[JsonPropertyName("valid_for_flash_call")]
public bool ValidForFlashCall { get; set; }
[JsonPropertyName("location_id")]
public string LocationId { get; set; } = string.Empty;
[JsonPropertyName("valid_for_viber")]
public bool ValidForViber { get; set; }
[JsonPropertyName("valid_for_whatsapp")]
public bool ValidForWhatsapp { get; set; }
[JsonPropertyName("valid_for_telegram")]
public bool ValidForTelegram { get; set; }
[JsonPropertyName("valid_for_sms")]
public bool ValidForSms { get; set; }
[JsonPropertyName("track_id")]
public string TrackId { get; set; } = string.Empty;
[JsonPropertyName("error")]
public string? Error { get; set; }
[JsonPropertyName("errors")]
public List<string>? Errors { get; set; }
}

View File

@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace YandexMusic.API.Models.Passport;
public class YValidateSquatter
{
[JsonPropertyName("require_flow_with_fio")]
public bool RequireFlowWithFio { get; set; }
[JsonPropertyName("require_flow_with_auth_hint")]
public bool RequireFlowWithAuthHint { get; set; }
[JsonPropertyName("auth_hint_question_id")]
public string AuthHintQuestionId { get; set; } = string.Empty;
[JsonPropertyName("suggestBy")]
public string SuggestBy { get; set; } = string.Empty;
}

View File

@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetAuthAppPasswordBuilder : YAuthRequestBuilder<YAuthBase?, string> internal class YGetAuthAppPasswordBuilder : YPassportRequestBuilder<YAuthBase?, string>
{ {
public YGetAuthAppPasswordBuilder(YandexMusicApi api) : base(api) { } public YGetAuthAppPasswordBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;

View File

@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetAuthCaptchaBuilder : YAuthRequestBuilder<YAuthCaptcha?, object> internal class YGetAuthCaptchaBuilder : YPassportRequestBuilder<YAuthCaptcha?, object>
{ {
public YGetAuthCaptchaBuilder(YandexMusicApi api) : base(api) { } public YGetAuthCaptchaBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;

View File

@@ -1,14 +1,55 @@
using System.Net; using System.Net;
using System.Net.Http.Headers;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Common; using YandexMusic.API.Requests.Common;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetAuthCookiesBuilder : YAuthRequestBuilder<YAccessToken?, object> internal class YGetAuthCookiesBuilder : YPassportRequestBuilder<YAccessToken?, object>
{ {
public YGetAuthCookiesBuilder(YandexMusicApi api) : base(api) { } public YGetAuthCookiesBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post; 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 string PathTemplate => "1/bundle/oauth/token_by_sessionid";
protected override HttpContent? GetContent(object _) protected override HttpContent? GetContent(object _)
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "client_id", YConstants.XClientId }, { "client_secret", YConstants.XClientSecret } }); => new FormUrlEncodedContent(new Dictionary<string, string> { { "client_id", YConstants.XClientId }, { "client_secret", YConstants.XClientSecret } });
protected override void SetCustomHeaders(HttpRequestHeaders headers)
{
base.SetCustomHeaders(headers);
headers.Add("ya-client-host", "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")
};
var cookies = new List<string>();
foreach (var uri in uris)
{
var cookieCollection = container.GetCookies(uri);
foreach (Cookie cookie in cookieCollection)
{
cookies.Add($"{cookie.Name}={cookie.Value}");
}
}
var distinct = cookies
.Select(c => c.Split('=')[0])
.Distinct()
.Select(name => cookies.First(c => c.StartsWith(name + "=")))
.ToList();
return string.Join("; ", distinct);
}
} }

View File

@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetAuthLetterBuilder : YAuthRequestBuilder<YAuthLetter?, object> internal class YGetAuthLetterBuilder : YPassportRequestBuilder<YAuthLetter?, object>
{ {
public YGetAuthLetterBuilder(YandexMusicApi api) : base(api) { } public YGetAuthLetterBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;

View File

@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetAuthLoginCaptchaBuilder : YAuthRequestBuilder<YAuthBase?, string> internal class YGetAuthLoginCaptchaBuilder : YPassportRequestBuilder<YAuthBase?, string>
{ {
public YGetAuthLoginCaptchaBuilder(YandexMusicApi api) : base(api) { } public YGetAuthLoginCaptchaBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;

View File

@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetAuthLoginLetterBuilder : YAuthRequestBuilder<YAuthLetterStatus?, object> internal class YGetAuthLoginLetterBuilder : YPassportRequestBuilder<YAuthLetterStatus?, object>
{ {
public YGetAuthLoginLetterBuilder(YandexMusicApi api) : base(api) { } public YGetAuthLoginLetterBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;

View File

@@ -1,23 +0,0 @@
using System.Net;
using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account;
internal class YGetAuthLoginQRBuilder : YAuthRequestBuilder<YAuthQRStatus, string>
{
public YGetAuthLoginQRBuilder(YandexMusicApi yandex) : base(yandex)
{
}
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "auth/new/magic/status/";
protected override HttpContent GetContent(string tuple)
{
return new FormUrlEncodedContent(new Dictionary<string, string> {
{ "csrf_token", Api.Storage.AuthToken.CsfrToken },
{ "track_id", Api.Storage.AuthToken.TrackId }
});
}
}

View File

@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetAuthLoginUserBuilder : YAuthRequestBuilder<YAuthTypes?, (string token, string login)> internal class YGetAuthLoginUserBuilder : YPassportRequestBuilder<YAuthTypes?, (string token, string login)>
{ {
public YGetAuthLoginUserBuilder(YandexMusicApi api) : base(api) { } public YGetAuthLoginUserBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;

View File

@@ -1,19 +0,0 @@
using System.Net;
using System.Net.Http.Headers;
using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account;
internal class YGetAuthQRBuilder : YAuthRequestBuilder<YAuthQR?, object>
{
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.AuthToken.CsfrToken);
headers.Add("Process-Uuid", Api.Storage.AuthToken.ProcessUuid);
}
}

View File

@@ -3,7 +3,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetLoginInfoBuilder : YAuthRequestBuilder<YLoginInfo?, object> internal class YGetLoginInfoBuilder : YPassportRequestBuilder<YLoginInfo?, object>
{ {
public YGetLoginInfoBuilder(YandexMusicApi api) : base(api) { } public YGetLoginInfoBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Get; protected override string Method => WebRequestMethods.Http.Get;

View File

@@ -5,7 +5,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YGetShortAccountInfoBuilder : YAuthRequestBuilder<YShortAccountInfo?, object> internal class YGetShortAccountInfoBuilder : YPassportRequestBuilder<YShortAccountInfo?, object>
{ {
public YGetShortAccountInfoBuilder(YandexMusicApi api) : base(api) { } public YGetShortAccountInfoBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Get; protected override string Method => WebRequestMethods.Http.Get;

View File

@@ -4,7 +4,7 @@ using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Account;
internal class YPostAuthStats : YAuthRequestBuilder<YAuthEmpty?, object> internal class YPostAuthStats : YPassportRequestBuilder<YAuthEmpty?, object>
{ {
public YPostAuthStats(YandexMusicApi api) : base(api) { } public YPostAuthStats(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;
@@ -13,7 +13,7 @@ internal class YPostAuthStats : YAuthRequestBuilder<YAuthEmpty?, object>
=> new FormUrlEncodedContent(new Dictionary<string, string> { { "messageType", "CLIENT_READY" } }); => new FormUrlEncodedContent(new Dictionary<string, string> { { "messageType", "CLIENT_READY" } });
protected override void SetCustomHeaders(HttpRequestHeaders headers) protected override void SetCustomHeaders(HttpRequestHeaders headers)
{ {
headers.Add("X-Csrf-Token", Api.Storage.AuthToken.CsfrToken); headers.Add("X-Csrf-Token", Api.Storage.HeaderToken.CsfrToken);
headers.Add("Process-Uuid", Api.Storage.AuthToken.ProcessUuid); headers.Add("Process-Uuid", Api.Storage.HeaderToken.ProcessUuid);
} }
} }

View File

@@ -12,5 +12,8 @@ internal class YGetArtistTrackBuilder : YMusicRequestBuilder<YTracksPage?, (stri
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 } }; => new() { { "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() } }; => new() {
{ "page", tuple.page.ToString() },
{ "pageSize", tuple.pageSize.ToString() },
};
} }

View File

@@ -12,5 +12,6 @@ internal class YConstants
{ {
public const string MusicUrl = "https://api.music.yandex.net"; public const string MusicUrl = "https://api.music.yandex.net";
public const string PassportUrl = "https://passport.yandex.ru/"; public const string PassportUrl = "https://passport.yandex.ru/";
public const string MobilePassportUrl = "https://mobileproxy.passport.yandex.net";
} }
} }

View File

@@ -10,7 +10,17 @@ namespace YandexMusic.API.Requests.Common;
/// </summary> /// </summary>
internal abstract class YJsonRequestBuilder<TResponse, TParams> : YRequestBuilder<TParams> internal abstract class YJsonRequestBuilder<TResponse, TParams> : YRequestBuilder<TParams>
{ {
protected YJsonRequestBuilder(YandexMusicApi api) : base(api) { } private readonly JsonSerializerOptions _jsonOptions;
protected YJsonRequestBuilder(YandexMusicApi api) : base(api)
{
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};
}
protected virtual async Task<TResponse?> DeserializeAsync(HttpResponseMessage response) protected virtual async Task<TResponse?> DeserializeAsync(HttpResponseMessage response)
{ {
@@ -43,6 +53,8 @@ internal abstract class YJsonRequestBuilder<TResponse, TParams> : YRequestBuilde
} }
} }
protected string SerializeJson(object data) => JsonSerializer.Serialize(data, _jsonOptions);
/// <summary> /// <summary>
/// Выполняет запрос и возвращает десериализованный объект типа TResponse. /// Выполняет запрос и возвращает десериализованный объект типа TResponse.
/// </summary> /// </summary>
@@ -51,4 +63,4 @@ internal abstract class YJsonRequestBuilder<TResponse, TParams> : YRequestBuilde
using var response = await ExecuteRawAsync(parameters); using var response = await ExecuteRawAsync(parameters);
return await DeserializeAsync(response); return await DeserializeAsync(response);
} }
} }

View File

@@ -4,15 +4,20 @@ using YandexMusic.API.Requests.Common;
namespace YandexMusic.API.Requests; namespace YandexMusic.API.Requests;
/// <summary>Базовый класс для запросов к Passport (passport.yandex.ru).</summary> /// <summary>Базовый класс для запросов к Passport (passport.yandex.ru).</summary>
internal abstract class YAuthRequestBuilder<TResponse, TParams> : YJsonRequestBuilder<TResponse, TParams> internal abstract class YPassportRequestBuilder<TResponse, TParams> : YJsonRequestBuilder<TResponse, TParams>
{ {
protected override string BaseUrl => YConstants.Endpoints.PassportUrl; protected override string BaseUrl => YConstants.Endpoints.PassportUrl;
protected YAuthRequestBuilder(YandexMusicApi api) : base(api) { } protected YPassportRequestBuilder(YandexMusicApi api) : base(api) { }
protected override void SetCustomHeaders(HttpRequestHeaders headers) protected override void SetCustomHeaders(HttpRequestHeaders headers)
{ {
base.SetCustomHeaders(headers); base.SetCustomHeaders(headers);
headers.Add("X-Requested-With", "XMLHttpRequest"); headers.Add("X-Requested-With", "XMLHttpRequest");
if (Api.Storage.HeaderToken != null)
{
headers.Add("X-Csrf-Token", Api.Storage.HeaderToken.CsfrToken);
headers.Add("Process-Uuid", Api.Storage.HeaderToken.ProcessUuid);
}
} }
} }

View File

@@ -2,8 +2,6 @@
using System.Net; using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Web; using System.Web;
using YandexMusic.API.Common; using YandexMusic.API.Common;
@@ -23,7 +21,8 @@ internal abstract class YRequestBuilder<TParams>
/// <summary>Шаблон пути (может содержать плейсхолдеры вида {id}).</summary> /// <summary>Шаблон пути (может содержать плейсхолдеры вида {id}).</summary>
protected abstract string PathTemplate { get; } protected abstract string PathTemplate { get; }
private readonly JsonSerializerOptions _jsonOptions; /// <summary>Определяет, нужно ли добавлять заголовок Authorization для этого запроса.</summary>
protected virtual bool ShouldAddAuthorization => true;
/// <summary>Основной экземпляр API.</summary> /// <summary>Основной экземпляр API.</summary>
protected YandexMusicApi Api { get; } protected YandexMusicApi Api { get; }
@@ -34,12 +33,6 @@ internal abstract class YRequestBuilder<TParams>
protected YRequestBuilder(YandexMusicApi api) protected YRequestBuilder(YandexMusicApi api)
{ {
Api = api; Api = api;
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};
} }
private string FullUrl => $"{BaseUrl.TrimEnd('/')}/{PathTemplate.TrimStart('/')}"; private string FullUrl => $"{BaseUrl.TrimEnd('/')}/{PathTemplate.TrimStart('/')}";
@@ -66,7 +59,7 @@ internal abstract class YRequestBuilder<TParams>
}; };
msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.AcceptCharset), Encoding.UTF8.WebName); msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.AcceptCharset), Encoding.UTF8.WebName);
msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.AcceptEncoding), "gzip"); msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.AcceptEncoding), "gzip");
if (!string.IsNullOrEmpty(Storage.Token)) if (ShouldAddAuthorization && !string.IsNullOrEmpty(Storage.Token))
msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.Authorization), $"OAuth {Storage.Token}"); msg.Headers.TryAddWithoutValidation(GetHeaderName(HttpRequestHeader.Authorization), $"OAuth {Storage.Token}");
SetCustomHeaders(msg.Headers); SetCustomHeaders(msg.Headers);
return msg; return msg;
@@ -120,8 +113,6 @@ internal abstract class YRequestBuilder<TParams>
protected virtual HttpContent? GetContent(TParams parameters) => 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);
/// <summary>Выполняет запрос и возвращает десериализованный ответ.</summary> /// <summary>Выполняет запрос и возвращает десериализованный ответ.</summary>
public async Task<HttpResponseMessage?> ExecuteRawAsync(TParams parameters) public async Task<HttpResponseMessage?> ExecuteRawAsync(TParams parameters)
{ {

View File

@@ -0,0 +1,47 @@
using System.Xml;
using System.Xml.Serialization;
namespace YandexMusic.API.Requests.Common;
/// <summary>
/// Строитель запросов с десериализацией XML-ответа в TResponse.
/// </summary>
internal abstract class YXmlRequestBuilder<TResponse, TParams> : YRequestBuilder<TParams>
{
protected YXmlRequestBuilder(YandexMusicApi api) : base(api) { }
/// <summary>
/// Десериализует XML-ответ в объект типа TResponse.
/// </summary>
protected virtual async Task<TResponse?> DeserializeAsync(HttpResponseMessage response)
{
var xml = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
// Для XML-ошибок можно создать отдельную модель, но для простоты выбрасываем исключение
throw new Exception($"Ошибка HTTP {response.StatusCode}: {xml}");
}
try
{
using var stringReader = new StringReader(xml);
using var xmlReader = XmlReader.Create(stringReader, new XmlReaderSettings { Async = true });
var serializer = new XmlSerializer(typeof(TResponse));
return (TResponse?)serializer.Deserialize(xmlReader);
}
catch (Exception ex)
{
throw new Exception($"Ошибка десериализации XML: {ex.Message}\nXML: {xml}", ex);
}
}
/// <summary>
/// Выполняет запрос и возвращает десериализованный объект типа TResponse.
/// </summary>
public async Task<TResponse?> ExecuteAsync(TParams parameters)
{
using var response = await ExecuteRawAsync(parameters);
return await DeserializeAsync(response);
}
}

View File

@@ -0,0 +1,27 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YCheckPhoneAvailabilityBuilder : YPassportRequestBuilder<YCheckAvailabilityResult?, string>
{
public YCheckPhoneAvailabilityBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/suggest/check_availability";
protected override HttpContent? GetContent(string phone)
{
var data = new
{
track_id = Api.Storage.AuthToken.TrackId,
phone_number = phone,
can_use_anmon = true,
check_for_push = true,
push_suggest_log_all_subscriptions = false
};
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,24 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Account;
namespace YandexMusic.API.Requests.Passport;
internal class YCheckPushCodeBuilder : YPassportRequestBuilder<YAuthEmpty?, string>
{
public YCheckPushCodeBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/auth/check-push-code";
protected override HttpContent? GetContent(string code)
{
var data = new
{
track_id = Api.Storage.AuthToken.TrackId,
code
};
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,19 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YCheckSessionBuilder : YPassportRequestBuilder<YPassportSessionStatus?, object>
{
public YCheckSessionBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/sessions/check_session";
protected override HttpContent? GetContent(object _)
{
var data = new { track_id = Api.Storage.AuthToken.TrackId };
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,13 @@
using System.Net;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YCreateTrackBuilder : YPassportRequestBuilder<YPassportTrack?, object>
{
public YCreateTrackBuilder(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", "" } });
}

View File

@@ -0,0 +1,22 @@
using System.Net;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YGetAuthLoginQRBuilder : YPassportRequestBuilder<YAuthQrSession, string>
{
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)
{
return new FormUrlEncodedContent(new Dictionary<string, string> {
{ "track_id", Api.Storage.AuthToken.SessionTrackId }
});
}
}

View File

@@ -3,20 +3,21 @@ using System.Net.Http.Headers;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
using YandexMusic.API.Requests.Common; using YandexMusic.API.Requests.Common;
namespace YandexMusic.API.Requests.Account; namespace YandexMusic.API.Requests.Passport;
internal class YGetMusicTokenBuilder : YAuthRequestBuilder<YAccessToken?, object> internal class YGetMusicTokenBuilder : YPassportRequestBuilder<YAccessToken?, string>
{ {
public YGetMusicTokenBuilder(YandexMusicApi api) : base(api) { } public YGetMusicTokenBuilder(YandexMusicApi api) : base(api) { }
protected override string BaseUrl => YConstants.Endpoints.MobilePassportUrl;
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "/1/token"; protected override string PathTemplate => "/1/token";
protected override HttpContent? GetContent(object _) protected override HttpContent? GetContent(string passportToken)
=> new FormUrlEncodedContent(new Dictionary<string, string> => 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", Api.Storage.AccessToken.AccessToken } { "access_token", passportToken }
}); });
protected override void SetCustomHeaders(HttpRequestHeaders headers) protected override void SetCustomHeaders(HttpRequestHeaders headers)
{ {

View File

@@ -0,0 +1,17 @@
using System.Net;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YGetQrStatus : YPassportRequestBuilder<YAuthQRStatus?, object>
{
public YGetQrStatus(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,
});
}

View File

@@ -0,0 +1,20 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YGetSessionBuilder : YPassportRequestBuilder<YPassportSession?, object>
{
public YGetSessionBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/sessions/get_session";
protected override HttpContent? GetContent(object _)
{
var data = new { track_id = Api.Storage.AuthToken.TrackId };
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,23 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YMultiStepPasswordBuilder : YPassportRequestBuilder<YPassportUser?, string>
{
public YMultiStepPasswordBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/auth/multistep_password";
protected override HttpContent? GetContent(string password)
{
var data = new
{
track_id = Api.Storage.AuthToken.TrackId,
password
};
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,35 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YMultistepStartBuilder : YPassportRequestBuilder<YMultistepStart?, string>
{
public YMultistepStartBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/auth/multistep_start";
protected override HttpContent? GetContent(string login)
{
var data = new
{
login,
track_id = Api.Storage.AuthToken.TrackId,
display_language = "ru",
retpath = string.Empty,
can_send_push_code = true,
check_for_xtokens_for_pictures = false,
force_check_for_protocols = true,
app_id = "ru.yandex.music",
am_version_name = "7.50.2(750024597)",
app_platform = "android",
app_version_name = "2026.02.3 #135rur",
device_id = Api.Storage.DeviceId,
device_connection_type = "9"
};
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,23 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YRfcOtpBuilder : YPassportRequestBuilder<YPassportUser?, string>
{
public YRfcOtpBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/auth/rfc-otp";
protected override HttpContent? GetContent(string otp)
{
var data = new
{
track_id = Api.Storage.AuthToken.TrackId,
otp
};
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,26 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YSendPushBuilder : YPassportRequestBuilder<YSendPushResult?, string>
{
public YSendPushBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/auth/suggest-send-push";
protected override HttpContent? GetContent(string phone)
{
var data = new
{
track_id = Api.Storage.AuthToken.TrackId,
phone_number = phone,
can_use_anmon = true,
force_show_code_in_notification = "1",
country = Api.Storage.Country
};
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,23 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YSuggestByPhoneBuilder : YPassportRequestBuilder<YSuggestByPhoneResult?, object>
{
public YSuggestByPhoneBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/suggest/by_phone";
protected override HttpContent? GetContent(object _)
{
var data = new
{
track_id = Api.Storage.AuthToken.TrackId,
can_use_anmon = true
};
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,24 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YValidatePhoneNumberBuilder : YPassportRequestBuilder<YValidatePhoneNumberResult?, string>
{
public YValidatePhoneNumberBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/validate/phone_number";
protected override HttpContent? GetContent(string phone)
{
var data = new
{
track_id = Api.Storage.AuthToken.TrackId,
phone_number = phone,
country = Api.Storage.Country
};
return JsonContent.Create(data);
}
}

View File

@@ -0,0 +1,25 @@
using System.Net;
using System.Net.Http.Json;
using YandexMusic.API.Models.Passport;
namespace YandexMusic.API.Requests.Passport;
internal class YValidateSquatterBuilder : YPassportRequestBuilder<YValidateSquatter?, string>
{
public YValidateSquatterBuilder(YandexMusicApi api) : base(api) { }
protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "pwl-yandex/api/passport/validate/squatter";
protected override HttpContent? GetContent(string phone)
{
var data = new
{
track_id = Api.Storage.AuthToken.TrackId,
phone_number = phone,
scenario = "auth",
can_use_anmon = true
};
return JsonContent.Create(data);
}
}

View File

@@ -6,27 +6,30 @@ using YandexMusic.API.Requests.Common;
namespace YandexMusic.API.Requests.Track; namespace YandexMusic.API.Requests.Track;
/// <summary>Особый запрос не к api.music.yandex.net, а к произвольному URL.</summary> /// <summary>Особый запрос не к api.music.yandex.net, а к произвольному URL.</summary>
internal class YStorageDownloadFileBuilder : YJsonRequestBuilder<YStorageDownloadFile?, string> internal class YStorageDownloadFileBuilder : YXmlRequestBuilder<YStorageDownloadFile?, string>
{ {
public YStorageDownloadFileBuilder(YandexMusicApi api) : base(api) { } public YStorageDownloadFileBuilder(YandexMusicApi api) : base(api) { }
protected override string BaseUrl => ""; // не используется, т.к. URL берётся из параметра protected override bool ShouldAddAuthorization => false;
protected override string BaseUrl => "{src}"; // не используется, т.к. URL берётся из параметра
protected override string Method => WebRequestMethods.Http.Get; protected override string Method => WebRequestMethods.Http.Get;
protected override string PathTemplate => "{src}"; protected override string PathTemplate => "";
protected override Dictionary<string, string> GetSubstitutions(string src) protected override Dictionary<string, string> GetSubstitutions(string src)
=> new() { { "src", src.Split('?')[0] } }; => new() { { "src", src } };
protected override NameValueCollection GetQueryParams(string src) protected override NameValueCollection GetQueryParams(string src)
{ {
var query = new NameValueCollection { { "format", "json" } }; var query = new NameValueCollection();
var parts = src.Split('?'); var parts = src.Split('?');
if (parts.Length > 1) if (parts.Length > 1)
{ {
foreach (var param in parts[1].Split('&')) foreach (var param in parts[1].Split('&'))
{ {
var kv = param.Split('='); var kv = param.Split('=');
if (kv.Length == 2) query.Add(kv[0], kv[1]); if (kv.Length >= 2) query.Add(kv[0], kv[1]);
} }
} }
return query; return query;

View File

@@ -8,11 +8,11 @@ namespace YandexMusic.API.Requests.Ugc;
internal class YUgcUploadBuilder : YJsonRequestBuilder<YResponse<string>?, (string postTargetLink, byte[] fileBytes)> internal class YUgcUploadBuilder : YJsonRequestBuilder<YResponse<string>?, (string postTargetLink, byte[] fileBytes)>
{ {
public YUgcUploadBuilder(YandexMusicApi api) : base(api) { } public YUgcUploadBuilder(YandexMusicApi api) : base(api) { }
protected override string BaseUrl => ""; protected override string BaseUrl => "{postTargetLink}";
protected override string Method => WebRequestMethods.Http.Post; protected override string Method => WebRequestMethods.Http.Post;
protected override string PathTemplate => "{postTargetLink}"; protected override string PathTemplate => "";
protected override Dictionary<string, string> GetSubstitutions((string postTargetLink, byte[] fileBytes) tuple) protected override Dictionary<string, string> GetSubstitutions((string postTargetLink, byte[] fileBytes) tuple)
=> new() { { "postTargetLink", tuple.postTargetLink } }; => new() { { "postTargetLink", tuple.postTargetLink } };

View File

@@ -6,9 +6,9 @@ namespace YandexMusic.API;
public class YandexMusicApi public class YandexMusicApi
{ {
/// <summary>HttpClient, используемый для всех запросов.</summary> /// <summary>HttpClient, используемый для всех запросов.</summary>
public HttpClient HttpClient { get; } internal HttpClient HttpClient { get; }
/// <summary>Хранилище данных авторизации.</summary> /// <summary>Хранилище данных авторизации.</summary>
public AuthStorage Storage { get; } internal AuthStorage Storage { get; }
/// <summary>API для работы с альбомами.</summary> /// <summary>API для работы с альбомами.</summary>
public YAlbumAPI Album { get; internal set; } = null!; public YAlbumAPI Album { get; internal set; } = null!;
@@ -33,11 +33,13 @@ public class YandexMusicApi
/// <summary>API для работы с очередями.</summary> /// <summary>API для работы с очередями.</summary>
public YQueueAPI Queue { get; internal set; } = null!; public YQueueAPI Queue { get; internal set; } = null!;
/// <summary>API для работы с пользователем и авторизацией.</summary> /// <summary>API для работы с пользователем и авторизацией.</summary>
public YUserAPI User { get; internal set; } = null!; public YAuthAPI Auth { get; internal set; } = null!;
/// <summary>API для загрузки пользовательского контента.</summary> /// <summary>API для загрузки пользовательского контента.</summary>
public YUgcAPI UserGeneratedContent { get; internal set; } = null!; public YUgcAPI UserGeneratedContent { get; internal set; } = null!;
/// <summary>API для работы с протоколом Ynison (WebSocket).</summary> /// <summary>API для работы с протоколом Ynison (WebSocket).</summary>
public YYnisonAPI Ynison { get; internal set; } = null!; public YYnisonAPI Ynison { get; internal set; } = null!;
/// <summary>API для работы с яндекс пасспорт.</summary>
public YPassportAPI Passport { get; internal set; } = null!;
/// <summary> /// <summary>
/// Инициализирует новый экземпляр API. /// Инициализирует новый экземпляр API.
@@ -49,19 +51,20 @@ public class YandexMusicApi
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
Storage = storage ?? throw new ArgumentNullException(nameof(storage)); Storage = storage ?? throw new ArgumentNullException(nameof(storage));
Album = new YAlbumAPI(this); Album = new(this);
Artist = new YArtistAPI(this); Artist = new(this);
Label = new YLabelAPI(this); Label = new(this);
Landing = new YLandingAPI(this); Landing = new(this);
Library = new YLibraryAPI(this); Library = new(this);
Playlist = new YPlaylistAPI(this); Playlist = new(this);
Pins = new YPinsAPI(this); Pins = new(this);
Radio = new YRadioAPI(this); Radio = new(this);
Search = new YSearchAPI(this); Search = new(this);
Track = new YTrackAPI(this); Track = new(this);
Queue = new YQueueAPI(this); Queue = new(this);
User = new YUserAPI(this); Auth = new(this);
UserGeneratedContent = new YUgcAPI(this); UserGeneratedContent = new(this);
Ynison = new YYnisonAPI(this); Ynison = new(this);
Passport = new(this);
} }
} }

View File

@@ -1,5 +1,5 @@
<Solution> <Solution>
<Project Path="YaMusicCli/YaMusicCli.csproj" Id="5cce354e-7517-4a94-9584-197daa3ad6a4" /> <Project Path="YaMusicCli/YaMusicCli.csproj" Id="5cce354e-7517-4a94-9584-197daa3ad6a4" />
<Project Path="YandexMusic.API/YandexMusic.API.csproj" /> <Project Path="YandexMusic.API/YandexMusic.API.csproj" />
<Project Path="YandexMusic/YandexMusic.csproj" Id="044fcef4-86d2-4cc9-9f7e-a577c19ae5c3" /> <Project Path="YandexMusic/YandexMusic.csproj" />
</Solution> </Solution>

View File

@@ -1,19 +1,8 @@
using YandexMusic.API; using System.Net;
using YandexMusic.API;
using YandexMusic.API.Common; using YandexMusic.API.Common;
using YandexMusic.API.Common.Ynison; using YandexMusic.API.Common.Ynison;
using YandexMusic.API.Models.Account; using YandexMusic.API.Models.Account;
using YandexMusic.API.Models.Album;
using YandexMusic.API.Models.Artist;
using YandexMusic.API.Models.Common;
using YandexMusic.API.Models.Feed;
using YandexMusic.API.Models.Landing;
using YandexMusic.API.Models.Landing.Entity.Entities.Context;
using YandexMusic.API.Models.Library;
using YandexMusic.API.Models.Playlist;
using YandexMusic.API.Models.Queue;
using YandexMusic.API.Models.Radio;
using YandexMusic.API.Models.Search;
using YandexMusic.API.Models.Track;
namespace YandexMusic; namespace YandexMusic;
@@ -23,7 +12,6 @@ public class YandexMusicClient : IDisposable
private readonly YandexMusicApi _api; private readonly YandexMusicApi _api;
private readonly AuthStorage _storage; private readonly AuthStorage _storage;
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly bool _ownsHttpClient;
private YnisonPlayer? _player; private YnisonPlayer? _player;
/// <summary>Хранилище авторизации.</summary> /// <summary>Хранилище авторизации.</summary>
@@ -41,363 +29,49 @@ public class YandexMusicClient : IDisposable
/// <summary>HttpClient, используемый клиентом (можно получить для настройки кук).</summary> /// <summary>HttpClient, используемый клиентом (можно получить для настройки кук).</summary>
public HttpClient HttpClient => _httpClient; public HttpClient HttpClient => _httpClient;
/// <summary>Создаёт новый экземпляр клиента с собственным HttpClient.</summary> /// <summary>API Яндекс Музыки.</summary>
public YandexMusicClient() : this(YandexMusicHttpClientFactory.CreateDefault()) public YandexMusicApi Api => _api;
{
_ownsHttpClient = true;
}
/// <summary> /// <summary>Создаёт новый экземпляр клиента с собственным HttpClient.</summary>
/// Создаёт новый экземпляр клиента с указанным HttpClient. public YandexMusicClient(
/// </summary> CookieContainer? cookieContainer = null,
/// <param name="httpClient">Экземпляр HttpClient (должен быть настроен с нужными куками, таймаутами).</param> IWebProxy? proxy = null,
/// <param name="ownsHttpClient">Если true, клиент будет отвечать за освобождение HttpClient при Dispose.</param> TimeSpan? timeout = null,
public YandexMusicClient(HttpClient httpClient) string? userAgent = null
)
{ {
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); if (cookieContainer == null) cookieContainer = new CookieContainer();
_storage = new AuthStorage();
var handler = new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
UseCookies = true,
CookieContainer = 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");
_httpClient = client;
_storage = new AuthStorage(cookieContainer);
_api = new YandexMusicApi(_httpClient, _storage); _api = new YandexMusicApi(_httpClient, _storage);
} }
#region Авторизация
/// <summary>Авторизация по готовому OAuth-токену.</summary>
public async Task<bool> Authorize(string token)
{
await _api.User.AuthorizeAsync(token);
return _storage.IsAuthorized;
}
/// <summary>Создание сеанса и получение доступных методов авторизации.</summary>
public Task<YAuthTypes?> CreateAuthSession(string userName)
=> _api.User.CreateAuthSessionAsync(userName);
/// <summary>Получение ссылки на QR-код для авторизации.</summary>
public Task<string?> GetAuthQRLink()
=> _api.User.GetAuthQRLinkAsync();
/// <summary>Авторизация по QR-коду (после сканирования).</summary>
public Task<YAuthQRStatus?> AuthorizeByQR()
=> _api.User.AuthorizeByQRAsync();
/// <summary>Получение капчи.</summary>
public Task<YAuthCaptcha?> GetCaptcha()
=> _api.User.GetCaptchaAsync();
/// <summary>Авторизация с вводом капчи.</summary>
public Task<YAuthBase?> AuthorizeByCaptcha(string captcha)
=> _api.User.AuthorizeByCaptchaAsync(captcha);
/// <summary>Запрос письма для авторизации.</summary>
public Task<YAuthLetter?> GetAuthLetter()
=> _api.User.GetAuthLetterAsync();
/// <summary>Подтверждение авторизации по письму.</summary>
public Task<bool> AuthorizeByLetter()
=> _api.User.AuthorizeByLetterAsync();
/// <summary>Авторизация по паролю приложения.</summary>
public Task<YAuthBase?> AuthorizeByAppPassword(string password)
=> _api.User.AuthorizeByAppPasswordAsync(password);
/// <summary>Получение AccessToken после успешной авторизации.</summary>
public Task<YAccessToken?> GetAccessToken()
=> _api.User.GetAccessTokenAsync();
/// <summary>Получение информации о пользователе через логин Яндекса.</summary>
public Task<YLoginInfo?> GetLoginInfo()
=> _api.User.GetLoginInfoAsync();
#endregion
#region Треки
/// <summary>Получает трек по идентификатору.</summary>
public async Task<YTrack?> GetTrackAsync(string id)
=> (await _api.Track.GetAsync(id));
/// <summary>Получает список треков по идентификаторам.</summary>
public async Task<List<YTrack>> GetTracksAsync(IEnumerable<string> ids)
=> (await _api.Track.GetAsync(ids)) ?? new List<YTrack>();
#endregion
#region Альбомы
/// <summary>Получает альбом по идентификатору.</summary>
public Task<YAlbum?> GetAlbumAsync(string id)
=> _api.Album.GetAsync(id);
/// <summary>Получает список альбомов по идентификаторам.</summary>
public async Task<List<YAlbum>> GetAlbumsAsync(IEnumerable<string> ids)
=> (await _api.Album.GetAsync(ids)) ?? new List<YAlbum>();
#endregion
#region Главная страница
/// <summary>Получает персональные блоки лендинга.</summary>
public Task<YLanding?> GetLandingAsync(params YLandingBlockType[] blocks)
=> _api.Landing.GetAsync(blocks);
/// <summary>Получает ленту событий.</summary>
public Task<YFeed?> GetFeedAsync()
=> _api.Landing.GetFeedAsync();
/// <summary>Получает детский лендинг.</summary>
public Task<YChildrenLanding?> GetChildrenLandingAsync()
=> _api.Landing.GetChildrenLandingAsync();
#endregion
#region Исполнители
/// <summary>Получает информацию об исполнителе.</summary>
public Task<YArtistBriefInfo?> GetArtistAsync(string id)
=> _api.Artist.GetAsync(id);
/// <summary>Получает список исполнителей.</summary>
public async Task<List<YArtist>> GetArtistsAsync(IEnumerable<string> ids)
=> (await _api.Artist.GetAsync(ids)) ?? new List<YArtist>();
#endregion
#region Плейлисты
/// <summary>Получает плейлист по пользователю и идентификатору.</summary>
public Task<YPlaylist?> GetPlaylistAsync(string user, string id)
=> _api.Playlist.GetAsync(user, id);
/// <summary>Получает плейлист по UUID.</summary>
public Task<YPlaylist?> GetPlaylistAsync(string uuid)
=> _api.Playlist.GetAsync(uuid);
/// <summary>Получает список плейлистов по списку пар (пользователь, идентификатор).</summary>
public async Task<List<YPlaylist>> GetPlaylistsAsync(IEnumerable<(string user, string id)> ids)
=> (await _api.Playlist.GetAsync(ids)) ?? new List<YPlaylist>();
/// <summary>Получает персональные плейлисты (с главной страницы).</summary>
public Task<List<YPlaylist>> GetPersonalPlaylistsAsync()
=> _api.Playlist.GetPersonalPlaylistsAsync();
/// <summary>Получает избранные плейлисты.</summary>
public async Task<List<YPlaylist>> GetFavoritesAsync()
=> (await _api.Playlist.FavoritesAsync()) ?? new List<YPlaylist>();
/// <summary>Получает плейлист «Дежавю».</summary>
public Task<YPlaylist?> GetDejaVuAsync()
=> _api.Playlist.DejaVuAsync();
/// <summary>Получает плейлист «Тайник».</summary>
public Task<YPlaylist?> GetMissedAsync()
=> _api.Playlist.MissedAsync();
/// <summary>Получает плейлист дня.</summary>
public Task<YPlaylist?> GetOfTheDayAsync()
=> _api.Playlist.OfTheDayAsync();
/// <summary>Получает плейлист «Кинопоиск».</summary>
public Task<YPlaylist?> GetKinopoiskAsync()
=> _api.Playlist.KinopoiskAsync();
/// <summary>Получает плейлист «Премьера».</summary>
public Task<YPlaylist?> GetPremiereAsync()
=> _api.Playlist.PremiereAsync();
/// <summary>Создаёт новый плейлист с заданным именем.</summary>
public Task<YPlaylist?> CreatePlaylistAsync(string name)
=> _api.Playlist.CreateAsync(name);
#endregion
#region Поиск
/// <summary>Выполняет поиск.</summary>
public Task<YSearch?> SearchAsync(string searchText, YSearchType searchType, int page = 0, int pageSize = 20)
=> _api.Search.SearchAsync(searchText, searchType, page, pageSize);
/// <summary>Получает подсказки для поискового запроса.</summary>
public Task<YSearchSuggest?> GetSearchSuggestionsAsync(string searchText)
=> _api.Search.GetSearchSuggestionsAsync(searchText);
#endregion
#region Библиотека
/// <summary>Получает лайкнутые треки.</summary>
public async Task<List<YTrack>> GetLikedTracksAsync()
{
var likes = await _api.Library.GetLikedTracksAsync();
var ids = likes?.Library?.Tracks.Select(t => t.Id).ToArray() ?? Array.Empty<string>();
if (ids.Length == 0) return new List<YTrack>();
var tracks = await _api.Track.GetAsync(ids);
return tracks ?? new List<YTrack>();
}
/// <summary>Получает дизлайкнутые треки.</summary>
public async Task<List<YTrack>> GetDislikedTracksAsync()
{
var dislikes = await _api.Library.GetDislikedTracksAsync();
var ids = dislikes?.Library?.Tracks.Select(t => t.Id).ToArray() ?? Array.Empty<string>();
if (ids.Length == 0) return new List<YTrack>();
var tracks = await _api.Track.GetAsync(ids);
return tracks ?? new List<YTrack>();
}
/// <summary>Получает лайкнутые альбомы.</summary>
public async Task<List<YAlbum>> GetLikedAlbumsAsync()
{
var albums = await _api.Library.GetLikedAlbumsAsync();
var ids = albums?.Select(a => a.Id).ToArray() ?? Array.Empty<string>();
if (ids.Length == 0) return new List<YAlbum>();
var result = await _api.Album.GetAsync(ids);
return result ?? new List<YAlbum>();
}
/// <summary>Получает лайкнутых исполнителей.</summary>
public async Task<List<YArtist>> GetLikedArtistsAsync()
{
var artists = await _api.Library.GetLikedArtistsAsync();
var ids = artists?.Select(a => a.Id).ToArray() ?? Array.Empty<string>();
if (ids.Length == 0) return new List<YArtist>();
var result = await _api.Artist.GetAsync(ids);
return result ?? new List<YArtist>();
}
/// <summary>Получает дизлайкнутых исполнителей.</summary>
public async Task<List<YArtist>> GetDislikedArtistsAsync()
{
var artists = await _api.Library.GetDislikedArtistsAsync();
var ids = artists?.Select(a => a.Id).ToArray() ?? Array.Empty<string>();
if (ids.Length == 0) return new List<YArtist>();
var result = await _api.Artist.GetAsync(ids);
return result ?? new List<YArtist>();
}
/// <summary>Получает лайкнутые плейлисты.</summary>
public async Task<List<YPlaylist>> GetLikedPlaylistsAsync()
{
var playlists = await _api.Library.GetLikedPlaylistsAsync();
var ids = playlists?
.Select(p => (p.Playlist.Uid, p.Playlist.Kind))
.ToArray() ?? Array.Empty<(string, string)>();
if (ids.Length == 0) return new List<YPlaylist>();
var result = await _api.Playlist.GetAsync(ids);
return result ?? new List<YPlaylist>();
}
/// <summary>Получает список недавно прослушанных треков.</summary>
public async Task<List<YRecentlyListened>> GetRecentlyListenedAsync(
IEnumerable<YPlayContextType> contextTypes,
int trackCount = 50,
int contextCount = 10)
{
var response = await _api.Library.GetRecentlyListenedAsync(contextTypes, trackCount, contextCount);
return response?.Contexts ?? new List<YRecentlyListened>();
}
#endregion
#region Радио
/// <summary>Получает список рекомендованных радиостанций.</summary>
public async Task<List<YStation>> GetRadioDashboardAsync()
{
var dashboard = await _api.Radio.GetStationsDashboardAsync();
return dashboard?.Stations ?? new List<YStation>();
}
/// <summary>Получает список всех радиостанций.</summary>
public async Task<List<YStation>> GetRadioStationsAsync()
=> (await _api.Radio.GetStationsAsync()) ?? new List<YStation>();
/// <summary>Получает информацию о радиостанции по идентификатору.</summary>
public async Task<YStation?> GetRadioStationAsync(YStationId id)
=> (await _api.Radio.GetStationAsync(id))?.FirstOrDefault();
#endregion
#region Очереди
/// <summary>Получает все очереди с разных устройств.</summary>
public Task<YQueueItemsContainer?> GetQueuesAsync(string? device = null)
=> _api.Queue.ListAsync(device);
/// <summary>Получает очередь по идентификатору.</summary>
public Task<YQueue?> GetQueueAsync(string queueId)
=> _api.Queue.GetAsync(queueId);
/// <summary>Создаёт новую очередь.</summary>
public Task<YNewQueue?> CreateQueueAsync(YQueue queue, string? device = null)
=> _api.Queue.CreateAsync(queue, device);
/// <summary>Обновляет позицию в очереди.</summary>
public Task<YUpdatedQueue?> UpdateQueuePositionAsync(string queueId, int currentIndex, bool isInteractive, string? device = null)
=> _api.Queue.UpdatePositionAsync(queueId, currentIndex, isInteractive, device);
#endregion
#region Загрузка треков (UGC)
/// <summary>Загружает трек из файла в плейлист.</summary>
public Task<string?> UploadTrackToPlaylistAsync(YPlaylist playlist, string fileName, string filePath)
=> _api.UserGeneratedContent.UploadTrackToPlaylistAsync(playlist, fileName, filePath);
/// <summary>Загружает трек из потока в плейлист.</summary>
public Task<string?> UploadTrackToPlaylistAsync(YPlaylist playlist, string fileName, Stream stream)
=> _api.UserGeneratedContent.UploadTrackToPlaylistAsync(playlist, fileName, stream);
/// <summary>Загружает трек из массива байтов в плейлист.</summary>
public Task<string?> UploadTrackToPlaylistAsync(YPlaylist playlist, string fileName, byte[] file)
=> _api.UserGeneratedContent.UploadTrackToPlaylistAsync(playlist, fileName, file);
#endregion
#region Лейблы
/// <summary>Получает альбомы лейбла с пагинацией.</summary>
public async Task<List<YAlbum>> GetAlbumsByLabelAsync(YLabel label, int page = 0)
=> (await _api.Label.GetAlbumsByLabelAsync(label, page))?.Albums ?? new List<YAlbum>();
/// <summary>Получает артистов лейбла с пагинацией.</summary>
public async Task<List<YArtist>> GetArtistsByLabelAsync(YLabel label, int page = 0)
=> (await _api.Label.GetArtistsByLabelAsync(label, page))?.Artists ?? new List<YArtist>();
#endregion
#region Ynison (WebSocket плеер)
/// <summary>Подключается к Ynison и запускает синхронизацию состояния плеера.</summary>
public async Task ConnectYnisonAsync()
{
if (_player == null)
_player = _api.Ynison.GetPlayer();
await _player.ConnectAsync();
}
/// <summary>Отключается от Ynison.</summary>
public async Task DisconnectYnisonAsync()
{
if (_player != null)
await _player.DisconnectAsync();
}
#endregion
#region IDisposable
private bool _disposed;
/// <summary>Освобождает ресурсы.</summary>
public void Dispose() public void Dispose()
{ {
if (_disposed) return; _httpClient?.Dispose();
_player?.Dispose(); _player?.Dispose();
if (_ownsHttpClient)
_httpClient.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
} }
#endregion
} }