173 lines
8.1 KiB
C#
173 lines
8.1 KiB
C#
using PlaylistShared.Data.Contexts;
|
||
using PlaylistShared.Data.Entities;
|
||
using System.Net.Http.Headers;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
|
||
namespace PlaylistShared.Services;
|
||
|
||
public class YandexMusicService : IYandexMusicService
|
||
{
|
||
private readonly AppDbContext _db;
|
||
private readonly HttpClient _http;
|
||
private readonly IConfiguration _config;
|
||
private readonly ILogger<YandexMusicService> _logger;
|
||
|
||
public YandexMusicService(AppDbContext db, IHttpClientFactory factory, IConfiguration config, ILogger<YandexMusicService> logger)
|
||
{
|
||
_db = db;
|
||
_http = factory.CreateClient();
|
||
_config = config;
|
||
_logger = logger;
|
||
}
|
||
|
||
private async Task<ApplicationUser> GetUserWithValidTokenAsync(string userId)
|
||
{
|
||
var user = await _db.Users.FindAsync(userId);
|
||
if (user == null) throw new Exception("User not found");
|
||
|
||
if (user.AccessTokenExpiresAt <= DateTime.UtcNow.AddMinutes(5))
|
||
{
|
||
var newToken = await RefreshYandexTokenAsync(user.RefreshToken!);
|
||
user.AccessToken = newToken.access_token;
|
||
user.RefreshToken = newToken.refresh_token;
|
||
user.AccessTokenExpiresAt = DateTime.UtcNow.AddSeconds(newToken.expires_in);
|
||
await _db.SaveChangesAsync();
|
||
}
|
||
return user;
|
||
}
|
||
|
||
private async Task<(string access_token, string refresh_token, int expires_in)> RefreshYandexTokenAsync(string refreshToken)
|
||
{
|
||
var content = new FormUrlEncodedContent(new[]
|
||
{
|
||
new KeyValuePair<string, string>("grant_type", "refresh_token"),
|
||
new KeyValuePair<string, string>("refresh_token", refreshToken),
|
||
new KeyValuePair<string, string>("client_id", _config["YandexOAuth:ClientId"]),
|
||
new KeyValuePair<string, string>("client_secret", _config["YandexOAuth:ClientSecret"])
|
||
});
|
||
var response = await _http.PostAsync(_config["YandexOAuth:TokenEndpoint"], content);
|
||
response.EnsureSuccessStatusCode();
|
||
var json = await response.Content.ReadAsStringAsync();
|
||
using var doc = JsonDocument.Parse(json);
|
||
return (
|
||
doc.RootElement.GetProperty("access_token").GetString()!,
|
||
doc.RootElement.GetProperty("refresh_token").GetString()!,
|
||
doc.RootElement.GetProperty("expires_in").GetInt32()
|
||
);
|
||
}
|
||
|
||
public async Task<string> CreatePlaylistAsync(string userId, string title)
|
||
{
|
||
var user = await GetUserWithValidTokenAsync(userId);
|
||
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", user.AccessToken);
|
||
var payload = new { title, visibility = "public" };
|
||
var json = JsonSerializer.Serialize(payload);
|
||
var response = await _http.PostAsync("https://api.music.yandex.net/users/self/playlists/create",
|
||
new StringContent(json, Encoding.UTF8, "application/json"));
|
||
response.EnsureSuccessStatusCode();
|
||
var resultJson = await response.Content.ReadAsStringAsync();
|
||
var result = JsonSerializer.Deserialize<YandexApiResponse<YandexPlaylistData>>(resultJson);
|
||
return result!.Result.Kind;
|
||
}
|
||
|
||
public async Task AddTrackToPlaylistAsync(string userId, string yandexPlaylistId, string trackId)
|
||
{
|
||
var user = await GetUserWithValidTokenAsync(userId);
|
||
var uid = user.YandexId; // или user.Id? В API Яндекса используется uid из аккаунта, но можно использовать "self"
|
||
// Получаем альбом трека (опционально, можно передать без albumId)
|
||
// Для простоты используем трек без albumId
|
||
var diff = new[]
|
||
{
|
||
new
|
||
{
|
||
op = "insert",
|
||
at = 0,
|
||
tracks = new[] { new { id = trackId } }
|
||
}
|
||
};
|
||
var json = JsonSerializer.Serialize(diff);
|
||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||
var url = $"https://api.music.yandex.net/users/{uid}/playlists/{yandexPlaylistId}/change";
|
||
var response = await _http.PostAsync(url, content);
|
||
response.EnsureSuccessStatusCode();
|
||
}
|
||
|
||
public async Task RemoveTrackFromPlaylistAsync(string userId, string yandexPlaylistId, string trackId)
|
||
{
|
||
var user = await GetUserWithValidTokenAsync(userId);
|
||
var uid = user.YandexId;
|
||
// Сначала нужно получить позицию трека в плейлисте – упростим: удаляем по индексу (не надёжно)
|
||
// Лучше получить плейлист, найти индекс трека, потом удалить
|
||
var playlist = await GetPlaylistInfoAsync(userId, yandexPlaylistId);
|
||
var tracks = await GetPlaylistTracksAsync(userId, yandexPlaylistId);
|
||
var index = tracks.FindIndex(t => t.Id == trackId);
|
||
if (index == -1) throw new Exception("Track not found in playlist");
|
||
var diff = new[]
|
||
{
|
||
new
|
||
{
|
||
op = "delete",
|
||
from = index,
|
||
to = index + 1
|
||
}
|
||
};
|
||
var json = JsonSerializer.Serialize(diff);
|
||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||
var url = $"https://api.music.yandex.net/users/{uid}/playlists/{yandexPlaylistId}/change";
|
||
var response = await _http.PostAsync(url, content);
|
||
response.EnsureSuccessStatusCode();
|
||
}
|
||
|
||
public async Task<List<YandexTrackInfo>> GetPlaylistTracksAsync(string userId, string yandexPlaylistId)
|
||
{
|
||
var user = await GetUserWithValidTokenAsync(userId);
|
||
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", user.AccessToken);
|
||
var uid = user.YandexId;
|
||
var url = $"https://api.music.yandex.net/users/{uid}/playlists/{yandexPlaylistId}";
|
||
var response = await _http.GetAsync(url);
|
||
response.EnsureSuccessStatusCode();
|
||
var json = await response.Content.ReadAsStringAsync();
|
||
var data = JsonSerializer.Deserialize<YandexApiResponse<YandexPlaylistFull>>(json);
|
||
return data!.Result.Tracks?.Select(t => new YandexTrackInfo(
|
||
t.Id.ToString(),
|
||
t.Title,
|
||
t.Artists?.FirstOrDefault()?.Name,
|
||
t.Albums?.FirstOrDefault()?.Title,
|
||
t.DurationMs
|
||
)).ToList() ?? new();
|
||
}
|
||
|
||
public async Task<YandexPlaylistInfo> GetPlaylistInfoAsync(string userId, string yandexPlaylistId)
|
||
{
|
||
var user = await GetUserWithValidTokenAsync(userId);
|
||
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", user.AccessToken);
|
||
var uid = user.YandexId;
|
||
var url = $"https://api.music.yandex.net/users/{uid}/playlists/{yandexPlaylistId}";
|
||
var response = await _http.GetAsync(url);
|
||
response.EnsureSuccessStatusCode();
|
||
var json = await response.Content.ReadAsStringAsync();
|
||
var data = JsonSerializer.Deserialize<YandexApiResponse<YandexPlaylistFull>>(json);
|
||
return new YandexPlaylistInfo(data!.Result.Kind, data.Result.Title, data.Result.TrackCount);
|
||
}
|
||
|
||
public async Task<string?> RefreshUserTokenAsync(string userId)
|
||
{
|
||
var user = await _db.Users.FindAsync(userId);
|
||
if (user?.RefreshToken == null) return null;
|
||
var newToken = await RefreshYandexTokenAsync(user.RefreshToken);
|
||
user.AccessToken = newToken.access_token;
|
||
user.RefreshToken = newToken.refresh_token;
|
||
user.AccessTokenExpiresAt = DateTime.UtcNow.AddSeconds(newToken.expires_in);
|
||
await _db.SaveChangesAsync();
|
||
return newToken.access_token;
|
||
}
|
||
|
||
// Вспомогательные record
|
||
private record YandexApiResponse<T>(T Result);
|
||
private record YandexPlaylistData(string Kind);
|
||
private record YandexPlaylistFull(string Kind, string Title, int TrackCount, List<YandexTrack> Tracks);
|
||
private record YandexTrack(string Id, string Title, int DurationMs, List<YandexArtist> Artists, List<YandexAlbum> Albums);
|
||
private record YandexArtist(string Name);
|
||
private record YandexAlbum(string Title);
|
||
} |