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

4
SWAD.API/.editorconfig Normal file
View File

@@ -0,0 +1,4 @@
[*.cs]
# CS1591: Отсутствует комментарий XML для открытого видимого типа или члена
dotnet_diagnostic.CS1591.severity = none

364
SWAD.API/.gitignore vendored Normal file
View File

@@ -0,0 +1,364 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# DocFx
/.vscode/
log/
obj/
_site/
.optemp/
_themes/
_themes.MSDN.Modern/
_themes.VS.Modern/
.openpublishing.buildcore.ps1
.openpublishing.redirection.sorted.json
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Folder configuration on Mac
.DS_Store
appsettings.Development.json

View File

@@ -0,0 +1,17 @@
namespace SWAD.API.Consts.Enums;
public enum MusicService
{
/// <summary>
/// Spotify Service
/// </summary>
Spotify,
/// <summary>
/// Tidal Service
/// </summary>
Tidal,
/// <summary>
/// Yandex Service
/// </summary>
Yandex
}

View File

@@ -0,0 +1,31 @@
namespace SWAD.API.Consts.Enums;
/// <summary>
/// Service errors for controller
/// </summary>
public enum ServiceResult
{
/// <summary>
/// Returns on the successful executed state
/// </summary>
Success,
/// <summary>
/// Returns on the unsuccessful executed state
/// </summary>
Failure,
/// <summary>
/// Returns on the unsuccessful executed state not by code
/// </summary>
NoResponse,
/// <summary>
/// Returns on the unsuccessful executed state by user
/// </summary>
BadRequest,
/// <summary>
/// Returns on the unsuccessful executed state by service
/// </summary>
NotFound
}

View File

@@ -0,0 +1,14 @@
namespace SWAD.API.Consts;
public static class ErrorResources
{
public const string SomethingWentWrong = "I don't know what but something went wrong";
public const string ApiNotRespond = "Service provider not respond, please try later";
public const string BadRequest = "Oops, request is bad, dude!";
public const string Unknown = "Problem really unknown... I don't even know what else to say";
public const string NotFound = "That service is really dummy, man... Not found that track";
//For exceptions
public const string Unsuccessful = "Request unsuccessful";
}

View File

@@ -0,0 +1,39 @@
using System.ComponentModel.DataAnnotations;
using SWAD.API.Consts.Enums;
namespace SWAD.API.Controllers.DTOs;
/// <summary>
/// Track directly from query
/// </summary>
/// <param Name="Name">Name of track</param>
/// <param Name="Artist">Artist of track</param>
/// <param Name="AlbumObject">AlbumObject of track</param>
/// <param Name="Service">Service provider</param>
public record TrackDto([Required] string Name, [Required] string Artist, string Album, MusicService Service);
/// <summary>
/// Track from service link
/// </summary>
/// <param Name="Link">
/// Spotify example: https://open.spotify.com/track/2K7xn816oNHJZ0aVqdQsha
/// Tidal example: https://tidal.com/track/294942856
/// Yandex.Music example: https://music.yandex.by/Albums/25851387/track/113810002 P.S.: Будда какая параша, даже тут
/// яндекс отличился
/// </param>
/// <param Name="Service">
/// </param>
public record TrackLinkDto([Required] string Link, [Required] MusicService Service);
/// <summary>
/// Service Name
/// </summary>
/// <param Name="Service">Enum MusicService</param>
/// <param Name="Name">MusicService.ToString</param>
public record ServiceDto(MusicService Service, string Name);
/// <summary>
/// Result of GetLink method
/// </summary>
/// <param Name="Link"></param>
public record LinkResultDto(string? Link);

View File

@@ -0,0 +1,111 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using SWAD.API.Consts;
using SWAD.API.Consts.Enums;
using SWAD.API.Controllers.DTOs;
using SWAD.API.Services.Links;
namespace SWAD.API.Controllers;
/// <summary>
/// Controller for get links from music providers
/// </summary>
[Tags($"{nameof(LinkController)}: Controller for get links from music providers")]
[Route("[controller]")]
[ApiController]
public class LinkController(LinksService linksService) : SwadController
{
/// <summary>
/// Get link from other link
/// </summary>
/// <param Name="track">Track query such as Name, artist, Albums</param>
/// <returns>Link for track</returns>
[HttpPost("FromLink")]
[ProducesResponseType(typeof(LinkResultDto), 200)]
[ProducesResponseType(typeof(ProblemDetails), 400)]
[ProducesResponseType(typeof(ProblemDetails), 404)]
[ProducesResponseType(typeof(ProblemDetails), 502)]
public async Task<IActionResult> GetLink(TrackLinkDto track)
{
var serviceResult = await linksService.MapLinks(track);
switch (serviceResult.result)
{
case ServiceResult.Success:
return Ok(new LinkResultDto(serviceResult.link));
case ServiceResult.Failure:
return BadRequest($"{track.Link} is not recognized. Try other link");
case ServiceResult.NoResponse:
return BadGateway("Bad gateway with one of services");
case ServiceResult.BadRequest:
return BadRequest($"Unknown service {track.Service} or parameters not valid");
case ServiceResult.NotFound:
return NotFound(ErrorResources.NotFound);
default:
throw new ApplicationException("Unknown response from service");
}
}
/// <summary>
/// Get link from search query such as artist and song Name
/// </summary>
/// <param Name="track">Track query such as Name, artist, Albums</param>
/// <returns>Link for track</returns>
[HttpPost("FromQuery")]
[ProducesResponseType(typeof(LinkResultDto), 200)]
[ProducesResponseType(typeof(ProblemDetails), 400)]
[ProducesResponseType(typeof(ProblemDetails), 404)]
[ProducesResponseType(typeof(ProblemDetails), 502)]
public async Task<IActionResult> GetLink(TrackDto track)
{
var serviceResult = await linksService.GetLinkByQuery(track);
switch (serviceResult.result)
{
case ServiceResult.Success:
return Ok(new LinkResultDto(serviceResult.link));
case ServiceResult.Failure:
return NotFound($"Track {track.Name} - {track.Artist} not found!");
case ServiceResult.NoResponse:
return BadGateway($"Bad gateway with service {track.Service}");
case ServiceResult.BadRequest:
return BadRequest($"Unknown service {track.Service} or parameters not valid");
case ServiceResult.NotFound:
return BadRequest(ErrorResources.NotFound);
default:
throw new ApplicationException($"Unknown response from service {linksService}");
}
}
/// <summary>
/// Get service what uses that link
/// </summary>
/// <param name="link"></param>
/// <returns></returns>
/// <exception cref="ApplicationException"></exception>
[HttpGet("GetService")]
[ProducesResponseType(typeof(ServiceDto), 200)]
[ProducesResponseType(typeof(ProblemDetails), 400)]
public async Task<IActionResult> GetServiceOfLink([Required] string link)
{
var serviceResult = await linksService.GetServiceByLink(link);
switch (serviceResult.result)
{
case ServiceResult.Success:
return Ok(new ServiceDto(serviceResult.service!.Value,
serviceResult.service.Value.ToString()));
case ServiceResult.Failure:
return BadRequest($"{link} is not recognized. Try other link");
default:
throw new ApplicationException("WTF are you doing?");
}
}
}

View File

@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Mvc;
namespace SWAD.API.Controllers;
public abstract class ProblemsController : ControllerBase
{
protected ObjectResult BadRequest(string? detail)
{
return Problem(statusCode: StatusCodes.Status400BadRequest, title: "Bad request!", detail: detail);
}
protected ObjectResult NotFound(string? detail)
{
return Problem(statusCode: StatusCodes.Status404NotFound, title: "Not found :c", detail: detail);
}
protected ObjectResult BadGateway(string? detail)
{
return Problem(statusCode: StatusCodes.Status502BadGateway, title: "Some problems in other side, dude!",
detail: detail);
}
}

View File

@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Mvc;
namespace SWAD.API.Controllers;
public class SwadController : ProblemsController
{
public static string GetTitleForSwagger(string controllerName, string description)
{
return $"{controllerName}: {description}";
}
}

15
SWAD.API/Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /source
# copy csproj and restore as distinct layers
COPY ./ ./
RUN dotnet restore
WORKDIR /source/SWAD.API
RUN dotnet publish -c release -o /app --no-restore
# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "/app/SWAD.API.dll"]

View File

@@ -0,0 +1,3 @@
namespace SWAD.API.Exceptions;
public class TrackNotFoundException(string? message) : Exception(message);

View File

@@ -0,0 +1,90 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;
namespace SWAD.API.Middlewares;
/// <summary>
/// DONT TOUCH THAT!!! Generate good messages for responses and handle to logger exceptions
/// </summary>
/// <param Name="logger"></param>
/// <param Name="env"></param>
/// <param Name="next"></param>
public class ExceptionMiddleware(ILogger<ExceptionMiddleware> logger, IWebHostEnvironment env, RequestDelegate next)
{
private const string DefaultMessage = "An unexpected error has occurred, dude.";
private const string ResponseStartedMessage =
"The response has already started, the http status code middleware will not be executed.";
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await next(httpContext);
}
catch (Exception e)
{
if (httpContext.Response.HasStarted)
{
logger.LogWarning(ResponseStartedMessage);
throw;
}
var id = string.IsNullOrEmpty(httpContext?.TraceIdentifier)
? Guid.NewGuid().ToString()
: httpContext.TraceIdentifier;
logger.LogError($"An exception was thrown during the request, dude. " +
$"{id} \n{(env.IsDevelopment() ? "strcstart:\n" + e : e.GetType().Name) + e.StackTrace + e.Source + e.InnerException}");
// logger.LogError($"An exception was thrown during the request, dude. " +
// $"{id} \n{(env.IsDevelopment() ? "strcstart:\n" + e : e.GetType().Name)}");
await ProblemExceptionResponse(httpContext!, e, id);
}
}
private async Task ProblemExceptionResponse(HttpContext httpContext, Exception e, string id)
{
//Create problem
var problem = new ProblemDetails
{
Title = DefaultMessage,
Status = StatusCodes.Status500InternalServerError
};
//Add stacktrace if env is development
if (env.IsDevelopment())
{
problem.Detail = $"{e.Message} in {e.GetType().Name}";
var lines = new List<string> { "Next strings is stacktrace. Enjoy, dude :)", "strcstart:" }
.Concat(e.StackTrace!.Split("\r\n").Select(e => e.TrimStart()));
problem.Extensions.Add(new KeyValuePair<string, object?>("StackTrace", lines));
}
else
{
problem.Detail = $"{e.Message} in {e}";
}
switch (e)
{
case NotImplementedException:
problem.Status = StatusCodes.Status501NotImplemented;
break;
case ValidationException ve:
return;
}
var jsonResponse = JsonSerializer
.Serialize(problem, new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
httpContext.Response.StatusCode = problem.Status ?? StatusCodes.Status500InternalServerError;
httpContext.Response.ContentType = "application/problem+json";
await httpContext.Response
.WriteAsync(jsonResponse);
}
}

View File

@@ -0,0 +1,13 @@
using SWAD.API.Models.Config.ApiServices.Structure;
namespace SWAD.API.Models.Config.ApiServices;
// ReSharper disable once ClassNeverInstantiated.Global because: Is structure for config
public class ApiServicesConfig
{
public const string ConfigName = "APIServices";
// ReSharper disable once PropertyCanBeMadeInitOnly.Global
// ReSharper disable once NullableWarningSuppressionIsUsed
public ApiServiceData[] ServicesData { get; set; } = null!;
}

View File

@@ -0,0 +1,10 @@
namespace SWAD.API.Models.Config.ApiServices.Structure;
// ReSharper disable once ClassNeverInstantiated.Global because: Is structure for config
public class ApiPaths
{
// ReSharper disable NullableWarningSuppressionIsUsed
public string Search { get; set; } = null!;
public string GetTrack { get; set; } = null!;
public string GetArtist { get; set; } = null!;
}

View File

@@ -0,0 +1,12 @@
namespace SWAD.API.Models.Config.ApiServices.Structure;
// ReSharper disable once ClassNeverInstantiated.Global because: Is structure for config
public class ApiServiceData
{
// ReSharper disable NullableWarningSuppressionIsUsed
public string Name { get; set; } = null!;
public string ClientId { get; set; } = null!;
public string Secret { get; set; } = null!;
public ApiServiceEndpoints Endpoints { get; set; } = null!;
public ApiPaths ApiPaths { get; set; } = null!;
}

View File

@@ -0,0 +1,11 @@
namespace SWAD.API.Models.Config.ApiServices.Structure;
// ReSharper disable once ClassNeverInstantiated.Global because: Is structure for config
public class ApiServiceEndpoints
{
// ReSharper disable NullableWarningSuppressionIsUsed
public string Base { get; set; } = null!;
public string Api { get; set; } = null!;
public string Token { get; set; } = null!;
public string[] MusicLink { get; set; } = null!;
}

View File

@@ -0,0 +1,8 @@
namespace SWAD.API.Models.Config;
public class ServicesEndpointsConfig
{
public const string ConfigName = "ServicesEndpoints";
// ReSharper disable once NullableWarningSuppressionIsUsed
public string Redis { get; init; } = null!;
}

View File

@@ -0,0 +1,28 @@
using System.Text.Json.Serialization;
using SWAD.API.Services.MusicAPI.Auth;
namespace SWAD.API.Models.JsonStructures.MusicAPI;
public abstract class DefaultAuthResponse : IAuthResponse
{
private int? _expire;
private DateTime? _revokedAt;
[JsonPropertyName("access_token")]
// ReSharper disable once NullableWarningSuppressionIsUsed
public string Token { get; set; } = null!;
[JsonPropertyName("expires_in")]
public int? ExpireTime
{
get => _expire;
set
{
_expire = value;
_revokedAt = DateTime.UtcNow;
}
}
public DateTime? ExpireAt => _revokedAt?.AddSeconds(ExpireTime ?? 0);
}

View File

@@ -0,0 +1,6 @@
namespace SWAD.API.Models.JsonStructures.MusicAPI.Spotify;
/// <summary>
/// Json Structure for spotify auth response
/// </summary>
public class SpotifyAuthResponse : DefaultAuthResponse;

View File

@@ -0,0 +1,31 @@
using System.Text.Json.Serialization;
namespace SWAD.API.Models.JsonStructures.MusicAPI.Spotify;
/// <summary>
/// Json Structure for spotify search response
/// </summary>
internal class SpotifySearchResponse
{
// ReSharper disable NullableWarningSuppressionIsUsed
[JsonPropertyName("tracks")] public TrackList Tracks { get; set; } = null!;
public class TrackList
{
[JsonPropertyName("items")] public List<Item> Items { get; set; } = null!;
}
public class ExternalUrls
{
[JsonPropertyName("spotify")] public string Spotify { get; set; } = null!;
}
public class Item
{
[JsonPropertyName("href")] public string? Href { get; set; } = null!;
[JsonPropertyName("id")] public string? Id { get; set; } = null!;
[JsonPropertyName("external_urls")] public ExternalUrls ExternalUrls { get; set; } = null!;
}
}

View File

@@ -0,0 +1,30 @@
using System.Text.Json.Serialization;
namespace SWAD.API.Models.JsonStructures.MusicAPI.Spotify;
/// <summary>
/// Json Structure for tidal track response
/// </summary>
public class SpotifyTrackResponse
{
// ReSharper disable NullableWarningSuppressionIsUsed
[JsonPropertyName("name")] public string Name { get; set; } = null!;
[JsonPropertyName("artists")] public Artist[] Artists { get; set; } = null!;
[JsonPropertyName("album")] public Album Albums { get; set; } = null!;
public class Artist
{
[JsonPropertyName("id")] public string Id { get; set; } = null!;
[JsonPropertyName("name")] public string Name { get; set; } = null!;
}
public class Album
{
[JsonPropertyName("id")] public string Id { get; set; } = null!;
[JsonPropertyName("name")] public string Name { get; set; } = null!;
}
}

View File

@@ -0,0 +1,25 @@
using System.Text.Json.Serialization;
// ReSharper disable NullableWarningSuppressionIsUsed
namespace SWAD.API.Models.JsonStructures.MusicAPI.Tidal;
/// <summary>
/// Json Structure for tidal track response
/// </summary>
public class TidalArtistResponse
{
[JsonPropertyName("data")] public DataObject Data { get; set; } = new();
public class DataObject
{
[JsonPropertyName("attributes")] public AttributesObject Attributes { get; set; } = new();
}
public class AttributesObject
{
[JsonPropertyName("name")] public string Name { get; set; } = null!;
}
}

View File

@@ -0,0 +1,3 @@
namespace SWAD.API.Models.JsonStructures.MusicAPI.Tidal;
public class TidalAuthResponse : DefaultAuthResponse;

View File

@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
// ReSharper disable NullableWarningSuppressionIsUsed
namespace SWAD.API.Models.JsonStructures.MusicAPI.Tidal;
internal class TidalSearchResponse
{
[JsonPropertyName("data")] public TidalTrackResponse.DataObject Data { get; set; } = null!;
public class Track
{
[JsonPropertyName("resource")] public TidalTrackResponse.DataObject DataObject { get; set; } = null!;
}
}

View File

@@ -0,0 +1,53 @@
using System.Text.Json.Serialization;
// ReSharper disable NullableWarningSuppressionIsUsed
namespace SWAD.API.Models.JsonStructures.MusicAPI.Tidal;
/// <summary>
/// Json Structure for tidal track response
/// </summary>
public class TidalTrackResponse
{
[JsonPropertyName("data")] public DataObject Data { get; set; } = new();
public class DataObject
{
[JsonPropertyName("attributes")] public AttributesObject Attributes { get; set; } = new();
[JsonPropertyName("relationships")] public RelationShipsObject RelationShips { get; set; } = new();
}
public class AttributesObject
{
[JsonPropertyName("title")] public string Title { get; set; } = null!;
}
public class RelationShipsObject
{
[JsonPropertyName("artists")] public ArtistObject Artist { get; set; } = null!;
[JsonPropertyName("tracks")] public ArtistObject Track { get; set; } = null!;
}
public class ArtistObject
{
[JsonPropertyName("data")] public List<ArtistDataObject> Data { get; set; } = null!;
}
public class TrackObject
{
[JsonPropertyName("data")] public List<TrackDataObject> Data { get; set; } = null!;
}
public class TrackDataObject
{
[JsonPropertyName("id")] public string Id { get; set; } = null!;
}
public class ArtistDataObject
{
[JsonPropertyName("id")] public string Id { get; set; } = null!;
}
}

View File

@@ -0,0 +1,268 @@
using System.Text.Json.Serialization;
namespace SWAD.API.Models.JsonStructures.MusicAPI.Yandex;
/*
* ВНИМАНИЕ
* Yandex API писали шизы поэтому когда будешь добавлять поля в YandexSearchResponse
* ПОМНИ: ID у артистов и альбомов в разных местах JSON в одних и тех же объектах то string то number
* ЭТО ВЫЗОВЕТ ИСКЛЮЧЕНИЕ
* Думой
*/
public class YandexSearchResponse
{
[JsonPropertyName("status")]
public string Status { get; set; }
[JsonPropertyName("entities")]
public List<Entity> Entities { get; set; }
}
public class Album
{
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("year")]
public int? Year { get; set; }
[JsonPropertyName("trackCount")]
public int? TrackCount { get; set; }
}
public class Album2
{
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("year")]
public int? Year { get; set; }
[JsonPropertyName("trackCount")]
public int? TrackCount { get; set; }
}
public class Artist
{
[JsonPropertyName("name")]
public string Name { get; set; }
}
public class Artist2
{
[JsonPropertyName("name")]
public string Name { get; set; }
}
public class Counts
{
[JsonPropertyName("tracks")]
public int? Tracks { get; set; }
[JsonPropertyName("directAlbums")]
public int? DirectAlbums { get; set; }
[JsonPropertyName("alsoAlbums")]
public int? AlsoAlbums { get; set; }
[JsonPropertyName("alsoTracks")]
public int? AlsoTracks { get; set; }
}
public class Cover
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("prefix")]
public string Prefix { get; set; }
[JsonPropertyName("uri")]
public string Uri { get; set; }
}
public class DerivedColors
{
[JsonPropertyName("average")]
public string Average { get; set; }
[JsonPropertyName("waveText")]
public string WaveText { get; set; }
[JsonPropertyName("miniPlayer")]
public string MiniPlayer { get; set; }
[JsonPropertyName("accent")]
public string Accent { get; set; }
}
public class Entity
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("results")]
public List<Result> Results { get; set; }
}
public class Fade
{
[JsonPropertyName("inStart")]
public double? InStart { get; set; }
[JsonPropertyName("inStop")]
public double? InStop { get; set; }
[JsonPropertyName("outStart")]
public double? OutStart { get; set; }
[JsonPropertyName("outStop")]
public double? OutStop { get; set; }
}
public class Label
{
[JsonPropertyName("id")]
public int? Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}
public class LyricsInfo
{
[JsonPropertyName("hasAvailableSyncLyrics")]
public bool? HasAvailableSyncLyrics { get; set; }
[JsonPropertyName("hasAvailableTextLyrics")]
public bool? HasAvailableTextLyrics { get; set; }
}
public class Major
{
[JsonPropertyName("id")]
public int? Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}
public class R128
{
[JsonPropertyName("i")]
public double? I { get; set; }
[JsonPropertyName("tp")]
public double? Tp { get; set; }
}
public class Ratings
{
[JsonPropertyName("month")]
public int? Month { get; set; }
}
public class Result
{
[JsonPropertyName("text")]
public string Text { get; set; }
[JsonPropertyName("track")]
public Track Track { get; set; }
}
public class Track
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("realId")]
public string RealId { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("version")]
public string Version { get; set; }
[JsonPropertyName("trackSource")]
public string TrackSource { get; set; }
[JsonPropertyName("major")]
public Major Major { get; set; }
[JsonPropertyName("available")]
public bool? Available { get; set; }
[JsonPropertyName("availableForPremiumUsers")]
public bool? AvailableForPremiumUsers { get; set; }
[JsonPropertyName("availableFullWithoutPermission")]
public bool? AvailableFullWithoutPermission { get; set; }
[JsonPropertyName("disclaimers")]
public List<object> Disclaimers { get; set; }
[JsonPropertyName("availableForOptions")]
public List<string> AvailableForOptions { get; set; }
[JsonPropertyName("albums")]
public List<Album> Albums { get; set; }
[JsonPropertyName("durationMs")]
public int? DurationMs { get; set; }
[JsonPropertyName("storageDir")]
public string StorageDir { get; set; }
[JsonPropertyName("fileSize")]
public int? FileSize { get; set; }
[JsonPropertyName("r128")]
public R128 R128 { get; set; }
[JsonPropertyName("fade")]
public Fade Fade { get; set; }
[JsonPropertyName("previewDurationMs")]
public int? PreviewDurationMs { get; set; }
[JsonPropertyName("coverUri")]
public string CoverUri { get; set; }
[JsonPropertyName("ogImage")]
public string OgImage { get; set; }
[JsonPropertyName("lyricsAvailable")]
public bool? LyricsAvailable { get; set; }
[JsonPropertyName("lyricsInfo")]
public LyricsInfo LyricsInfo { get; set; }
[JsonPropertyName("derivedColors")]
public DerivedColors DerivedColors { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("rememberPosition")]
public bool? RememberPosition { get; set; }
[JsonPropertyName("trackSharingFlag")]
public string TrackSharingFlag { get; set; }
[JsonPropertyName("contentWarning")]
public string ContentWarning { get; set; }
}
public class TrackPosition
{
[JsonPropertyName("volume")]
public int? Volume { get; set; }
[JsonPropertyName("index")]
public int? Index { get; set; }
}

View File

@@ -0,0 +1,222 @@
using System.Text.Json.Serialization;
namespace SWAD.API.Models.JsonStructures.MusicAPI.Yandex;
public class YandexTrackResponse
{
[JsonPropertyName("artists")]
public List<Artist> Artists { get; set; }
[JsonPropertyName("track")] public Track Track { get; set; }
[JsonPropertyName("lyric")] public List<Lyric> Lyric { get; set; }
}
public class AlsoInAlbum
{
[JsonPropertyName("id")] public int Id { get; set; }
[JsonPropertyName("title")] public string Title { get; set; }
[JsonPropertyName("metaType")] public string MetaType { get; set; }
[JsonPropertyName("contentWarning")] public string ContentWarning { get; set; }
[JsonPropertyName("year")] public int Year { get; set; }
[JsonPropertyName("releaseDate")] public DateTime ReleaseDate { get; set; }
[JsonPropertyName("coverUri")] public string CoverUri { get; set; }
[JsonPropertyName("ogImage")] public string OgImage { get; set; }
[JsonPropertyName("genre")] public string Genre { get; set; }
[JsonPropertyName("trackCount")] public int TrackCount { get; set; }
[JsonPropertyName("likesCount")] public int LikesCount { get; set; }
[JsonPropertyName("recent")] public bool Recent { get; set; }
[JsonPropertyName("veryImportant")] public bool VeryImportant { get; set; }
[JsonPropertyName("artists")] public List<Artist> Artists { get; set; }
[JsonPropertyName("labels")] public List<System.Reflection.Emit.Label> Labels { get; set; }
[JsonPropertyName("available")] public bool Available { get; set; }
[JsonPropertyName("availableForPremiumUsers")]
public bool AvailableForPremiumUsers { get; set; }
[JsonPropertyName("availableForOptions")]
public List<string> AvailableForOptions { get; set; }
[JsonPropertyName("availableForMobile")]
public bool AvailableForMobile { get; set; }
[JsonPropertyName("availablePartially")]
public bool AvailablePartially { get; set; }
[JsonPropertyName("bests")] public List<int> Bests { get; set; }
[JsonPropertyName("disclaimers")] public List<object> Disclaimers { get; set; }
[JsonPropertyName("trackPosition")] public TrackPosition TrackPosition { get; set; }
}
public class Credit
{
[JsonPropertyName("title")] public string Title { get; set; }
[JsonPropertyName("value")] public string Value { get; set; }
}
public class Link
{
[JsonPropertyName("title")] public string Title { get; set; }
[JsonPropertyName("href")] public string Href { get; set; }
[JsonPropertyName("type")] public string Type { get; set; }
[JsonPropertyName("socialNetwork")] public string SocialNetwork { get; set; }
}
public class Live
{
[JsonPropertyName("id")] public string Id { get; set; }
[JsonPropertyName("realId")] public string RealId { get; set; }
[JsonPropertyName("title")] public string Title { get; set; }
[JsonPropertyName("version")] public string Version { get; set; }
[JsonPropertyName("contentWarning")] public string ContentWarning { get; set; }
[JsonPropertyName("major")] public Major Major { get; set; }
[JsonPropertyName("available")] public bool Available { get; set; }
[JsonPropertyName("availableForPremiumUsers")]
public bool AvailableForPremiumUsers { get; set; }
[JsonPropertyName("availableFullWithoutPermission")]
public bool AvailableFullWithoutPermission { get; set; }
[JsonPropertyName("availableForOptions")]
public List<string> AvailableForOptions { get; set; }
[JsonPropertyName("disclaimers")] public List<object> Disclaimers { get; set; }
[JsonPropertyName("storageDir")] public string StorageDir { get; set; }
[JsonPropertyName("durationMs")] public int DurationMs { get; set; }
[JsonPropertyName("fileSize")] public int FileSize { get; set; }
[JsonPropertyName("r128")] public R128 R128 { get; set; }
[JsonPropertyName("fade")] public Fade Fade { get; set; }
[JsonPropertyName("previewDurationMs")]
public int PreviewDurationMs { get; set; }
[JsonPropertyName("artists")] public List<Artist> Artists { get; set; }
[JsonPropertyName("albums")] public List<Album> Albums { get; set; }
[JsonPropertyName("coverUri")] public string CoverUri { get; set; }
[JsonPropertyName("derivedColors")] public DerivedColors DerivedColors { get; set; }
[JsonPropertyName("ogImage")] public string OgImage { get; set; }
[JsonPropertyName("lyricsAvailable")] public bool LyricsAvailable { get; set; }
[JsonPropertyName("type")] public string Type { get; set; }
[JsonPropertyName("rememberPosition")] public bool RememberPosition { get; set; }
[JsonPropertyName("trackSharingFlag")] public string TrackSharingFlag { get; set; }
[JsonPropertyName("lyricsInfo")] public LyricsInfo LyricsInfo { get; set; }
[JsonPropertyName("trackSource")] public string TrackSource { get; set; }
}
public class Lyric
{
[JsonPropertyName("lyricsAvailable")] public bool LyricsAvailable { get; set; }
[JsonPropertyName("fullLyrics")] public string FullLyrics { get; set; }
[JsonPropertyName("lyrics")] public string Lyrics { get; set; }
}
public class OtherVersions
{
[JsonPropertyName("live")] public List<Live> Live { get; set; }
}
public class SimilarTrack
{
[JsonPropertyName("id")] public string Id { get; set; }
[JsonPropertyName("realId")] public string RealId { get; set; }
[JsonPropertyName("title")] public string Title { get; set; }
[JsonPropertyName("major")] public Major Major { get; set; }
[JsonPropertyName("available")] public bool Available { get; set; }
[JsonPropertyName("availableForPremiumUsers")]
public bool AvailableForPremiumUsers { get; set; }
[JsonPropertyName("availableFullWithoutPermission")]
public bool AvailableFullWithoutPermission { get; set; }
[JsonPropertyName("availableForOptions")]
public List<string> AvailableForOptions { get; set; }
[JsonPropertyName("disclaimers")] public List<object> Disclaimers { get; set; }
[JsonPropertyName("storageDir")] public string StorageDir { get; set; }
[JsonPropertyName("durationMs")] public int DurationMs { get; set; }
[JsonPropertyName("fileSize")] public int FileSize { get; set; }
[JsonPropertyName("r128")] public R128 R128 { get; set; }
[JsonPropertyName("fade")] public Fade Fade { get; set; }
[JsonPropertyName("previewDurationMs")]
public int PreviewDurationMs { get; set; }
[JsonPropertyName("artists")] public List<Artist> Artists { get; set; }
[JsonPropertyName("albums")] public List<Album> Albums { get; set; }
[JsonPropertyName("coverUri")] public string CoverUri { get; set; }
[JsonPropertyName("derivedColors")] public DerivedColors DerivedColors { get; set; }
[JsonPropertyName("ogImage")] public string OgImage { get; set; }
[JsonPropertyName("lyricsAvailable")] public bool LyricsAvailable { get; set; }
[JsonPropertyName("type")] public string Type { get; set; }
[JsonPropertyName("rememberPosition")] public bool RememberPosition { get; set; }
[JsonPropertyName("trackSharingFlag")] public string TrackSharingFlag { get; set; }
[JsonPropertyName("lyricsInfo")] public LyricsInfo LyricsInfo { get; set; }
[JsonPropertyName("trackSource")] public string TrackSource { get; set; }
[JsonPropertyName("contentWarning")] public string ContentWarning { get; set; }
}

34
SWAD.API/Program.cs Normal file
View File

@@ -0,0 +1,34 @@
using System.Diagnostics;
using System.Reflection;
namespace SWAD.API;
public static class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
Startup.Configure(builder);
Startup.ConfigureServices(builder.Services);
var app = builder.Build();
Startup.ConfigureApplication(app);
Splash(app.Logger);
app.Run();
}
private static void Splash(ILogger logger)
{
var splash = """
____ ____ _ _ ____ ___
/ ___|_ ____ _ _ __ | _ \ _ _ __| | ___ / \ | _ \_ _|
\___ \ \ /\ / / _` | '_ \| | | | | | |/ _` |/ _ \ / _ \ | |_) | |
___) \ V V / (_| | |_) | |_| | |_| | (_| | __/_ / ___ \| __/| |
|____/ \_/\_/ \__,_| .__/|____/ \__,_|\__,_|\___(_)_/ \_\_| |___|
|_|
""";
logger.LogWarning(splash);
logger.LogInformation(
$"Build assembly version: {FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion}");
}
}

View File

@@ -0,0 +1,37 @@
{
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5287"
},
"https": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7013;http://localhost:5287"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:20004",
"sslPort": 44385
}
}
}

37
SWAD.API/SWAD.API.csproj Normal file
View File

@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>SWAD.API</RootNamespace>
<Nullable>enable</Nullable>
<AssemblyName>SWAD.API</AssemblyName>
<VersionSuffix>0.1.$([System.DateTime]::UtcNow.ToString(MMdd)).$([System.DateTime]::Now.ToString(HHmm))</VersionSuffix>
<AssemblyVersion Condition=" '$(VersionSuffix)' == '' ">0.0.0.1</AssemblyVersion>
<AssemblyVersion Condition=" '$(VersionSuffix)' != '' ">$(VersionSuffix)</AssemblyVersion>
<Version Condition=" '$(VersionSuffix)' == '' ">0.0.1.0</Version>
<Version Condition=" '$(VersionSuffix)' != '' ">$(VersionSuffix)</Version>
<Company>SpectruMTeamCode</Company>
<Authors>Lisoveliy</Authors>
<Copyright>Copyright © $(Company) $([System.DateTime]::UtcNow.ToString(yyyy))</Copyright>
<Product>SWAD Platform</Product>
<Description>Platform for sharing music links from one music service to another for free! (REST API Back-end)</Description>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<DocumentationFile>doc.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="index.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<PackageReference Include="StackExchange.Redis" Version="2.7.33" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
<ItemGroup>
<None Include="wwwroot\swagger-ui\theme-flattop.css" />
</ItemGroup>
</Project>

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;
}
}

117
SWAD.API/Startup.cs Normal file
View File

@@ -0,0 +1,117 @@
using System.Reflection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using StackExchange.Redis;
using SWAD.API.Middlewares;
using SWAD.API.Models.Config;
using SWAD.API.Models.Config.ApiServices;
using SWAD.API.Services.Links;
using SWAD.API.Services.MusicAPI.Api;
using SWAD.API.Services.MusicAPI.Auth;
namespace SWAD.API;
public static class Startup
{
private static ServicesEndpointsConfig? _servicesEndpointsConfig;
public static void Configure(WebApplicationBuilder builder)
{
SetConfigurations(builder);
}
public static void ConfigureServices(IServiceCollection services)
{
SetServiceUsages(services);
SetCustomServices(services);
}
public static void ConfigureApplication(WebApplication app)
{
SetAppUsages(app);
SetAppMappings(app);
}
private static void SetConfigurations(WebApplicationBuilder builder)
{
//Json config setup
builder.Configuration.AddJsonFile("appsettings.json", false, true) //load base settings
.AddJsonFile("appsettings.local.json", true, true); //load local settings
builder.Services.Configure<ApiServicesConfig>
(builder.Configuration.GetSection(ApiServicesConfig.ConfigName));
builder.Services.Configure<ServicesEndpointsConfig>
(builder.Configuration.GetSection(ServicesEndpointsConfig.ConfigName));
//Get config for setup
_servicesEndpointsConfig = builder.Configuration
.GetSection(ServicesEndpointsConfig.ConfigName)
.Get<ServicesEndpointsConfig>();
}
private static void SetServiceUsages(IServiceCollection services)
{
services.AddProblemDetails();
services.AddControllers();
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString(),
Title = "SWAD.API",
Description =
"Share With All, Dude - Platform for sharing music links from one music service to another for free!\n" +
"API Reference for SWAD.Front and for guys who trying to make themself stuff :)",
TermsOfService = new Uri("https://github.com/SpectruMTeamCode/SWAD/tree/main"),
Contact = new OpenApiContact
{
Name = "GitHub",
Url = new Uri("https://github.com/SpectruMTeamCode/SWAD/tree/main")
}
});
options.IncludeXmlComments(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "doc.xml"));
options.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
});
}
private static void SetCustomServices(IServiceCollection services)
{
//Custom services setup
//TODO read from config
services.AddSingleton(ConnectionMultiplexer.Connect(_servicesEndpointsConfig.Redis));
services.AddSingleton(p =>
p.GetService<ConnectionMultiplexer>()?.GetDatabase() ??
throw new NullReferenceException("Чёт с инжектом редиса не то"));
services.AddSingleton(p => ConnectionMultiplexer.Connect(_servicesEndpointsConfig.Redis));
services.AddSingleton(p => p.GetService<ConnectionMultiplexer>()!.GetDatabase());
services.AddSingleton<LinksService>();
//Register all IAPIService implementations
foreach (var service in ApiService.GetAllImplementations()) services.AddSingleton(typeof(ApiService), service);
//Register all AuthService implementations
foreach (var service in AbstractAuthService.GetAllImplementations())
services.AddSingleton(typeof(AbstractAuthService), service);
}
private static void SetAppUsages(WebApplication app)
{
app.UseMiddleware<ExceptionMiddleware>();
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.InjectStylesheet("/swagger-ui/theme-flattop.css");
options.IndexStream = () => Assembly.GetEntryAssembly()!.GetManifestResourceStream("SWAD.API.index.html");
});
app.UseStaticFiles();
}
private static void SetAppMappings(WebApplication app)
{
app.MapControllers();
//Redirect to swagger
app.Map("/", () => { return Results.LocalRedirect("/swagger"); });
}
}

73
SWAD.API/appsettings.json Normal file
View File

@@ -0,0 +1,73 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"APIServices": {
"ServicesData": [
{
"Name": "Spotify",
"ClientId": "INSERT CLIENTID HERE",
"Secret": "INSERT SECRET HERE",
"Endpoints": {
"Base": "https://open.spotify.com/",
"Api": "https://api.spotify.com/v1/",
"Token": "https://accounts.spotify.com/api/token",
"MusicLink": [
"https://play.spotify.com/track/",
"https://open.spotify.com/track/"
]
},
"APIPaths": {
"Search": "search",
"GetTrack": "tracks"
}
},
{
"Name": "Tidal",
"ClientId": "INSERT CLIENTID HERE",
"Secret": "INSERT SECRET HERE",
"Endpoints": {
"Base": "https://tidal.com",
"Api": "https://openapi.tidal.com/v2/",
"Token": "https://auth.tidal.com/v1/oauth2/token",
"MusicLink": [
"https://tidal.com/track/",
"https://tidal.com/browse/track/"
]
},
"APIPaths": {
"Search": "searchresults",
"GetTrack": "tracks",
"GetArtist": "artists"
}
},
{
"Name": "Yandex",
"ClientId": "",
"Secret": "",
"Endpoints": {
"Base": "https://music.yandex.ru",
"Api": "https://music.yandex.ru",
"Token": "https://music.yandex.ru",
"MusicLink": [
"https://music.yandex.ru/track/",
"https://music.yandex.ru/album/",
"https://music.yandex.by/track/",
"https://music.yandex.by/album/"
]
},
"APIPaths": {
"Search": "handlers/suggest.jsx",
"GetTrack": "handlers/track.jsx"
}
}
]
},
"ServicesEndpoints": {
"Redis": "redis:6379"
}
}

249
SWAD.API/doc.xml Normal file
View File

@@ -0,0 +1,249 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>SWAD.API</name>
</assembly>
<members>
<member name="F:SWAD.API.Consts.Enums.MusicService.Spotify">
<summary>
Spotify Service
</summary>
</member>
<member name="F:SWAD.API.Consts.Enums.MusicService.Tidal">
<summary>
Tidal Service
</summary>
</member>
<member name="F:SWAD.API.Consts.Enums.MusicService.Yandex">
<summary>
Yandex Service
</summary>
</member>
<member name="T:SWAD.API.Consts.Enums.ServiceResult">
<summary>
Service errors for controller
</summary>
</member>
<member name="F:SWAD.API.Consts.Enums.ServiceResult.Success">
<summary>
Returns on the successful executed state
</summary>
</member>
<member name="F:SWAD.API.Consts.Enums.ServiceResult.Failure">
<summary>
Returns on the unsuccessful executed state
</summary>
</member>
<member name="F:SWAD.API.Consts.Enums.ServiceResult.NoResponse">
<summary>
Returns on the unsuccessful executed state not by code
</summary>
</member>
<member name="F:SWAD.API.Consts.Enums.ServiceResult.BadRequest">
<summary>
Returns on the unsuccessful executed state by user
</summary>
</member>
<member name="F:SWAD.API.Consts.Enums.ServiceResult.NotFound">
<summary>
Returns on the unsuccessful executed state by service
</summary>
</member>
<member name="T:SWAD.API.Controllers.DTOs.TrackDto">
<summary>
Track directly from query
</summary>
<param Name="Name">Name of track</param>
<param Name="Artist">Artist of track</param>
<param Name="AlbumObject">AlbumObject of track</param>
<param Name="Service">Service provider</param>
</member>
<member name="M:SWAD.API.Controllers.DTOs.TrackDto.#ctor(System.String,System.String,System.String,SWAD.API.Consts.Enums.MusicService)">
<summary>
Track directly from query
</summary>
<param Name="Name">Name of track</param>
<param Name="Artist">Artist of track</param>
<param Name="AlbumObject">AlbumObject of track</param>
<param Name="Service">Service provider</param>
</member>
<member name="T:SWAD.API.Controllers.DTOs.TrackLinkDto">
<summary>
Track from service link
</summary>
<param Name="Link">
Spotify example: https://open.spotify.com/track/2K7xn816oNHJZ0aVqdQsha
Tidal example: https://tidal.com/track/294942856
Yandex.Music example: https://music.yandex.by/Albums/25851387/track/113810002 P.S.: Будда какая параша, даже тут
яндекс отличился
</param>
<param Name="Service">
</param>
</member>
<member name="M:SWAD.API.Controllers.DTOs.TrackLinkDto.#ctor(System.String,SWAD.API.Consts.Enums.MusicService)">
<summary>
Track from service link
</summary>
<param Name="Link">
Spotify example: https://open.spotify.com/track/2K7xn816oNHJZ0aVqdQsha
Tidal example: https://tidal.com/track/294942856
Yandex.Music example: https://music.yandex.by/Albums/25851387/track/113810002 P.S.: Будда какая параша, даже тут
яндекс отличился
</param>
<param Name="Service">
</param>
</member>
<member name="T:SWAD.API.Controllers.DTOs.ServiceDto">
<summary>
Service Name
</summary>
<param Name="Service">Enum MusicService</param>
<param Name="Name">MusicService.ToString</param>
</member>
<member name="M:SWAD.API.Controllers.DTOs.ServiceDto.#ctor(SWAD.API.Consts.Enums.MusicService,System.String)">
<summary>
Service Name
</summary>
<param Name="Service">Enum MusicService</param>
<param Name="Name">MusicService.ToString</param>
</member>
<member name="T:SWAD.API.Controllers.DTOs.LinkResultDto">
<summary>
Result of GetLink method
</summary>
<param Name="Link"></param>
</member>
<member name="M:SWAD.API.Controllers.DTOs.LinkResultDto.#ctor(System.String)">
<summary>
Result of GetLink method
</summary>
<param Name="Link"></param>
</member>
<member name="T:SWAD.API.Controllers.LinkController">
<summary>
Controller for get links from music providers
</summary>
</member>
<member name="M:SWAD.API.Controllers.LinkController.#ctor(SWAD.API.Services.Links.LinksService)">
<summary>
Controller for get links from music providers
</summary>
</member>
<member name="M:SWAD.API.Controllers.LinkController.GetLink(SWAD.API.Controllers.DTOs.TrackLinkDto)">
<summary>
Get link from other link
</summary>
<param Name="track">Track query such as Name, artist, Albums</param>
<returns>Link for track</returns>
</member>
<member name="M:SWAD.API.Controllers.LinkController.GetLink(SWAD.API.Controllers.DTOs.TrackDto)">
<summary>
Get link from search query such as artist and song Name
</summary>
<param Name="track">Track query such as Name, artist, Albums</param>
<returns>Link for track</returns>
</member>
<member name="M:SWAD.API.Controllers.LinkController.GetServiceOfLink(System.String)">
<summary>
Get service what uses that link
</summary>
<param name="link"></param>
<returns></returns>
<exception cref="T:System.ApplicationException"></exception>
</member>
<member name="T:SWAD.API.Middlewares.ExceptionMiddleware">
<summary>
DONT TOUCH THAT!!! Generate good messages for responses and handle to logger exceptions
</summary>
<param Name="logger"></param>
<param Name="env"></param>
<param Name="next"></param>
</member>
<member name="M:SWAD.API.Middlewares.ExceptionMiddleware.#ctor(Microsoft.Extensions.Logging.ILogger{SWAD.API.Middlewares.ExceptionMiddleware},Microsoft.AspNetCore.Hosting.IWebHostEnvironment,Microsoft.AspNetCore.Http.RequestDelegate)">
<summary>
DONT TOUCH THAT!!! Generate good messages for responses and handle to logger exceptions
</summary>
<param Name="logger"></param>
<param Name="env"></param>
<param Name="next"></param>
</member>
<member name="T:SWAD.API.Models.JsonStructures.MusicAPI.Spotify.SpotifyAuthResponse">
<summary>
Json Structure for spotify auth response
</summary>
</member>
<member name="T:SWAD.API.Models.JsonStructures.MusicAPI.Spotify.SpotifySearchResponse">
<summary>
Json Structure for spotify search response
</summary>
</member>
<member name="T:SWAD.API.Models.JsonStructures.MusicAPI.Spotify.SpotifyTrackResponse">
<summary>
Json Structure for tidal track response
</summary>
</member>
<member name="T:SWAD.API.Models.JsonStructures.MusicAPI.Tidal.TidalTrackResponse">
<summary>
Json Structure for tidal track response
</summary>
</member>
<member name="T:SWAD.API.Services.Links.LinksService">
<summary>
Service for manipulating with links (LinkController)
</summary>
</member>
<member name="M:SWAD.API.Services.Links.LinksService.GetLinkByQuery(SWAD.API.Controllers.DTOs.TrackDto)">
<summary>
Get link by query
</summary>
<param Name="query">Search query</param>
<returns>Success, Failure, BadRequest, NoResponse</returns>
</member>
<member name="M:SWAD.API.Services.Links.LinksService.GetServiceByLink(System.String)">
<summary>
Get MusicService by url
</summary>
<param Name="link"></param>
<returns>Success, Failure</returns>
</member>
<member name="M:SWAD.API.Services.Links.LinksService.MapLinks(SWAD.API.Controllers.DTOs.TrackLinkDto)">
<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>
</member>
<member name="T:SWAD.API.Services.MusicAPI.Api.ApiService">
<summary>
Abstract ApiService of music services
</summary>
</member>
<member name="M:SWAD.API.Services.MusicAPI.Api.ApiService.GetAllImplementations">
<summary>
Get all implemented API Services
</summary>
<returns>API Services types</returns>
</member>
<member name="M:SWAD.API.Services.MusicAPI.Api.SpotifyService.GetLinkByQuery(SWAD.API.Controllers.DTOs.TrackDto)">
<summary>
Get link to spotify by search query
</summary>
<param Name="query">DTO with search query</param>
<returns>link from spotify</returns>
<exception cref="T:Microsoft.AspNetCore.Authentication.AuthenticationFailureException">If token problems</exception>
<exception cref="T:System.Net.Http.HttpRequestException">If response is bad</exception>
</member>
<member name="M:SWAD.API.Services.MusicAPI.Auth.AbstractAuthService.GetAllImplementations">
<summary>
Get all implemented Auth Services
</summary>
<returns>API Services types</returns>
</member>
<member name="T:SWAD.API.Services.MusicAPI.Auth.SpotifyAuthService">
<inheritdoc />
</member>
<member name="T:SWAD.API.Services.MusicAPI.Auth.TidalAuthService">
<inheritdoc />
</member>
</members>
</doc>

119
SWAD.API/index.html Normal file
View File

@@ -0,0 +1,119 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SWAD.API Reference</title>
<link href="./swagger-ui.css" rel="stylesheet" type="text/css">
<link href="./favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"/>
<link href="./favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"/>
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
</style>
%(HeadContent)
</head>
<body>
<div id="swagger-ui"></div>
<!-- Workaround for https://github.com/swagger-api/swagger-editor/issues/1371 -->
<script>
console.log("Swagger loaded!");
if (window.navigator.userAgent.indexOf("Edge") > -1) {
console.log("Removing native Edge fetch in favor of swagger-ui's polyfill")
window.fetch = undefined;
}
</script>
<script src="./swagger-ui-bundle.js"></script>
<script src="./swagger-ui-standalone-preset.js"></script>
<script>
/* Source: https://gist.github.com/lamberta/3768814
* Parse a string function definition and return a function object. Does not use eval.
* @param {string} str
* @return {function}
*
* Example:
* var f = function (x, y) { return x * y; };
* var g = parseFunction(f.toString());
* g(33, 3); //=> 99
*/
function parseFunction(str) {
if (!str) return void (0);
var fn_body_idx = str.indexOf('{'),
fn_body = str.substring(fn_body_idx + 1, str.lastIndexOf('}')),
fn_declare = str.substring(0, fn_body_idx),
fn_params = fn_declare.substring(fn_declare.indexOf('(') + 1, fn_declare.lastIndexOf(')')),
args = fn_params.split(',');
args.push(fn_body);
function Fn() {
return Function.apply(this, args);
}
Fn.prototype = Function.prototype;
return new Fn();
}
window.onload = function () {
var configObject = JSON.parse('%(ConfigObject)');
var oauthConfigObject = JSON.parse('%(OAuthConfigObject)');
// Workaround for https://github.com/swagger-api/swagger-ui/issues/5945
configObject.urls.forEach(function (item) {
if (item.url.startsWith("http") || item.url.startsWith("/")) return;
item.url = window.location.href.replace("index.html", item.url).split('#')[0];
});
// If validatorUrl is not explicitly provided, disable the feature by setting to null
if (!configObject.hasOwnProperty("validatorUrl"))
configObject.validatorUrl = null
// If oauth2RedirectUrl isn't specified, use the built-in default
if (!configObject.hasOwnProperty("oauth2RedirectUrl"))
configObject.oauth2RedirectUrl = (new URL("oauth2-redirect.html", window.location.href)).href;
// Apply mandatory parameters
configObject.dom_id = "#swagger-ui";
configObject.presets = [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset];
configObject.layout = "StandaloneLayout";
// Parse and add interceptor functions
var interceptors = JSON.parse('%(Interceptors)');
if (interceptors.RequestInterceptorFunction)
configObject.requestInterceptor = parseFunction(interceptors.RequestInterceptorFunction);
if (interceptors.ResponseInterceptorFunction)
configObject.responseInterceptor = parseFunction(interceptors.ResponseInterceptorFunction);
// Begin Swagger UI call region
const ui = SwaggerUIBundle(configObject);
ui.initOAuth(oauthConfigObject);
// End Swagger UI call region
window.ui = ui
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff