Добавлен вывод QR яндекса

This commit is contained in:
FrigaT
2026-04-19 21:06:36 +03:00
parent 4324b86512
commit 12241639dc
21 changed files with 1349 additions and 46 deletions

View File

@@ -0,0 +1,138 @@
using Microsoft.AspNetCore.DataProtection;
using PlaylistShared.Api.Entities;
using System.Net;
using YandexMusic;
using YandexMusic.API.Common;
namespace PlaylistShared.Api.Services;
/// <summary>
/// Сервис для работы с API Яндекс Музыки в ASP.NET Core.
/// </summary>
public class YandexApiService : IDisposable
{
private readonly IDataProtector _dataProtector;
private readonly HttpClient _httpClient;
private readonly YandexMusicClient _client;
private readonly CookieContainer _cookieContainer;
/// <summary>
/// Экземпляр клиента Яндекс Музыки.
/// </summary>
public YandexMusicClient Client => _client;
/// <summary>
/// Контейнер кук, используемый клиентом.
/// </summary>
public CookieContainer CookieContainer => _cookieContainer;
/// <summary>
/// Создаёт сервис с автоматическим созданием HttpClient (рекомендуется).
/// </summary>
public YandexApiService(IDataProtectionProvider provider, IWebProxy? proxy = null, TimeSpan? timeout = null)
{
_dataProtector = provider.CreateProtector("YandexTokens");
_cookieContainer = new();
_httpClient = YandexMusicHttpClientFactory.CreateDefault(
cookieContainer: _cookieContainer,
proxy: proxy,
timeout: timeout
);
_client = new YandexMusicClient(_httpClient);
}
public async Task<bool?> AuthAsync(ApplicationUser user)
{
if (string.IsNullOrEmpty(user.YandexAccessToken))
return null;
var decryptedToken = DecryptToken(user.YandexAccessToken);
if (decryptedToken == null)
return null;
return await _client.Authorize(decryptedToken);
}
/// <summary>
/// Засшифровывает и возвращает токен для хранения в базе данных.
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public string EncryptToken(string token) => _dataProtector.Protect(token);
/// <summary>
/// Расшифровывает ключ из базы данных. Если токен повреждён или недействителен, возвращает null.
/// </summary>
/// <param name="encryptedToken"></param>
/// <returns></returns>
public string DecryptToken(string encryptedToken)
{
try
{
return _dataProtector.Unprotect(encryptedToken);
}
catch
{
return null;
}
}
/// <summary>
/// Устанавливает куки из строки для указанного домена.
/// </summary>
public void SetCookies(string cookieString, string domain)
{
var uri = new Uri(domain.StartsWith("http") ? domain : $"https://{domain}");
_cookieContainer.SetCookies(uri, cookieString);
}
/// <summary>
/// Получает все куки для указанного домена в виде строки.
/// </summary>
public string GetCookies(string domain)
{
var uri = new Uri(domain.StartsWith("http") ? domain : $"https://{domain}");
var cookies = _cookieContainer.GetCookies(uri);
return string.Join("; ", cookies.Cast<Cookie>().Select(c => $"{c.Name}={c.Value}"));
}
/// <summary>
/// Получает значение конкретной куки.
/// </summary>
public string? GetCookie(string domain, string cookieName)
{
var uri = new Uri(domain.StartsWith("http") ? domain : $"https://{domain}");
var cookie = _cookieContainer.GetCookies(uri)[cookieName];
return cookie?.Value;
}
private void UpdateHttpClientCookieContainer(CookieContainer container)
{
var handler = GetInnerHandler(_httpClient);
if (handler is HttpClientHandler httpHandler)
httpHandler.CookieContainer = container;
}
private static HttpMessageHandler GetInnerHandler(HttpClient client)
{
var field = client.GetType().GetField("_handler", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (field?.GetValue(client) is HttpMessageHandler handler)
return handler;
return new HttpClientHandler();
}
/// <summary>
/// Авторизуется с помощью OAuth-токена.
/// </summary>
public async Task<bool> AuthorizeAsync(string token)
{
return await _client.Authorize(token);
}
public void Dispose()
{
_client.Dispose();
_httpClient.Dispose();
}
}

View File

@@ -0,0 +1,123 @@
using PlaylistShared.Api.Data;
using PlaylistShared.Api.Entities;
using PlaylistShared.Shared.Yandex;
using System.Net;
using System.Text.Json;
using YandexMusic.API.Models.Account;
namespace PlaylistShared.Api.Services;
public class YandexAuthService
{
private readonly YandexApiService _apiService;
private readonly ApplicationDbContext _dbContext;
public YandexApiService Service => _apiService;
public YandexAuthService(YandexApiService apiService, ApplicationDbContext dbContext)
{
_apiService = apiService;
_dbContext = dbContext;
}
internal async Task<YandexAuthQr> GenerateQrAsync(ApplicationUser user)
{
var client = _apiService.Client;
var qr = await client.GetAuthQRLink();
var trackId = client.AuthStorage.AuthToken.TrackId;
var csfrToken = client.AuthStorage.AuthToken.CsfrToken;
if (string.IsNullOrEmpty(qr))
throw new Exception("Не удалось получить QR-ссылку");
var cookiesJson = SerializeCookies(_apiService.CookieContainer);
var session = new YandexAuthSession
{
UserId = user.Id,
QrCodeUrl = qr,
SerializedCookies = cookiesJson,
CreatedAt = DateTime.UtcNow,
IsConfirmed = false,
TrackId = trackId,
CsfrToken = csfrToken,
};
_dbContext.YandexAuthSessions.Add(session);
await _dbContext.SaveChangesAsync();
return new YandexAuthQr
{
QrLink = qr,
SessionId = session.Id.ToString()
};
}
internal async Task<YandexAuthQrCheck?> CheckQrAsync(int sessionId)
{
var session = await _dbContext.YandexAuthSessions.FindAsync(sessionId);
if (session == null) return null;
RestoreCookies(_apiService.CookieContainer, session.SerializedCookies);
if (_apiService.Client.AuthStorage.AuthToken is null)
{
_apiService.Client.AuthStorage.AuthToken = new();
}
_apiService.Client.AuthStorage.AuthToken.CsfrToken = session?.CsfrToken ?? "";
_apiService.Client.AuthStorage.AuthToken.TrackId = session?.TrackId ?? "";
var status = await _apiService.Client.AuthorizeByQR();
if (status?.Status == YAuthStatus.Ok)
{
session.ConfirmedAt = DateTime.UtcNow;
session.IsConfirmed = true;
await _dbContext.SaveChangesAsync();
return new()
{
Status = Shared.Enums.YandexAuthQrStatus.Authorized,
};
}
return new()
{
Status = Shared.Enums.YandexAuthQrStatus.Pending,
};
}
private string SerializeCookies(CookieContainer container)
{
var domains = new[] { "yandex.ru", "passport.yandex.ru", ".yandex.ru" };
var allCookies = new List<object>();
var cookies = container.GetAllCookies();
foreach (Cookie cookie in cookies)
{
allCookies.Add(new { cookie.Name, cookie.Value, cookie.Domain, cookie.Path });
}
return JsonSerializer.Serialize(allCookies);
}
private void RestoreCookies(CookieContainer container, string serializedCookies)
{
var cookies = JsonSerializer.Deserialize<List<CookieData>>(serializedCookies);
foreach (var c in cookies)
{
var uri = new Uri($"{c.Domain}");
container.Add(uri, new Cookie(c.Name, c.Value, c.Path, c.Domain));
}
}
private class CookieData
{
public string Name { get; set; }
public string Value { get; set; }
public string Domain { get; set; }
public string Path { get; set; }
}
}

View File

@@ -0,0 +1,307 @@
using Microsoft.AspNetCore.DataProtection;
using PlaylistShared.Api.Entities;
using PlaylistShared.Api.Extensions;
using PlaylistShared.Shared.Enums;
using PlaylistShared.Shared.Yandex;
using YandexMusic;
using YandexMusic.API.Extensions.API;
using YandexMusic.API.Models.Playlist;
using YandexMusic.API.Models.Track;
namespace PlaylistShared.Api.Services;
public class YandexMusicService
{
private readonly IDataProtector _dataProtector;
public YandexMusicService(IDataProtectionProvider provider)
{
_dataProtector = provider.CreateProtector("YandexTokens");
}
private async Task<YandexMusicClient?> CreateClientAsync(ApplicationUser user)
{
if (string.IsNullOrEmpty(user.YandexAccessToken))
return null;
string decryptedToken;
try
{
decryptedToken = _dataProtector.Unprotect(user.YandexAccessToken);
}
catch
{
return null;
}
var client = new YandexMusicClient();
var success = await client.Authorize(decryptedToken);
return success ? client : null;
}
public async Task<YPlaylist?> GetPlaylistAsync(ApplicationUser user, string ownerUid, string kind)
{
var client = await CreateClientAsync(user);
if (client == null) return null;
return await client.GetPlaylistAsync(ownerUid, kind);
}
public async Task<YPlaylist?> CreatePlaylistAsync(ApplicationUser user, string title)
{
var client = await CreateClientAsync(user);
if (client == null) return null;
return await client.CreatePlaylistAsync(title);
}
public async Task<YPlaylist?> AddTracksAsync(ApplicationUser user, string ownerUid, string kind, IEnumerable<string> trackIds)
{
var client = await CreateClientAsync(user);
if (client == null) return null;
var playlist = await client.GetPlaylistAsync(ownerUid, kind);
if (playlist == null) return null;
var tracks = await client.GetTracksAsync(trackIds);
if (tracks == null || !tracks.Any()) return null;
var insertedTracks = tracks.Where(t => !playlist.Tracks.Any(p => p.Track.Id == t.Id)).ToArray();
return await playlist.InsertTracksAsync(insertedTracks);
}
public async Task<YPlaylist?> RemoveTracksAsync(ApplicationUser user, string ownerUid, string kind, IEnumerable<string> trackIds)
{
var client = await CreateClientAsync(user);
if (client == null) return null;
var tracks = await client.GetTracksAsync(trackIds);
if (tracks == null || !tracks.Any()) return null;
var playlist = await client.GetPlaylistAsync(ownerUid, kind);
if (playlist == null) return null;
return await playlist.RemoveTracksAsync(tracks.ToArray());
}
public async Task<string?> GetTrackFileUrlAsync(ApplicationUser user, string trackId)
{
var track = await GetYTrackAsync(user, trackId);
if (track == null) return null;
return await track.GetLinkAsync();
}
public async Task<YTrack?> GetYTrackAsync(ApplicationUser user, string trackId)
{
using var client = await CreateClientAsync(user);
if (client == null) return null;
var track = await client.GetTrackAsync(trackId);
return track;
}
public async Task<YandexSearchResult> SearchAsync(
ApplicationUser user,
string query,
TrackSearchType? searchType = TrackSearchType.All,
int limit = 20
)
{
var client = await CreateClientAsync(user);
if (client == null) return new YandexSearchResult();
var ySerchType = searchType switch
{
TrackSearchType.Artist => YandexMusic.API.Models.Common.YSearchType.Artist,
TrackSearchType.Album => YandexMusic.API.Models.Common.YSearchType.Album,
TrackSearchType.Playlist => YandexMusic.API.Models.Common.YSearchType.Playlist,
TrackSearchType.Track => YandexMusic.API.Models.Common.YSearchType.Track,
_ => YandexMusic.API.Models.Common.YSearchType.All
};
var searchResult = await client.SearchAsync(query, ySerchType, page: 0, pageSize: limit);
if (searchResult?.Tracks?.Results == null) return new YandexSearchResult();
return new YandexSearchResult
{
Tracks = searchResult.Tracks.Results.Select(t => new YandexTrack
{
TrackId = t.Id,
Title = t.Title,
Artists = t.Artists.Select(t => new YandexArtist()
{
Id = t.Id,
Name = t.Name,
CoverUrl = t.Cover.GetUrl(),
Description = t.Description?.Text ?? string.Empty,
}).ToList(),
CoverUri = t.CoverUri,
DurationMs = t.DurationMs,
}).ToList(),
Playlists = searchResult.Playlists?.Results.Select(p => new YandexPlaylist
{
Uuid = p.PlaylistUuid,
Kind = p.Kind,
OwnerUid = p.Owner?.Uid ?? string.Empty,
Title = p.Title,
Description = p.Description,
CoverUrl = string.IsNullOrEmpty(p.CoverUri) ? p.Cover.GetUrl() : p.CoverUri,
TrackCount = p.TrackCount,
}).ToList(),
Artists = searchResult.Artists?.Results.Select(a => new YandexArtist
{
Id = a.Id,
Name = a.Name,
CoverUrl = a.Cover.GetUrl(),
Description = a.Description?.Text ?? string.Empty,
}).ToList(),
Albums = searchResult.Albums?.Results.Select(a => new YandexAlbum
{
Id = a.Id,
Title = a.Title,
Artists = a.Artists.Select(t => new YandexArtist()
{
Id = t.Id,
Name = t.Name,
CoverUrl = t.Cover.GetUrl(),
Description = t.Description?.Text ?? string.Empty,
}).ToList(),
CoverUrl = string.IsNullOrEmpty(a.CoverUri) ? a.Cover.GetUrl() : a.CoverUri,
Description = a.Description,
}).ToList(),
};
}
public async Task<YandexSearchResult> SearchTracksByIdAsync(
ApplicationUser user,
string id,
TrackSearchType searchType,
int limit = 20
)
{
YandexSearchResult result = new();
var client = await CreateClientAsync(user);
if (client == null) return result;
if (searchType == TrackSearchType.All)
{
throw new Exception("Для поиска по ID необходимо указать конкретный тип (трек, альбом, исполнитель или плейлист).");
}
else if (searchType == TrackSearchType.Track)
{
var track = await client.GetTrackAsync(id);
if (track != null)
{
result.Tracks = new List<YandexTrack>()
{
new()
{
TrackId = track.Id,
Title = track.Title,
Artists = track.Artists.Select(t => new YandexArtist()
{
Id = t.Id,
Name = t.Name,
CoverUrl = t.Cover.GetUrl(),
Description = t.Description?.Text ?? string.Empty,
}).ToList(),
CoverUri = track.CoverUri ?? string.Empty,
DurationMs = track.DurationMs,
}
};
}
}
else if (searchType == TrackSearchType.Album)
{
var album = await client.GetAlbumAsync(id);
result.Tracks = album?.Volumes.SelectMany(v => v).Select(t => new YandexTrack
{
TrackId = t.Id,
Title = t.Title,
Artists = t.Artists.Select(t => new YandexArtist()
{
Id = t.Id,
Name = t.Name,
CoverUrl = t.Cover.GetUrl(),
Description = t.Description?.Text ?? string.Empty,
}).ToList(),
CoverUri = t.CoverUri ?? string.Empty,
DurationMs = t.DurationMs,
}).ToList();
}
else if (searchType == TrackSearchType.Artist)
{
var artist = await client.GetArtistAsync(id);
if (artist != null)
{
result.Albums = artist.Albums.Select(a => new YandexAlbum()
{
Id = a.Id,
Title = a.Title,
Artists = a.Artists.Select(t => new YandexArtist()
{
Id = t.Id,
Name = t.Name,
CoverUrl = t.Cover.GetUrl(),
Description = t.Description?.Text ?? string.Empty,
}).ToList(),
CoverUrl = string.IsNullOrEmpty(a.CoverUri) ? a.Cover.GetUrl() : a.CoverUri,
Description = a.Description,
}).ToList();
result.Playlists = artist.Playlists.Select(p => new YandexPlaylist
{
Uuid = p.PlaylistUuid,
Kind = p.Kind,
OwnerUid = p.Owner?.Uid ?? string.Empty,
Title = p.Title,
Description = p.Description,
CoverUrl = p.Cover.GetUrl(),
TrackCount = p.TrackCount,
}).ToList();
result.Tracks = artist.PopularTracks.Select(t => new YandexTrack
{
TrackId = t.Id,
Title = t.Title,
Artists = t.Artists.Select(a => new YandexArtist()
{
Id = a.Id,
Name = a.Name,
CoverUrl = a.Cover.GetUrl(),
Description = a.Description?.Text ?? string.Empty,
}).ToList(),
CoverUri = t.CoverUri ?? string.Empty,
DurationMs = t.DurationMs,
}).ToList();
}
}
else if (searchType == TrackSearchType.Playlist)
{
var playlist = await client.GetPlaylistAsync(id);
result.Tracks = playlist?.Tracks.Select(p => new YandexTrack
{
TrackId = p.Track.Id,
CoverUri = p.Track.CoverUri,
Artists = p.Track.Artists.Select(a => new YandexArtist
{
Id = a.Id,
Name = a.Name,
CoverUrl = a.Cover.GetUrl(),
Description = a.Description?.Text ?? string.Empty,
}).ToList(),
Title = p.Track.Title,
DurationMs = p.Track.DurationMs,
}).ToList();
}
return result;
}
}