From 0bbaac5689bc34ebb99be387bb7bc2c2f5691937 Mon Sep 17 00:00:00 2001 From: FrigaT Date: Mon, 20 Apr 2026 14:31:47 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD=20=D1=81=D0=BF=D0=BE=D1=81=D0=BE=D0=B1=20=D0=B0?= =?UTF-8?q?=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=20qr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- YandexMusic.API/API/YTrackAPI.cs | 4 +- YandexMusic.API/API/YUserAPI.cs | 28 +++++++--- YandexMusic.API/Common/AuthStorage.cs | 15 ++++++ YandexMusic.API/Models/Account/YAuthEmpty.cs | 2 +- .../Models/Account/YAuthQRSession.cs | 21 ++++++++ .../Models/Account/YAuthQrState.cs | 9 ++++ .../Models/Account/YAuthQrStatus.cs | 18 ++----- YandexMusic.API/Models/Account/YAuthToken.cs | 9 ++-- .../Account/YGetAuthCookiesBuilder.cs | 41 +++++++++++++++ .../Account/YGetAuthLoginQRBuilder.cs | 13 +++-- .../Requests/Account/YGetAuthQRBuilder.cs | 4 +- .../Requests/Account/YPostAuthStats.cs | 4 +- .../Requests/Account/YPostQrStatus.cs | 23 ++++++++ YandexMusic.API/Requests/Common/YConstants.cs | 1 + YandexMusic/YandexMusicClient.cs | 52 ++++++++++++++----- 15 files changed, 194 insertions(+), 50 deletions(-) create mode 100644 YandexMusic.API/Models/Account/YAuthQRSession.cs create mode 100644 YandexMusic.API/Models/Account/YAuthQrState.cs create mode 100644 YandexMusic.API/Requests/Account/YPostQrStatus.cs diff --git a/YandexMusic.API/API/YTrackAPI.cs b/YandexMusic.API/API/YTrackAPI.cs index 290cdd4..4d6b7f8 100644 --- a/YandexMusic.API/API/YTrackAPI.cs +++ b/YandexMusic.API/API/YTrackAPI.cs @@ -26,8 +26,8 @@ public class YTrackAPI : YCommonAPI return $"https://{host}/get-{codec}/{sign}/{ts}{path}"; } - public Task GetAsync(string trackId) - => GetAsync(trackId); + public async Task GetAsync(string trackId) + => (await GetAsync([trackId]))?.FirstOrDefault(); public Task?> GetAsync(IEnumerable trackIds) => new YGetTracksBuilder(Api).ExecuteAsync(trackIds); diff --git a/YandexMusic.API/API/YUserAPI.cs b/YandexMusic.API/API/YUserAPI.cs index 140e60f..960b864 100644 --- a/YandexMusic.API/API/YUserAPI.cs +++ b/YandexMusic.API/API/YUserAPI.cs @@ -23,7 +23,7 @@ public class YUserAPI : YCommonAPI if (!csrfMatch.Success || !processMatch.Success) return false; - Api.Storage.AuthToken = new YAuthToken + Api.Storage.HeaderToken = new YAuthToken { CsfrToken = csrfMatch.Groups[1].Value, ProcessUuid = processMatch.Groups[1].Value @@ -45,9 +45,7 @@ public class YUserAPI : YCommonAPI 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("Не удалось подтвердить авторизацию"); + await AuthorizeAsync(accessToken.AccessToken); return true; } @@ -96,13 +94,31 @@ public class YUserAPI : YCommonAPI return $"https://passport.yandex.ru/auth/magic/code/?track_id={qr.TrackId}"; } - public async Task AuthorizeByQRAsync() + public async Task CheckQRStatusAsync() { if (Api.Storage.AuthToken == null) throw new Exception("Сессия не инициализирована"); + var status = await new YPostQrStatus(Api).ExecuteAsync(null!); + + if (!string.IsNullOrWhiteSpace(status?.TrackId)) + { + Api.Storage.AuthToken.SessionTrackId = status.TrackId; + } + + return status; + } + + public async Task 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?.Status == YAuthStatus.Ok && await LoginByCookiesAsync()) + if (status != null && status.DefaultUid != 0 && await LoginByCookiesAsync()) return status; throw new AuthenticationException("Ошибка авторизации по QR"); } diff --git a/YandexMusic.API/Common/AuthStorage.cs b/YandexMusic.API/Common/AuthStorage.cs index daf159d..e7e5e12 100644 --- a/YandexMusic.API/Common/AuthStorage.cs +++ b/YandexMusic.API/Common/AuthStorage.cs @@ -1,3 +1,4 @@ +using System.Net; using YandexMusic.API.Models.Account; namespace YandexMusic.API.Common; @@ -7,6 +8,15 @@ namespace YandexMusic.API.Common; /// public class AuthStorage { + private CookieContainer _cookieContainer; + + public AuthStorage(CookieContainer cookieContainer) + { + _cookieContainer = cookieContainer; + } + + public CookieContainer CookieContainer => _cookieContainer; + /// /// Флаг, указывающий, авторизован ли пользователь. /// @@ -32,6 +42,11 @@ public class AuthStorage /// public YAccessToken AccessToken { get; internal set; } = new(); + /// + /// Внутренние данные авторизации (CSRF, track_id и т.д.). + /// + public YAuthToken HeaderToken { get; set; } = new(); + /// /// Внутренние данные авторизации (CSRF, track_id и т.д.). /// diff --git a/YandexMusic.API/Models/Account/YAuthEmpty.cs b/YandexMusic.API/Models/Account/YAuthEmpty.cs index 0df55ba..ddd222d 100644 --- a/YandexMusic.API/Models/Account/YAuthEmpty.cs +++ b/YandexMusic.API/Models/Account/YAuthEmpty.cs @@ -2,4 +2,4 @@ public class YAuthEmpty { -} \ No newline at end of file +} diff --git a/YandexMusic.API/Models/Account/YAuthQRSession.cs b/YandexMusic.API/Models/Account/YAuthQRSession.cs new file mode 100644 index 0000000..f2f6bdf --- /dev/null +++ b/YandexMusic.API/Models/Account/YAuthQRSession.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace YandexMusic.API.Models.Account; + +public class YAuthQRSession +{ + [JsonPropertyName("default_uid")] + public int DefaultUid { get; set; } + + [JsonPropertyName("retpath")] + public string RetPath { get; set; } + + [JsonPropertyName("track_id")] + public string TrackId { get; set; } + + public string Id { get; set; } + + public string State { get; set; } + + public YAuthCaptcha Captcha { get; set; } +} diff --git a/YandexMusic.API/Models/Account/YAuthQrState.cs b/YandexMusic.API/Models/Account/YAuthQrState.cs new file mode 100644 index 0000000..047cc7a --- /dev/null +++ b/YandexMusic.API/Models/Account/YAuthQrState.cs @@ -0,0 +1,9 @@ +using System.Runtime.Serialization; + +namespace YandexMusic.API.Models.Account; + +public enum YAuthQrState +{ + [EnumMember(Value = "otp_auth_finished")] + OtpAuthFinished, +} \ No newline at end of file diff --git a/YandexMusic.API/Models/Account/YAuthQrStatus.cs b/YandexMusic.API/Models/Account/YAuthQrStatus.cs index 491e7d5..b551706 100644 --- a/YandexMusic.API/Models/Account/YAuthQrStatus.cs +++ b/YandexMusic.API/Models/Account/YAuthQrStatus.cs @@ -2,19 +2,11 @@ namespace YandexMusic.API.Models.Account; -public class YAuthQRStatus : YAuthBase +public class YAuthQRStatus { - [JsonPropertyName("default_uid")] - public int DefaultUid { get; set; } + [JsonPropertyName("state")] + public string? State { get; set; } = null; - public string RetPath { get; set; } - - [JsonPropertyName("track_id")] - public string TrackId { get; set; } - - public string Id { get; set; } - - public string State { get; set; } - - public YAuthCaptcha Captcha { get; set; } + [JsonPropertyName("trackId")] + public string TrackId { get; set; } = string.Empty; } diff --git a/YandexMusic.API/Models/Account/YAuthToken.cs b/YandexMusic.API/Models/Account/YAuthToken.cs index b59465f..cb54de9 100644 --- a/YandexMusic.API/Models/Account/YAuthToken.cs +++ b/YandexMusic.API/Models/Account/YAuthToken.cs @@ -1,16 +1,13 @@ -using System.Text.Json.Serialization; - -namespace YandexMusic.API.Models.Account; +namespace YandexMusic.API.Models.Account; public class YAuthToken { - [JsonPropertyName("csfr_token")] public string CsfrToken { get; set; } - [JsonPropertyName("track_id")] public string TrackId { get; set; } - [JsonPropertyName("process_uuid")] + public string SessionTrackId { get; set; } + public string ProcessUuid { get; set; } public Dictionary Cookie { get; set; } = new(); diff --git a/YandexMusic.API/Requests/Account/YGetAuthCookiesBuilder.cs b/YandexMusic.API/Requests/Account/YGetAuthCookiesBuilder.cs index a405019..f9c31dc 100644 --- a/YandexMusic.API/Requests/Account/YGetAuthCookiesBuilder.cs +++ b/YandexMusic.API/Requests/Account/YGetAuthCookiesBuilder.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http.Headers; using YandexMusic.API.Models.Account; using YandexMusic.API.Requests.Common; @@ -8,7 +9,47 @@ internal class YGetAuthCookiesBuilder : YAuthRequestBuilder WebRequestMethods.Http.Post; + protected override string BaseUrl => YConstants.Endpoints.MobilePassportUrl; protected override string PathTemplate => "1/bundle/oauth/token_by_sessionid"; protected override HttpContent? GetContent(object _) => new FormUrlEncodedContent(new Dictionary { { "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(); + 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); + } } \ No newline at end of file diff --git a/YandexMusic.API/Requests/Account/YGetAuthLoginQRBuilder.cs b/YandexMusic.API/Requests/Account/YGetAuthLoginQRBuilder.cs index 009ed22..9436ce3 100644 --- a/YandexMusic.API/Requests/Account/YGetAuthLoginQRBuilder.cs +++ b/YandexMusic.API/Requests/Account/YGetAuthLoginQRBuilder.cs @@ -1,9 +1,10 @@ using System.Net; +using System.Net.Http.Headers; using YandexMusic.API.Models.Account; namespace YandexMusic.API.Requests.Account; -internal class YGetAuthLoginQRBuilder : YAuthRequestBuilder +internal class YGetAuthLoginQRBuilder : YAuthRequestBuilder { public YGetAuthLoginQRBuilder(YandexMusicApi yandex) : base(yandex) { @@ -11,13 +12,17 @@ internal class YGetAuthLoginQRBuilder : YAuthRequestBuilder WebRequestMethods.Http.Post; - protected override string PathTemplate => "auth/new/magic/status/"; + protected override string PathTemplate => "pwl-yandex/api/passport/sessions/get_session"; protected override HttpContent GetContent(string tuple) { return new FormUrlEncodedContent(new Dictionary { - { "csrf_token", Api.Storage.AuthToken.CsfrToken }, - { "track_id", Api.Storage.AuthToken.TrackId } + { "track_id", Api.Storage.AuthToken.SessionTrackId } }); } + protected override void SetCustomHeaders(HttpRequestHeaders headers) + { + headers.Add("X-Csrf-Token", Api.Storage.HeaderToken.CsfrToken); + headers.Add("Process-Uuid", Api.Storage.HeaderToken.ProcessUuid); + } } \ No newline at end of file diff --git a/YandexMusic.API/Requests/Account/YGetAuthQRBuilder.cs b/YandexMusic.API/Requests/Account/YGetAuthQRBuilder.cs index 18afa94..fef50ae 100644 --- a/YandexMusic.API/Requests/Account/YGetAuthQRBuilder.cs +++ b/YandexMusic.API/Requests/Account/YGetAuthQRBuilder.cs @@ -13,7 +13,7 @@ internal class YGetAuthQRBuilder : YAuthRequestBuilder => new FormUrlEncodedContent(new Dictionary { { "retpath", "" } }); protected override void SetCustomHeaders(HttpRequestHeaders headers) { - headers.Add("X-Csrf-Token", Api.Storage.AuthToken.CsfrToken); - headers.Add("Process-Uuid", Api.Storage.AuthToken.ProcessUuid); + headers.Add("X-Csrf-Token", Api.Storage.HeaderToken.CsfrToken); + headers.Add("Process-Uuid", Api.Storage.HeaderToken.ProcessUuid); } } \ No newline at end of file diff --git a/YandexMusic.API/Requests/Account/YPostAuthStats.cs b/YandexMusic.API/Requests/Account/YPostAuthStats.cs index d2efe3e..3fe84e8 100644 --- a/YandexMusic.API/Requests/Account/YPostAuthStats.cs +++ b/YandexMusic.API/Requests/Account/YPostAuthStats.cs @@ -13,7 +13,7 @@ internal class YPostAuthStats : YAuthRequestBuilder => new FormUrlEncodedContent(new Dictionary { { "messageType", "CLIENT_READY" } }); protected override void SetCustomHeaders(HttpRequestHeaders headers) { - headers.Add("X-Csrf-Token", Api.Storage.AuthToken.CsfrToken); - headers.Add("Process-Uuid", Api.Storage.AuthToken.ProcessUuid); + headers.Add("X-Csrf-Token", Api.Storage.HeaderToken.CsfrToken); + headers.Add("Process-Uuid", Api.Storage.HeaderToken.ProcessUuid); } } \ No newline at end of file diff --git a/YandexMusic.API/Requests/Account/YPostQrStatus.cs b/YandexMusic.API/Requests/Account/YPostQrStatus.cs new file mode 100644 index 0000000..8271559 --- /dev/null +++ b/YandexMusic.API/Requests/Account/YPostQrStatus.cs @@ -0,0 +1,23 @@ +using System.Net; +using System.Net.Http.Headers; +using YandexMusic.API.Models.Account; + +namespace YandexMusic.API.Requests.Account; + +internal class YPostQrStatus : YAuthRequestBuilder +{ + public YPostQrStatus(YandexMusicApi api) : base(api) { } + protected override string Method => WebRequestMethods.Http.Post; + protected override string PathTemplate => "pwl-yandex/api/passport/auth/magic/code/status"; + protected override HttpContent? GetContent(object _) + => new FormUrlEncodedContent(new Dictionary + { + ["csrf_token"] = Api.Storage.AuthToken.CsfrToken, + ["track_id"] = Api.Storage.AuthToken.TrackId, + }); + protected override void SetCustomHeaders(HttpRequestHeaders headers) + { + headers.Add("X-Csrf-Token", Api.Storage.HeaderToken.CsfrToken); + headers.Add("Process-Uuid", Api.Storage.HeaderToken.ProcessUuid); + } +} \ No newline at end of file diff --git a/YandexMusic.API/Requests/Common/YConstants.cs b/YandexMusic.API/Requests/Common/YConstants.cs index ee7d2da..1aa4e83 100644 --- a/YandexMusic.API/Requests/Common/YConstants.cs +++ b/YandexMusic.API/Requests/Common/YConstants.cs @@ -12,5 +12,6 @@ internal class YConstants { public const string MusicUrl = "https://api.music.yandex.net"; public const string PassportUrl = "https://passport.yandex.ru/"; + public const string MobilePassportUrl = "https://mobileproxy.passport.yandex.net"; } } \ No newline at end of file diff --git a/YandexMusic/YandexMusicClient.cs b/YandexMusic/YandexMusicClient.cs index 6da909a..54f11e9 100644 --- a/YandexMusic/YandexMusicClient.cs +++ b/YandexMusic/YandexMusicClient.cs @@ -1,4 +1,5 @@ -using YandexMusic.API; +using System.Net; +using YandexMusic.API; using YandexMusic.API.Common; using YandexMusic.API.Common.Ynison; using YandexMusic.API.Models.Account; @@ -42,20 +43,39 @@ public class YandexMusicClient : IDisposable public HttpClient HttpClient => _httpClient; /// Создаёт новый экземпляр клиента с собственным HttpClient. - public YandexMusicClient() : this(YandexMusicHttpClientFactory.CreateDefault()) + public YandexMusicClient( + CookieContainer? cookieContainer = null, + IWebProxy? proxy = null, + TimeSpan? timeout = null, + string? userAgent = null + ) { - _ownsHttpClient = true; - } + if (cookieContainer == null) cookieContainer = new CookieContainer(); - /// - /// Создаёт новый экземпляр клиента с указанным HttpClient. - /// - /// Экземпляр HttpClient (должен быть настроен с нужными куками, таймаутами). - /// Если true, клиент будет отвечать за освобождение HttpClient при Dispose. - public YandexMusicClient(HttpClient httpClient) - { - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - _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); } @@ -76,8 +96,12 @@ public class YandexMusicClient : IDisposable public Task GetAuthQRLink() => _api.User.GetAuthQRLinkAsync(); + /// Проверка состояния сканирования QR-кода. + public Task CheckQRStatusAsync() + => _api.User.CheckQRStatusAsync(); + /// Авторизация по QR-коду (после сканирования). - public Task AuthorizeByQR() + public Task AuthorizeByQR() => _api.User.AuthorizeByQRAsync(); /// Получение капчи.