chore: init commit from GitHub
This commit is contained in:
4
SWAD.API/.editorconfig
Normal file
4
SWAD.API/.editorconfig
Normal file
@@ -0,0 +1,4 @@
|
||||
[*.cs]
|
||||
|
||||
# CS1591: Отсутствует комментарий XML для открытого видимого типа или члена
|
||||
dotnet_diagnostic.CS1591.severity = none
|
||||
364
SWAD.API/.gitignore
vendored
Normal file
364
SWAD.API/.gitignore
vendored
Normal 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
|
||||
17
SWAD.API/Consts/Enums/MusicService.cs
Normal file
17
SWAD.API/Consts/Enums/MusicService.cs
Normal 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
|
||||
}
|
||||
31
SWAD.API/Consts/Enums/ServiceResult.cs
Normal file
31
SWAD.API/Consts/Enums/ServiceResult.cs
Normal 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
|
||||
}
|
||||
14
SWAD.API/Consts/ErrorResources.cs
Normal file
14
SWAD.API/Consts/ErrorResources.cs
Normal 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";
|
||||
}
|
||||
39
SWAD.API/Controllers/DTOs/LinkDtos.cs
Normal file
39
SWAD.API/Controllers/DTOs/LinkDtos.cs
Normal 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);
|
||||
111
SWAD.API/Controllers/LinkController.cs
Normal file
111
SWAD.API/Controllers/LinkController.cs
Normal 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?");
|
||||
}
|
||||
}
|
||||
}
|
||||
22
SWAD.API/Controllers/ProblemsController.cs
Normal file
22
SWAD.API/Controllers/ProblemsController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
11
SWAD.API/Controllers/SwadController.cs
Normal file
11
SWAD.API/Controllers/SwadController.cs
Normal 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
15
SWAD.API/Dockerfile
Normal 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"]
|
||||
3
SWAD.API/Exceptions/TrackNotFoundException.cs
Normal file
3
SWAD.API/Exceptions/TrackNotFoundException.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace SWAD.API.Exceptions;
|
||||
|
||||
public class TrackNotFoundException(string? message) : Exception(message);
|
||||
90
SWAD.API/Middlewares/ExceptionMiddleware.cs
Normal file
90
SWAD.API/Middlewares/ExceptionMiddleware.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
13
SWAD.API/Models/Config/ApiServices/ApiServicesConfig.cs
Normal file
13
SWAD.API/Models/Config/ApiServices/ApiServicesConfig.cs
Normal 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!;
|
||||
}
|
||||
10
SWAD.API/Models/Config/ApiServices/Structure/ApiPaths.cs
Normal file
10
SWAD.API/Models/Config/ApiServices/Structure/ApiPaths.cs
Normal 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!;
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
8
SWAD.API/Models/Config/ServicesEndpointsConfig.cs
Normal file
8
SWAD.API/Models/Config/ServicesEndpointsConfig.cs
Normal 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!;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace SWAD.API.Models.JsonStructures.MusicAPI.Spotify;
|
||||
|
||||
/// <summary>
|
||||
/// Json Structure for spotify auth response
|
||||
/// </summary>
|
||||
public class SpotifyAuthResponse : DefaultAuthResponse;
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace SWAD.API.Models.JsonStructures.MusicAPI.Tidal;
|
||||
|
||||
public class TidalAuthResponse : DefaultAuthResponse;
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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
34
SWAD.API/Program.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
37
SWAD.API/Properties/launchSettings.json
Normal file
37
SWAD.API/Properties/launchSettings.json
Normal 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
37
SWAD.API/SWAD.API.csproj
Normal 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>
|
||||
121
SWAD.API/Services/Links/LinksService.cs
Normal file
121
SWAD.API/Services/Links/LinksService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
SWAD.API/Services/MusicAPI/Api/ApiService.cs
Normal file
35
SWAD.API/Services/MusicAPI/Api/ApiService.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
109
SWAD.API/Services/MusicAPI/Api/SpotifyService.cs
Normal file
109
SWAD.API/Services/MusicAPI/Api/SpotifyService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
189
SWAD.API/Services/MusicAPI/Api/TidalService.cs
Normal file
189
SWAD.API/Services/MusicAPI/Api/TidalService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
88
SWAD.API/Services/MusicAPI/Api/YandexService.cs
Normal file
88
SWAD.API/Services/MusicAPI/Api/YandexService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
33
SWAD.API/Services/MusicAPI/Auth/AbstractAuthService.cs
Normal file
33
SWAD.API/Services/MusicAPI/Auth/AbstractAuthService.cs
Normal 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; }
|
||||
}
|
||||
44
SWAD.API/Services/MusicAPI/Auth/SpotifyAuthService.cs
Normal file
44
SWAD.API/Services/MusicAPI/Auth/SpotifyAuthService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
46
SWAD.API/Services/MusicAPI/Auth/TidalAuthService.cs
Normal file
46
SWAD.API/Services/MusicAPI/Auth/TidalAuthService.cs
Normal 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
117
SWAD.API/Startup.cs
Normal 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
73
SWAD.API/appsettings.json
Normal 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
249
SWAD.API/doc.xml
Normal 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
119
SWAD.API/index.html
Normal 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>
|
||||
1673
SWAD.API/wwwroot/swagger-ui/theme-flattop.css
Normal file
1673
SWAD.API/wwwroot/swagger-ui/theme-flattop.css
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user