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

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

19
.github/workflows/docker-deploy.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Deploy
on:
push:
branches: [ "main" ]
jobs:
update:
runs-on: self-hosted
steps:
- name: Update Git Repository
working-directory: /home/swapdude/MainStack
run: |
pwd
git fetch --all
git reset --hard origin/main
- name: Build & Deploy on docker-compose
working-directory: /home/swapdude/MainStack
run: |
docker compose up -d --build

26
.github/workflows/dotnet.yml vendored Normal file
View File

@ -0,0 +1,26 @@
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
name: Build Project .NET
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore

415
.gitignore vendored
View File

@ -1,402 +1,13 @@
# ---> VisualStudio
## 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/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# 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
nunit-*.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/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.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
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# 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
# NuGet Symbol Packages
*.snupkg
# 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
*.appxbundle
*.appxupload
# 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
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# 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 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# 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/
# 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/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
################################################################################
# Данный GITIGNORE-файл был автоматически создан Microsoft(R) Visual Studio.
################################################################################
/SWAD.API/appsettings.local.json
/SWAD.API/appsettings.Development.json
.vs
.idea
#Rider sucks
.idea/.idea.SWAD.API/.idea/workspace.xml
SWAD.Front/node_modules
TelegramBot/obj

365
ApiTest/.gitignore vendored Normal file
View File

@ -0,0 +1,365 @@
## 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
# Custom added by ghogen
settings.json

38
ApiTest/ApiTest.csproj Normal file
View File

@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Bogus" Version="35.5.0" />
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="NUnit" Version="3.14.0"/>
<PackageReference Include="NUnit.Analyzers" Version="3.9.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SWAD.API\SWAD.API.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.json">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
</Project>

View File

@ -0,0 +1,26 @@
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using SWAD.API.Models.Config.ApiServices;
using SWAD.API.Services.MusicAPI.Auth;
namespace ApiTest.MusicApi;
public class Spotify
{
[Test]
public async Task SpotifyAuthTest()
{
var confbuilder = new ConfigurationBuilder().SetBasePath(Environment.CurrentDirectory)
.AddJsonFile("appsettings.json")
.Build().GetSection(ApiServicesConfig.ConfigName);
//throw new Exception(Environment.CurrentDirectory);
IOptions<ApiServicesConfig> config = new OptionsWrapper<ApiServicesConfig>(confbuilder.Get<ApiServicesConfig>());
var authService = new SpotifyAuthService(config);
var authResponse = await authService.GetToken();
authResponse.Should()
.NotBeNull();
authResponse.Token.Should()
.NotBeNull();
}
}

View File

@ -1,3 +1,41 @@
# SwapDude
Platform for sharing music links from one music service to another for free!
SwapDude - Platform for sharing music links from one music service to another for free!
![build dotnet](https://github.com/SpectruMProjects/SwapDude/actions/workflows/dotnet.yml/badge.svg)
## Supported Services*:
- [X] Spotify
- [ ] Deezer
- [X] Tidal
- [ ] Apple Music
- [X] Yandex.Music
- [ ] VK/BOOM
- ~~SoundCloud~~
_*Where_ &#9745; _is released and_ &#9744; _in development or ~~not planned~~_
## Features*:
- [ ] Creating link for multiplie services
- [X] Finding links with track on another services
- [ ] Proxy links to service based on your SWAD account
_*Where_ &#9745; _is released and_ &#9744; _in development._
## Source code:
### Stack:
#### Angular (Front-end SPA)
##### Projects:
SWAD.Front
#### C# Asp.NET (Back-end REST API)
##### Projects:
SWAD.API
### Build:
```docker compose up```
So i think you are not so stupid to guess it yourself. Enjoy!
## Problem songs:
https://open.spotify.com/track/0IF9nrOlmE3iVKRkJ1QkuQ - (Possible latin symbols instead cyrilic)
response: not found

42
SWAD.API.sln Normal file
View File

@ -0,0 +1,42 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34330.188
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SWAD.API", ".\SWAD.API\SWAD.API.csproj", "{36313BFE-46A8-44E7-BD08-2652A90C03D1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{30A2E1AB-5617-47D9-9FB5-F441645C4513}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiTest", "ApiTest\ApiTest.csproj", "{DF986307-4329-49A6-926E-C23D34C65173}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TelegramBot", "TelegramBot\TelegramBot.csproj", "{E4B6CD72-213C-404C-824F-3A74C1722EAD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{36313BFE-46A8-44E7-BD08-2652A90C03D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{36313BFE-46A8-44E7-BD08-2652A90C03D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36313BFE-46A8-44E7-BD08-2652A90C03D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36313BFE-46A8-44E7-BD08-2652A90C03D1}.Release|Any CPU.Build.0 = Release|Any CPU
{DF986307-4329-49A6-926E-C23D34C65173}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DF986307-4329-49A6-926E-C23D34C65173}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DF986307-4329-49A6-926E-C23D34C65173}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DF986307-4329-49A6-926E-C23D34C65173}.Release|Any CPU.Build.0 = Release|Any CPU
{E4B6CD72-213C-404C-824F-3A74C1722EAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E4B6CD72-213C-404C-824F-3A74C1722EAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E4B6CD72-213C-404C-824F-3A74C1722EAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E4B6CD72-213C-404C-824F-3A74C1722EAD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {31ECAE7B-5C42-4C80-A7F8-81DADD6DAD8E}
EndGlobalSection
EndGlobal

3
SWAD.API.sln.DotSettings Normal file
View File

@ -0,0 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=appsettings/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=SWAD/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

4
SWAD.API/.editorconfig Normal file
View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

15
SWAD.API/Dockerfile Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

8
SWAD.code-workspace Normal file
View File

@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

42
SwapDude.Front/.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

13
SwapDude.Front/.hintrc Normal file
View File

@ -0,0 +1,13 @@
{
"extends": [
"development"
],
"hints": {
"axe/name-role-value": [
"default",
{
"link-name": "off"
}
]
}
}

View File

@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

20
SwapDude.Front/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

42
SwapDude.Front/.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

27
SwapDude.Front/README.md Normal file
View File

@ -0,0 +1,27 @@
# SwapDudeFront
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.1.3.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

View File

@ -0,0 +1,99 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"SwapDude.Front": {
"i18n": {
"sourceLocale": "en-UK",
"locales": { "ru-RU": "src/locale/messages.ru.xlf" }
},
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
//"localize": ["ru-RU"],
"outputPath": "dist/swap-dude.front",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
"scripts": [],
"server": "src/main.server.ts",
"prerender": true,
"ssr": {
"entry": "server.ts"
}
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "SwapDude.Front:build:production"
},
"development": {
"buildTarget": "SwapDude.Front:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "SwapDude.Front:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
"scripts": []
}
}
}
}
},
"cli": {
"analytics": "be9d04c5-9ecd-4e58-a026-e4c863827ae9"
}
}

12343
SwapDude.Front/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,48 @@
{
"name": "swap-dude.front",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"serve:ssr:SwapDude.Front": "node dist/swap-dude.front/server/server.mjs"
},
"private": true,
"dependencies": {
"@angular/animations": "^17.1.0",
"@angular/common": "^17.1.0",
"@angular/compiler": "^17.1.0",
"@angular/core": "^17.1.0",
"@angular/forms": "^17.1.0",
"@angular/platform-browser": "^17.1.0",
"@angular/platform-browser-dynamic": "^17.1.0",
"@angular/platform-server": "^17.1.0",
"@angular/router": "^17.1.0",
"@angular/ssr": "^17.1.3",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.2",
"express": "^4.18.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.1.3",
"@angular/cli": "^17.1.3",
"@angular/compiler-cli": "^17.1.0",
"@angular/localize": "^17.1.0",
"@types/express": "^4.17.17",
"@types/jasmine": "~5.1.0",
"@types/node": "^18.18.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.3.2"
}
}

56
SwapDude.Front/server.ts Normal file
View File

@ -0,0 +1,56 @@
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import bootstrap from './src/main.server';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const indexHtml = join(serverDistFolder, 'index.server.html');
const commonEngine = new CommonEngine();
server.set('view engine', 'html');
server.set('views', browserDistFolder);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('*.*', express.static(browserDistFolder, {
maxAge: '1y'
}));
// All regular routes use the Angular engine
server.get('*', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
})
.then((html) => res.send(html))
.catch((err) => next(err));
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
run();

View File

@ -0,0 +1,2 @@
<app-home></app-home>
<router-outlet />

View File

@ -0,0 +1,29 @@
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have the 'SwapDude.Front' title`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('SwapDude.Front');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, SwapDude.Front');
});
});

View File

@ -0,0 +1,14 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { HomeComponent } from "./pages/home/home.component";
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
imports: [RouterOutlet, HomeComponent]
})
export class AppComponent {
title = 'SwapDude.Front';
}

View File

@ -0,0 +1,11 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering()
]
};
export const config = mergeApplicationConfig(appConfig, serverConfig);

View File

@ -0,0 +1,9 @@
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideClientHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes), provideClientHydration()]
};

View File

@ -0,0 +1,3 @@
import { Routes } from '@angular/router';
export const routes: Routes = [];

View File

@ -0,0 +1,4 @@
<div class="container text-center">
<h1><a href="{{ domain }}">SwapDude.pro</a></h1>
<h3 i18n>Get links for your friends now so easy!</h3>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HomeComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,18 @@
import { DOCUMENT } from '@angular/common';
import { Component, Inject } from '@angular/core';
@Component({
selector: 'app-home',
standalone: true,
imports: [],
templateUrl: './home.component.html',
styleUrl: './home.component.scss'
})
export class HomeComponent {
domain: string
title = "SwapDude.pro - Home"
constructor(@Inject(DOCUMENT) private document: Document) { }
ngOnInit(){
this.domain = this.document.location.hostname;
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>SwapDudeFront</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en-UK" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="899810183811423241" datatype="html">
<source>Делится ссылками на музыку с друзьями теперь ТАК просто!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/pages/home/home.component.html</context>
<context context-type="linenumber">3,4</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en-UK" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="899810183811423241" datatype="html">
<source>Get links for your friends now easy!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/pages/home/home.component.html</context>
<context context-type="linenumber">3,4</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -0,0 +1,7 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { config } from './app/app.config.server';
const bootstrap = () => bootstrapApplication(AppComponent, config);
export default bootstrap;

View File

@ -0,0 +1,8 @@
/// <reference types="@angular/localize" />
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));

View File

@ -0,0 +1,4 @@
/* You can add global styles to this file, and also import other style files */
/* Importing Bootstrap SCSS file. */
@import 'bootstrap/scss/bootstrap';

View File

@ -0,0 +1,19 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": [
"node",
"@angular/localize"
]
},
"files": [
"src/main.ts",
"src/main.server.ts",
"server.ts"
],
"include": [
"src/**/*.d.ts"
]
}

View File

@ -0,0 +1,34 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"strictPropertyInitialization": false,
"lib": [
"ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@ -0,0 +1,15 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine",
"@angular/localize"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

366
TelegramBot/.gitignore vendored Normal file
View File

@ -0,0 +1,366 @@
## 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
# Custom added by ghogen
settings.json
telegramconfig.local.json

61
TelegramBot/BotHandler.cs Normal file
View File

@ -0,0 +1,61 @@
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using TelegramBot.Commands;
using TelegramBot.Commands.CommandMessages;
namespace TelegramBot;
public static class BotHandler
{
private static readonly LinkMessage LinkMessageProvider = new();
private static readonly StartCommandMessage StartCommandMessageProvider = new();
private static readonly ChangelogCommandMessage ChangelogCommandMessageProvider = new();
private static readonly ErrorMessage ErrorMessageProvider = new();
public static async Task HandleUpdateAsync(
ITelegramBotClient botClient,
Update update,
CancellationToken cancellationToken
)
{
switch (update.Type)
{
case UpdateType.Message:
switch (update.Message?.Text)
{
case StartCommandMessage.CommandName:
await StartCommandMessageProvider.InvokeFromMessage(botClient, update, cancellationToken);
break;
case ChangelogCommandMessage.CommandName:
await ChangelogCommandMessageProvider.InvokeFromMessage(botClient, update, cancellationToken);
break;
default:
Console.WriteLine(update.Message?.Text);
if (string.IsNullOrEmpty(update.Message?.Text))
{
await ErrorMessageProvider.InvokeFromMessage(botClient, update, cancellationToken);
break;
}
await LinkMessageProvider.InvokeFromMessage(botClient, update, cancellationToken);
break;
}
break;
case UpdateType.InlineQuery:
await LinkMessageProvider.InvokeFromInlineQuery(botClient, update, cancellationToken);
break;
}
}
public static Task HandleErrorAsync(
ITelegramBotClient botClient,
Exception exception,
CancellationToken cancellationToken
)
{
Console.WriteLine("Ошибка ядра:\n--------------------------");
Console.WriteLine(exception);
return Task.CompletedTask;
// throw exception;
}
}

View File

@ -0,0 +1,20 @@
using System.Text;
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
namespace TelegramBot.Commands.CommandMessages;
public class ChangelogCommandMessage : CommandMessage
{
public const string CommandName = "/changelog";
public override async Task InvokeFromMessage(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
{
var text = new StringBuilder();
foreach (var log in Program.Config.Changelog)
{
text.Append($"Нововведения от <b>{log.Date}</b>:\n\n{log.Caption}\n\n");
}
await botClient.SendTextMessageAsync(update.Message?.Chat!, text.ToString(), cancellationToken: cancellationToken, parseMode: ParseMode.Html);
}
}

View File

@ -0,0 +1,12 @@
using System.Diagnostics;
using System.Reflection;
using Telegram.Bot;
using Telegram.Bot.Types;
namespace TelegramBot.Commands.CommandMessages;
public abstract class CommandMessage : IMessage
{
public abstract Task InvokeFromMessage(ITelegramBotClient botClient, Update update,
CancellationToken cancellationToken);
}

View File

@ -0,0 +1,20 @@
using System.Diagnostics;
using System.Reflection;
using Telegram.Bot;
using Telegram.Bot.Types;
namespace TelegramBot.Commands.CommandMessages;
public class StartCommandMessage : CommandMessage
{
public const string CommandName = "/start";
public override async Task InvokeFromMessage(ITelegramBotClient botClient,
Update update,
CancellationToken cancellationToken)
{
var text = Program.Config.StartMessage;
text += $"\nBuild assembly version: {FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion}";
text += $"\n\nНововведения по комманде /changelog";
await botClient.SendTextMessageAsync(update.Message?.Chat!, text, cancellationToken: cancellationToken);
}
}

View File

@ -0,0 +1,12 @@
using Telegram.Bot;
using Telegram.Bot.Types;
namespace TelegramBot.Commands;
public class ErrorMessage : IMessage
{
public async Task InvokeFromMessage(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
{
await botClient.SendTextMessageAsync(update.Message?.Chat!, "Ошибка! Команда не найдена", cancellationToken: cancellationToken);
}
}

View File

@ -0,0 +1,11 @@
using Telegram.Bot;
using Telegram.Bot.Types;
namespace TelegramBot.Commands;
public interface IMessage
{
public Task InvokeFromMessage(ITelegramBotClient botClient,
Update update,
CancellationToken cancellationToken);
}

View File

@ -0,0 +1,98 @@
using System.Net.Http.Json;
using System.Text;
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegram.Bot.Types.InlineQueryResults;
using TelegramBot.DTOs;
namespace TelegramBot.Commands;
public class LinkMessage : IMessage
{
/// <summary>
/// Create message from API
/// </summary>
/// <param name="botClient"></param>
/// <param name="inputUrl"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
async Task<(string? message, string? preview)> GetMessage(ITelegramBotClient botClient, string inputUrl,
CancellationToken cancellationToken)
{
try
{
using var client = new HttpClient();
var res = await client.GetAsync($"{Program.Config.ApiPath}/link/getService?link=" + inputUrl,
cancellationToken);
var typeRes = await res.Content.ReadFromJsonAsync<GetLink>(cancellationToken: cancellationToken);
if (!res.IsSuccessStatusCode)
{
return (string.Empty, $"Произошла ошибка при обработке запроса\n{res.StatusCode.ToString()}");
}
var message = new StringBuilder();
var preview = new StringBuilder();
foreach (var service in typeRes?.service.Another()!)
{
res = await client.PostAsJsonAsync($"{Program.Config.ApiPath}/link/fromLink", new
{
link = inputUrl,
service,
}, cancellationToken: cancellationToken);
if (!res.IsSuccessStatusCode)
{
preview.AppendLine($"❌ {service}\n");
message.AppendLine(
$"❌ {service}: 🤨👨🏿‍🦰😡😮‍💨😰 {(await res.Content.ReadFromJsonAsync<ErrorDto>(cancellationToken: cancellationToken))?.title}");
message.AppendLine();
continue;
}
var apiRes = await res.Content.ReadFromJsonAsync<LinkDto>(cancellationToken: cancellationToken);
preview.AppendLine($"✅ {service}\n");
message.AppendLine($"✅ {service} - {apiRes?.link}");
message.AppendLine();
}
return (message.ToString(), preview.ToString());
}
catch (Exception e)
{
Console.WriteLine("Ошибка:\n--------------------------");
Console.WriteLine(e);
return (e.ToString(), "Произошла ошибка при обработке запроса\nдетали в сообщении");
}
}
public async Task InvokeFromMessage(ITelegramBotClient botClient, Update update,
CancellationToken cancellationToken)
{
var result = (await GetMessage(botClient, update.Message?.Text!, cancellationToken)).message;
await botClient.SendTextMessageAsync(update.Message?.Chat!, string.IsNullOrEmpty(result) ? "Ошибка🤔:\nНе удалось определить сервис исходной ссылки.\nДоступные сервисы для конвертации:\nTidal\nYandex\nSpotify" : result, cancellationToken: cancellationToken);
}
public async Task InvokeFromInlineQuery(ITelegramBotClient botClient, Update update,
CancellationToken cancellationToken)
{
if(string.IsNullOrEmpty(update.InlineQuery!.Query))
return;
var message = await new LinkMessage().GetMessage(botClient, update.InlineQuery!.Query, cancellationToken);
if(message.message != null)
try
{
await botClient.AnswerInlineQueryAsync(update.InlineQuery.Id,
new[]
{
new InlineQueryResultArticle("0", message.preview!,
new InputTextMessageContent(message.message))
}, cancellationToken: cancellationToken);
}
catch (Exception e)
{
Console.WriteLine($"Failed to send answer to {update.Message?.Chat.Username}");
}
}
}

View File

@ -0,0 +1,15 @@
namespace TelegramBot.Config;
public class AppSettings
{
public string? Token { get; init; }
public string? ApiPath { get; init; }
public string? StartMessage { get; init; }
public List<Changelog> Changelog { get; init; } = null!;
}
public class Changelog
{
public string? Date { get; init; }
public string? Caption { get; init; }
}

View File

@ -0,0 +1,15 @@
using SWAD.API.Consts.Enums;
namespace TelegramBot.DTOs;
public record GetLink(
MusicService service
);
public record LinkDto(
string? link
);
public record ErrorDto(
string title
);

15
TelegramBot/Dockerfile Normal file
View File

@ -0,0 +1,15 @@
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /source
# copy csproj and restore as distinct layers
COPY ./ ./
RUN dotnet restore
WORKDIR /source/TelegramBot
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/TelegramBot.dll"]

View File

@ -0,0 +1,13 @@
using SWAD.API.Consts.Enums;
namespace TelegramBot;
public static class EnumExtensions
{
public static List<MusicService> Another(this MusicService service)
{
var list = Enum.GetValues<MusicService>().ToList();
list.Remove(service);
return list;
}
}

71
TelegramBot/Program.cs Normal file
View File

@ -0,0 +1,71 @@
using System.Diagnostics;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Telegram.Bot;
using Telegram.Bot.Polling;
using Microsoft.Extensions.Configuration;
using TelegramBot.Config;
namespace TelegramBot;
public static class Program
{
private static ILogger Logger { get; } = LoggerFactory.Create(x => { x.AddConsole(); })
.CreateLogger("MainThread");
private static ITelegramBotClient bot { get; set; }
public static AppSettings Config { get; private set; }
public static void Main()
{
Splash(Logger);
LoadConfig();
var cts = LoadBot();
while(!cts.IsCancellationRequested)
Thread.Sleep(1000);
}
private static void Splash(ILogger logger)
{
var splash = """
____ ____ _ ____ _
/ ___|_ ____ _ _ __ | _ \ _ _ __| | ___ | __ ) ___ | |_
\___ \ \ /\ / / _` | '_ \| | | | | | |/ _` |/ _ \ | _ \ / _ \| __|
___) \ V V / (_| | |_) | |_| | |_| | (_| | __/_| |_) | (_) | |_
|____/ \_/\_/ \__,_| .__/|____/ \__,_|\__,_|\___(_)____/ \___/ \__|
|_|
""";
logger.LogWarning(splash);
logger.LogInformation(
$"Build assembly version: {FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion}");
}
private static void LoadConfig()
{
Logger.LogInformation("Loading config...");
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddJsonFile("telegramconfig.json", optional: false)
#if DEBUG
.AddJsonFile("telegramconfig.local.json", true, true)
#endif
.Build();
Config = configuration.GetSection("AppSettings").Get<AppSettings>() ?? throw new NullReferenceException();
}
private static CancellationTokenSource LoadBot()
{
Logger.LogInformation("Creating bot...");
bot = new TelegramBotClient(Config.Token ?? throw new NullReferenceException());
var cts = new CancellationTokenSource();
var receiverOptions = new ReceiverOptions
{
AllowedUpdates = { }
};
bot.StartReceiving(BotHandler.HandleUpdateAsync,
BotHandler.HandleErrorAsync, receiverOptions, cts.Token);
return cts;
}
}

View File

@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<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, Sluppy(Gl3b4ty)</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>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Telegram.Bot" Version="19.0.0" />
<PackageReference Include="Telegram.Bots.Extensions.Polling" Version="5.9.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SWAD.API\SWAD.API.csproj" />
</ItemGroup>
<ItemGroup>
<None Remove="telegramconfig.json" />
<Content Include="telegramconfig.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="telegramconfig.local.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Remove="telegramconfig.local.json" />
<Content Include="telegramconfig.local.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

Some files were not shown because too many files have changed in this diff Show More