chore: init commit from GitHub
Some checks are pending
Deploy / update (push) Waiting to run
Build Project .NET / build (push) Waiting to run

This commit is contained in:
Lisoveliy
2025-05-12 19:44:33 +03:00
parent 11a3f40596
commit aebe654c38
107 changed files with 18953 additions and 403 deletions

View File

@@ -0,0 +1,121 @@
using StackExchange.Redis;
using SWAD.API.Consts.Enums;
using SWAD.API.Controllers.DTOs;
using SWAD.API.Exceptions;
using SWAD.API.Services.MusicAPI.Api;
namespace SWAD.API.Services.Links;
/// <summary>
/// Service for manipulating with links (LinkController)
/// </summary>
public class LinksService
{
private readonly Dictionary<MusicService, ApiService> _services;
private readonly IDatabase _redis;
private readonly ILogger _logger;
public LinksService(IEnumerable<ApiService> services, IDatabase redis, ILogger<LinksService> logger)
{
_services = services.ToList()
.ConvertAll(x => new KeyValuePair<MusicService, ApiService>(x.ServiceType, x)).ToDictionary();
_redis = redis;
_logger = logger;
}
/// <summary>
/// Get link by query
/// </summary>
/// <param Name="query">Search query</param>
/// <returns>Success, Failure, BadRequest, NoResponse</returns>
public async Task<(string? link, ServiceResult result)> GetLinkByQuery(TrackDto query)
{
//Mapping service
var service = _services.GetValueOrDefault(query.Service);
//Throw result
try
{
var output = await service!.GetLinkByQuery(query);
return (output, output != null ? ServiceResult.Success : ServiceResult.Failure);
}
catch (HttpRequestException)
{
return (null, ServiceResult.BadRequest);
}
catch (TimeoutException)
{
return (null, ServiceResult.NoResponse);
}
}
/// <summary>
/// Get MusicService by url
/// </summary>
/// <param Name="link"></param>
/// <returns>Success, Failure</returns>
public async Task<(MusicService? service, ServiceResult result)> GetServiceByLink(string link)
{
var cacheKey = $"GET_SERVICE_{link}";
var cache = await _redis.StringGetAsync(cacheKey);
if (cache.HasValue)
{
var result = (MusicService?)Enum.Parse(typeof(MusicService), cache.ToString());
return (result, ServiceResult.Success);
}
_logger.LogInformation($"Getting service for {link}...");
foreach (var service in _services.Values)
if (service.Config.Endpoints.MusicLink.Any(link.StartsWith))
{
await _redis.StringSetAsync(cacheKey, service.ServiceType.ToString(), TimeSpan.FromDays(30));
return (service.ServiceType, ServiceResult.Success);
}
_logger.LogInformation($"FAIL");
return (null, ServiceResult.Failure);
}
/// <summary>
/// Get Link from other service
/// </summary>
/// <param Name="trackLink">link from first service</param>
/// <returns>link from another service, Success, Failure, BadRequest, NoResponse</returns>
public async Task<(string? link, ServiceResult result)> MapLinks(TrackLinkDto trackLink)
{
var service = await GetServiceByLink(trackLink.Link);
if (service.result != ServiceResult.Success) return (null, service.result);
var cacheKey = $"{service.service}_{trackLink.Link}_{Enum.GetName(trackLink.Service)}";
var cache = await _redis.StringGetAsync(cacheKey);
if (cache.HasValue)
{
return (cache.ToString(), ServiceResult.Success);
}
_logger.LogInformation($"Getting {trackLink.Link} for {Enum.GetName(trackLink.Service)}...");
try
{
var apiService = _services[service.service!.Value];
var query = await apiService.GetQueryObject(trackLink.Link);
var linkApiService = _services[trackLink.Service];
var link = await linkApiService.GetLinkByQuery(query);
if (link == null)
return (null, ServiceResult.Failure);
await _redis.StringSetAsync(cacheKey, link, TimeSpan.FromDays(30));
return (link, ServiceResult.Success);
}
catch (TrackNotFoundException)
{
return (null, ServiceResult.NotFound);
}
catch (HttpRequestException)
{
return (null, ServiceResult.BadRequest);
}
catch (TimeoutException)
{
return (null, ServiceResult.NoResponse);
}
}
}

View File

@@ -0,0 +1,35 @@
using SWAD.API.Consts.Enums;
using SWAD.API.Controllers.DTOs;
using SWAD.API.Models.Config.ApiServices.Structure;
namespace SWAD.API.Services.MusicAPI.Api;
/// <summary>
/// Abstract ApiService of music services
/// </summary>
public abstract class ApiService
{
public ApiServiceData Config { get; protected init; } = null!;
public MusicService ServiceType { get; protected init; }
public abstract Task<string?> GetLinkByQuery(TrackDto query);
public abstract Task<TrackDto> GetQueryObject(string link, string CountryCode = "US");
protected string GetQuery(TrackDto dto) => $"{dto.Name} - {dto.Artist}";
/// <summary>
/// Get all implemented API Services
/// </summary>
/// <returns>API Services types</returns>
public static IEnumerable<Type> GetAllImplementations()
{
var type = typeof(ApiService);
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p) && !p.IsInterface && !p.IsAbstract);
return types;
}
protected struct Messages
{
public const string AuthFailMessage = "Update token request for service {0} failed";
}
}

View File

@@ -0,0 +1,109 @@
using System.Net.Http.Headers;
using System.Text.Json;
using System.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using SWAD.API.Consts.Enums;
using SWAD.API.Controllers.DTOs;
using SWAD.API.Models.Config.ApiServices;
using SWAD.API.Models.JsonStructures.MusicAPI.Spotify;
using SWAD.API.Services.MusicAPI.Auth;
namespace SWAD.API.Services.MusicAPI.Api;
public class SpotifyService : ApiService
{
private readonly SpotifyAuthService _authService;
private readonly Uri _searchUri;
private SpotifyAuthResponse? _token;
public SpotifyService(IOptions<ApiServicesConfig> config, IEnumerable<AbstractAuthService> authServices)
{
ServiceType = MusicService.Spotify;
var configServices = config.Value.ServicesData;
Config = configServices.First(x => x.Name == ServiceType.ToString());
_authService = (authServices.First(x => x.ServiceType == ServiceType) as SpotifyAuthService) ?? throw new ApplicationException("Auth service not found");
_searchUri = new Uri(new Uri(Config.Endpoints.Api), Config.ApiPaths.Search);
}
//Auto revoke on Expire
private SpotifyAuthResponse? Token
{
get => _token?.ExpireAt < DateTime.UtcNow ? null : _token;
set => _token = value;
}
/// <summary>
/// Get link to spotify by search query
/// </summary>
/// <param Name="query">DTO with search query</param>
/// <returns>link from spotify</returns>
/// <exception cref="AuthenticationFailureException">If token problems</exception>
/// <exception cref="HttpRequestException">If response is bad</exception>
public override async Task<string?> GetLinkByQuery(TrackDto query)
{
if (Token == null)
{
var newToken = await _authService.GetToken() as SpotifyAuthResponse;
Token = newToken ??
throw new AuthenticationFailureException(string.Format(Messages.AuthFailMessage, ServiceType));
}
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token.Token);
var url = new UriBuilder(_searchUri)
{
Port = -1
};
var urlQuery = HttpUtility.ParseQueryString(url.Query);
urlQuery["q"] = GetQuery(query);
urlQuery["type"] = "track";
urlQuery["limit"] = "1";
urlQuery["offset"] = "0";
url.Query = urlQuery.ToString();
var response = await client.GetAsync(url.Uri);
if (!response.IsSuccessStatusCode) throw new HttpRequestException("Request unsuccessful");
var json = await JsonSerializer.DeserializeAsync<SpotifySearchResponse>(
await response.Content.ReadAsStreamAsync());
// ReSharper disable once NullableWarningSuppressionIsUsed
return json!.Tracks.Items[0].ExternalUrls.Spotify;
}
public override async Task<TrackDto> GetQueryObject(string link, string countryCode = "US")
{
var url = new Uri(link);
var id = url.Segments[^1];
if (Token == null)
{
var newToken = await _authService.GetToken() as SpotifyAuthResponse;
Token = newToken ??
throw new AuthenticationFailureException(string.Format(Messages.AuthFailMessage, ServiceType));
}
using var client = new HttpClient();
//Prepare request
var clientHeaders = client.DefaultRequestHeaders;
clientHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token.Token);
var builder = new UriBuilder(Config.Endpoints.Api + Path.Combine(Config.ApiPaths.GetTrack, id))
{
Port = -1,
Query = $"market={countryCode}"
};
var httpRequest = new HttpRequestMessage(HttpMethod.Get, builder.Uri);
//Get response
var response = await client.SendAsync(httpRequest);
if (!response.IsSuccessStatusCode) throw new HttpRequestException("Request unsuccessful");
var json =
await JsonSerializer.DeserializeAsync<SpotifyTrackResponse>(await response.Content.ReadAsStreamAsync());
// ReSharper disable once NullableWarningSuppressionIsUsed
var artists = string.Join(", ", json!.Artists.ToList().ConvertAll(x => x.Name));
return new TrackDto(json.Name, artists, json.Albums.Name, ServiceType);
}
}

View File

@@ -0,0 +1,189 @@
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using SWAD.API.Consts;
using SWAD.API.Consts.Enums;
using SWAD.API.Controllers.DTOs;
using SWAD.API.Exceptions;
using SWAD.API.Models.Config.ApiServices;
using SWAD.API.Models.JsonStructures.MusicAPI.Tidal;
using SWAD.API.Services.MusicAPI.Auth;
namespace SWAD.API.Services.MusicAPI.Api;
public class TidalService : ApiService
{
private readonly TidalAuthService _authService;
private TidalAuthResponse? _token;
public TidalService(IOptions<ApiServicesConfig> config, IEnumerable<AbstractAuthService> authServices)
{
ServiceType = MusicService.Tidal;
var configServices = config.Value.ServicesData;
Config = configServices.First(x => x.Name == ServiceType.ToString());
_authService = authServices.First(x => x.ServiceType == ServiceType) as TidalAuthService
?? throw new ApplicationException("Auth service not found");
}
//Auto revoke on Expire
private TidalAuthResponse? Token
{
get => _token?.ExpireAt < DateTime.UtcNow ? null : _token;
set => _token = value;
}
public override async Task<string?> GetLinkByQuery(TrackDto query)
{
var searchUri = new UriBuilder(Config.Endpoints.Api);
if (Token == null)
{
var newToken = await _authService.GetToken() as TidalAuthResponse;
Token = newToken ??
throw new AuthenticationFailureException(string.Format(Messages.AuthFailMessage, ServiceType));
}
using var client = new HttpClient();
var clientHeaders = client.DefaultRequestHeaders;
clientHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token.Token);
clientHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.api+json"));
searchUri.Scheme = "https";
searchUri.Path = Path.Combine(searchUri.Path, Config.ApiPaths.Search, WebUtility.UrlEncode(GetQuery(query)
//TODO: Это нужно, разработчики TIDAL дауны
));
var url = new UriBuilder(searchUri.Uri)
{
Port = -1
};
var urlQuery = HttpUtility.ParseQueryString(url.Query);
//TODO: придумать что-то с этим
urlQuery["countryCode"] = "US";
urlQuery["include"] = "tracks";
url.Query = urlQuery.ToString();
var httpRequest = new HttpRequestMessage(HttpMethod.Get, url.Uri);
httpRequest.Content =
new StringContent(string.Empty, new MediaTypeHeaderValue("application/vnd.tidal.v1+json"));
var response = await client.SendAsync(httpRequest);
if (!response.IsSuccessStatusCode)
{
var requestDebugData = await response.Content.ReadAsStringAsync();
throw new HttpRequestException(ErrorResources.Unsuccessful);
}
var json =
await JsonSerializer.DeserializeAsync<TidalSearchResponse>(await response.Content.ReadAsStreamAsync());
// ReSharper disable once NullableWarningSuppressionIsUsed
if (json.Data.RelationShips.Track.Data.Count < 1)
{
throw new TrackNotFoundException($"Track is not found in {ServiceType}");
}
//return null!; //TODO: Rewrite whole service
return Config.Endpoints.MusicLink[0] + json.Data.RelationShips.Track.Data[0].Id;
}
public override async Task<TrackDto> GetQueryObject(string link, string countryCode = "US")
{
var url = new Uri(link);
var id = url.Segments[^1];
if (Token == null)
{
var newToken = await _authService.GetToken() as TidalAuthResponse;
Token = newToken ??
throw new AuthenticationFailureException(string.Format(Messages.AuthFailMessage, ServiceType));
}
//Prepare request
using var client = new HttpClient();
//Get response
var nameResponse = await client.SendAsync(GetRequestMessage(client, id, countryCode));
if (!nameResponse.IsSuccessStatusCode)
if (nameResponse.StatusCode == HttpStatusCode.NotFound)
{
//TODO: переписать на норм обработку CountryCode
Thread.Sleep(1000);
nameResponse = await client.SendAsync(GetRequestMessage(client, id, "GB")); //Оверрайдим на европу
if (!nameResponse.IsSuccessStatusCode)
{
throw new HttpRequestException(ErrorResources.Unsuccessful);
}
}
else
throw new HttpRequestException(ErrorResources.Unsuccessful);
var trackJson =
await JsonSerializer.DeserializeAsync<TidalTrackResponse>(await nameResponse.Content.ReadAsStreamAsync());
StringBuilder artists = new();
foreach (var data in trackJson?.Data.RelationShips.Artist.Data)
{
var artistsResponse = await client.SendAsync(GetRequestMessage(client, data.Id, countryCode, true));
if (!artistsResponse.IsSuccessStatusCode)
if (artistsResponse.StatusCode == HttpStatusCode.NotFound)
{
//TODO: переписать на норм обработку CountryCode
Thread.Sleep(1000);
artistsResponse =
await client.SendAsync(GetRequestMessage(client, id, "GB", true)); //Оверрайдим на европу
if (!artistsResponse.IsSuccessStatusCode)
{
throw new HttpRequestException(ErrorResources.Unsuccessful);
}
}
else
throw new HttpRequestException(ErrorResources.Unsuccessful);
var artistJson =
await JsonSerializer.DeserializeAsync<TidalArtistResponse>(
await artistsResponse.Content.ReadAsStreamAsync());
artists.AppendJoin(",", artistJson?.Data.Attributes.Name);
}
// ReSharper disable once NullableWarningSuppressionIsUsed
//var artists = string.Join(", ", json!.Resource.Artists.ToList().ConvertAll(x => x.Name));
return new TrackDto(trackJson?.Data.Attributes.Title!, artists.ToString(), null!, ServiceType);
}
private HttpRequestMessage GetRequestMessage(HttpClient client, string id, string countryCode,
bool isArtist = false)
{
var clientHeaders = client.DefaultRequestHeaders;
clientHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token.Token);
clientHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.tidal.v1+json"));
UriBuilder builder;
if (isArtist)
{
builder = new UriBuilder(Config.Endpoints.Api + Path.Combine(Config.ApiPaths.GetArtist, id))
{
Port = -1
};
}
else
{
builder = new UriBuilder(Config.Endpoints.Api + Path.Combine(Config.ApiPaths.GetTrack, id))
{
Port = -1,
Query = "include=artists"
};
}
var urlQuery = HttpUtility.ParseQueryString(builder.Query);
//TODO: Придумать что-то с countryCode
urlQuery["countryCode"] = countryCode;
builder.Query = urlQuery.ToString();
var httpRequest = new HttpRequestMessage(HttpMethod.Get, builder.Uri);
httpRequest.Content = new StringContent(string.Empty, Encoding.UTF8, "application/vnd.tidal.v1+json");
return httpRequest;
}
}

View File

@@ -0,0 +1,88 @@
using System.Net;
using System.Web;
using Microsoft.Extensions.Options;
using SWAD.API.Consts;
using SWAD.API.Consts.Enums;
using SWAD.API.Controllers.DTOs;
using SWAD.API.Exceptions;
using SWAD.API.Models.Config.ApiServices;
using SWAD.API.Models.JsonStructures.MusicAPI.Yandex;
using SWAD.API.Services.MusicAPI.Auth;
namespace SWAD.API.Services.MusicAPI.Api;
public class YandexService : ApiService
{
public YandexService(IOptions<ApiServicesConfig> config, IEnumerable<AbstractAuthService> authServices)
{
ServiceType = MusicService.Yandex;
var configServices = config.Value.ServicesData;
Config = configServices.First(x => x.Name == ServiceType.ToString());
}
public override async Task<string?> GetLinkByQuery(TrackDto query)
{
var searchUri = new Uri(new Uri(Config.Endpoints.Api), Config.ApiPaths.Search);
using var client = new HttpClient();
var clientHeaders = client.DefaultRequestHeaders;
var response = await client.PostAsync(searchUri, new FormUrlEncodedContent([
new("text", GetQuery(query))
]));
if (!response.IsSuccessStatusCode)
{
var requestDebugData = await response.Content.ReadAsStringAsync();
throw new HttpRequestException($"{ErrorResources.Unsuccessful}\n Service returned: {requestDebugData}");
}
var json = await response.Content.ReadFromJsonAsync<YandexSearchResponse>();
// ReSharper disable once NullableWarningSuppressionIsUsed
if (json.Entities.Count < 1)
{
throw new TrackNotFoundException($"Track is not found in {ServiceType}");
}
var results = json.Entities.FirstOrDefault(x => x.Type == "track").Results;
/*
* ВНИМАНИЕ
* Yandex API писали шизы поэтому когда будешь добавлять поля в YandexSearchResponse и здесь их юзать
* ПОМНИ: ID у артистов и альбомов в разных местах JSON в одних и тех же объектах то string то number
* ЭТО ВЫЗОВЕТ ИСКЛЮЧЕНИЕ
* Думой
*/
var resultTrack = results.FirstOrDefault(x => x.Track.Title == query.Name) ??
results[0];
return $"{Config.Endpoints.MusicLink[0]}{resultTrack.Track.Id}";
}
public override async Task<TrackDto> GetQueryObject(string link, string countryCode = "RU")
{
var url = new Uri(link);
var id = url.Segments[^1];
//Prepare request
using var client = new HttpClient();
var getTrackUrl = new UriBuilder(new Uri(new Uri(Config.Endpoints.Api), Config.ApiPaths.GetTrack))
{
Port = -1
};
var urlQuery = HttpUtility.ParseQueryString(url.Query);
urlQuery["track"] = id;
getTrackUrl.Query = urlQuery.ToString();
//Get response
var response = await client.GetAsync(getTrackUrl.Uri);
if (!response.IsSuccessStatusCode)
if (response.StatusCode == HttpStatusCode.NotFound)
{
throw new TrackNotFoundException("Ну, сори, яндекс кал");
}
else
throw new HttpRequestException(ErrorResources.Unsuccessful);
var json = await response.Content.ReadFromJsonAsync<YandexTrackResponse>();
// ReSharper disable once NullableWarningSuppressionIsUsed
var artists = string.Join(", ", json!.Artists.ToList().ConvertAll(x => x.Name));
return new TrackDto(json.Track.Title, artists, json.Track.Albums[0].Title, ServiceType);
}
}

View File

@@ -0,0 +1,33 @@
using SWAD.API.Consts.Enums;
using SWAD.API.Models.Config.ApiServices.Structure;
namespace SWAD.API.Services.MusicAPI.Auth;
public abstract class AbstractAuthService
{
public MusicService ServiceType { get; protected init; }
// ReSharper disable once NullableWarningSuppressionIsUsed
protected ApiServiceData Data { get; init; } = null!;
public abstract Task<IAuthResponse?> GetToken();
/// <summary>
/// Get all implemented Auth Services
/// </summary>
/// <returns>API Services types</returns>
public static IEnumerable<Type> GetAllImplementations()
{
var type = typeof(AbstractAuthService);
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p) && !p.IsInterface && !p.IsAbstract);
return types;
}
}
public interface IAuthResponse
{
public string Token { get; set; }
public int? ExpireTime { get; set; }
public DateTime? ExpireAt { get; }
}

View File

@@ -0,0 +1,44 @@
using System.Text.Json;
using Microsoft.Extensions.Options;
using SWAD.API.Consts.Enums;
using SWAD.API.Models.Config.ApiServices;
using SWAD.API.Models.JsonStructures.MusicAPI.Spotify;
namespace SWAD.API.Services.MusicAPI.Auth;
/// <inheritdoc />
public class SpotifyAuthService : AbstractAuthService
{
public SpotifyAuthService(IOptions<ApiServicesConfig> data)
{
ServiceType = MusicService.Spotify;
Data = data.Value.ServicesData.First(x => x.Name == ServiceType.ToString());
}
public override async Task<IAuthResponse?> GetToken()
{
using var httpClient = new HttpClient();
var content = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", Data.ClientId },
{ "client_secret", Data.Secret }
};
try
{
var response = await httpClient.PostAsync(Data.Endpoints.Token, new FormUrlEncodedContent(content));
if (response.IsSuccessStatusCode)
{
var ans = await JsonSerializer.DeserializeAsync<SpotifyAuthResponse>(
await response.Content.ReadAsStreamAsync());
return ans;
}
}
catch (Exception)
{
return null;
}
return null;
}
}

View File

@@ -0,0 +1,46 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Options;
using SWAD.API.Consts.Enums;
using SWAD.API.Models.Config.ApiServices;
using SWAD.API.Models.JsonStructures.MusicAPI.Tidal;
namespace SWAD.API.Services.MusicAPI.Auth;
/// <inheritdoc />
public class TidalAuthService : AbstractAuthService
{
public TidalAuthService(IOptions<ApiServicesConfig> data)
{
ServiceType = MusicService.Tidal;
Data = data.Value.ServicesData.First(x => x.Name == ServiceType.ToString());
}
public override async Task<IAuthResponse?> GetToken()
{
using var httpClient = new HttpClient();
var base64Token = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Data.ClientId}:{Data.Secret}"));
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64Token);
var content = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" }
};
try
{
var response = await httpClient.PostAsync(Data.Endpoints.Token, new FormUrlEncodedContent(content));
if (response.IsSuccessStatusCode)
{
var ans = await JsonSerializer.DeserializeAsync<TidalAuthResponse>(
await response.Content.ReadAsStreamAsync());
return ans;
}
}
catch (Exception)
{
return null;
}
return null;
}
}