Compare commits

..

99 Commits

Author SHA1 Message Date
kdeng00 a1edbeb164 Fixed issue of the configuration verification test failing 2020-02-15 17:37:51 -05:00
kdeng00 437a6b1453 Fixed issue with special characters not being recorded properly 2020-02-15 17:32:52 -05:00
Kun Deng f2d0947b36 Merge pull request #63 from kdeng00/dist_change
API is functional again after the dependency updates
2020-02-02 20:33:48 -05:00
kdeng00 b067882c09 API is functional again after the dependency updates 2020-02-02 20:32:09 -05:00
kdeng00 57f76ee4a4 api is functional after the dependency updates 2020-02-01 20:58:48 -05:00
kdeng00 e846989a7f Not finished updating dependencies. oatpp has changed 2020-01-30 19:30:21 -05:00
kdeng00 91ae3a908e Updated json dependency 2020-01-29 19:12:23 -05:00
kdeng00 a970ba207e updating dependencies and code refactoring for the new changes 2020-01-28 21:03:56 -05:00
kdeng00 979dd66842 Made it easier to build the bcrypt library 2019-11-17 13:55:01 -05:00
kdeng00 69f5a37eb3 Code cleanup 2019-11-17 13:33:58 -05:00
kdeng00 ca1fcb801d Added functionality to check if the song already exists when uploading 2019-11-13 20:22:29 -05:00
kdeng00 3b0f199dca forgout about one file 2019-11-12 23:03:42 -05:00
kdeng00 41ae06e19a Fixed bug when song upload HTTP endpoint returns a response of the uploading song with errors 2019-11-12 22:51:07 -05:00
kdeng00 9b4a0db67a Added albumArtist field memeber to Song model. Automatically apply disc value 1 for song's metadata that have an empty disc field. Switched from TagLib::FileRef to TagLib::MPEG::File 2019-11-04 22:26:03 -05:00
kdeng00 c7bbdfe581 Code clean up 2019-11-03 13:03:32 -05:00
Kun Deng 01faa60699 Update README.md 2019-10-26 10:31:46 -04:00
amazing-username 8714d76a2a Changes to response returns when song does not exist. The cause of a segmentation fault 2019-10-26 14:23:42 +00:00
kdeng00 e764bc45de When song is uploaded the response returned is info about the song and a response 2019-10-22 20:30:43 -04:00
kdeng00 5fefc451d5 Adding coverart id field to Song dto object to return the coverart id that the song is associated with so it is easier to get the cover art 2019-10-21 21:54:50 -04:00
kdeng00 024134b801 some changes 2019-10-13 11:29:40 -04:00
kdeng00 32ed611112 Authorized endpoints and updated README 2019-09-25 23:03:38 -04:00
kdeng00 698ccf9099 slight change in README.md 2019-09-24 22:31:23 -04:00
kdeng00 1b8c28e67b added user authentication and updated README.md 2019-09-24 22:29:09 -04:00
kdeng00 e936c2da17 Solved issue where I couldn't retirever a user record from the database. Didn't correctly set the MYSql type to string. 2019-09-23 21:38:33 -04:00
kdeng00 4bd794c288 Working on User authentication 2019-09-22 23:57:14 -04:00
kdeng00 89ed9f9252 Updated README.md and completed verification process 2019-09-20 20:56:24 -04:00
kdeng00 8ee39e13c0 Confirmed database connectivity 2019-09-19 22:03:23 -04:00
kdeng00 6a5ebc0078 path configuration file is verified at the start of execution 2019-09-17 20:47:53 -04:00
kdeng00 bb783c61ad started working on function to verify the configuration settings 2019-09-16 22:26:17 -04:00
kdeng00 6e570cd495 finished updating song functionality 2019-09-15 20:57:41 -04:00
kdeng00 9a894487ad Almost done with updating metadata of song, just need to handle records that have no related records associated with them 2019-09-15 00:23:08 -04:00
kdeng00 a82e66ff83 Working on updating a song's metadata 2019-09-13 23:12:09 -04:00
kdeng00 2fdc86e0b3 Figured out how to check if Int32 values from the ObjectWrapper have any values. Just get the pointer of it and see if it is a nullptr 2019-09-12 21:59:29 -04:00
kdeng00 0a97f695e3 Working on updating a song's metadata 2019-09-11 22:12:41 -04:00
kdeng00 a00a70029e Implemented deletion functionality, and lingering records are removed fixes #49 2019-09-10 22:27:28 -04:00
kdeng00 8c1c2340b2 Working on HTTP endpoint to delete songs. Finished with album. Need to work on the Artist, Genre, and Year aspects 2019-09-09 21:57:04 -04:00
kdeng00 19ddb9be0b Added song streamingh endpoint and removed redundant making oatpp::String shared when it already is a shared object #61 2019-09-08 18:48:14 -04:00
kdeng00 949abf8244 stream does not contain the whole song, also when the stream is terminated on the client's side, the service crashed on the server side 2019-09-07 18:30:03 -04:00
kdeng00 fd3a84fbea Working on streaming the song, running into issues trying to figure this out. Wanted to at least commit the work 2019-09-07 15:41:38 -04:00
kdeng00 7ec88397c0 Able to retrieve single and multiple records (Album, Artist, Genre, Year) 2019-09-07 12:00:12 -04:00
kdeng00 e9241cf6a5 Need to work on the ability to fetch records for Artist, Genre, and Year 2019-09-06 22:38:14 -04:00
kdeng00 de838da040 Can fetch cover art records and download a single cover art 2019-09-06 22:13:52 -04:00
kdeng00 ada50b3d5c Still in the process of switching to prepared statements. Left TODO for removing the make_shared_ptr for the raw song data. Added feature to retrieve a sing cover art record in json format 2019-09-05 20:57:13 -04:00
kdeng00 5b9f2a0505 fix for duplicate records being saved. I'm going to continually monitor it, some minor changes might need to be made. The changes are changing to prepared statements for queries 2019-09-04 23:48:37 -04:00
amazing-username d4e9b8c71f Tidied up code, going to put up the coding convention later on 2019-09-04 15:41:43 +00:00
Kun Deng dd995269df Merge pull request #60 from kdeng00/experiment
Switching to C++
2019-09-03 16:17:19 -04:00
kdeng00 12a53ed2b1 Added some TODO's. Need to switch to prepared statements for album, artist, genre, and year repositories 2019-09-03 16:15:37 -04:00
kdeng00 c55d8261a2 Added TODO in the albumRepository.cpp on where to pick up from. Short story: duplicate related records 2019-09-02 20:16:30 -04:00
kdeng00 4306c174b3 Able to save and retrieve records (Artist, Album, Genre, etc.) 2019-09-02 13:59:49 -04:00
kdeng00 fafb39326b Left off at adding album record to the database and retrieving the album record to assign the album id to the song record's album id foreign key 2019-09-01 18:46:02 -04:00
kdeng00 ab3000c2a0 Moved controllers to include directory 2019-09-01 17:13:31 -04:00
kdeng00 c0fba2c9d8 Move Dto's to Dto in include directory and created Dto namespace 2019-09-01 17:08:00 -04:00
kdeng00 dfd4906371 Added Utility and Type namespaces 2019-09-01 16:56:52 -04:00
kdeng00 1ec3511bc5 Added Controller namespace 2019-09-01 16:16:06 -04:00
kdeng00 f75ebe14a7 Added manager classes for song metadata 2019-09-01 15:10:46 -04:00
kdeng00 d16b8dc3c9 Added Manager namespace 2019-09-01 14:14:46 -04:00
kdeng00 d44a5bb1bc Added Model namespace 2019-09-01 13:54:16 -04:00
kdeng00 1255aa0ff7 track and disc numbers are now part of the song's metadata and saved to the database 2019-08-28 22:16:33 -04:00
kdeng00 b885e3d2c8 Ok 2019-08-27 20:33:00 -04:00
amazing-username d0d1008044 Fixed bug that caused a failure in retrieving records. Empty directories are deleted when deleting a song. If the song contains the stock cover art, the stock cover art will not be deleted 2019-08-27 20:13:18 +00:00
kdeng00 ed6f98eb5a Added song delete endpoint 2019-08-26 23:10:24 -04:00
kdeng00 48e97e7cd1 Added support for saving downloading song. It doesn't work outside of curl from the command line. The response body is empty, I'll look into it 2019-08-25 22:21:21 -04:00
kdeng00 fb03354189 Working on downloading the song 2019-08-25 16:47:52 -04:00
kdeng00 540d665a7e Can retrieve all song records 2019-08-25 15:26:47 -04:00
kdeng00 2145adb039 Some clean up for insert song records into the database 2019-08-25 14:54:09 -04:00
kdeng00 d54715ac25 Able to insert song records into the database. Left TODO for clean up 2019-08-25 01:30:13 -04:00
kdeng00 1db3eb2f8c Cover art record saved in the database. Next is to save the song record to the database and then try to download the song afterwards 2019-08-24 18:41:01 -04:00
kdeng00 9281293fad Working on interfacing with the database. Could do queryies via string/cstring but I'm going to try prepared statements 2019-08-23 23:16:42 -04:00
kdeng00 764185da7d Able to save song and cover art to the appropriate paths. Next is saving it to the database 2019-08-22 22:40:49 -04:00
kdeng00 592853695d Added paths.json file that contain paths 2019-08-22 19:49:17 -04:00
kdeng00 2a8290a929 Added script to create database 2019-08-21 07:09:39 -04:00
kdeng00 92f0d4702b Factoring 2019-08-20 22:45:25 -04:00
kdeng00 e42a551193 Now have to check to see if the token has the correct scope 2019-08-20 19:56:26 -04:00
kdeng00 3e94e0e22e Left some TODO's. Next thing is to check if the token is valid 2019-08-19 22:19:47 -04:00
kdeng00 4072526d35 Able to upload song. Left TODO on what's next 2019-08-18 22:58:04 -04:00
kdeng00 bbd8186114 Added songcontroller, work on uploading a song 2019-08-18 16:44:59 -04:00
kdeng00 e9aa5f799c Added database configuration file 2019-08-18 16:15:47 -04:00
kdeng00 10b546d8b8 removed mysqlconnector c++ submodule. Linking issues, using the C API instead 2019-08-18 16:13:14 -04:00
kdeng00 d26c16aa69 database stuff 2019-08-18 11:57:08 -04:00
kdeng00 5230e69832 more changes 2019-08-17 18:53:50 -04:00
kdeng00 9ae9b08520 changed path of jwt-cpp submodule 2019-08-17 16:17:43 -04:00
kdeng00 0fc16daf86 Tying something out 2019-08-17 16:04:54 -04:00
kdeng00 533a8d81a9 Finished up #55 2019-08-13 21:05:11 -04:00
amazing-username 4d59b311e6 Removed TagLib# and RestSharp dependencies. Updated README.md 2019-08-13 14:38:34 +00:00
amazing-username a561ab0642 Fixed bug where user was not being authenticated 2019-08-13 14:08:03 +00:00
kdeng00 f0a50ba70a Implemented TokenManager #57 2019-08-13 00:11:04 -04:00
kdeng00 da8d4a9002 Implemented #56 2019-08-11 23:24:23 -04:00
kdeng00 51d40f270d Tricky implementing taglib's APIC extracting/saving feature, but I got it. Left some TODO's on where to pick up #56 2019-08-11 17:51:42 -04:00
kdeng00 023c467a9d Fixed issue where stock cover art was not correctly being copied 2019-08-11 16:10:18 -04:00
kdeng00 ea25480ad1 #56 2019-08-10 12:40:26 -04:00
kdeng00 5b19cdaa46 Working on #56 2019-08-10 01:03:52 -04:00
kdeng00 16f0c1aed0 Minor changes to metadata reading 2019-08-08 21:31:13 -04:00
kdeng00 ead71da802 Working on migrating metadata over to c++ 2019-08-08 18:40:15 -04:00
kdeng00 47723c7d94 Starting to switch Metadata portion to c++. Switching from TagLibSharp to Taglib (c++) 2019-08-06 22:51:20 -04:00
kdeng00 02bc671883 Code cleanup, removed some unused dependencies, adding more to C++ lib. #54 and #55 2019-08-05 23:47:43 -04:00
kdeng00 1531cad134 Handling song being moved on the c++ side #55 2019-08-04 17:08:49 -04:00
kdeng00 610d4fb08d Contining work on #55 2019-08-04 14:25:45 -04:00
amazing-username 15948c4783 Directories are created using C++ when a song is uploaded 2019-08-04 14:04:40 +00:00
kdeng00 2d984813ec Updated Entity Framework to 2.2.6, Removed PrintCredentials() Method call on login, working on #55 2019-08-03 14:26:18 -04:00
167 changed files with 7299 additions and 4903 deletions
-13
View File
@@ -1,13 +0,0 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "8.0.7",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}
-25
View File
@@ -1,25 +0,0 @@
name: .NET
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
-18
View File
@@ -3,24 +3,6 @@
################################################################################
/.vs/Icarus
.vscode/*
/bin/*
/bin/Debug/netcoreapp2.2
/Migrations
/obj
/obj/*
/Icarus.txt
/appsettings.json
/appsettings.Development.json
appsettings.Development.json
appsettings.json
Icarus/appsettings.Development.json
Icarus/appsettings.json
Icarus/bin
Icarus/obj
Models/bin
Models/obj
Migrations
Icarus.txt
Storage
.DS_Store
+21
View File
@@ -0,0 +1,21 @@
[submodule "Libs/cpr"]
path = 3rdparty/cpr
url = https://github.com/whoshuu/cpr.git
[submodule "3rdparty/taglib"]
path = 3rdparty/taglib
url = https://github.com/taglib/taglib
[submodule "3rdparty/oatpp"]
path = 3rdparty/oatpp
url = https://github.com/oatpp/oatpp
[submodule "build/3rdparty/jwt-cpp"]
path = build/3rdparty/jwt-cpp
url = https://github.com/Thalhammer/jwt-cpp
[submodule "3rdparty/jwt-cpp"]
path = 3rdparty/jwt-cpp
url = https://github.com/Thalhammer/jwt-cpp
[submodule "3rdparty/ormpp"]
path = 3rdparty/ormpp
url = https://github.com/qicosmos/ormpp
[submodule "3rdparty/libbcrypt"]
path = 3rdparty/libbcrypt
url = https://github.com/rg3/libbcrypt
Vendored Submodule
+1
Submodule 3rdparty/cpr added at 07d784ccfe
Vendored Submodule
+1
Submodule 3rdparty/jwt-cpp added at 65b632b674
Vendored Submodule
+1
Submodule 3rdparty/libbcrypt added at 8aa32ad94e
Vendored Submodule
+1
Submodule 3rdparty/oatpp added at 27c46444db
Vendored Submodule
+1
Submodule 3rdparty/taglib added at e36a9cabb9
+152
View File
@@ -0,0 +1,152 @@
cmake_minimum_required(VERSION 3.10)
project(icarus)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 17)
set(SOURCES
src/callback/StreamCallback.cpp
src/database/AlbumRepository.cpp
src/database/ArtistRepository.cpp
src/database/BaseRepository.cpp
src/database/CoverArtRepository.cpp
src/database/GenreRepository.cpp
src/database/SongRepository.cpp
src/database/UserRepository.cpp
src/database/YearRepository.cpp
src/dto/conversion/DtoConversions.cpp
src/Main.cpp
src/manager/AlbumManager.cpp
src/manager/ArtistManager.cpp
src/manager/CoverArtManager.cpp
src/manager/DirectoryManager.cpp
src/manager/GenreManager.cpp
src/manager/SongManager.cpp
src/manager/TokenManager.cpp
src/manager/UserManager.cpp
src/manager/YearManager.cpp
src/utility/ImageFile.cpp
src/utility/MetadataRetriever.cpp
src/utility/PasswordEncryption.cpp
src/verify/Initialization.cpp
)
set(HEADERS
include/callback/StreamCallback.h
include/component/AppComponent.hpp
include/controller/AlbumController.hpp
include/controller/ArtistController.hpp
include/controller/CoverArtController.hpp
include/controller/GenreController.hpp
include/controller/LoginController.hpp
include/controller/RegisterController.hpp
include/controller/SongController.hpp
include/controller/YearController.hpp
include/database/AlbumRepository.h
include/database/ArtistRepository.h
include/database/BaseRepository.h
include/database/CoverArtRepository.h
include/database/GenreRepository.h
include/database/SongRepository.h
include/database/UserRepository.h
include/database/YearRepository.h
include/dto/AlbumDto.hpp
include/dto/ArtistDto.hpp
include/dto/CoverArtDto.hpp
include/dto/GenreDto.hpp
include/dto/LoginResultDto.hpp
include/dto/SongDto.hpp
include/dto/YearDto.hpp
include/dto/conversion/DtoConversions.h
include/manager/AlbumManager.h
include/manager/ArtistManager.h
include/manager/CoverArtManager.h
include/manager/DirectoryManager.h
include/manager/GenreManager.h
include/manager/SongManager.h
include/manager/TokenManager.h
include/manager/UserManager.h
include/manager/YearManager.h
include/model/Models.h
include/utility/ImageFile.h
include/utility/MetadataRetriever.h
include/utility/PasswordEncryption.h
include/type/AlbumFilter.h
include/type/ArtistFilter.h
include/type/CoverFilter.h
include/type/GenreFilter.h
include/type/PathType.h
include/type/SaltFilter.h
include/type/Scopes.h
include/type/SongChanged.h
include/type/SongFilter.h
include/type/SongUpload.h
include/type/UserFilter.h
include/type/YearFilter.h
include/verify/Initialization.h
)
set (TAGLIB
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/taglib/3rdparty
)
set (BCRYPTLIB
${CMAKE_SOURCE_DIR}/3rdparty/libbcrypt
)
set(TAGLIB_HEADERS
"${CMAKE_SOURCE_DIR}/build/3rdparty/taglib"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/ape"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/asf"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/dsdiff"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/dsf"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/flac"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/it"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/mod"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/mp4"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/mpc"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/mpeg"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/mpeg/id3v2"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/mpeg/id3v2/frames"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/ogg"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/riff"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/s3m"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/toolkit"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/trueaudio"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/wavpack"
"${CMAKE_SOURCE_DIR}/3rdparty/taglib/taglib/xm"
)
set(JWT_CPP_INCLUDE
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/jwt-cpp/include
)
include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()
#find_library(BCRYPT bcrypt ${BCRYPTLIB})
include_directories(include ${CPR_INCLUDE_DIRS} ${TAGLIB} ${TAGLIB_HEADERS} ${BCRYPTLIB})
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/cpr)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/taglib)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/oatpp)
add_library(bcrypt STATIC IMPORTED)
set_target_properties(bcrypt PROPERTIES IMPORTED_LOCATION
"${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/libbcrypt/bcrypt.a"
)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/appsettings.json ${CMAKE_BINARY_DIR}/bin/appsettings.json COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/authcredentials.json ${CMAKE_BINARY_DIR}/bin/authcredentials.json COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/database.json ${CMAKE_BINARY_DIR}/bin/database.json COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/paths.json ${CMAKE_BINARY_DIR}/bin/paths.json COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/Images/Stock/CoverArt.png ${CMAKE_BINARY_DIR}/bin/CoverArt.png COPYONLY)
add_executable(icarus ${SOURCES} ${HEADERS})
target_include_directories(icarus PUBLIC ${JWT_CPP_INCLUDE})
target_link_libraries(icarus "-lstdc++fs" tag oatpp mysqlclient ${CONAN_LIBS} ${CPR_LIBRARIES} bcrypt)
-22
View File
@@ -1,22 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Icarus", "Icarus\Icarus.csproj", "{EC7ADFAA-9CCA-40E4-B035-DC2ADF117CB4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{EC7ADFAA-9CCA-40E4-B035-DC2ADF117CB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EC7ADFAA-9CCA-40E4-B035-DC2ADF117CB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EC7ADFAA-9CCA-40E4-B035-DC2ADF117CB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EC7ADFAA-9CCA-40E4-B035-DC2ADF117CB4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
@@ -1,26 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Icarus.Authorization;
namespace Icarus.Authorization.Handlers;
public class HasScopeHandler : AuthorizationHandler<HasScopeRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement)
{
if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer))
{
return Task.CompletedTask;
}
var scopes = context.User.FindFirst(c =>
c.Type == "scope" && c.Issuer == requirement.Issuer)!.Value.Split(' ');
if (scopes.Any(s => s == requirement.Scope))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
@@ -1,18 +0,0 @@
using System;
using Microsoft.AspNetCore.Authorization;
namespace Icarus.Authorization;
public class HasScopeRequirement : IAuthorizationRequirement
{
public string Issuer { get; }
public string Scope { get; }
public HasScopeRequirement(string scope, string issuer)
{
Scope = scope ?? throw new ArgumentNullException(nameof(scope));
Issuer = issuer ?? throw new ArgumentNullException(nameof(issuer));
}
}
-78
View File
@@ -1,78 +0,0 @@
using System;
using Microsoft.IdentityModel.Tokens;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
namespace Icarus.Certs;
public class SigningIssuerCertificate : IDisposable
{
private readonly System.Security.Cryptography.RSACryptoServiceProvider _rsa;
public SigningIssuerCertificate()
{
_rsa = new System.Security.Cryptography.RSACryptoServiceProvider();
}
public RsaSecurityKey GetIssuerSigningKey(string publicKeyPath)
{
var file = publicKeyPath;
var publicKey = System.IO.File.ReadAllText(file);
using (var reader = System.IO.File.OpenText(file))
{
var pem = new PemReader(reader);
var o = (RsaKeyParameters)pem.ReadObject();
var parameters = new System.Security.Cryptography.RSAParameters();
parameters.Modulus = o.Modulus.ToByteArray();
parameters.Exponent = o.Exponent.ToByteArray();
_rsa.ImportParameters(parameters);
}
return new RsaSecurityKey(_rsa);
}
public void Dispose()
{
_rsa?.Dispose();
}
}
public class SigningAudienceCertificate : IDisposable
{
private readonly System.Security.Cryptography.RSACryptoServiceProvider _rsa;
public SigningAudienceCertificate()
{
_rsa = new System.Security.Cryptography.RSACryptoServiceProvider();
}
public SigningCredentials GetAudienceSigningKey(string keyPath)
{
var file = keyPath;
var publicKey = System.IO.File.ReadAllText(file);
using (var reader = System.IO.File.OpenText(file))
{
var pem = new PemReader(reader);
var o = (RsaKeyParameters)pem.ReadObject();
var parameters = new System.Security.Cryptography.RSAParameters();
parameters.Modulus = o.Modulus.ToByteArray();
parameters.Exponent = o.Exponent.ToByteArray();
_rsa.ImportParameters(parameters);
}
return new SigningCredentials(
key: new RsaSecurityKey(_rsa),
algorithm: SecurityAlgorithms.RsaSha256);
}
public void Dispose()
{
_rsa?.Dispose();
}
}
-146
View File
@@ -1,146 +0,0 @@
using Icarus.Models;
using Icarus.Database.Contexts;
namespace Icarus.Controllers.Managers;
public class AlbumManager : BaseManager
{
#region Fields
private AlbumContext? _albumContext;
#endregion
#region Properties
#endregion
#region Constructors
public AlbumManager(IConfiguration config)
{
_config = config;
_connectionString = _config.GetConnectionString("DefaultConnection");
_albumContext = new AlbumContext(_connectionString!);
}
#endregion
#region Methods
public void SaveAlbumToDatabase(ref Song song)
{
_logger.Info("Starting process to save the album record of the song to the database");
var album = new Album();
album.Title = song.AlbumTitle;
album.AlbumArtist = song.Artist;
album.Year = song.Year!.Value;
var albumTitle = song.AlbumTitle;
var albumArtist = song.Artist;
var albumRetrieved = _albumContext!.Albums!.FirstOrDefault(alb => alb.Title!.Equals(albumTitle) && alb.AlbumArtist!.Equals(albumArtist));
if (albumRetrieved == null)
{
album.SongCount = 1;
_albumContext.Add(album);
_albumContext.SaveChanges();
Console.WriteLine($"Album Id {album.Id}");
}
else
{
album.Id = albumRetrieved.Id;
}
song.AlbumId = album.Id;
}
public void DeleteAlbumFromDatabase(Song song)
{
var album = _albumContext!.Albums!.FirstOrDefault(alb => alb.Title!.Equals(song.AlbumTitle));
if (album == null)
{
_logger.Info("Cannot delete the album record because it does not exist");
return;
}
DeleteAlbumFromDb(album);
}
public Album UpdateAlbumInDatabase(Song oldSong, Song newSong)
{
var albumRecord = _albumContext!.Albums!.FirstOrDefault(alb => alb.Title!.Equals(oldSong.AlbumTitle));
var oldAlbumTitle = oldSong.AlbumTitle;
var oldAlbumArtist = oldSong.Artist;
var newAlbumTitle = newSong.AlbumTitle;
var newAlbumArtist = newSong.Artist;
var info = string.Empty;
if (string.IsNullOrEmpty(newAlbumArtist))
newAlbumArtist = oldAlbumArtist;
if (string.IsNullOrEmpty(newAlbumTitle))
newAlbumTitle = oldAlbumTitle;
if ((string.IsNullOrEmpty(newAlbumTitle) && string.IsNullOrEmpty(newAlbumArtist) ||
oldAlbumTitle!.Equals(newAlbumTitle) && oldAlbumArtist!.Equals(newAlbumArtist)))
{
_logger.Info("No change to the song's album");
return albumRecord!;
}
info = "Change to the song's album";
_logger.Info(info);
var existingAlbumRecord = _albumContext.Albums!.FirstOrDefault(alb => alb.Title!.Equals(oldSong.AlbumTitle));
if (existingAlbumRecord == null)
{
_logger.Info("Creating new album record");
var newAlbumRecord = new Album
{
Title = newAlbumTitle,
AlbumArtist = newAlbumArtist,
Year = newSong.Year!.Value
};
_albumContext.Add(newAlbumRecord);
_albumContext.SaveChanges();
return newAlbumRecord;
}
else
{
_logger.Info("Updating existing album record");
existingAlbumRecord = _albumContext.Albums!.FirstOrDefault(alb => alb.Title!.Equals(newSong.AlbumTitle));
existingAlbumRecord!.AlbumArtist = newAlbumArtist;
_albumContext.Update(existingAlbumRecord);
_albumContext.SaveChanges();
return existingAlbumRecord;
}
}
private void DeleteAlbumFromDb(Album album)
{
if (SongsInAlbum(album) <= 1)
{
_albumContext!.Remove(album);
_albumContext.SaveChanges();
}
}
private int SongsInAlbum(Album album)
{
var sngContext = new SongContext(_connectionString!);
var songs = sngContext!.Songs!.Where(sng => sng.AlbumId == album.Id).ToList();
return songs.Count;
}
#endregion
}
@@ -1,128 +0,0 @@
using Icarus.Models;
using Icarus.Database.Contexts;
namespace Icarus.Controllers.Managers;
public class ArtistManager : BaseManager
{
#region Fields
private ArtistContext? _artistContext;
#endregion
#region Properties
#endregion
#region Constructors
public ArtistManager(IConfiguration config)
{
_config = config;
_connectionString = _config.GetConnectionString("DefaultConnection");
_artistContext = new ArtistContext(_connectionString!);
}
#endregion
#region Methods
public void SaveArtistToDatabase(ref Song song)
{
_logger.Info("Starting process to save the artist record of the song to the database");
var artist = new Artist
{
Name = song.Artist,
SongCount = 1
};
var artistRetrieved = _artistContext!.Artists.FirstOrDefault(art => art.Name!.Equals(artist.Name));
if (artistRetrieved == null)
{
artist.SongCount = 1;
_artistContext.Add(artist);
_artistContext.SaveChanges();
}
else
{
artist.Id = artistRetrieved.Id;
}
song.ArtistId = artist.Id;
}
public Artist UpdateArtistInDatabase(Song oldSongRecord, Song newSongRecord)
{
var oldArtistRecord = _artistContext!.Artists.FirstOrDefault(art => art.Name!.Equals(oldSongRecord.AlbumTitle));
var oldArtistName = oldArtistRecord!.Name;
var newArtistName = newSongRecord.Artist;
if (string.IsNullOrEmpty(newArtistName) || oldArtistName!.Equals(newArtistName))
{
_logger.Info("No change to the song's Artist");
return oldArtistRecord;
}
_logger.Info("Change to the song's record found");
if (oldArtistRecord.SongCount <= 1)
{
_logger.Info("Deleting artist record that no longer has any songs");
_artistContext.Remove(oldArtistRecord);
_artistContext.SaveChanges();
}
if (!(_artistContext.Artists.FirstOrDefault(art => art.Name!.Equals(oldSongRecord.AlbumTitle)) != null))
{
_logger.Info("Creating new artist record");
var newArtistRecord = new Artist
{
Name = newArtistName
};
_artistContext.Add(newArtistRecord);
_artistContext.SaveChanges();
return newArtistRecord;
}
else
{
_logger.Info("Updating existing artist record");
var existingArtistRecord = _artistContext.Artists.FirstOrDefault(art => art.Name!.Equals(newSongRecord.AlbumTitle));
_artistContext.Update(existingArtistRecord!);
_artistContext.SaveChanges();
return existingArtistRecord!;
}
}
public void DeleteArtistFromDatabase(Song song)
{
if (!(_artistContext!.Artists.FirstOrDefault(art => art.Name!.Equals(song.Artist)) != null))
{
_logger.Info("Cannot delete the artist record because it does not exist");
return;
}
var artist = _artistContext.Artists.FirstOrDefault(art => art.Name!.Equals(song.Artist));
if (SongsOfArtist(artist!) <= 1)
{
_artistContext!.Remove(artist!);
_artistContext.SaveChanges();
}
}
private int SongsOfArtist(Artist artist)
{
var sngContext = new SongContext(_connectionString!);
var songs = sngContext.Songs!.Where(sng => sng.ArtistId == artist.Id).ToList();
return songs.Count;
}
#endregion
}
@@ -1,13 +0,0 @@
using Microsoft.Extensions.Configuration;
using NLog;
namespace Icarus.Controllers.Managers;
public class BaseManager
{
#region Fields
protected static Logger _logger = NLog.Web.NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
protected IConfiguration? _config;
protected string? _connectionString;
#endregion
}
@@ -1,182 +0,0 @@
using Icarus.Constants;
using Icarus.Controllers.Utilities;
using Icarus.Database.Contexts;
using Icarus.Models;
namespace Icarus.Controllers.Managers;
public class CoverArtManager : BaseManager
{
#region Fields
private string? _rootCoverArtPath;
private CoverArtContext? _coverArtContext;
private byte[]? _stockCoverArt = null;
private const string? _filename = "CoverArt.png";
#endregion
#region Constructors
public CoverArtManager(IConfiguration config)
{
_config = config;
_connectionString = _config.GetConnectionString("DefaultConnection");
_rootCoverArtPath = _config.GetValue<string>("CoverArtPath");
Initialize();
}
#endregion
#region Methods
public void SaveCoverArtToDatabase(ref Song song, ref CoverArt coverArt)
{
_logger.Info("Saving cover art record to the database");
_coverArtContext!.Add(coverArt);
_coverArtContext.SaveChanges();
song.CoverArtId = coverArt.Id;
}
public void DeleteCoverArtFromDatabase(CoverArt coverArt)
{
_logger.Info("Attempting to delete cover art from the database");
_coverArtContext!.Attach(coverArt);
_coverArtContext.Remove(coverArt);
_coverArtContext.SaveChanges();
}
public void DeleteCoverArt(CoverArt coverArt)
{
try
{
var stockCoverArtPath = _rootCoverArtPath + _filename;
if (!string.Equals(stockCoverArtPath, coverArt.ImagePath(),
StringComparison.CurrentCultureIgnoreCase))
{
_logger.Info("Song does not contain the stock cover art");
File.Delete(coverArt.ImagePath());
_logger.Info("Cover art deleted from the filesystem");
}
else
{
_logger.Info("Song contains the stock cover art");
_logger.Info("Will not delete from from the filesystem");
}
}
catch (Exception ex)
{
var msg = ex.Message;
_logger.Error(msg, "An error occurred");
}
}
public CoverArt? SaveCoverArt(Song song)
{
try
{
var dirMgr = new DirectoryManager(_rootCoverArtPath!);
var defaultExtension = ".png";
dirMgr.CreateDirectory(song);
var coverArt = new CoverArt
{
SongTitle = song.Title!
};
var segment = coverArt.GenerateFilename(0);
coverArt.Directory = dirMgr.SongDirectory;
coverArt.Filename = segment + defaultExtension;
var metaData = new MetadataRetriever();
var imgBytes = metaData.RetrieveCoverArtBytes(song);
if (imgBytes != null)
{
_logger.Info("Saving cover art to the filesystem");
File.WriteAllBytes(coverArt.ImagePath(), imgBytes);
}
else
{
_logger.Info("Song has no cover art, applying stock cover art");
var coverArtFilePath = _rootCoverArtPath + $"{segment}{defaultExtension}";
coverArt.Directory = DirectoryPaths.CoverArtDirectory;
coverArt.Filename = DirectoryPaths.CoverArtFilename;
metaData.UpdateCoverArt(song, coverArt);
coverArt.Directory = this._rootCoverArtPath;
coverArt.Filename = $"{segment}{defaultExtension}";
File.WriteAllBytes(coverArt.ImagePath(), _stockCoverArt!);
}
coverArt.Type = metaData.CoverArtFileExtensionType(coverArt);
if (string.IsNullOrEmpty(coverArt.Type))
{
Console.WriteLine("File type is empty");
Console.WriteLine($"Directory: {coverArt.Directory}");
Console.WriteLine($"Filename: {coverArt.Filename}");
}
return coverArt;
}
catch (Exception ex)
{
var msg = ex.Message;
_logger.Error(msg, "An error occurred");
}
return null;
}
public CoverArt SaveCoverArt(IFormFile data, Song song)
{
var cover = new CoverArt { SongTitle = song.Title };
try
{
MetadataRetriever metaData = new MetadataRetriever();
var dirMgr = new DirectoryManager(_rootCoverArtPath!);
cover.Type = metaData.FileExtensionType(data);
var defaultExtension = "." + cover.Type;
dirMgr.CreateDirectory(song);
var segment = cover.GenerateFilename(0);
var imagePath = dirMgr.SongDirectory + segment + defaultExtension;
cover.Directory = dirMgr.SongDirectory;
cover.Filename = segment + defaultExtension;
using (var fileStream = new FileStream(cover.ImagePath(), FileMode.Create))
{
data.CopyTo(fileStream);
}
}
catch (Exception ex)
{
_logger.Error(ex.Message, "An error occurred");
}
return cover;
}
public CoverArt GetCoverArt(Song song)
{
var title = song.Title;
var cov = _coverArtContext!.CoverArtImages!.FirstOrDefault(cov => cov.SongTitle!.Equals(title));
return cov!;
}
private void Initialize()
{
_coverArtContext = new CoverArtContext(_connectionString!);
var path = DirectoryPaths.CoverArtDirectory + DirectoryPaths.CoverArtFilename;
if (System.IO.File.Exists(path))
_stockCoverArt = File.ReadAllBytes(path);
if (!File.Exists(_rootCoverArtPath + _filename))
{
File.WriteAllBytes(_rootCoverArtPath + _filename,
_stockCoverArt!);
Console.WriteLine("Copied Stock Cover Art");
}
}
#endregion
}
@@ -1,207 +0,0 @@
using Icarus.Models;
using Icarus.Types;
namespace Icarus.Controllers.Managers;
// NOTE: Do not use metadata for the song's metadata
public class DirectoryManager : BaseManager
{
#region Fields
private Song? _song;
private string? _rootSongDirectory;
private string? _songDirectory;
#endregion
#region Properties
public string? SongDirectory
{
get => _songDirectory;
set => _songDirectory = value;
}
#endregion
#region Constructors
public DirectoryManager(IConfiguration config, Song song)
{
_config = config;
_song = song;
Initialize();
}
public DirectoryManager(IConfiguration config)
{
_config = config;
Initialize();
}
public DirectoryManager(string rootDirectory)
{
_rootSongDirectory = rootDirectory;
}
#endregion
#region Methods
// Does not include extension
public static string GenerateFilename(int length)
{
string chars = Constants.DirectoryPaths.FILENAME_CHARACTERS;
var random = new Random();
return new string(Enumerable.Repeat(chars, length).Select(s =>
s[random.Next(s.Length)]).ToArray());
}
public static string GenerateDownloadFilename(int length, string extension, string title, bool? randomize)
{
if (randomize.HasValue && randomize.Value)
{
return GenerateFilename(length) + extension;
}
else
{
return title + extension;
}
}
public void CreateDirectory()
{
this.CreateDirectory(_song!);
}
public void CreateDirectory(Song song)
{
this._song = song;
try
{
_songDirectory = AlbumDirectory();
if (!Directory.Exists(_songDirectory))
{
Directory.CreateDirectory(_songDirectory);
Console.WriteLine($"The directory has been created");
}
Console.WriteLine($"The song will be saved in the following" +
$" directory: {_songDirectory}");
}
catch (Exception ex)
{
var msg = ex.Message;
_logger.Error(msg, "An error occurred");
}
}
public int DeleteEmptyDirectories(string? directory, int level)
{
var deleted = 0;
try
{
var curDir = directory;
for (var i = 0; i < level; i++)
{
if (!System.IO.Directory.Exists(curDir))
{
continue;
}
if (this.IsDirectoryEmpty(curDir))
{
System.IO.Directory.Delete(curDir);
deleted++;
}
var parentDirectory = System.IO.Directory.GetParent(curDir);
curDir = parentDirectory!.ToString();
}
}
catch (Exception ex)
{
var exMsg = ex.Message;
Console.WriteLine($"An error occurred {exMsg}");
}
return deleted;
}
public void DeleteEmptyDirectories(Song song)
{
try
{
var albumDirectory = AlbumDirectory(song);
var artistDirectory = ArtistDirectory(song);
if (Directory.Exists(albumDirectory) && IsDirectoryEmpty(albumDirectory))
{
Directory.Delete(albumDirectory);
_logger.Info("Album directory deleted");
}
if (Directory.Exists(artistDirectory) && IsDirectoryEmpty(artistDirectory))
{
Directory.Delete(artistDirectory);
_logger.Info("Artist directory deleted");
}
}
catch (Exception ex)
{
var msg = ex.Message;
_logger.Error(msg, "An error occurred");
}
}
private void Initialize(DirectoryType dirTypes = DirectoryType.Music)
{
switch (dirTypes)
{
case DirectoryType.Music:
_rootSongDirectory = _config!.GetValue<string>("RootMusicPath")!;
break;
case DirectoryType.CoverArt:
_rootSongDirectory = _config!.GetValue<string>("CoverArtPath")!;
break;
}
}
private bool IsDirectoryEmpty(string path) =>
!(Directory.EnumerateFileSystemEntries(path).Any());
private string AlbumDirectory()
{
return AlbumDirectory(_song!);
}
private string AlbumDirectory(Song song)
{
var directory = ArtistDirectory(song);
var segment = SerializeValue(song.AlbumTitle!);
directory += $@"{segment}/";
Console.WriteLine($"Album directory {directory}");
return directory;
}
private string ArtistDirectory(Song song)
{
var directory = _rootSongDirectory;
var segment = SerializeValue(song.Artist!);
directory += $@"{segment}/";
Console.WriteLine($"Artist directory {directory}");
return directory;
}
private string SerializeValue(string value)
{
const int length = 15;
const string chars = "ABCDEF0123456789";
var random = new Random();
var output = new string(Enumerable.Repeat(chars, length).Select(s =>
s[random.Next(s.Length)]).ToArray());
return output;
}
#endregion
}
-128
View File
@@ -1,128 +0,0 @@
using Icarus.Models;
using Icarus.Database.Contexts;
namespace Icarus.Controllers.Managers;
public class GenreManager : BaseManager
{
#region Fields
private GenreContext? _genreContext;
#endregion
#region Properties
#endregion
#region Constructors
public GenreManager(IConfiguration config)
{
_config = config;
_connectionString = _config.GetConnectionString("DefaultConnection");
_genreContext = new GenreContext(_connectionString!);
}
#endregion
#region Methods
public void SaveGenreToDatabase(ref Song song)
{
_logger.Info("Starting process to save the genre record of the song to the database");
var genre = new Genre
{
GenreName = song.Genre,
SongCount = 1
};
var genreName = song.Genre;
var genreRetrieved = _genreContext!.Genres!.FirstOrDefault(gnr => gnr.GenreName!.Equals(genreName));
if (genreRetrieved == null)
{
_genreContext.Add(genre);
_genreContext.SaveChanges();
}
else
{
genre.Id = genreRetrieved.Id;
}
song.GenreId = genre.Id;
}
public Genre UpdateGenreInDatabase(Song oldSongRecord, Song newSongRecord)
{
var oldGenreRecord = _genreContext!.Genres!.FirstOrDefault(gnr => gnr.GenreName!.Equals(oldSongRecord.Genre));
var oldGenreName = oldGenreRecord!.GenreName;
var newGenreName = newSongRecord.Genre;
if (string.IsNullOrEmpty(newGenreName) || oldGenreName!.Equals(newGenreName))
{
_logger.Info("No change to the song's Genre");
return oldGenreRecord;
}
_logger.Info("Change to the song's genre found");
if (oldGenreRecord.SongCount <= 1)
{
_logger.Info("Deleting genre record");
_genreContext.Remove(oldGenreRecord);
_genreContext.SaveChanges();
}
if (!(_genreContext.Genres!.FirstOrDefault(gnr => gnr.GenreName!.Equals(oldSongRecord.Genre)) != null))
{
_logger.Info("Creating new genre record");
var newGenreRecord = new Genre
{
GenreName = newGenreName
};
_genreContext.Add(newGenreRecord);
_genreContext.SaveChanges();
return newGenreRecord;
}
else
{
_logger.Info("Updating existing genre record");
var existingGenreRecord = _genreContext.Genres!.FirstOrDefault(gnr => gnr.GenreName!.Equals(oldGenreRecord.GenreName));
_genreContext.Update(existingGenreRecord!);
_genreContext.SaveChanges();
return existingGenreRecord!;
}
}
public void DeleteGenreFromDatabase(Song song)
{
if (!(_genreContext!.Genres!.FirstOrDefault(gnr => gnr.GenreName!.Equals(song.Genre)) != null))
{
_logger.Info("Cannot delete the genre record because it does not exist");
return;
}
var genre = _genreContext.Genres!.FirstOrDefault(gnr => gnr.GenreName!.Equals(song.Genre));
if (SongsInGenre(genre!) <= 1)
{
_genreContext.Remove(genre!);
_genreContext.SaveChanges();
}
}
private int SongsInGenre(Genre genre)
{
var sngContext = new SongContext(_connectionString!);
var songs = sngContext.Songs!.Where(sng => sng.GenreId == genre.Id).ToList();
return songs.Count;
}
#endregion
}
-479
View File
@@ -1,479 +0,0 @@
using NLog;
using Icarus.Controllers.Utilities;
using Icarus.Models;
using Icarus.Database.Contexts;
namespace Icarus.Controllers.Managers;
public class SongManager : BaseManager
{
#region Fields
private string? _tempDirectoryRoot;
private string? _archiveDirectoryRoot;
private string? _compressedSongFilename;
private string? _message;
private SongContext? _songContext;
#endregion
#region Properties
public string? ArchiveDirectoryRoot
{
get => _archiveDirectoryRoot;
set => _archiveDirectoryRoot = value;
}
public string? CompressedSongFilename
{
get => _compressedSongFilename;
set => _compressedSongFilename = value;
}
public string? Message
{
get => _message;
set => _message = value;
}
#endregion
#region Constructors
public SongManager(IConfiguration config)
{
_config = config;
Initialize();
}
public SongManager(IConfiguration config, string tempDirectoryRoot)
{
_config = config;
_tempDirectoryRoot = tempDirectoryRoot;
Initialize();
}
#endregion
#region Methods
public SongResult UpdateSong(Song song)
{
var result = new SongResult();
if (!DoesSongExist(song))
{
result.SongTitle = song.Title;
result.Message = "Song does not exist";
return result;
}
try
{
var oldSongRecord = _songContext!.RetrieveRecord(song);
song.Filename = oldSongRecord.Filename;
song.SongDirectory = oldSongRecord.SongDirectory;
MetadataRetriever updateMetadata = new MetadataRetriever();
updateMetadata.UpdateMetadata(song, oldSongRecord);
var updatedSong = updateMetadata.UpdatedSongRecord;
var albMgr = new AlbumManager(_config!);
var gnrMgr = new GenreManager(_config!);
var artMgr = new ArtistManager(_config!);
var updatedAlbum = albMgr.UpdateAlbumInDatabase(oldSongRecord, updatedSong!);
oldSongRecord.AlbumId = updatedAlbum.Id;
var updatedArtist = artMgr.UpdateArtistInDatabase(oldSongRecord, updatedSong!);
oldSongRecord.ArtistId = updatedArtist.Id;
var updatedGenre = gnrMgr.UpdateGenreInDatabase(oldSongRecord, updatedSong!);
oldSongRecord.GenreId = updatedGenre.Id;
UpdateSongInDatabase(ref oldSongRecord, ref updatedSong!, ref result);
DeleteEmptyDirectories(ref oldSongRecord, ref updatedSong);
}
catch (Exception ex)
{
var msg = ex.Message;
_logger.Error(msg, "An error occurred");
result.Message = $"An error occurred: {msg}";
result.SongTitle = song.Title;
}
return result;
}
public bool DeleteSongFromFileSystem(Song songMetaData)
{
bool successful = false;
try
{
var songPath = songMetaData.SongPath();
System.IO.File.Delete(songPath);
successful = !System.IO.File.Exists(songPath);
if (successful)
{
Console.WriteLine("Song successfully deleted");
}
DirectoryManager dirMgr = new DirectoryManager(_config!, songMetaData);
var deletedAmount = dirMgr.DeleteEmptyDirectories(songMetaData.SongDirectory, 1);
if (deletedAmount > 0)
{
Console.WriteLine($"{deletedAmount} directories deleted");
}
}
catch (Exception ex)
{
var exMsg = ex.Message;
}
return successful;
}
public bool DoesSongExist(Song song)
{
if (!_songContext!.DoesRecordExist(song))
{
return false;
}
return true;
}
public void DeleteSong(Song song)
{
try
{
if (DeleteSongFromFilesystem(song, true))
{
_logger.Error("Failed to delete the song");
throw new Exception("Failed to delete the song");
}
_logger.Info("Song deleted from the filesystem");
var coverMgr = new CoverArtManager(_config!);
var coverArt = coverMgr.GetCoverArt(song);
coverMgr.DeleteCoverArt(coverArt);
DeleteSongFromDatabase(song);
coverMgr.DeleteCoverArtFromDatabase(coverArt);
var accessLevelContext = new AccessLevelContext(_config!.GetConnectionString("DefaultConnection")!);
var accessLevel = accessLevelContext.AccessLevels!.FirstOrDefault(al => al.SongId == song.Id);
accessLevelContext.AccessLevels!.Remove(accessLevel!);
accessLevelContext.SaveChanges();
}
catch (Exception ex)
{
var msg = ex.Message;
_logger.Error(msg, "An error occurred");
}
}
public Song SaveFlacSongToFileSystem(IFormFile songFile, IFormFile coverArtData, Song song)
{
// Save temp song (Should already be saved to the filesystem by the time it gets to this method)
// Save cover art
// Update the song's metadata with the song object
// Save song to its final directory
// Save cover art to the database
// Save song to the database
var coverMgr = new CoverArtManager(_config!);
var coverArt = coverMgr.SaveCoverArt(coverArtData, song);
var meta = new Utilities.MetadataRetriever();
meta.UpdateMetadata(song, song);
DirectoryManager dirMgr = new DirectoryManager(_config!, song);
dirMgr.CreateDirectory();
var tempPath = song.SongPath();
song.SongDirectory = dirMgr.SongDirectory;
song.Filename = song.GenerateFilename(true, AudioFileExtensionsType.FLAC);
var filePath = song.SongPath();
_logger.Info($"Absolute song path: {filePath}");
this.MoveSongToFinalDestination(tempPath, filePath);
SaveSongToDatabase(song, coverArt);
return song;
}
private void MoveSongToFinalDestination(string sourcePath, string targetPath)
{
using (var fileStream = new FileStream(targetPath, FileMode.Create))
{
var songBytes = System.IO.File.ReadAllBytes(sourcePath);
try
{
if (System.IO.File.Exists(sourcePath) && System.IO.File.Exists(sourcePath) && fileStream.Length > 0)
{
System.IO.File.Delete(sourcePath);
_logger.Info("Deleted temp song from filesystem: {0}", sourcePath);
}
else
{
fileStream.Write(songBytes, 0, songBytes.Count());
_logger.Info("Saved song to filesystem: {0}", targetPath);
System.IO.File.Delete(sourcePath);
_logger.Info("Deleted temp song from filesystem: {0}", sourcePath);
}
}
catch (Exception ex)
{
var msg = ex.Message;
_logger.Error($"An error occurred: {msg}");
}
_logger.Info("Song successfully saved to filesystem");
}
}
public async Task<SongData> RetrieveSong(Song songMetaData)
{
var song = new SongData();
try
{
Console.WriteLine("Fetching song from filesystem");
song = await RetrieveSongFromFileSystem(songMetaData);
}
catch (Exception ex)
{
var exMsg = ex.Message;
Console.WriteLine($"An error occurred: {exMsg}");
}
return song;
}
private async Task<SongData> RetrieveSongFromFileSystem(Song details)
{
byte[] uncompressedSong = await System.IO.File.ReadAllBytesAsync(details.SongPath());
return new SongData
{
Data = uncompressedSong
};
}
public async Task<Song> SaveSongTemp(IFormFile songFile)
{
_logger.Info("Assigning song filename");
var song = new Song { SongDirectory = this._tempDirectoryRoot! };
var filename = await song.GenerateFilenameAsync(false) + "-" + songFile.FileName;
song.Filename = filename;
var songPath = song.SongPath();
using (var filestream = new FileStream(songPath, FileMode.Create))
{
_logger.Info("Saving temp song: {0}", songPath);
await songFile.CopyToAsync(filestream);
}
song.DateCreated = DateTime.Now;
return song;
}
public Icarus.Models.CreateFileResult Create(IFormFile file, string filePath, string prompt)
{
if (System.IO.File.Exists(filePath))
{
return CreateFileResult.AlreadyExists;
}
using (var filestream = new FileStream(filePath, FileMode.Create))
{
Console.WriteLine(prompt);
file.CopyTo(filestream);
if (System.IO.File.Exists(filePath))
{
return CreateFileResult.FileCreatedAndExists;
}
}
return 0;
}
private bool SongRecordChanged(Song currentSong, Song songUpdates)
{
var currentTitle = currentSong.Title;
var currentArtist = currentSong.Artist;
var currentAlbum = currentSong.AlbumTitle;
var currentGenre = currentSong.Genre;
var currentYear = currentSong.Year;
if (!currentTitle!.Equals(songUpdates.Title) || !currentArtist!.Equals(songUpdates.Artist) ||
!currentAlbum!.Equals(songUpdates.AlbumTitle) ||
!currentGenre!.Equals(songUpdates.Genre) || currentYear != songUpdates.Year)
return true;
return false;
}
private void DeleteEmptyDirectories(ref Song oldSong, ref Song updatedSong)
{
DirectoryManager mgr = new DirectoryManager(_config!);
_logger.Info("Checking to see if there are any directories to delete");
mgr.DeleteEmptyDirectories(oldSong);
}
private void Initialize()
{
try
{
_connectionString = _config!.GetConnectionString("DefaultConnection");
_songContext = new SongContext(_connectionString!);
}
catch (Exception ex)
{
Console.WriteLine($"Error Occurred: {ex.Message}");
}
}
private void SaveSongToDatabase(Song song, CoverArt? cover)
{
_logger.Info("Starting process to save the song to the database");
var albumMgr = new AlbumManager(this._config!);
var artistMgr = new ArtistManager(this._config!);
var genreMgr = new GenreManager(this._config!);
var coverMgr = new CoverArtManager(this._config!);
albumMgr.SaveAlbumToDatabase(ref song);
artistMgr.SaveArtistToDatabase(ref song);
genreMgr.SaveGenreToDatabase(ref song);
var info = "Saving Song to DB";
_logger.Info(info);
this._songContext!.Add(song);
this._songContext!.SaveChanges();
if (cover != null)
{
coverMgr.SaveCoverArtToDatabase(ref song, ref cover!);
}
var accessLevel = Icarus.Models.AccessLevel.DefaultLevel();
accessLevel.SongId = song.Id;
var accessLevelContext = new AccessLevelContext(this._connectionString!);
accessLevelContext.Add(accessLevel);
accessLevelContext.SaveChanges();
}
private bool DeleteSongFromFilesystem(Song song, bool deleteDirectory = false)
{
var songPath = song.SongPath();
_logger.Info("Deleting song from the filesystem");
try
{
System.IO.File.Delete(songPath);
DeleteEmptyDirectories(ref song, ref song);
}
catch (Exception ex)
{
var msg = ex.Message;
_logger.Error(msg, "An error occurred when attempting to delete the song from the filesystem");
return false;
}
return DoesSongExistOnFilesystem(song);
}
private bool DoesSongExistOnFilesystem(Song song)
{
if (!System.IO.File.Exists(song.SongPath()))
{
_logger.Info("Song does not exist on the filesystem");
return false;
}
_logger.Info("Song exists on the filesystem");
return true;
}
private void UpdateSongInDatabase(ref Song oldSongRecord, ref Song newSongRecord, ref SongResult result)
{
var updatedSongRecord = oldSongRecord;
var songContext = new SongContext(_connectionString!);
if (!SongRecordChanged(oldSongRecord, newSongRecord))
{
_logger.Info("No change to the song record");
return;
}
_logger.Info("Changes to song record found");
if (!string.IsNullOrEmpty(newSongRecord.Title))
updatedSongRecord.Title = newSongRecord.Title;
if (!string.IsNullOrEmpty(newSongRecord.AlbumTitle))
{
updatedSongRecord.AlbumTitle = newSongRecord.AlbumTitle;
}
if (!string.IsNullOrEmpty(newSongRecord.Artist))
{
updatedSongRecord.Artist = newSongRecord.Artist;
}
if (!string.IsNullOrEmpty(newSongRecord.Genre))
{
updatedSongRecord.Genre = newSongRecord.Genre;
Console.WriteLine("Genre changed");
Console.WriteLine($"{updatedSongRecord.Genre} {newSongRecord.Genre}");
}
if (newSongRecord.Year != null || newSongRecord.Year > 0)
updatedSongRecord.Year = newSongRecord.Year;
_logger.Info("Applied changes to song record");
_logger.Info("Saving song metadata to the database");
songContext.Update(updatedSongRecord);
songContext.SaveChanges();
newSongRecord = updatedSongRecord;
result.Message = "Successfully updated song";
result.SongTitle = updatedSongRecord.Title!;
}
private void DeleteSongFromDatabase(Song song)
{
_logger.Info("Starting process to delete records related to the song from the database");
var albumMgr = new AlbumManager(_config!);
var artistMgr = new ArtistManager(_config!);
var genreMgr = new GenreManager(_config!);
var sngContext = new SongContext(_connectionString!);
sngContext.Songs!.Remove(song);
sngContext.SaveChanges();
artistMgr.DeleteArtistFromDatabase(song);
albumMgr.DeleteAlbumFromDatabase(song);
genreMgr.DeleteGenreFromDatabase(song);
}
#endregion
}
-308
View File
@@ -1,308 +0,0 @@
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using RestSharp;
using Icarus.Models;
using Microsoft.VisualBasic;
namespace Icarus.Controllers.Managers;
#region Classes
public class TokenManager : BaseManager
{
#region Fields
private string? _clientId;
private string? _clientSecret;
private string? _privateKeyPath = string.Empty;
private string? _privateKey = string.Empty;
private string? _publicKeyPath = string.Empty;
private string? _publicKey = string.Empty;
private string? _audience;
private string? _grantType;
private string? _url;
private string? SUCCESSFUL_TOKEN_MESSAGE = "Successfully retrieved token";
#endregion
#region Properties
#endregion
#region Constructors
public TokenManager(IConfiguration config)
{
_config = config;
InitializeValues();
}
#endregion
#region Methods
public LoginResult RetrieveLoginResult(User user)
{
_logger.Info("Preparing Auth0 API request");
var client = new RestClient(_url!);
var request = new RestRequest("oauth/token", Method.POST);
var tokenRequest = RetrieveTokenRequest();
_logger.Info("Serializing token object into JSON");
var tokenObject = JsonConvert.SerializeObject(tokenRequest);
request.AddParameter("application/json; charset=utf-8",
tokenObject, ParameterType.RequestBody);
request.RequestFormat = DataFormat.Json;
_logger.Info("Sending request");
IRestResponse response = client.Execute(request);
_logger.Info("Response received");
_logger.Info("Deserializing response");
var tokenResult = JsonConvert
.DeserializeObject<TokenTierOne>(response.Content);
_logger.Info("Response deserialized");
return this.InitializeLoginResult(user, tokenResult!);
}
public LoginResult LoginSymmetric(User user)
{
var tokenResult = new TokenTierOne { TokenType = "JWT" };
var payload = Payload();
payload.Add(new Claim("user_id", user.Id.ToString(), ClaimValueTypes.Integer));
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_config!["JWT:Secret"]!);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim("user_id", user.Id.ToString(), ClaimValueTypes.Integer)
// Add more claims as needed
}),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
Issuer = _config["Jwt:Issuer"], // Add this line
Audience = _config["Jwt:Audience"]
};
tokenResult.AccessToken = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
var expClaim = payload.FirstOrDefault(cl =>
{
return cl.Type.Equals("exp");
});
var expiredDate = DateTime.Parse(expClaim!.Value);
var exp = Math.Floor((expiredDate - DateTime.UnixEpoch).TotalSeconds);
tokenResult.Expiration = Convert.ToInt32(exp);
return this.InitializeLoginResult(user, tokenResult);
}
private LoginResult InitializeLoginResult(User user, TokenTierOne token)
{
return new LoginResult
{
UserId = user.Id,
Username = user.Username,
Token = token.AccessToken,
TokenType = token.TokenType,
Expiration = token.Expiration,
Message = SUCCESSFUL_TOKEN_MESSAGE
};
}
public bool CanAccessSong(string token, Song song, AccessLevel accessLevel)
{
if (accessLevel!.Level!.Equals(Models.AccessLevel.DefaultLevel().Level))
{
return true;
}
var tokenUserId = this.RetrieveUserIdFromToken(token);
if (tokenUserId == null)
{
return false;
}
return tokenUserId.Value == song.UserId;
}
public string? GetBearerToken(HttpContext context)
{
string authorizationHeader = context.Request.Headers["Authorization"]!;
if (!authorizationHeader.IsNullOrEmpty() && authorizationHeader.StartsWith("Bearer", StringComparison.OrdinalIgnoreCase))
{
string token = authorizationHeader.Substring("Bearer ".Length).Trim();
return token;
}
return null;
}
public int? RetrieveUserIdFromToken(string token)
{
var parsedToken = this.ContainsBearer(token) ? this.StripBearer(token) : token;
var tokenHandler = new JwtSecurityTokenHandler();
var readTok = tokenHandler.ReadJwtToken(parsedToken);
foreach (var item in readTok.Payload)
{
if (item.Key == "user_id")
{
return Convert.ToInt32(item.Value);
}
}
return null;
}
private string StripBearer(string token)
{
var start = 6;
var strippedToken = token.Substring(start);
return Strings.Trim(strippedToken);
}
private bool ContainsBearer(string token)
{
return token.Contains("Bearer");
}
private string AllScopes()
{
var allScopes = new List<String>()
{
"download:songs",
"read:song_details",
"upload:songs",
"delete:songs",
"read:albums",
"read:artists",
"update:songs",
"stream:songs",
"read:genre",
"read:year",
"download:cover_art"
};
var scopes = string.Empty;
for (var i = 0; i < allScopes.Count; i++)
{
if (i == allScopes.Count - 1)
{
scopes += allScopes[i];
}
else
{
scopes += allScopes[i] + " ";
}
}
return scopes;
}
private List<Claim> Payload()
{
// TODO: Remove this hard coding
var expLimit = 30;
var currentDate = DateTime.Now;
var expiredDate = currentDate.AddMinutes(expLimit);
var issuer = "http://localhost:5002";
var audience = "http://localhost:5002";
var subject = _config!["JWT:Subject"];
var claim = new List<System.Security.Claims.Claim>()
{
new Claim("scope", AllScopes(), "string"),
new Claim(JwtRegisteredClaimNames.Exp, expiredDate.ToString()),
new Claim(JwtRegisteredClaimNames.Aud, audience),
new Claim(JwtRegisteredClaimNames.Iss, issuer),
new Claim(JwtRegisteredClaimNames.Sub, subject!),
new Claim(JwtRegisteredClaimNames.Iat, currentDate.ToString())
};
return claim;
}
[Obsolete("Deprecated function")]
private async Task<string> ReadKeyContent(string filepath)
{
return await System.IO.File.ReadAllTextAsync(filepath);
}
private TokenRequest RetrieveTokenRequest()
{
_logger.Info("Retrieving token object");
return new TokenRequest
{
ClientId = _clientId,
ClientSecret = _clientSecret,
Audience = _audience,
GrantType = _grantType
};
}
private void InitializeValues()
{
_logger.Info("Analyzing Auth0 information");
_clientId = _config!["Auth0:ClientId"];
_clientSecret = _config["Auth0:ClientSecret"];
_audience = _config["Auth0:ApiIdentifier"];
_grantType = "client_credentials";
_url = $"https://{_config["Auth0:Domain"]}";
}
#region Testing Methods
// For testing purposes
private void PrintCredentials()
{
Console.WriteLine("Auth0 credentials:");
Console.WriteLine($"Client Id: {_clientId}");
Console.WriteLine($"Client Secret: {_clientSecret}");
Console.WriteLine($"Audience: {_audience}");
Console.WriteLine($"Url: {_url}");
}
#endregion
#endregion
#region Classes
private class TokenRequest
{
[JsonProperty("client_id")]
public string? ClientId { get; set; }
[JsonProperty("client_secret")]
public string? ClientSecret { get; set; }
[JsonProperty("audience")]
public string? Audience { get; set; }
[JsonProperty("grant_type")]
public string? GrantType { get; set; }
}
private class TokenTierOne
{
[JsonProperty("access_token")]
public string? AccessToken { get; set; }
[JsonProperty("expires_in")]
public int Expiration { get; set; }
[JsonProperty("token_type")]
public string? TokenType { get; set; }
}
#endregion
}
#endregion
@@ -1,371 +0,0 @@
using NLog;
using TagLib;
using Icarus.Models;
namespace Icarus.Controllers.Utilities;
public class MetadataRetriever
{
#region Fields
private static NLog.Logger? _logger = NLog.Web.NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
private List<string>? _supportedAudioFileTypes = new List<string> { "wav", "flac" };
private List<string>? _supportedImageFileTypes = new List<string> { "jpeg", "jpg", "png" };
private Song? _updatedSong;
private string? _message;
private string? _title;
private string? _artist;
private string? _album;
private string? _genre;
private int _year;
private int _duration;
#endregion
#region Properties
public Song? UpdatedSongRecord
{
get => _updatedSong;
set => _updatedSong = value;
}
public string? Message
{
get => _message;
set => _message = value;
}
#endregion
#region Constructors
#endregion
#region Methods
public string CoverArtFileExtensionType(CoverArt cover)
{
Console.WriteLine("Retrieving CoverArt file extension type");
using (var fileStream = System.IO.File.OpenRead(cover.ImagePath()))
{
var isRecognizableType = FileTypeChecker.FileTypeValidator.IsTypeRecognizable(fileStream);
if (!isRecognizableType)
{
return string.Empty;
}
var fileType = FileTypeChecker.FileTypeValidator.GetFileType(fileStream);
Console.WriteLine($"Filetype: {fileType}");
return fileType.Extension;
}
}
public string FileExtensionType(IFormFile file)
{
using (var fileStream = file.OpenReadStream())
{
var isRecognizableType = FileTypeChecker.FileTypeValidator.IsTypeRecognizable(fileStream);
if (!isRecognizableType)
{
return string.Empty;
}
var fileType = FileTypeChecker.FileTypeValidator.GetFileType(fileStream);
Console.WriteLine($"Filetype: {fileType}");
Console.WriteLine($"Extension: {fileType.Extension}");
return fileType.Extension;
}
}
public string FileExtensionType(string path)
{
var extensionRaw = System.IO.Path.GetExtension(path);
if (extensionRaw[0] == '.')
{
return extensionRaw.Substring(1);
}
else
{
return extensionRaw;
}
}
public bool IsSupportedFile(IFormFile file)
{
var supportedTypes = this._supportedAudioFileTypes;
this._supportedImageFileTypes!.ForEach(t =>
{
if (!supportedTypes!.Contains(t))
{
supportedTypes.Add(t);
}
});
var extensionType = this.FileExtensionType(file).ToLower();
return supportedTypes!.Contains(extensionType);
}
public bool IsSupportedFile(string path)
{
var supportedTypes = this._supportedAudioFileTypes;
this._supportedImageFileTypes!.ForEach(t =>
{
if (!supportedTypes!.Contains(t))
{
supportedTypes.Add(t);
}
});
var extensionType = this.FileExtensionType(path).ToLower();
return supportedTypes!.Contains(extensionType);
}
public Song RetrieveMetaData(string filePath)
{
Song song = new Song();
try
{
TagLib.File fileTag = TagLib.File.Create(filePath);
_title = fileTag.Tag.Title;
_artist = string.Join("", fileTag.Tag.Performers);
_album = fileTag.Tag.Album;
_genre = string.Join("", fileTag.Tag.Genres);
_year = (int)fileTag.Tag.Year;
_duration = (int)fileTag.Properties.Duration.TotalSeconds;
var albumArtist = string.Join("", fileTag.Tag.AlbumArtists);
var track = (int)fileTag.Tag.Track;
var disc = (int)fileTag.Tag.Disc;
song.Title = _title;
song.Artist = _artist;
song.AlbumTitle = _album;
song.AlbumArtist = albumArtist;
song.Genre = _genre;
song.Year = _year;
song.Duration = _duration;
song.Track = track;
song.Disc = disc;
song.TrackCount = (int)fileTag.Tag.TrackCount;
song.DiscCount = (int)fileTag.Tag.DiscCount;
}
catch (Exception ex)
{
var msg = ex.Message;
Console.WriteLine("An error occurred in MetadataRetriever");
Console.WriteLine(msg);
_logger!.Error(msg, "An error occurred in MetadataRetriever");
}
return song;
}
public int RetrieveSongDuration(string filepath)
{
var fileTag = TagLib.File.Create(filepath);
var duration = (int)fileTag.Properties.Duration.TotalSeconds;
return duration;
}
public byte[] RetrieveCoverArtBytes(Song song)
{
try
{
Console.WriteLine("Fetching image");
var tag = TagLib.File.Create(song.SongPath());
if (tag.Tag.Pictures.Count() == 0)
{
return [];
}
return tag.Tag.Pictures[0].Data.Data;
}
catch (Exception ex)
{
var msg = ex.Message;
_logger!.Error(msg, "An error occurred in MetadataRetriever");
}
return [];
}
public void UpdateMetadata(Song updatedSong, Song oldSong)
{
try
{
InitializeUpdatedSong(oldSong);
var songValues = CheckSongValues(updatedSong);
PerformUpdate(updatedSong, songValues);
Message = "Successfully updated metadata";
}
catch (Exception ex)
{
var msg = ex.Message;
Console.WriteLine($"An error occurred: {msg}");
_logger!.Error(msg, "An error occurred");
Message = "Failed to update metadata";
}
}
public void UpdateCoverArt(Song song, CoverArt coverArt)
{
Console.WriteLine("Updating song's cover art");
var tag = TagLib.File.Create(song.SongPath());
var pics = tag.Tag.Pictures;
Array.Resize(ref pics, 1);
pics[0] = new Picture(coverArt.ImagePath())
{
Description = "Cover Art"
};
tag.Tag.Pictures = pics;
tag.Save();
}
private void PerformUpdate(Song updatedSong, SortedDictionary<string, bool> checkedValues)
{
var filePath = updatedSong.SongPath();
var title = updatedSong.Title;
var artist = updatedSong.Artist;
var album = updatedSong.AlbumTitle;
var genre = updatedSong.Genre;
var year = updatedSong.Year;
var albumArtist = updatedSong.AlbumArtist;
var track = updatedSong.Track;
var trackCount = updatedSong.TrackCount;
var disc = updatedSong.Disc;
var discCount = updatedSong.DiscCount;
TagLib.File fileTag = TagLib.File.Create(filePath);
try
{
Console.WriteLine($"Updating metadata of {title}");
_logger!.Info($"Updating metadata of {title}");
foreach (var key in checkedValues.Keys)
{
bool result = checkedValues[key];
if (!result)
switch (key.ToLower())
{
case "title":
_updatedSong!.Title = title;
fileTag.Tag.Title = title;
break;
case "artists":
_updatedSong!.Artist = artist;
fileTag.Tag.Performers = new[] { artist };
break;
case "album":
_updatedSong!.AlbumTitle = album;
fileTag.Tag.Album = album;
break;
case "genre":
_updatedSong!.Genre = genre;
fileTag.Tag.Genres = new[] { genre };
break;
case "year":
_updatedSong!.Year = year;
fileTag.Tag.Year = (uint)year!;
break;
case "albumartist":
_updatedSong!.AlbumArtist = albumArtist;
fileTag.Tag.AlbumArtists = new[] { albumArtist };
break;
case "track":
_updatedSong!.Track = track;
fileTag.Tag.Track = (uint)(track);
break;
case "trackcount":
_updatedSong!.TrackCount = trackCount;
fileTag.Tag.TrackCount = (uint)(trackCount);
break;
case "disc":
_updatedSong!.Disc = disc;
fileTag.Tag.Disc = (uint)(disc);
break;
case "disccount":
_updatedSong!.DiscCount = discCount;
fileTag.Tag.DiscCount = (uint)(discCount);
break;
}
}
fileTag.Save();
Console.WriteLine("Successfully updated metadata");
_logger.Info("Successfully updated metadata");
}
catch (Exception ex)
{
var msg = ex.Message;
Console.WriteLine($"An error occurred:\n{msg}");
_logger!.Error(msg, "An error occurred");
}
}
private void InitializeUpdatedSong(Song song)
{
_updatedSong = song;
}
private SortedDictionary<string, bool> CheckSongValues(Song song)
{
var songValues = new SortedDictionary<string, bool>();
Console.WriteLine("Checking for null data");
_logger!.Info("Checking for null data");
try
{
songValues["Title"] = String.IsNullOrEmpty(song.Title);
songValues["Artists"] = String.IsNullOrEmpty(song.Artist);
songValues["Album"] = String.IsNullOrEmpty(song.AlbumTitle);
songValues["Genre"] = String.IsNullOrEmpty(song.Genre);
songValues["AlbumArtist"] = String.IsNullOrEmpty(song.AlbumArtist);
songValues["Year"] = CheckIntField(song.Year);
songValues["Track"] = CheckIntField(song.Track);
songValues["TrackCount"] = CheckIntField(song.TrackCount);
songValues["Disc"] = CheckIntField(song.Disc);
songValues["DiscCount"] = CheckIntField(song.Disc);
Console.WriteLine("Checking for null data completed");
_logger.Info("Checking for null data completed");
}
catch (Exception ex)
{
var msg = ex.Message;
Console.WriteLine($"An error occurred: \n{msg}");
_logger.Error(msg, "An error occurred");
}
return songValues;
}
private bool CheckIntField(int? value)
{
if (value == null)
{
return true;
}
else if (value == 0)
{
return true;
}
return false;
}
#endregion
}
@@ -1,89 +0,0 @@
using System;
using System.Security.Cryptography;
using BCrypt.Net;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using NLog;
using Icarus.Models;
namespace Icarus.Controllers.Utilities;
public class PasswordEncryption
{
#region Fields
private static Logger _logger = NLog.LogManager.GetCurrentClassLogger();
#endregion
#region Properties
#endregion
#region Constructor
#endregion
#region Methods
public bool VerifyPassword(User user, string password)
{
try
{
var result = BCrypt.Net.BCrypt.Verify(password, user.Password);
return result;
}
catch (Exception ex)
{
var msg = ex.Message;
_logger.Error(msg, "An error occurred");
}
return false;
}
public string HashPassword(User user)
{
try
{
string hashedPassword = string.Empty;
hashedPassword = BCrypt.Net.BCrypt.HashPassword(user.Password);
_logger.Info("Successfully hashed password");
return hashedPassword;
}
catch (Exception ex)
{
var exMsg = ex.Message;
_logger.Error(exMsg, "An error occurred");
}
return string.Empty;
}
string GenerateHash(string password, byte[] salt)
{
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: password, salt: salt,
prf: KeyDerivationPrf.HMACSHA1,
iterationCount: 10000,
numBytesRequested: 256 / 8));
return hashed;
}
byte[] GenerateSalt()
{
byte[] salt = new byte[128 / 8];
using (var rng = RandomNumberGenerator.Create())
rng.GetBytes(salt);
return salt;
}
#endregion
}
@@ -1,126 +0,0 @@
using Icarus.Models;
namespace Icarus.Controllers.Utilities;
public class SongCompression
{
#region Fields
string? _compressedSongFilename;
string? _tempDirectory;
byte[]? _uncompressedSong;
#endregion
#region Propterties
public string? CompressedSongFilename
{
get => _compressedSongFilename;
set => _compressedSongFilename = value;
}
#endregion
#region Constructors
public SongCompression()
{
}
public SongCompression(string tempDirectory)
{
_tempDirectory = tempDirectory;
}
public SongCompression(byte[] uncompressedSong)
{
_uncompressedSong = uncompressedSong;
}
#endregion
#region Methods
public async Task<SongData> RetrieveCompressedSong(Song song)
{
SongData songData = new SongData();
try
{
var archivePath = RetrieveCompressesSongPath(song);
Console.WriteLine($"Compressed song saved to: {archivePath}");
songData.Data = await System.IO.File.ReadAllBytesAsync(archivePath);
}
catch (Exception ex)
{
var exMsg = ex.Message;
Console.WriteLine($"An error ocurred: \n{exMsg}");
}
return songData;
}
public string RetrieveCompressesSongPath(Song songDetails)
{
string tmpZipFilePath = _tempDirectory + songDetails.Filename;
try
{
using (var fi = new FileStream(songDetails.SongPath(), FileMode.Open))
{
using (var z = new Ionic.Zlib.ZlibStream(fi, Ionic.Zlib.CompressionMode.Compress))
{
using (var tr = new FileStream(tmpZipFilePath, FileMode.CreateNew))
{
z.CopyTo(tr);
}
}
}
Console.WriteLine("Successfully compressed");
}
catch (Exception ex)
{
var exMsg = ex.Message;
Console.WriteLine("An error ocurred");
Console.WriteLine(exMsg);
}
if (songDetails.Filename!.Contains(Constants.FileExtensions.WAV_EXTENSION))
{
_compressedSongFilename = StripExtension(songDetails.Filename);
}
return tmpZipFilePath;
}
// Method not being used
public byte[] CompressedSong(byte[] uncompressedSong)
{
byte[]? compressedSong = null;
try
{
Console.WriteLine("Song has been successfully compressed");
}
catch (Exception ex)
{
var exMsg = ex.Message;
Console.WriteLine("An error ocurred:");
Console.WriteLine(exMsg);
}
return compressedSong!;
}
string StripExtension(string filename)
{
Console.WriteLine($"Before: {filename}");
int filenameLength = filename.Length;
Console.WriteLine($"Filename length {filenameLength}");
var endIndex = filenameLength - 1;
var startIndex = endIndex - 3;
Console.WriteLine($"Starting index {startIndex} and ending index {endIndex}");
var stripped = filename.Remove(startIndex, 4);
stripped += Constants.FileExtensions.ZIP_EXTENSION;
Console.WriteLine($"After {stripped}");
return stripped;
}
#endregion
}
@@ -1,131 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Icarus.Controllers.V1;
[Route("api/v1/accesslevel")]
[ApiController]
[Authorize]
public class AccessLevelController : BaseController
{
#region Fields
private readonly ILogger<AccessLevelController>? _logger;
private string? _connectionString;
#endregion
#region Properties
#endregion
#region Constructors
public AccessLevelController(ILogger<AccessLevelController> logger, IConfiguration config)
{
this._logger = logger;
this._config = config;
this._connectionString = this._config.GetConnectionString("DefaultConnection");
}
#endregion
#region HTTP Routes
[HttpGet]
public IActionResult GetAccessLevels(int? id, int? songId)
{
var accLevel = new Models.AccessLevel { Id = 0 };
var accessLevelContext = new Database.Contexts.AccessLevelContext(_connectionString!);
if (id != null)
{
accLevel = accessLevelContext.AccessLevels!.FirstOrDefault(al => al.Id == id);
}
else if (songId != null)
{
accLevel = accessLevelContext.GetAccessLevel(songId.Value);
}
var response = new GetAccessLevelsResponse { Data = new List<Models.AccessLevel>() };
if (accLevel?.Id > 0)
{
response.Subject = "Successful";
response.Data.Add(accLevel);
return Ok(response);
}
else
{
response.Subject = "Failure";
return BadRequest(response);
}
}
[HttpPatch("{id}")]
public IActionResult UpdateAccessLevel(int id, [FromBody] Models.AccessLevel accessLevel)
{
var response = new UpdateAccessLevelResponse { Data = new List<Models.AccessLevel>() };
var targetLevel = accessLevel.Level;
if (targetLevel == null)
{
response.Subject = "No level provided";
return BadRequest(response);
}
else
{
if (!Models.AccessLevel.IsAccessLevelValid(targetLevel))
{
response.Subject = "Invalid level";
return BadRequest(response);
}
}
var accessLevelContext = new Database.Contexts.AccessLevelContext(this._connectionString!);
var fetchedAccLevel = accessLevelContext.AccessLevels!.FirstOrDefault(al => al.Id == id);
if (fetchedAccLevel == null)
{
response.Subject = "Nothing found";
return NotFound(response);
}
var fetchedLevel = fetchedAccLevel!.Level;
if (fetchedLevel!.Equals(targetLevel))
{
// No change
response.Subject = "No change";
response.Data.Add(fetchedAccLevel);
return Ok(response);
}
else
{
fetchedAccLevel.Level = targetLevel;
response.Subject = "Successful";
accessLevelContext.Update(fetchedAccLevel);
accessLevelContext.SaveChanges();
response.Data.Add(fetchedAccLevel);
return Ok(response);
}
}
#endregion
}
#region Responses
public class GetAccessLevelsResponse
{
#region Properties
[Newtonsoft.Json.JsonProperty("subject")]
public string? Subject { get; set; }
[Newtonsoft.Json.JsonProperty("data")]
public List<Models.AccessLevel>? Data { get; set; }
#endregion
}
public class UpdateAccessLevelResponse
{
#region Properties
[Newtonsoft.Json.JsonProperty("subject")]
public string? Subject { get; set; }
[Newtonsoft.Json.JsonProperty("data")]
public List<Models.AccessLevel>? Data { get; set; }
#endregion
}
#endregion
-65
View File
@@ -1,65 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Icarus.Models;
using Icarus.Database.Contexts;
namespace Icarus.Controllers.V1;
[Route("api/v1/album")]
[ApiController]
[Authorize]
public class AlbumController : BaseController
{
#region Fields
private readonly ILogger<AlbumController>? _logger;
private string? _connectionString;
#endregion
#region Properties
#endregion
#region Constructors
public AlbumController(ILogger<AlbumController> logger, IConfiguration config)
{
_logger = logger;
_config = config;
_connectionString = _config.GetConnectionString("DefaultConnection");
}
#endregion
#region HTTP Routes
[HttpGet]
public IActionResult GetAlbums()
{
var albumContext = new AlbumContext(_connectionString!);
var albums = albumContext.Albums!.ToList();
if (albums.Count > 0)
return Ok(albums);
else
return NotFound();
}
[HttpGet("{id}")]
public IActionResult GetAlbum(int id)
{
Album album = new Album { Id = id };
var albumContext = new AlbumContext(_connectionString!);
if (albumContext.DoesRecordExist(album))
{
album = albumContext.RetrieveRecord(album);
return Ok(album);
}
else
return NotFound();
}
#endregion
}
-67
View File
@@ -1,67 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Icarus.Models;
using Icarus.Database.Contexts;
namespace Icarus.Controllers.V1;
[Route("api/v1/artist")]
[ApiController]
[Authorize]
public class ArtistController : BaseController
{
#region Fields
private readonly ILogger<ArtistController>? _logger;
private string? _connectionString;
#endregion
#region Properties
#endregion
#region Constructors
public ArtistController(ILogger<ArtistController> logger, IConfiguration config)
{
_logger = logger;
_config = config;
_connectionString = _config.GetConnectionString("DefaultConnection");
}
#endregion
#region HTTP Routes
[HttpGet]
public IActionResult GetArtists()
{
var artistContext = new ArtistContext(_connectionString!);
var artists = artistContext.Artists.ToList();
if (artists.Count > 0)
return Ok(artists);
else
return NotFound();
}
[HttpGet("{id}")]
public IActionResult GetArtist(int id)
{
Artist artist = new Artist { Id = id };
var artistContext = new ArtistContext(_connectionString!);
if (artistContext.DoesRecordExist(artist))
{
artist = artistContext.RetrieveRecord(artist);
return Ok(artist);
}
else
{
return NotFound();
}
}
#endregion
}
-37
View File
@@ -1,37 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace Icarus.Controllers.V1;
public class BaseController : ControllerBase
{
#region Fiends
protected IConfiguration? _config;
#endregion
#region Methods
[ApiExplorerSettings(IgnoreApi = true)]
[Obsolete("Asymmetric key signing for tokens have been deprecated")]
protected string ParseBearerTokenFromHeader()
{
var token = string.Empty;
const string tokenType = "Bearer";
const string otherTokenType = "Jwt";
var req = Request;
var auth = req.Headers.Authorization;
var val = auth.ToString();
if ((val.Contains(tokenType) || val.Contains(otherTokenType)) && val.Split(" ").Count() > 1)
{
var split = val.Split(" ");
token = split[1];
}
return token;
}
#endregion
}
@@ -1,92 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Icarus.Controllers.Managers;
using Icarus.Database.Contexts;
using Icarus.Models;
namespace Icarus.Controllers.V1;
[Route("api/v1/coverart")]
[ApiController]
[Authorize]
public class CoverArtController : BaseController
{
#region Fields
private readonly ILogger<CoverArtController>? _logger;
private string? _connectionString;
#endregion
#region Constructors
public CoverArtController(ILogger<CoverArtController> logger, IConfiguration config)
{
_logger = logger;
_config = config;
_connectionString = _config.GetConnectionString("DefaultConnection");
}
#endregion
#region HTTP Routes
[HttpGet]
public IActionResult GetCoverArts()
{
var coverArtContext = new CoverArtContext(_connectionString!);
var coverArtRecords = coverArtContext.CoverArtImages!.ToList();
if (coverArtRecords == null)
{
_logger!.LogInformation("No cover art records");
return NotFound();
}
else
{
_logger!.LogInformation("Found cover art records");
return Ok(coverArtRecords);
}
}
[HttpGet("{id}")]
public IActionResult GetCoverArt(int id)
{
var coverArt = new CoverArt { Id = id };
var coverArtContext = new CoverArtContext(_connectionString!);
coverArt = coverArtContext.RetrieveRecord(coverArt);
if (coverArt != null)
{
_logger!.LogInformation("Found cover art record");
var coverArtBytes = System.IO.File.ReadAllBytes(
coverArt.ImagePath());
return File(coverArtBytes, "application/x-msdownload",
coverArt.SongTitle);
}
else
{
_logger!.LogInformation("Cover art not found");
return NotFound();
}
}
[HttpGet("data/download/{id}")]
public async Task<IActionResult> Download(int id, [FromQuery] bool? randomizeFilename)
{
var songContext = new SongContext(_connectionString!);
var covMgr = new CoverArtManager(this._config!);
var songMetaData = songContext.RetrieveRecord(new Song { Id = id });
var c = covMgr.GetCoverArt(songMetaData);
var filename = DirectoryManager.GenerateDownloadFilename(10, Constants.FileExtensions.JPG_EXTENSION, songMetaData.Title!, randomizeFilename);
var data = await c.GetData();
return File(data, "application/x-msdownload", filename);
}
#endregion
}
-71
View File
@@ -1,71 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Icarus.Models;
using Icarus.Database.Contexts;
namespace Icarus.Controllers.V1;
[Route("api/v1/genre")]
[ApiController]
[Authorize]
public class GenreController : BaseController
{
#region Fields
private readonly ILogger<GenreController>? _logger;
private string? _connectionString;
#endregion
#region Properties
#endregion
#region Constructors
public GenreController(ILogger<GenreController> logger, IConfiguration config)
{
_logger = logger;
_config = config;
_connectionString = _config.GetConnectionString("DefaultConnection");
}
#endregion
#region HTTP Routes
[HttpGet]
public IActionResult GetGenres()
{
var genreStore = new GenreContext(_connectionString!);
var genres = genreStore!.Genres!.ToList();
if (genres.Count > 0)
{
return Ok(genres);
}
else
{
return NotFound(new List<Genre>());
}
}
[HttpGet("{id}")]
public IActionResult GetGenre(int id)
{
var genre = new Genre { Id = id };
var genreStore = new GenreContext(_connectionString!);
if (genreStore.DoesRecordExist(genre))
{
genre = genreStore.RetrieveRecord(genre);
return Ok(genre);
}
else
{
return NotFound(new Genre());
}
}
#endregion
}
-96
View File
@@ -1,96 +0,0 @@
using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Icarus.Controllers.Managers;
using Icarus.Controllers.Utilities;
using Icarus.Models;
using Icarus.Database.Contexts;
namespace Icarus.Controllers.V1;
[Route("api/v1/login")]
[ApiController]
public class LoginController : ControllerBase
{
#region Fields
private string? _connectionString;
private IConfiguration? _config;
private ILogger<LoginController> _logger;
#endregion
#region Properties
#endregion
#region Contructors
public LoginController(IConfiguration config, ILogger<LoginController> logger)
{
_logger = logger;
_config = config;
_connectionString = _config.GetConnectionString("DefaultConnection");
}
#endregion
#region HTTP endpoints
[HttpPost]
public IActionResult Login([FromBody] User user)
{
var context = new UserContext(_connectionString!);
_logger.LogInformation("Starting process of validating credentials");
var message = "Invalid credentials";
var password = user.Password;
var loginRes = new LoginResult
{
Username = user.Username
};
try
{
if (context.Users.FirstOrDefault(usr => usr.Username!.Equals(user.Username)) != null)
{
user = context.Users.FirstOrDefault(usr => usr.Username!.Equals(user.Username))!;
var validatePass = new PasswordEncryption();
var validated = validatePass.VerifyPassword(user!, password!);
if (!validated)
{
loginRes.Message = message;
_logger.LogInformation(message);
return Ok(loginRes);
}
_logger.LogInformation("Successfully validated user credentials");
TokenManager tk = new TokenManager(_config!);
loginRes = tk.LoginSymmetric(user!);
return Ok(loginRes);
}
else
{
loginRes.Message = message;
return NotFound(loginRes);
}
}
catch (Exception ex)
{
_logger.LogError("An error occurred: {0}", ex.Message);
_logger.LogError("Inner Exception: {0}", ex.InnerException!.Message);
}
return NotFound(loginRes);
}
#endregion
}
@@ -1,75 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Icarus.Controllers.Utilities;
using Icarus.Models;
using Icarus.Database.Contexts;
namespace Icarus.Controllers.V1;
[Route("api/v1/register")]
[ApiController]
public class RegisterController : ControllerBase
{
#region Fields
private IConfiguration? _config;
#endregion
#region Properties
#endregion
#region Constructor
public RegisterController(IConfiguration config)
{
_config = config;
}
#endregion
[HttpPost]
public IActionResult RegisterUser([FromBody] User user)
{
PasswordEncryption pe = new PasswordEncryption();
user.Password = pe.HashPassword(user);
user.EmailVerified = false;
user.Status = "Registered";
user.DateCreated = DateTime.Now;
UserContext? context = null;
try
{
var connString = _config!.GetConnectionString("DefaultConnection");
context = new UserContext(connString!);
context.Add(user);
context.SaveChanges();
}
catch (Exception ex)
{
var msg = ex.Message;
var stackTrace = ex.StackTrace;
Console.WriteLine($"An error occurred: {msg}");
}
var registerResult = new RegisterResult
{
Username = user.Username
};
if (context!.Users.FirstOrDefault(sng => sng.Username!.Equals(user.Username)) != null)
{
registerResult.Message = "Successful registration";
registerResult.SuccessfullyRegistered = true;
return Ok(registerResult);
}
else
{
registerResult.Message = "Registration failed";
registerResult.SuccessfullyRegistered = false;
return Ok(registerResult);
}
}
}
@@ -1,71 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Icarus.Controllers.Utilities;
using Icarus.Models;
using Icarus.Database.Contexts;
using Icarus.Controllers.Managers;
namespace Icarus.Controllers.V1;
[Route("api/v1/song/compressed/data")]
[ApiController]
[Authorize]
public class SongCompressedDataController : BaseController
{
#region Fields
private string? _connectionString;
private string? _songTempDir;
private string? _archiveDir;
#endregion
#region Properties
#endregion
#region Constructor
public SongCompressedDataController(IConfiguration config)
{
_config = config;
_songTempDir = _config.GetValue<string>("TemporaryMusicPath");
_archiveDir = _config.GetValue<string>("ArchivePath");
_connectionString = _config.GetConnectionString("DefaultConnection");
}
#endregion
#region API Routes
[HttpGet("{id}")]
public async Task<IActionResult> DownloadCompressedSong(int id, [FromQuery] bool? randomizeFilename)
{
var context = new SongContext(_connectionString!);
SongCompression cmp = new SongCompression(_archiveDir!);
Console.WriteLine($"Archive directory root: {_archiveDir}");
Console.WriteLine("Starting process of retrieving comrpessed song");
var sng = context.RetrieveRecord(new Song { Id = id });
var tokenManager = new TokenManager(this._config!);
var accLvlContext = new AccessLevelContext(this._connectionString!);
var accessLevel = accLvlContext.GetAccessLevel(sng.Id);
var token = tokenManager.GetBearerToken(HttpContext);
if (token == null || accessLevel == null)
{
return BadRequest();
}
if (!tokenManager.CanAccessSong(token, sng, accessLevel))
{
return BadRequest();
}
SongData song = await cmp.RetrieveCompressedSong(sng);
var filename = DirectoryManager.GenerateDownloadFilename(10, Constants.FileExtensions.ZIP_EXTENSION, sng.Title!, randomizeFilename);
return File(song.Data!, "application/x-msdownload", filename);
}
#endregion
}
-109
View File
@@ -1,109 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Icarus.Controllers.Managers;
using Icarus.Models;
using Icarus.Database.Contexts;
namespace Icarus.Controllers.V1;
[Route("api/v1/song")]
[ApiController]
[Authorize]
public class SongController : BaseController
{
#region Fields
private readonly ILogger<SongController>? _logger;
private string? _connectionString;
private SongManager? _songMgr;
#endregion
#region Properties
#endregion
#region Constructor
public SongController(IConfiguration config, ILogger<SongController> logger)
{
_config = config;
_logger = logger;
_connectionString = _config.GetConnectionString("DefaultConnection");
_songMgr = new SongManager(config);
}
#endregion
#region Methods
#region HTTP Endpoints
[HttpGet]
public IActionResult GetSongs()
{
Console.WriteLine("Attemtping to retrieve songs");
_logger!.LogInformation("Attempting to retrieve songs");
var context = new SongContext(_connectionString!);
var songs = context.Songs!.ToList();
if (songs.Count > 0)
{
return Ok(songs);
}
else
{
return NotFound();
}
}
[HttpGet("{id}")]
public IActionResult GetSong(int id)
{
var context = new SongContext(_connectionString!);
var song = context.RetrieveRecord(new Song { Id = id });
Console.WriteLine("Here");
if (song.Id != 0)
return Ok(song);
else
return NotFound();
}
[HttpPut("{id}")]
public IActionResult UpdateSong(int id, [FromBody] Song song)
{
song.Id = id;
Console.WriteLine("Retrieving filepath of song");
_logger!.LogInformation("Retrieving filepath of song");
if (!_songMgr!.DoesSongExist(song))
{
return NotFound(new SongResult
{
Message = "Song does not exist"
});
}
var tokenManager = new TokenManager(this._config!);
var accLvlContext = new AccessLevelContext(this._connectionString!);
var accessLevel = accLvlContext.GetAccessLevel(song.Id);
var token = tokenManager.GetBearerToken(HttpContext);
if (token == null || accessLevel == null)
{
return BadRequest();
}
if (!tokenManager.CanAccessSong(token, song, accessLevel))
{
return BadRequest();
}
var songRes = _songMgr.UpdateSong(song);
return Ok(songRes);
}
#endregion
#endregion
}
-231
View File
@@ -1,231 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Icarus.Controllers.Managers;
using Icarus.Models;
using Icarus.Database.Contexts;
namespace Icarus.Controllers.V1;
[Route("api/v1/song/data")]
[ApiController]
[Authorize]
public class SongDataController : BaseController
{
#region Fields
private string? _connectionString;
private ILogger<SongDataController>? _logger;
private SongManager? _songMgr;
private string? _songTempDir;
#endregion
#region Properties
#endregion
#region Constructor
public SongDataController(IConfiguration config, ILogger<SongDataController> logger)
{
_config = config;
_connectionString = _config.GetConnectionString("DefaultConnection");
_logger = logger;
_songTempDir = _config.GetValue<string>("TemporaryMusicPath");
_songMgr = new SongManager(config, _songTempDir!);
}
#endregion
[HttpGet("download/{id}")]
public IActionResult Download(int id, [FromQuery] bool? randomizeFilename)
{
var tokenManager = new TokenManager(this._config!);
var songContext = new SongContext(this._connectionString!);
var accLvlContext = new AccessLevelContext(this._connectionString!);
var songMetaData = songContext.RetrieveRecord(new Song { Id = id });
var accessLevel = accLvlContext.GetAccessLevel(songMetaData.Id);
var token = tokenManager.GetBearerToken(HttpContext);
if (token == null || accessLevel == null)
{
return BadRequest();
}
if (!tokenManager.CanAccessSong(token, songMetaData, accessLevel))
{
return BadRequest();
}
var song = _songMgr!.RetrieveSong(songMetaData).Result;
string filename;
switch (songMetaData.AudioType)
{
case "wav":
filename = DirectoryManager.GenerateDownloadFilename(10, Constants.FileExtensions.WAV_EXTENSION,
songMetaData.Title!, randomizeFilename);
break;
case "flac":
filename = DirectoryManager.GenerateDownloadFilename(10, Constants.FileExtensions.FLAC_EXTENSION,
songMetaData.Title!, randomizeFilename);
break;
default:
filename = DirectoryManager.GenerateDownloadFilename(10, Constants.FileExtensions.DEFAULT_AUDIO_EXTENSION,
songMetaData.Title!, randomizeFilename);
break;
}
return File(song.Data!, "application/x-msdownload", filename);
}
// NOTE: No longer being used
// Assumes that the song already has metadata such as
// Title
// Artist
// Album
// Genre
// Year
// Track
// Track count
// Disc
// Disc count
// Cover art
//
[HttpPost("upload"), DisableRequestSizeLimit]
[Obsolete("Support for this endoint is no longer supported")]
public IActionResult Upload([FromForm(Name = "file")] List<IFormFile> songData)
{
return new StatusCodeResult(StatusCodes.Status410Gone);
}
// The client is expected to send the file, metadata, and cover art separately.
// Any metadata already on the file will be overwritten with values from the metadata
// as well as the cover art
//
[HttpPost("upload/with/data")]
public IActionResult UploadWithData([FromForm] UploadSongWithDataForm up)
{
try
{
if (up.SongData!.Length > 0 && up.CoverArtData!.Length > 0 && !string.IsNullOrEmpty(up.SongFile))
{
var meta = new Utilities.MetadataRetriever();
var tmpSong = this._songMgr!.SaveSongTemp(up.SongData).Result;
if (!meta.IsSupportedFile(tmpSong.SongPath()) && !meta.IsSupportedFile(up.CoverArtData))
{
return BadRequest("Media is not supported");
}
else if (!meta.IsSupportedFile(tmpSong.SongPath()))
{
return BadRequest("Song is not supported");
}
else if (!meta.IsSupportedFile(up.CoverArtData))
{
return BadRequest("Cover art is not supported");
}
var song = Newtonsoft.Json.JsonConvert.DeserializeObject<Song>(up.SongFile);
var tokMgr = new TokenManager(this._config!);
var accessToken = Request.Headers["Authorization"];
var userId = tokMgr.RetrieveUserIdFromToken(accessToken!);
if (userId != null)
{
song!.UserId = userId.Value;
}
_logger!.LogInformation($"Song title: {song!.Title}");
song.Filename = tmpSong.Filename;
song.SongDirectory = tmpSong.SongDirectory;
song.DateCreated = tmpSong.DateCreated;
song.AudioType = meta.FileExtensionType(tmpSong.SongPath());
switch (song.AudioType)
{
case "wav":
var _ = _songMgr.DeleteSongFromFileSystem(tmpSong);
return BadRequest(new UploadSongWithDataResponse
{
Subject = "No longer supported",
Message = "No support for .wav files",
Songs = new List<Song>()
});
case "flac":
song = _songMgr.SaveFlacSongToFileSystem(up.SongData, up.CoverArtData, song);
break;
default:
return BadRequest();
}
return Ok(song);
}
}
catch (Exception ex)
{
_logger!.LogError(ex.Message, "An error occurred");
}
return BadRequest();
}
[HttpDelete("delete/{id}")]
public IActionResult DeleteSong(int id)
{
var songContext = new SongContext(_connectionString!);
var songMetaData = songContext.RetrieveRecord(new Song { Id = id });
var tokenManager = new TokenManager(this._config!);
var accLvlContext = new AccessLevelContext(this._connectionString!);
var accessLevel = accLvlContext.GetAccessLevel(songMetaData.Id);
var token = tokenManager.GetBearerToken(HttpContext);
if (token == null || accessLevel == null)
{
return BadRequest();
}
if (!tokenManager.CanAccessSong(token, songMetaData, accessLevel))
{
return BadRequest();
}
if (string.IsNullOrEmpty(songMetaData.Title))
{
_logger!.LogInformation("Song does not exist");
return NotFound("Song does not exist");
}
else
{
_logger!.LogInformation("Starting process of deleting song from the filesystem and database");
_songMgr!.DeleteSong(songMetaData);
return Ok(songMetaData);
}
}
public class UploadSongWithDataForm
{
[FromForm(Name = "file")]
public IFormFile? SongData { get; set; }
// NOTE: Think about making this optional and if it is not provided, use the stock cover art
[FromForm(Name = "cover")]
public IFormFile? CoverArtData { get; set; }
[FromForm(Name = "metadata")]
public string? SongFile { get; set; }
}
public class UploadSongWithDataResponse
{
#region Properties
[Newtonsoft.Json.JsonProperty("message")]
public string? Message { get; set; }
[Newtonsoft.Json.JsonProperty("subject")]
public string? Subject { get; set; }
[Newtonsoft.Json.JsonProperty("data")]
public List<Song>? Songs { get; set; }
#endregion
}
}
@@ -1,76 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Icarus.Database.Contexts;
namespace Icarus.Controllers.V1;
[Route("api/v1/song/stream")]
[ApiController]
[Authorize]
public class SongStreamController : BaseController
{
#region Fields
private string? _connectionString;
private ILogger<SongStreamController>? _logger;
#endregion
#region Properties
#endregion
#region Constructor
public SongStreamController(ILogger<SongStreamController> logger, IConfiguration config)
{
this._logger = logger;
this._config = config;
this._connectionString = this._config.GetConnectionString("DefaultConnection");
}
#endregion
#region HTTP endpoints
[HttpGet("{id}")]
public async Task<IActionResult> StreamSong(int id)
{
var context = new SongContext(_config!.GetConnectionString("DefaultConnection")!);
var song = context.Songs!.FirstOrDefault(sng => sng.Id == id);
if (song == null)
{
return BadRequest();
}
var tokenManager = new Managers.TokenManager(this._config!);
var accLvlContext = new AccessLevelContext(this._connectionString!);
var accessLevel = accLvlContext.GetAccessLevel(song.Id);
var token = tokenManager.GetBearerToken(HttpContext);
if (token == null || accessLevel == null)
{
return BadRequest();
}
if (!tokenManager.CanAccessSong(token, song, accessLevel))
{
return BadRequest();
}
var stream = new FileStream(song!.SongPath(), FileMode.Open, FileAccess.Read);
stream.Position = 0;
var filename = song.Filename;
if (string.IsNullOrEmpty(song.Filename))
{
filename = song.GenerateFilename();
}
_logger!.LogInformation("Starting to stream song...>");
Console.WriteLine("Starting to streamsong...");
return await Task.Run(() =>
{
return File(stream, "application/octet-stream", filename);
});
}
#endregion
}
@@ -1,36 +0,0 @@
using Microsoft.EntityFrameworkCore;
namespace Icarus.Database.Contexts;
public class AccessLevelContext : DbContext
{
#region Properties
public DbSet<Models.AccessLevel>? AccessLevels { get; set; }
#endregion
#region Constructors
public AccessLevelContext(DbContextOptions<AccessLevelContext> options) : base(options) { }
public AccessLevelContext(string connString) : base(new DbContextOptionsBuilder<AccessLevelContext>()
.UseMySQL(connString).Options)
{
if (this.AccessLevels == null)
{
}
}
#endregion
#region Methods
public Models.AccessLevel? GetAccessLevel(int songId)
{
var accessLevel = this.AccessLevels!.FirstOrDefault(acc => acc.SongId == songId);
return accessLevel;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Models.AccessLevel>().ToTable("AccessLevel");
modelBuilder.Entity<Models.AccessLevel>().Property(m => m.Level).IsRequired(true);
}
#endregion
}
-33
View File
@@ -1,33 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Icarus.Models;
namespace Icarus.Database.Contexts;
public class AlbumContext : DbContext
{
public DbSet<Album>? Albums { get; set; }
public AlbumContext(DbContextOptions<AlbumContext> options) : base(options) { }
public AlbumContext(string connString) : base(new DbContextOptionsBuilder<AlbumContext>()
.UseMySQL(connString).Options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Album>()
.ToTable("Album");
}
public Album RetrieveRecord(Album album)
{
var albm = Albums!.FirstOrDefault(alb => alb.Id == album.Id);
return albm!;
}
public bool DoesRecordExist(Album album)
{
return Albums!.FirstOrDefault(alb => alb.Id == album.Id) != null ? true : false;
}
}
-34
View File
@@ -1,34 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Icarus.Models;
namespace Icarus.Database.Contexts;
public class ArtistContext : DbContext
{
public DbSet<Artist> Artists { get; set; }
public ArtistContext(DbContextOptions<ArtistContext> options) : base(options) { }
public ArtistContext(string connString) : base(new DbContextOptionsBuilder<ArtistContext>()
.UseMySQL(connString).Options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Artist>()
.ToTable("Artist");
}
public Artist RetrieveRecord(Artist artist)
{
return Artists.FirstOrDefault(arst => arst.Id == artist.Id)!;
}
public bool DoesRecordExist(Artist artist)
{
return Artists.FirstOrDefault(arst => arst.Id == artist.Id) != null ? true : false;
}
}
@@ -1,34 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Icarus.Models;
namespace Icarus.Database.Contexts;
public class CoverArtContext : DbContext
{
#region Properties
public DbSet<CoverArt>? CoverArtImages { get; set; }
#endregion
public CoverArtContext(DbContextOptions<CoverArtContext> options) : base(options) { }
public CoverArtContext(string connString) : base(new DbContextOptionsBuilder<CoverArtContext>()
.UseMySQL(connString).Options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CoverArt>()
.ToTable("CoverArt");
}
public CoverArt RetrieveRecord(CoverArt cover)
{
return CoverArtImages!.FirstOrDefault(cov => cov.Id == cover.Id)!;
}
public bool DoesRecordExist(CoverArt cover)
{
return CoverArtImages!.FirstOrDefault(cov => cov.Id == cover.Id) != null ? true : false;
}
}
-36
View File
@@ -1,36 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Icarus.Models;
namespace Icarus.Database.Contexts;
public class GenreContext : DbContext
{
#region Properties
public DbSet<Genre>? Genres { get; set; }
#endregion
public GenreContext(DbContextOptions<GenreContext> options) : base(options) { }
public GenreContext(string connString) : base(new DbContextOptionsBuilder<GenreContext>()
.UseMySQL(connString).Options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Genre>()
.ToTable("Genre");
}
public Genre RetrieveRecord(Genre genre)
{
var gnre = Genres!.FirstOrDefault(gnr => gnr.Id == genre.Id);
return gnre!;
}
public bool DoesRecordExist(Genre genre)
{
return Genres!.FirstOrDefault(gnr => gnr.Id == genre.Id) != null ? true : false;
}
}
-53
View File
@@ -1,53 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Icarus.Models;
namespace Icarus.Database.Contexts;
public class SongContext : DbContext
{
public DbSet<Song>? Songs { get; set; }
public SongContext(string connString) : base(new DbContextOptionsBuilder<SongContext>()
.UseMySQL(connString).Options)
{
}
public SongContext(DbContextOptions<SongContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Song>()
.ToTable("Song");
modelBuilder.Entity<Song>()
.Property(s => s.Year)
.IsRequired(false);
modelBuilder.Entity<Song>()
.Property(s => s.GenreId)
.IsRequired(false);
modelBuilder.Entity<Song>()
.Property(s => s.ArtistId)
.IsRequired(false);
modelBuilder.Entity<Song>()
.Property(s => s.AlbumId)
.IsRequired(false);
modelBuilder.Entity<Song>()
.Property(s => s.CoverArtId)
.IsRequired(false);
}
public Song RetrieveRecord(Song song)
{
var sng = Songs!.FirstOrDefault(sng => sng.Id == song.Id);
return sng!;
}
public bool DoesRecordExist(Song song)
{
return Songs!.FirstOrDefault(sng => sng.Id == song.Id) != null ? true : false;
}
}
-42
View File
@@ -1,42 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Icarus.Models;
namespace Icarus.Database.Contexts;
public class UserContext : DbContext
{
public DbSet<User> Users { get; set; }
#region Constructors
public UserContext(DbContextOptions<UserContext> options) : base(options) { }
[ActivatorUtilitiesConstructor]
public UserContext(string connString) : base(new DbContextOptionsBuilder<UserContext>()
.UseMySQL(connString).Options)
{
}
#endregion
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.ToTable("User");
modelBuilder.Entity<User>()
.Property(u => u.LastLogin).IsRequired(false);
modelBuilder.Entity<User>()
.Property(u => u.DateCreated).HasDefaultValue(DateTime.Now);
}
public User RetrieveRecord(User user)
{
return Users.FirstOrDefault(usr => usr.Id == user.Id)!;
}
public bool DoesRecordExist(User user)
{
return Users.FirstOrDefault(usr => usr.Id == user.Id) != null ? true : false;
}
}
-46
View File
@@ -1,46 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
<Version>0.1.30</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" />
<PackageReference Include="EntityFramework" Version="6.5.1" />
<PackageReference Include="File.TypeChecker" Version="4.1.1" />
<PackageReference Include="Iconic.Zlib.Netstandard" Version="1.0.0" />
<PackageReference Include="JWT" Version="11.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.14" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.14">
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MySql.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog.Web.AspNetCore" Version="4.14.0" />
<PackageReference Include="RestSharp" Version="106.15.0" />
<PackageReference Include="SevenZip" Version="19.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.7.1" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageReference Include="taglib" Version="2.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Models\Models.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="nlog.config" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Images/Stock/*.*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
-110
View File
@@ -1,110 +0,0 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Icarus.Database.Contexts;
var builder = WebApplication.CreateBuilder(args);
var MAX_REQUEST_BODY_SIZE = 51200000000;
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Icarus", Version = "v1" });
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
Name = "Authorization",
Scheme = "Bearer",
BearerFormat = "JWT",
Type = SecuritySchemeType.ApiKey,
In = ParameterLocation.Header,
Description = "Bearer *Auth Token*",
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] {}
}
});
});
builder.Services.AddControllers();
builder.WebHost.UseKestrel(option =>
{
option.Limits.MaxRequestBodySize = MAX_REQUEST_BODY_SIZE;
});
var Configuration = builder.Configuration;
var connString = Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
var audience = Configuration["JWT:Audience"];
var issuer = Configuration["JWT:Issuer"];
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidAudience = audience,
ValidIssuer = issuer,
IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(Configuration["JWT:Secret"]!))
};
});
builder.Services.AddDbContext<SongContext>(options => options.UseMySQL(connString!));
builder.Services.AddDbContext<AlbumContext>(options => options.UseMySQL(connString!));
builder.Services.AddDbContext<ArtistContext>(options => options.UseMySQL(connString!));
builder.Services.AddDbContext<UserContext>(options => options.UseMySQL(connString!));
builder.Services.AddDbContext<GenreContext>(options => options.UseMySQL(connString!));
builder.Services.AddDbContext<CoverArtContext>(options => options.UseMySQL(connString!));
builder.Services.AddDbContext<AccessLevelContext>(options => options.UseMySQL(connString!));
builder.Services.AddControllers()
.AddNewtonsoftJson();
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// NOTE: This should be enabled at some point
// app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
#pragma warning disable ASP0014
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
#pragma warning restore ASP0014
app.Run();
-31
View File
@@ -1,31 +0,0 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:20781",
"sslPort": 44394
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Icarus": {
"commandName": "Project",
"launchBrowser": true,
"dotnetRuneMessage": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5002",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
-9
View File
@@ -1,9 +0,0 @@
using System;
namespace Icarus.Types;
public enum CoverArtField
{
SongTitle = 0,
ImagePath
};
-9
View File
@@ -1,9 +0,0 @@
using System;
namespace Icarus.Types;
public enum DirectoryType
{
Music = 0,
CoverArt
};
-28
View File
@@ -1,28 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"Auth0": {
"Domain": "[domain].auth0.com",
"ApiIdentifier": "https://[identifier]/api",
"ClientId": "",
"ClientSecret": ""
},
"JWT": {
"Issuer": "",
"Audience": "",
"Secret": "",
"Subject": ""
},
"ConnectionStrings": {
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
},
"RootMusicPath": "/music/path/",
"TemporaryMusicPath": "/music/temp/path/",
"ArchivePath": "/archive/path/",
"CoverArtPath": "/cover/art/path/"
}
-33
View File
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
internalLogLevel="Info"
internalLogFile="Icarus.txt">
<!-- enable asp.net core layout renderers -->
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
</extensions>
<!-- the targets to write to -->
<targets>
<!-- write logs to file -->
<target xsi:type="File" name="allfile" fileName="Icarus-${shortdate}.log"
layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
<!-- another file log, only own logs. Uses some ASP.NET core renderers -->
<target xsi:type="File" name="ownFile-web" fileName="Icarus-${shortdate}.log"
layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}" />
</targets>
<!-- rules to map from logger name to target -->
<rules>
<!--All logs, including from Microsoft-->
<logger name="*" minlevel="Trace" writeTo="allfile" />
<!--Skip non-critical Microsoft logs and so log only own logs-->
<logger name="Microsoft.*" maxlevel="Info" final="true" /> <!-- BlackHole without writeTo -->
<logger name="*" minlevel="Trace" writeTo="ownFile-web" />
</rules>
</nlog>

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 Kun Deng
Copyright (c) 2019 Kun Deng
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
View File
-51
View File
@@ -1,51 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Icarus.Models;
public class AccessLevel
{
#region Properties
[Newtonsoft.Json.JsonProperty("id")]
public int Id { get; set; }
[Newtonsoft.Json.JsonProperty("level")]
public string? Level { get; set; }
[Newtonsoft.Json.JsonProperty("song_id")]
public int SongId { get; set; }
#endregion
#region Methods
public static AccessLevel DefaultLevel()
{
return new AccessLevel
{
Level = "Public"
};
}
public static AccessLevel PrivateLevel()
{
return new AccessLevel
{
Level = "Private"
};
}
public static bool IsAccessLevelValid(string level)
{
if (level.Equals(DefaultLevel().Level))
{
return true;
}
else if (level.Equals(PrivateLevel().Level))
{
return true;
}
else
{
return false;
}
}
#endregion
}
-23
View File
@@ -1,23 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
namespace Icarus.Models;
public class Album
{
#region Properties
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("title")]
public string? Title { get; set; }
[JsonProperty("album_artist")]
[Column("Artist")]
public string? AlbumArtist { get; set; }
[JsonProperty("song_count")]
[NotMapped]
public int SongCount { get; set; }
[JsonProperty("year")]
public int Year { get; set; }
#endregion
}
-19
View File
@@ -1,19 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
namespace Icarus.Models;
public class Artist
{
#region Properties
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
[Column("Artist")]
public string? Name { get; set; }
[JsonProperty("song_count")]
[NotMapped]
public int SongCount { get; set; }
#endregion
}
-9
View File
@@ -1,9 +0,0 @@
using Newtonsoft.Json;
namespace Icarus.Models;
public class BaseResult
{
[JsonProperty("message")]
public string? Message { get; set; }
}
-9
View File
@@ -1,9 +0,0 @@
namespace Icarus.Constants;
public class DirectoryPaths
{
public static string CoverArtDirectory => Directory.GetCurrentDirectory() + "/Images/Stock/";
public static string CoverArtFilename => "CoverArt.png";
public static string FILENAME_CHARACTERS = "ABCDEF0123456789";
public static int FILENAME_LENGTH = 25;
}
-22
View File
@@ -1,22 +0,0 @@
namespace Icarus.Constants;
public class FileExtensions
{
// Contains the default audio file extension with period at the beginning
public static string DEFAULT_AUDIO_EXTENSION = WAV_EXTENSION!;
// Contains file extension with period at the beginning
public static string MP3_EXTENSION = ".mp3";
// Contains file extension with period at the beginning
public static string WAV_EXTENSION = ".wav";
// Contains file extension with period at the beginning
public static string JPG_EXTENSION = ".jpg";
// Contains file extension with period at the beginning
public static string FLAC_EXTENSION = ".flac";
// Contains file extension with period at the beginning
public static string ZIP_EXTENSION = ".zip";
}
-50
View File
@@ -1,50 +0,0 @@
using Newtonsoft.Json;
namespace Icarus.Models;
public class CoverArt
{
#region Properties
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("title")]
public string? SongTitle { get; set; }
[JsonIgnore]
public string? Directory { get; set; }
[JsonProperty("filename")]
public string? Filename { get; set; }
[JsonProperty("type")]
public string? Type { get; set; }
#endregion
#region Methods
public string ImagePath()
{
var fullPath = this.Directory;
if (fullPath![fullPath.Length - 1] != '/')
{
fullPath += "/";
}
fullPath += Filename;
return fullPath;
}
public string GenerateFilename(int flag)
{
int length = Constants.DirectoryPaths.FILENAME_LENGTH;
string chars = Constants.DirectoryPaths.FILENAME_CHARACTERS;
var random = new Random();
var filename = new string(Enumerable.Repeat(chars, length).Select(s =>
s[random.Next(s.Length)]).ToArray());
var extension = Constants.FileExtensions.JPG_EXTENSION;
return (flag == 0) ? filename : $"{filename}{extension}";
}
public async Task<byte[]> GetData() => await File.ReadAllBytesAsync(this.ImagePath());
#endregion
}
-11
View File
@@ -1,11 +0,0 @@
namespace Icarus.Models;
public enum CreateFileResult
{
Unknwon = 0,
AlreadyExists = 1,
FileCreatedAndExists = 2
}
-19
View File
@@ -1,19 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
namespace Icarus.Models;
public class Genre
{
#region Properties
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("genre")]
[Column("Category")]
public string? GenreName { get; set; }
[JsonProperty("song_count")]
[NotMapped]
public int SongCount { get; set; }
#endregion
}
-19
View File
@@ -1,19 +0,0 @@
using Newtonsoft.Json;
namespace Icarus.Models;
public class LoginResult : BaseResult
{
#region Properties
[JsonProperty("user_id")]
public int UserId { get; set; }
[JsonProperty("username")]
public string? Username { get; set; }
[JsonProperty("token")]
public string? Token { get; set; }
[JsonProperty("token_type")]
public string? TokenType { get; set; }
[JsonProperty("expiration")]
public int Expiration { get; set; }
#endregion
}
-19
View File
@@ -1,19 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PackageId>Icarus.Models</PackageId>
<Product>Icarus.Models</Product>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="newtonsoft.json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>
-13
View File
@@ -1,13 +0,0 @@
using Newtonsoft.Json;
namespace Icarus.Models;
public class RegisterResult : BaseResult
{
#region Properties
[JsonProperty("username")]
public string? Username { get; set; }
[JsonProperty("successfully_registered")]
public bool SuccessfullyRegistered { get; set; }
#endregion
}
-177
View File
@@ -1,177 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
namespace Icarus.Models;
public class Song
{
#region Properties
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("title")]
public string? Title { get; set; }
[JsonProperty("album")]
[Column("Album")]
public string? AlbumTitle { get; set; }
[JsonProperty("artist")]
public string? Artist { get; set; }
[JsonProperty("album_artist")]
public string? AlbumArtist { get; set; }
[JsonProperty("year")]
public int? Year { get; set; }
[JsonProperty("genre")]
public string? Genre { get; set; }
[JsonProperty("duration")]
public int Duration { get; set; }
[JsonProperty("filename")]
public string? Filename { get; set; }
[JsonIgnore]
public string? SongDirectory { get; set; }
[JsonProperty("audio_type")]
public string? AudioType { get; set; }
[JsonProperty("track")]
public int Track { get; set; } = 0;
[JsonProperty("track_count")]
public int TrackCount { get; set; } = 0;
[JsonProperty("disc")]
public int Disc { get; set; } = 0;
[JsonProperty("disc_count")]
public int DiscCount { get; set; } = 0;
[JsonIgnore]
public int? AlbumId { get; set; }
[JsonIgnore]
public int? ArtistId { get; set; }
[JsonIgnore]
public int? GenreId { get; set; }
[JsonIgnore]
public int? CoverArtId { get; set; }
[JsonProperty("date_created")]
public DateTime DateCreated { get; set; }
[JsonProperty("user_id")]
public int UserId { get; set; }
#endregion
#region Constructors
#endregion
#region Methods
public void PrintMetadata()
{
Console.WriteLine("\n\nMetadata of the song:");
Console.WriteLine($"ID: {this.Id}");
Console.WriteLine($"Title: {this.Title}");
Console.WriteLine($"Artist: {this.Artist}");
Console.WriteLine($"Album: {this.AlbumTitle}");
Console.WriteLine($"Genre: {this.Genre}");
Console.WriteLine($"Year: {this.Year}");
Console.WriteLine($"Duration: {this.Duration}");
Console.WriteLine($"AlbumID: {this.AlbumId}");
Console.WriteLine($"ArtistID: {this.ArtistId}");
Console.WriteLine($"GenreID: {this.GenreId}");
Console.WriteLine($"Song Path: {this.SongPath()}");
Console.WriteLine($"Filename: {this.Filename}");
Console.WriteLine("\n");
}
public string SongPath()
{
var fullPath = SongDirectory;
if (fullPath![fullPath.Length - 1] != '/')
{
fullPath += "/";
}
fullPath += Filename;
return fullPath;
}
public string GenerateFilename(bool includeExtension = false, AudioFileExtensionsType flag = AudioFileExtensionsType.Default)
{
int length = Constants.DirectoryPaths.FILENAME_LENGTH;
string chars = Constants.DirectoryPaths.FILENAME_CHARACTERS;
var filename = this.Generate(length, chars);
var extension = this.DetermineFileExtension(flag);
return includeExtension ? $"{filename}{extension}" : filename;
}
public async Task<string> GenerateFilenameAsync(bool includeExtension = false, AudioFileExtensionsType flag = AudioFileExtensionsType.Default)
{
int length = Constants.DirectoryPaths.FILENAME_LENGTH;
string chars = Constants.DirectoryPaths.FILENAME_CHARACTERS;
var extension = this.DetermineFileExtension(flag);
var filename = await Task.Run(() =>
{
return this.Generate(length, chars);
});
return includeExtension ? $"{filename}{extension}" : filename;
}
public CreateSongResult Create(Microsoft.AspNetCore.Http.IFormFile file, string filePath, string prompt)
{
if (System.IO.File.Exists(filePath))
{
return CreateSongResult.AlreadyExists;
}
using (var filestream = new FileStream(filePath, FileMode.Create))
{
Console.WriteLine(prompt);
file.CopyTo(filestream);
if (System.IO.File.Exists(filePath))
{
return CreateSongResult.Created;
}
}
return CreateSongResult.NotCreated;
}
private string DetermineFileExtension(AudioFileExtensionsType flag)
{
switch (flag)
{
case AudioFileExtensionsType.Default:
return Constants.FileExtensions.DEFAULT_AUDIO_EXTENSION;
case AudioFileExtensionsType.WAV:
return Constants.FileExtensions.WAV_EXTENSION;
case AudioFileExtensionsType.FLAC:
return Constants.FileExtensions.FLAC_EXTENSION;
default:
return "";
}
}
private string Generate(int length, string chars)
{
var random = new Random();
var filename = new string(Enumerable.Repeat(chars, length).Select(s =>
s[random.Next(s.Length)]).ToArray());
return filename;
}
#endregion
}
#region Enums
public enum AudioFileExtensionsType
{
Default = 0,
WAV = 1,
FLAC = 2
}
public enum CreateSongResult
{
NotCreated = 0,
AlreadyExists = 1,
Created = 2
}
#endregion
-10
View File
@@ -1,10 +0,0 @@
namespace Icarus.Models;
public class SongData
{
#region Properties
public int ID { get; set; }
public byte[]? Data { get; set; }
public int SongID { get; set; }
#endregion
}
-13
View File
@@ -1,13 +0,0 @@
using Newtonsoft.Json;
namespace Icarus.Models;
public class SongResult
{
#region Properties
[JsonProperty("message")]
public string? Message { get; set; }
[JsonProperty("song_title")]
public string? SongTitle { get; set; }
#endregion
}
-47
View File
@@ -1,47 +0,0 @@
using Newtonsoft.Json;
namespace Icarus.Models;
public class Token
{
#region Properties
[JsonProperty("scope")]
public string? Scope { get; set; }
[JsonProperty("exp")]
public int Expiration { get; set; }
[JsonProperty("aud")]
public string? Audience { get; set; }
[JsonProperty("iss")]
public string? Issuer { get; set; }
[JsonProperty("iat")]
public int Issued { get; set; }
#endregion
#region Methods
public bool TokenExpired()
{
var currentDate = DateTime.Now;
var currentDateInSeconds = Math.Floor((currentDate - DateTime.UnixEpoch).TotalSeconds);
var result = (currentDateInSeconds >= Expiration) ? true : false;
return result;
}
public bool ContainsScope(string desiredScope)
{
var result = Scope!.Contains(desiredScope);
return result;
}
public bool Erroneous()
{
Func<string, bool> isEmpty = a => string.IsNullOrEmpty(a);
var result = (!isEmpty(Scope!) && !isEmpty(Audience!) && !isEmpty(Issuer!) &&
Expiration > 0 && Issued > 0) ? false : true;
return result;
}
#endregion
}
-54
View File
@@ -1,54 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
namespace Icarus.Models;
[Table("User")]
public class User
{
#region Properties
[JsonProperty("id")]
[Column("Id")]
[Key]
public int Id { get; set; }
[JsonProperty("username")]
public string? Username { get; set; }
[JsonProperty("password")]
public string? Password { get; set; }
[JsonProperty("email")]
public string? Email { get; set; }
[JsonProperty("phone")]
[Column("Phone")]
public string? Phone { get; set; }
[JsonProperty("firstname")]
public string? Firstname { get; set; }
[JsonProperty("lastname")]
public string? Lastname { get; set; }
[JsonProperty("email_verified")]
[NotMapped]
public bool EmailVerified { get; set; }
[JsonProperty("date_created")]
public DateTime DateCreated { get; set; }
[JsonProperty("status")]
public string? Status { get; set; }
[JsonProperty("last_login")]
public DateTime? LastLogin { get; set; }
[JsonIgnore]
[NotMapped]
public System.Collections.Generic.IEnumerable<string>? Roles { get; set; }
#endregion
#region Methods
public System.Collections.Generic.IEnumerable<System.Security.Claims.Claim> Claims()
{
var claims = new System.Collections.Generic.List<System.Security.Claims.Claim> { new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, Username!) };
claims.AddRange(Roles!.Select(role => new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Role, role)));
return claims;
}
#endregion
}
+105 -85
View File
@@ -1,153 +1,173 @@
# Icarus
Icarus is a music streaming API Server that interacts with [Mear](https://github.com/kdeng00/mear).
Icarus is a music streaming API Server, allowing access to stream your personal music collection
### Interfacing With Icarus
One can interface with Icarus the music server either by:
* [Mear](https://github.com/kdeng00/mear) - Partially implemented (under development)
* [IcarusDownloadManager](https://github.com/kdeng00/IcarusDownloadManager)
* [Mear](https://github.com/amazing-username/mear) - Partially implemented (under development)
* [IcarusDownloadManager](https://github.com/amazing-username/IcarusDownloadManager)
* Create your own client to interact with the API
## Built With
* C# [.NET](https://dotnet.microsoft.com/) 8
* [MySql](https://www.nuget.org/packages/MySql.Data/)
* OpenSSL
* BCrypt.Net-Next
* Iconic.Zlib.Netstandard
* JWT
* Microsoft.AspNetCore.Authentication.JwtBearer
* Microsoft.AspNetCore.Mvc.NewtonsoftJson
* Microsoft.EntityFrameworkCore
* Microsoft.EntityFrameworkCore.Tools
* MySql.EntityFrameworkCore
* [Newtonsoft.Json](https://www.newtonsoft.com/json)
* NLog.Web.AspNetCpre
* Portable.BouncyCastle
* RestSharp
* SevenZip
* System.IdentityModel.Tokens.Jwt
* [TagLib#](https://github.com/mono/taglib-sharp)
* C++ >= C++17
* GCC >= 8.0
* [json](https://www.github.com/nlohmann/json)
* [cpr](https://www.github.com/whoshuu/cpr)
* [TagLib](https://github.com/taglib/taglib)
* [jwt-cpp](https://github.com/Thalhammer/jwt-cpp)
* [libbcrypt](https://github.com/rg3/libbcrypt)
* [oatpp](https://github.com/oatpp/oatpp)
![image](https://user-images.githubusercontent.com/14333136/56252069-28532d00-6084-11e9-896d-1a3c378014ef.png)
## Getting started
There are several things that need to be completed to properly setup and secure the API.
This API uses OpenAPI Specification 3.0. After configuring the API, launch the software
and navigate your browser to https://localhost:5001/swagger to view the endpoints.
1. JWT Information
1. Auth0 API configuration
2. API filesystem paths
3. Database connection string
4. Migrations
### Auth0 API configuration
### JWT Information
Securing Icarus is required, preventing the API from being publicly accessible. To do so, create an Auth0 account (it's free), for the sake of this section of the documentation, I will not go over how to create an Auth0 account. Once created, create a tentant and proceed to create an API
<h1 align=center>
<img src="Images/Configuration/create_api.png" width=100%>
</h1>
Configure JWT information. Notably the Secret
Create the API and enter an approrpiate name and identified. For the identified, append **api** like in the example
<h1 align="center">
<img src="Images/Configuration/enter_api_info.png" width=100%>
</h1>
Replace [domain] with the domain name of the created tenant. This can be found in the Default App from the Application menu. Replace [identifier] with the identifer root name in the appsettings environment file. Not the friendly name but the root name of the identifier, omitting the http protocol and the *api* path.
```Json
"JWT": {
"Issuer": "IcarusAPI",
"Audience": "IcarusAPIClient",
"Secret": "Manaiswhatyouthinkitis",
"Subject": "Authorization"
},
"domain": "[domain].auth0.com",
"api_identifier": "https://[identifier]/api",
"client_id": "iamunique",
"client_secret": "Icankeepasecret"
```
Click on permissions to create the permissions for the API.
<h1 align "center">
<img src="Images/Configuration/configure_api.png" width=100%>
</h1>
Replace [domain] with the domain name that represent's your domain. Replace [identifier] with the identifer root name in the appsettings environment file. Not the friendly name but the root name of the identifier, omitting the http protocol and the *api* path.
The permissions ensure that a validated user can interact with the API with a token that has not expired. Ensure that the permissions match, the description can change but the permission identifier must match.
<h1 align="center">
<img src="Images/Configuration/permissions.png" width=100%>
</h1>
On the left side, click on Application and create a new Application. Choose the Machine to Machine Application
<h1 align="center">
<img src="Images/Configuration/create_m2m.png" width=100%>
</h1>
With the grant permissions you created from the API, enable all the permissions. This is important because if they are not enabled then even with a valid token the request will return 403 (unauthorized)
<h1 align="center">
<img src="Images/Configuration/authorize_app.png" width=100%>
</h1>
From the Application page, copy the client id and client secret. These values will be used for the API to interact with API.
<h1 align="center">
<img src="Images/Configuration/api_cred.png" width=100%>
</h1>
Enter the information in the corresponding ``authcredentials.json`` file
```Json
"Auth0": {
"Domain": "[domain].auth0.com",
"ApiIdentifier": "https://[identifier]/api"
},
{
"domain": "somedomain.auth0.com",
"api_identifier": "https://squawk/api"
"client_id": "clientidhere",
"client_secret": "illkeepyoumydirtylittlesecret"
}
```
**Note**: The Auth0 section is likely to be changed or removed in future releases.
### API filesystem paths
For the purposes of properly uploading, downloading, updating, deleting, and streaming songs the API filesystem paths must be configured. What is meant by this is that the `RootMusicPath` directory where all music will be stored must exist as well as the `ArchivePath` and `TemporaryMusicPath` paths. An example on a Linux system:
For the purposes of properly uploading, downloading, updating, deleting, and streaming songs the API filesystem paths must be configured. For that purpose you have to open the ``paths.json`` file. What is meant by this is that the `root_music_path` directory where all music will be stored must exist. The `cover_root_path`, `archive_root_path`, and `temp_root_path` paths must exist and be accessible. An example on a Linux system:
```Json
{
"RootMusicPath": "/home/dev/null/music/",
"TemporaryMusicPath": "/home/dev/null/music/temp/",
"ArchivePath": "/home/dev/null/music/archive/",
"CoverArtPath": "/home/dev/null/music/coverart/"
"root_music_path": "/dev/null/music/",
"temp_root_path": "/dev/null/music/temp/",
"cover_root_path": "/dev/null/music/coverArt/",
"archive_root_path": "/dev/null/music/archive/"
}
```
* RootMusicPath - Where music will be stored in the following convention: *`Artist/Album/Songs`*
* TemporaryMusicPath - Where music will be stored when uploding songs to the server until the metadata has been fully parsed and entered into the database. Upon completion the files will be deleted and moved to the appropriate path in the `RootMusicPath`
* ArchivePath - When downloading compressed songs this is the path where songs will be compressed prior to dataa being read into memory, deleting the compressed file, and sending the compressed file from memory to the client
* CoverArtPath - Root directory where cover art will be saved to
* `root_music_path` - Where music will be stored in the following convention: *`Artist/Album/Songs`*
* `temp_music_path` - Where music will be stored when uploding songs to the server until the metadata has been fully parsed and entered into the database. Upon completion the files will be deleted and moved to the appropriate path in the `root_music_path`
* `cover_root_path` - Where cover art of music will be saved to.
* `archive_root_path` - When downloading compressed songs this is the path where songs will be compressed prior to dataa being read into memory, deleting the compressed file, and sending the compressed file from memory to the client
**Note**: The `TemporaryMusic` or `ArchivePath` does not have to be located in the `RootMusicPath`. Ensure that the permissions are properly set for all of the paths.
**Note**: The `temp_root_path`, `cover_root_path`, or `archive_root_path` does not have to be located in the same parent directory as `root_music_path`. Ensure that the permissions are properly set for all of the paths.
### Database connection string
In order for Database functionality to be operable, there must be a valid connection string and credentials with appropriate permissions. **At the moment there is only support for MySQL**. Depending on your environment `Release` or `Debug` you will need to edit the appsettings.json or appsettings.Development.json accordingly. An example of the fields to change are below:
In order for Database functionality to be operable, there must be a valid connection string and MySQL credentials with appropriate permissions. **At the moment there is only support for MySQL**. Edit the database.json file accordingly. An example of the fields to change are below:
```Json
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=my_db;Uid=admin;Pwd=toughpassword;"
}
"server": "localhost",
"database": "my_db",
"username": "admin",
"password": "toughpassword"
}
```
* Server - The address or domain name of the MySQL server
* Database - The database name
* Uid - Username
* Password - Self-explanatory
* server - The address or domain name of the MySQL server
* database - The database name
* username - Username
* password - Self-explanatory
The only requirement of the User is that the user should have full permissions to the database as well as permissions to create a database. Other than that, that is all that is required.
### Migrations
### Database
In order to utilize migrations the `dotnet-ef` tool needs to be installed. This can be done with the following command
```BASH
dotnet tool restore
```
Prior to starting the API, the Migrations must be applied. There are 6 tables with migrations being applied and thy are:
* Users
Prior to starting the API, the database must be created. The following tables are required:
* User
* Salt
* Song
* Album
* Artist
* CoverArt
* Year
* Genre
* CoverArt
There is a script for Linux systems to apply these migrations, it can be found in the [Scripts/Migrations/Linux](https://github.com/kdeng00/Icarus/blob/master/Scripts/Migrations/Linux/AddUpdate.sh) directory. Just merely execute:
There is a MySQL script to create these tables, it can be found in the [Scripts/MySQL/](https://github.com/amazing-username/Icarus/blob/master/Scripts/MySQL/create_database.sql) directory. Just merely execute:
```shell
scripts/Migrations/Linux/AddUpdate.sh
```
Or you can manually add the migrations like so for each migration:
```shell
dotnet dotnet-ef migrations Add InitialCreate --context UserContext
```
Then update the migrations to the database like so<sup>*</sup>:
```shell
dotnet dotnet-ef database update --context UserContext
mysql -u dblikedecibel -p < Scripts/MySQL/create_database.sql
```
All of the contexts can be found in Database/Contexts folder.
From this point the database has been successfully created. Metadata and song filesystem locations can be saved.
From this point the database has been successfully configured. Metadata and song filesystem locations can be saved.
<sup>*</sup> Will only need to execute this for UserContext and SongContext because the Song table has relational constraints with Album, Artist, Year, and Genre.
## Building and Running
```
git clone --recursive https://github.com/kdeng00/icarus
cd 3rdparty/libbcrypt/
make
cd ../..
mkdir build
cd build
conan install ..
cmake .. -DCMAKE_BUILD_TYPE=RELEASE -DUSE_SYSTEM_CURL=ON -DBUILD_CPR_TESTS=OFF -DOATPP_BUILD_TESTS=OFF
make
bin/icarus
```
Runs the server on localhost port 5002
## Contributing
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the code of conduct, and the process for submitting pull requests to the project.
## Authors
* **Kun Deng** - [amazing-username](https://github.com/amazing-username)
## License
+19 -26
View File
@@ -1,33 +1,26 @@
echo "Adding migrations..."
echo "Adding User migration"
dotnet dotnet-ef migrations add User --context UserContext
echo "Adding Album migration"
dotnet dotnet-ef migrations add Album --context AlbumContext
echo "Adding Artist migration"
dotnet dotnet-ef migrations add Artist --context ArtistContext
echo "Adding Genre migration"
dotnet dotnet-ef migrations add Genre --context GenreContext
echo "Adding Cover art migration"
dotnet dotnet-ef migrations add CoverArt --context CoverArtContext
dotnet ef migrations add User --context UserContext
echo "Adding Song migration"
dotnet dotnet-ef migrations add Song --context SongContext
echo "Adding AccessLevel migration"
dotnet dotnet-ef migrations add AccessLevel --context AccessLevelContext
dotnet ef migrations add Song --context SongContext
echo "Adding Album migration"
dotnet ef migrations add Album --context AlbumContext
echo "Adding Artist migration"
dotnet ef migrations add Artist --context ArtistContext
echo "Adding Genre migration"
dotnet ef migrations add Genre --context GenreContext
echo "Adding Year migration"
dotnet ef migrations add Year --context YearContext
echo "Adding Cover art migration"
dotnet ef migrations add CoverArt --context CoverArtContext
echo "Updating migrations.."
echo "Updating User migration"
dotnet dotnet-ef database update --context UserContext
echo "Updating Album migration"
dotnet dotnet-ef database update --context AlbumContext
echo "Updating Artist migration"
dotnet dotnet-ef database update --context ArtistContext
echo "Updating Genre migration"
dotnet dotnet-ef database update --context GenreContext
echo "Updating Cover art migration"
dotnet dotnet-ef database update --context CoverArtContext
dotnet ef database update --context UserContext
echo "Updating Song migration"
dotnet dotnet-ef database update --context SongContext
echo "Updating AccessLevel migration"
dotnet dotnet-ef database update --context AccessLevelContext
echo "Updating Album migration"
echo "Updating Artist migration"
echo "Updating Genre migration"
echo "Updating Year migration"
echo "Updating Cover art migration"
dotnet ef database update --context SongContext
+34 -25
View File
@@ -3,76 +3,85 @@ CREATE DATABASE Icarus;
USE Icarus;
CREATE TABLE CoverArt (
Id INT NOT NULL AUTO_INCREMENT,
CoverArtId INT NOT NULL AUTO_INCREMENT,
SongTitle TEXT NOT NULL,
ImagePath TEXT NOT NULL,
PRIMARY KEY (Id)
PRIMARY KEY (CoverArtId)
);
CREATE TABLE Album (
Id INT NOT NULL AUTO_INCREMENT,
AlbumId INT NOT NULL AUTO_INCREMENT,
Title TEXT NOT NULL,
Artist TEXT NOT NULL,
Year INT NOT NULL,
PRIMARY KEY (Id)
PRIMARY KEY (AlbumId)
);
CREATE TABLE Artist (
Id INT NOT NULL AUTO_INCREMENT,
ArtistId INT NOT NULL AUTO_INCREMENT,
Artist TEXT NOT NULL,
PRIMARY KEY (Id)
PRIMARY KEY (ArtistId)
);
CREATE TABLE Genre (
Id INT NOT NULL AUTO_INCREMENT,
GenreId INT NOT NULL AUTO_INCREMENT,
Category TEXT NOT NULL,
PRIMARY KEY (Id)
PRIMARY KEY (GenreId)
);
CREATE TABLE Year (
YearId INT NOT NULL AUTO_INCREMENT,
Year INT NOT NULL,
PRIMARY KEY (YearId)
);
CREATE TABLE Song (
Id INT NOT NULL AUTO_INCREMENT,
SongId INT NOT NULL AUTO_INCREMENT,
Title TEXT NOT NULL,
Artist TEXT NOT NULL,
Album TEXT NOT NULL,
AlbumArtist TEXT NOT NULL,
Genre TEXT NOT NULL,
Year INT NOT NULL,
Duration INT NOT NULL,
Track INT NOT NULL,
TrackCount INT NOT NULL,
Disc INT NOT NULL,
DiscCount INT NOT NULL,
SongDirectory TEXT NOT NULL,
Filename TEXT NOT NULL,
SongPath TEXT NOT NULL,
CoverArtId INT NOT NULL,
ArtistId INT NOT NULL,
AlbumId INT NOT NULL,
GenreId INT NOT NULL,
DateCreated DATETIME DEFAULT CURRENT_TIMESTAMP,
YearId INT NOT NULL,
PRIMARY KEY (Id),
CONSTRAINT FK_Song_CoverArtId FOREIGN KEY (CoverArtId) REFERENCES CoverArt (Id),
CONSTRAINT FK_Song_ArtistId FOREIGN KEY (ArtistId) REFERENCES Artist (Id),
CONSTRAINT FK_Song_AlbumId FOREIGN KEY (AlbumId) REFERENCES Album (Id),
CONSTRAINT FK_Song_GenreId FOREIGN KEY (GenreId) REFERENCES Genre (Id)
PRIMARY KEY (SongId),
CONSTRAINT FK_CoverArtId FOREIGN KEY (CoverArtId) REFERENCES CoverArt (CoverArtId),
CONSTRAINT FK_ArtistId FOREIGN KEY (ArtistId) REFERENCES Artist (ArtistId),
CONSTRAINT FK_AlbumId FOREIGN KEY (AlbumId) REFERENCES Album (AlbumId),
CONSTRAINT FK_GenreId FOREIGN KEY (GenreId) REFERENCES Genre (GenreId),
CONSTRAINT FK_YearId FOREIGN KEY (YearId) REFERENCES Year (YearId)
);
CREATE TABLE User (
Id INT NOT NULL AUTO_INCREMENT,
UserId INT NOT NULL AUTO_INCREMENT,
Firstname TEXT NOT NULL,
Lastname TEXT NOT NULL,
Email TEXT NOT NULL,
Phone TEXT NOT NULL,
Username TEXT NOT NULL,
Password TEXT NOT NULL,
DateCreated DATETIME DEFAULT CURRENT_TIMESTAMP,
Status TEXT DEFAULT 'Active',
LastLogin DATETIME NULL,
PRIMARY KEY (Id)
PRIMARY KEY (UserId)
);
CREATE TABLE Salt (
SaltId INT NOT NULL AUTO_INCREMENT,
Salt TEXT NOT NULL,
UserId INT NOT NULL,
PRIMARY KEY (SaltId),
CONSTRAINT FK_UserId FOREIGN KEY (UserId) REFERENCES User (UserId)
);
+2 -3
View File
@@ -1,7 +1,6 @@
delete from Song;
delete from Album;
delete from Artist;
delete from CoverArt;
delete from Genre;
delete from Year;
delete from CoverArt;
delete from Song;
delete from User;
@@ -1,3 +0,0 @@
drop database Icarus;
create database Icarus;
-1
View File
@@ -1 +0,0 @@
drop database Icarus;
-19
View File
@@ -1,19 +0,0 @@
CREATE TABLE IF NOT EXISTS `__EFMigrationsHistory` (
`MigrationId` varchar(150) NOT NULL,
`ProductVersion` varchar(32) NOT NULL,
PRIMARY KEY (`MigrationId`)
);
START TRANSACTION;
CREATE TABLE `Artist` (
`Id` int NOT NULL AUTO_INCREMENT,
`Artist` longtext NULL,
PRIMARY KEY (`Id`)
);
INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`)
VALUES ('20240620205519_ModelIdChanges', '8.0.6');
COMMIT;
-137
View File
@@ -1,137 +0,0 @@
#!/usr/bin/zsh
#
# Working directory
# Output directory
# Folder name creation
# Sub-folder name creation
#
# Validation
# Check if WORK_DIR is an actual directory
# Check if OUTPUT_DIR exists
# Check if the Folder and Sub folder has been created
# Check if WORK_DIR contains .wav files
#
# Conversion
# Convert wav files to flac files
# Create folder and sub folder if it does not exist
# Move the converted flac files to the sub folder
#
# Final
# Check if OUTPUT_DIR has the folder structure. Delete if so
# Move the folder and sub folder to OUTPUT_DIR
if [[ $# -eq 0 ]]; then
my_text="No arguments provided. Provide four arguments:\n"
my_text+="script.zsh \"working_dir\" \"output_dir\" \"folder_name\" \"sub_folder_name\""
echo "$my_text"
exit
fi
if [[ ! -n "$2" ]]; then
echo "The \"output_dir\" has not been provided"
exit
fi
if [[ ! -n "$3" ]]; then
echo "The \"folder_name\" has not been provided"
exit
fi
if [[ ! -n "$4" ]]; then
echo "The \"sub_folder_name\" has not been provided"
exit
fi
WORK_DIR=$1
OUTPUT_DIR=$2
FOLDER=$3
SUB_FOLDER=$4
# Validation
if [[ ! -d $WORK_DIR ]]; then
echo "$WORK_DIR is not a directory"
exit
fi
if [[ ! -d $OUTPUT_DIR ]]; then
echo "$OUTPUT_DIR does not exist"
exit
fi
FOLDER_DIR="$OUTPUT_DIR/$FOLDER"
if [[ ! -d $FOLDER_DIR ]]; then
echo "$FOLDER_DIR directory does not exist. Creating directory"
mkdir $FOLDER_DIR
echo "Created $FOLDER_DIR directory"
fi
FOLDER_DIR="$FOLDER_DIR/$SUB_FOLDER"
if [[ ! -d $FOLDER_DIR ]]; then
echo "$FOLDER_DIR directory does not exist. Creating directory"
mkdir $FOLDER_DIR
echo "Created $FOLDER_DIR directory"
fi
# Find all files matching the pattern in the directory
matched_files=( "$WORK_DIR"/*.wav )
# Check if any files were found
if [[ ${#matched_files[@]} -gt 0 ]]; then
echo "Files matching the pattern exist in the directory:"
else
echo "No files matching the pattern were found."
exit
fi
echo "Working directory: $WORK_DIR"
echo "Output directory: $OUTPUT_DIR"
echo "Folder name: $FOLDER"
echo "Sub folder name: $SUB_FOLDER"
# Conversion
i=1
for file in "${matched_files[@]}"; do
file_output="track"
if [[ $i -lt 10 ]]; then
file_output="track0$i.flac"
else
file_output="track$i.flac"
fi
echo "$i file: $file"
output_file_path="$FOLDER_DIR/$file_output"
echo "Output file path: $output_file_path"
echo "Converting wav file to flac"
flac --best $file -o $output_file_path
ALBUM_FILE="$WORK_DIR/new-album.json"
TARGET_ALBUM_FILE="$FOLDER_DIR/album.json"
if [[ -f $ALBUM_FILE ]]; then
echo "Copying album file"
cp -a $ALBUM_FILE $TARGET_ALBUM_FILE
fi
matched_img_files=( "$WORK_DIR"/*.[j,J,p,P][p,P,n,N]* )
# Check if any files were found
if [[ ${#matched_img_files[@]} -gt 0 ]]; then
echo "Files matching the pattern exist in the directory:"
echo "Copying cover art file(s)"
for img_file in "${matched_img_files[@]}"; do
cp -a $img_file $FOLDER_DIR
done
fi
i=$((i + 1))
done
@@ -12,12 +12,6 @@
"ClientId": "",
"ClientSecret": ""
},
"JWT": {
"Issuer": "",
"Audience": "",
"Secret": "",
"Subject": ""
},
"ConnectionStrings": {
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
},
+6
View File
@@ -0,0 +1,6 @@
{
"domain": "[domain].auth0.com",
"api_identifier": "https://[identifier]/api",
"client_id": "dfdfdfdf",
"client_secret": "dfdfdfdf"
}
+5
View File
@@ -0,0 +1,5 @@
[requires]
jsonformoderncpp/3.7.3@vthiery/stable
[generators]
cmake
+7
View File
@@ -0,0 +1,7 @@
{
"server": "localhost",
"database": "Icarus",
"username": "icarus-admin",
"password": "dreamofpeace"
}
+27
View File
@@ -0,0 +1,27 @@
#ifndef STREAMCALLBACK_H_
#define STREAMCALLBACK_H_
#include <string>
#include "oatpp/core/data/stream/FileStream.hpp"
#include "oatpp/core/data/stream/Stream.hpp"
#include "oatpp/core/async/Coroutine.hpp"
//#include "oatpp/web/protocol/http/outgoing/ChunkedBody.hpp"
namespace callback {
class StreamCallback : public oatpp::data::stream::ReadCallback {
public:
StreamCallback();
StreamCallback(const std::string&);
oatpp::v_io_size read(void*, v_buff_size, oatpp::async::Action&);
private:
std::string m_songPath;
long m_bytesRead;
long m_counter;
long m_fileSize;
};
}
#endif
+40
View File
@@ -0,0 +1,40 @@
#ifndef APPCOMPONENT_H_
#define APPCOMPONENT_H_
#include <memory>
#include "oatpp/core/macro/component.hpp"
#include "oatpp/network/server/SimpleTCPConnectionProvider.hpp"
#include "oatpp/parser/json/mapping/ObjectMapper.hpp"
#include "oatpp/web/server/HttpConnectionHandler.hpp"
namespace component {
class AppComponent {
public:
OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::network::ServerConnectionProvider>,
serverConnectionProvider)([&] {
return oatpp::network::server::SimpleTCPConnectionProvider::
createShared(appPort());
}());
OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::web::server::HttpRouter>, httpRouter)([] {
return oatpp::web::server::HttpRouter::createShared();
}());
OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::network::server::ConnectionHandler>,
serverConnectionHandler)([] {
OATPP_COMPONENT(std::shared_ptr<oatpp::web::server::HttpRouter>, router);
return oatpp::web::server::HttpConnectionHandler::createShared(router);
}());
OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::data::mapping::ObjectMapper>,
apiObjectMapper)([] {
return oatpp::parser::json::mapping::ObjectMapper::createShared();
}());
private:
constexpr int appPort() noexcept { return 5002; }
};
}
#endif
+94
View File
@@ -0,0 +1,94 @@
#ifndef ALBUMCONTROLLER_H_
#define ALBUMCONTROLLER_H_
#include <filesystem>
#include <iostream>
#include <fstream>
#include <limits>
#include <string>
#include <memory>
#include <vector>
#include "oatpp/core/data/stream/ChunkedBuffer.hpp"
#include "oatpp/core/data/stream/FileStream.hpp"
#include "oatpp/core/macro/codegen.hpp"
#include "oatpp/core/macro/component.hpp"
#include "oatpp/web/mime/multipart/InMemoryPartReader.hpp"
#include "oatpp/web/mime/multipart/Reader.hpp"
#include "oatpp/web/server/api/ApiController.hpp"
#include "database/AlbumRepository.h"
#include "dto/AlbumDto.hpp"
#include "dto/conversion/DtoConversions.h"
#include "manager/AlbumManager.h"
#include "manager/TokenManager.h"
#include "model/Models.h"
#include "type/Scopes.h"
#include "type/AlbumFilter.h"
namespace fs = std::filesystem;
namespace controller {
class AlbumController : public oatpp::web::server::api::ApiController {
public:
AlbumController(const model::BinaryPath& bConf,
OATPP_COMPONENT(std::shared_ptr<ObjectMapper>, objectMapper))
: oatpp::web::server::api::ApiController(objectMapper), m_bConf(bConf) { }
#include OATPP_CODEGEN_BEGIN(ApiController)
// endpoint for retrieving all album records in json format
ENDPOINT("GET", "/api/v1/album", albumRecords,
REQUEST(std::shared_ptr<IncomingRequest>, request)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth,
type::Scope::retrieveAlbum), Status::CODE_403, "Not allowed");
std::cout << "starting process of retrieving album\n";
database::AlbumRepository albRepo(m_bConf);
auto albsDb = albRepo.retrieveRecords();
auto albums = oatpp::data::mapping::type::
List<dto::AlbumDto::ObjectWrapper>::createShared();
for (auto& albDb : albsDb) {
auto alb = dto::conversion::DtoConversions::toAlbumDto(albDb);
albums->pushBack(alb);
}
return createDtoResponse(Status::CODE_200, albums);
}
// endpoint for retrieving single album record by the album id in json format
ENDPOINT("GET", "/api/v1/album/{id}", albumRecord,
REQUEST(std::shared_ptr<IncomingRequest>, request), PATH(Int32, id)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth,
type::Scope::retrieveAlbum), Status::CODE_403, "Not allowed");
database::AlbumRepository albRepo(m_bConf);
model::Album albDb(id);
OATPP_ASSERT_HTTP(albRepo.doesAlbumExists(albDb,
type::AlbumFilter::id) , Status::CODE_403, "album does not exist");
std::cout << "album exists\n";
albDb = albRepo.retrieveRecord(albDb, type::AlbumFilter::id);
auto album = dto::conversion::DtoConversions::toAlbumDto(albDb);
return createDtoResponse(Status::CODE_200, album);
}
#include OATPP_CODEGEN_END(ApiController)
private:
model::BinaryPath m_bConf;
};
}
#endif
+97
View File
@@ -0,0 +1,97 @@
#ifndef ARTISTCONTROLLER_H_
#define ARTISTCONTROLLER_H_
#include <filesystem>
#include <iostream>
#include <fstream>
#include <limits>
#include <string>
#include <memory>
#include <vector>
#include "oatpp/core/data/stream/ChunkedBuffer.hpp"
#include "oatpp/core/data/stream/FileStream.hpp"
#include "oatpp/core/macro/codegen.hpp"
#include "oatpp/core/macro/component.hpp"
#include "oatpp/web/mime/multipart/InMemoryPartReader.hpp"
#include "oatpp/web/mime/multipart/Reader.hpp"
#include "oatpp/web/server/api/ApiController.hpp"
#include "database/ArtistRepository.h"
#include "dto/ArtistDto.hpp"
#include "manager/ArtistManager.h"
#include "manager/TokenManager.h"
#include "model/Models.h"
#include "type/Scopes.h"
#include "type/ArtistFilter.h"
namespace fs = std::filesystem;
namespace controller {
class ArtistController : public oatpp::web::server::api::ApiController {
public:
ArtistController(const model::BinaryPath& bConf,
OATPP_COMPONENT(std::shared_ptr<ObjectMapper>, objectMapper))
: oatpp::web::server::api::ApiController(objectMapper), m_bConf(bConf) { }
#include OATPP_CODEGEN_BEGIN(ApiController)
// endpoint for retrieving all artist records in json format
ENDPOINT("GET", "/api/v1/artist", artistRecords,
REQUEST(std::shared_ptr<IncomingRequest>, request)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth,
type::Scope::retrieveArtist), Status::CODE_403, "Not allowed");
std::cout << "starting process of retrieving artist\n";
database::ArtistRepository artRepo(m_bConf);
auto artsDb = artRepo.retrieveRecords();
auto artists = oatpp::data::mapping::type::
List<dto::ArtistDto::ObjectWrapper>::createShared();
for (auto& artDb : artsDb) {
auto art = dto::ArtistDto::createShared();
art->id = artDb.id;
art->artist = artDb.artist.c_str();
artists->pushBack(art);
}
return createDtoResponse(Status::CODE_200, artists);
}
// endpoint for retrieving single artist record by the artist id in json format
ENDPOINT("GET", "/api/v1/artist/{id}", artistRecord,
REQUEST(std::shared_ptr<IncomingRequest>, request), PATH(Int32, id)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth,
type::Scope::retrieveArtist), Status::CODE_403, "Not allowed");
database::ArtistRepository artRepo(m_bConf);
model::Artist artDb(id);
OATPP_ASSERT_HTTP(artRepo.doesArtistExist(artDb,
type::ArtistFilter::id) , Status::CODE_403, "artist does not exist");
std::cout << "artist exist\n";
artDb = artRepo.retrieveRecord(artDb, type::ArtistFilter::id);
auto artist = dto::ArtistDto::createShared();
artist->id = artDb.id;
artist->artist = artDb.artist.c_str();
return createDtoResponse(Status::CODE_200, artist);
}
#include OATPP_CODEGEN_END(ApiController)
private:
model::BinaryPath m_bConf;
};
}
#endif
+119
View File
@@ -0,0 +1,119 @@
#ifndef COVERARTCONTROLLER_H_
#define COVERARTCONTROLLER_H_
#include <filesystem>
#include <iostream>
#include <fstream>
#include <limits>
#include <string>
#include <memory>
#include <vector>
#include "oatpp/core/data/stream/ChunkedBuffer.hpp"
#include "oatpp/core/data/stream/FileStream.hpp"
#include "oatpp/core/macro/codegen.hpp"
#include "oatpp/core/macro/component.hpp"
#include "oatpp/web/mime/multipart/InMemoryPartReader.hpp"
#include "oatpp/web/mime/multipart/Reader.hpp"
#include "oatpp/web/server/api/ApiController.hpp"
#include "database/CoverArtRepository.h"
#include "dto/CoverArtDto.hpp"
#include "manager/CoverArtManager.h"
#include "model/Models.h"
#include "type/Scopes.h"
#include "type/CoverFilter.h"
namespace fs = std::filesystem;
namespace controller {
class CoverArtController : public oatpp::web::server::api::ApiController {
public:
CoverArtController(const model::BinaryPath& bConf,
OATPP_COMPONENT(std::shared_ptr<ObjectMapper>, objectMapper))
: oatpp::web::server::api::ApiController(objectMapper), m_bConf(bConf) { }
#include OATPP_CODEGEN_BEGIN(ApiController)
// endpoint for retrieving all cover art records in json format
ENDPOINT("GET", "/api/v1/coverart", coverArtRecords,
REQUEST(std::shared_ptr<IncomingRequest>, request)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth,
type::Scope::downloadCoverArt), Status::CODE_403, "Not allowed");
std::cout << "starting process of retrieving cover art\n";
database::CoverArtRepository covRepo(m_bConf);
auto covsDb = covRepo.retrieveRecords();
auto coverArts = oatpp::data::mapping::type::
List<dto::CoverArtDto::ObjectWrapper>::createShared();
for (auto& covDb : covsDb) {
auto cov = dto::CoverArtDto::createShared();
cov->id = covDb.id;
cov->songTitle = covDb.songTitle.c_str();
coverArts->pushBack(cov);
}
return createDtoResponse(Status::CODE_200, coverArts);
}
// endpoint for retrieving single cover art record by the cover art id in json format
ENDPOINT("GET", "/api/v1/coverart/{id}", coverArtRecord,
REQUEST(std::shared_ptr<IncomingRequest>, request), PATH(Int32, id)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth,
type::Scope::downloadCoverArt), Status::CODE_403, "Not allowed");
database::CoverArtRepository covRepo(m_bConf);
model::Cover covDb;
covDb.id = id;
OATPP_ASSERT_HTTP(covRepo.doesCoverArtExist(covDb,
type::CoverFilter::id) , Status::CODE_403, "song does not exist");
std::cout << "cover art exists\n";
covDb = covRepo.retrieveRecord(covDb, type::CoverFilter::id);
auto coverArt = dto::CoverArtDto::createShared();
coverArt->id = covDb.id;
coverArt->songTitle = covDb.songTitle.c_str();
return createDtoResponse(Status::CODE_200, coverArt);
}
ENDPOINT("GET", "/api/v1/coverart/download/{id}", downloadCoverArt,
REQUEST(std::shared_ptr<IncomingRequest>, request), PATH(Int32, id)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth,
type::Scope::downloadCoverArt), Status::CODE_403, "Not allowed");
database::CoverArtRepository covRepo(m_bConf);
model::Cover covDb;
covDb.id = id;
covDb = covRepo.retrieveRecord(covDb, type::CoverFilter::id);
auto rawCover = oatpp::base::StrBuffer::loadFromFile(covDb.imagePath.c_str());
auto response = createResponse(Status::CODE_200, rawCover);
response->putHeader(Header::CONTENT_TYPE, "image/*");
return response;
}
#include OATPP_CODEGEN_END(ApiController)
private:
model::BinaryPath m_bConf;
};
}
#endif
+97
View File
@@ -0,0 +1,97 @@
#ifndef GENRECONTROLLER_H_
#define GENRECONTROLLER_H_
#include <filesystem>
#include <iostream>
#include <fstream>
#include <limits>
#include <string>
#include <memory>
#include <vector>
#include "oatpp/core/data/stream/ChunkedBuffer.hpp"
#include "oatpp/core/data/stream/FileStream.hpp"
#include "oatpp/core/macro/codegen.hpp"
#include "oatpp/core/macro/component.hpp"
#include "oatpp/web/mime/multipart/InMemoryPartReader.hpp"
#include "oatpp/web/mime/multipart/Reader.hpp"
#include "oatpp/web/server/api/ApiController.hpp"
#include "database/GenreRepository.h"
#include "dto/GenreDto.hpp"
#include "manager/GenreManager.h"
#include "manager/YearManager.h"
#include "model/Models.h"
#include "type/Scopes.h"
#include "type/GenreFilter.h"
namespace fs = std::filesystem;
namespace controller {
class GenreController : public oatpp::web::server::api::ApiController {
public:
GenreController(const model::BinaryPath& bConf,
OATPP_COMPONENT(std::shared_ptr<ObjectMapper>, objectMapper))
: oatpp::web::server::api::ApiController(objectMapper), m_bConf(bConf) { }
#include OATPP_CODEGEN_BEGIN(ApiController)
// endpoint for retrieving all genre records in json format
ENDPOINT("GET", "/api/v1/genre", genreRecords,
REQUEST(std::shared_ptr<IncomingRequest>, request)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth,
type::Scope::retrieveGenre), Status::CODE_403, "Not allowed");
std::cout << "starting process of retrieving genre\n";
database::GenreRepository gnrRepo(m_bConf);
auto gnrsDb = gnrRepo.retrieveRecords();
auto genres = oatpp::data::mapping::type::
List<dto::GenreDto::ObjectWrapper>::createShared();
for (auto& gnrDb : gnrsDb) {
auto gnr = dto::GenreDto::createShared();
gnr->id = gnrDb.id;
gnr->category = gnrDb.category.c_str();
genres->pushBack(gnr);
}
return createDtoResponse(Status::CODE_200, genres);
}
// endpoint for retrieving single genre record by the genre id in json format
ENDPOINT("GET", "/api/v1/genre/{id}", genreRecord,
REQUEST(std::shared_ptr<IncomingRequest>, request), PATH(Int32, id)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth,
type::Scope::retrieveGenre), Status::CODE_403, "Not allowed");
database::GenreRepository gnrRepo(m_bConf);
model::Genre gnrDb(id);
OATPP_ASSERT_HTTP(gnrRepo.doesGenreExist(gnrDb,
type::GenreFilter::id) , Status::CODE_403, "genre does not exist");
std::cout << "genre exist\n";
gnrDb = gnrRepo.retrieveRecord(gnrDb, type::GenreFilter::id);
auto genre = dto::GenreDto::createShared();
genre->id = gnrDb.id;
genre->category= gnrDb.category.c_str();
return createDtoResponse(Status::CODE_200, genre);
}
#include OATPP_CODEGEN_END(ApiController)
private:
model::BinaryPath m_bConf;
};
}
#endif
+57
View File
@@ -0,0 +1,57 @@
#ifndef LOGINCONTROLLER_H_
#define LOGINCONTROLLER_H_
#include <iostream>
#include <string>
#include <memory>
#include "oatpp/core/macro/codegen.hpp"
#include "oatpp/core/macro/component.hpp"
#include "oatpp/web/server/api/ApiController.hpp"
#include "dto/LoginResultDto.hpp"
#include "dto/conversion/DtoConversions.h"
#include "manager/TokenManager.h"
#include "manager/UserManager.h"
#include "model/Models.h"
namespace controller {
class LoginController : public oatpp::web::server::api::ApiController {
public:
LoginController(const model::BinaryPath& bConf,
OATPP_COMPONENT(std::shared_ptr<ObjectMapper>, objectMapper))
:oatpp::web::server::api::ApiController(objectMapper), m_bConf(bConf) { }
#include OATPP_CODEGEN_BEGIN(ApiController)
ENDPOINT("POST", "/api/v1/login", data, BODY_DTO(dto::UserDto::ObjectWrapper, usr)) {
OATPP_LOGI("icarus", "logging in");
manager::UserManager usrMgr(m_bConf);
auto user = dto::conversion::DtoConversions::toUser(usr);
if (!usrMgr.doesUserExist(user) || !usrMgr.validatePassword(user)) {
auto logRes = dto::LoginResultDto::createShared();
logRes->message = "invalid credentials";
std::cout << "user does not exist\n";
return createDtoResponse(Status::CODE_401, logRes);
}
std::cout << "user exists\n";
manager::TokenManager tok;
auto token = tok.retrieveToken(m_bConf);
auto logRes = dto::conversion::DtoConversions::toLoginResultDto(user, token);
logRes->message = "Successful";
return createDtoResponse(Status::CODE_200, logRes);
}
#include OATPP_CODEGEN_END(ApiController)
private:
model::BinaryPath m_bConf;
};
}
#endif
+44
View File
@@ -0,0 +1,44 @@
#ifndef REGISTERCONTROLLER_H_
#define REGISTERCONTROLLER_H_
#include <iostream>
#include <memory>
#include "oatpp/core/macro/codegen.hpp"
#include "oatpp/core/macro/component.hpp"
#include "oatpp/web/server/api/ApiController.hpp"
#include "dto/LoginResultDto.hpp"
#include "dto/conversion/DtoConversions.h"
#include "manager/UserManager.h"
#include "model/Models.h"
namespace controller {
class RegisterController : public oatpp::web::server::api::ApiController {
public:
RegisterController(const model::BinaryPath& bConf,
OATPP_COMPONENT(std::shared_ptr<ObjectMapper>, objectMapper)) :
oatpp::web::server::api::ApiController(objectMapper), m_bConf(bConf) { }
#include OATPP_CODEGEN_BEGIN(ApiController)
ENDPOINT("POST", "api/v1/register", registerUser,
BODY_DTO(dto::UserDto::ObjectWrapper, usr)) {
manager::UserManager usrMgr(m_bConf);
auto user = dto::conversion::DtoConversions::toUser(usr);
if (usrMgr.doesUserExist(user)) {
return createResponse(Status::CODE_401, "user already exists");
}
auto res = usrMgr.registerUser(user);
auto registerResult = dto::conversion::DtoConversions::toRegisterResultDto(res);
return createDtoResponse(Status::CODE_200, registerResult);
}
#include OATPP_CODEGEN_END(ApiController)
private:
model::BinaryPath m_bConf;
};
}
#endif
+255
View File
@@ -0,0 +1,255 @@
#ifndef SONGCONTROLLER_H_
#define SONGCONTROLLER_H_
#include <filesystem>
#include <iostream>
#include <fstream>
#include <limits>
#include <string>
#include <memory>
#include <vector>
#include <oatpp/core/async/Coroutine.hpp>
#include "oatpp/core/data/stream/ChunkedBuffer.hpp"
#include "oatpp/core/data/stream/FileStream.hpp"
#include "oatpp/core/base/StrBuffer.hpp"
#include "oatpp/core/macro/codegen.hpp"
#include "oatpp/core/macro/component.hpp"
#include "oatpp/web/mime/multipart/InMemoryPartReader.hpp"
#include "oatpp/web/mime/multipart/Reader.hpp"
#include "oatpp/web/protocol/http/outgoing/StreamingBody.hpp"
#include "oatpp/web/server/api/ApiController.hpp"
#include "callback/StreamCallback.h"
#include "database/SongRepository.h"
#include "dto/SongDto.hpp"
#include "dto/conversion/DtoConversions.h"
#include "manager/SongManager.h"
#include "manager/TokenManager.h"
#include "model/Models.h"
#include "type/Scopes.h"
#include "type/SongFilter.h"
#include "type/SongUpload.h"
namespace fs = std::filesystem;
namespace controller {
class SongController : public oatpp::web::server::api::ApiController {
public:
SongController(const model::BinaryPath& bConf, OATPP_COMPONENT(std::shared_ptr<ObjectMapper>, objectMapper))
: oatpp::web::server::api::ApiController(objectMapper), m_bConf(bConf) { }
#include OATPP_CODEGEN_BEGIN(ApiController)
// endpoint for uploading a song
ENDPOINT("POST", "/api/v1/song/data", songUpload,
//AUTHORIZATION(std::shared_ptr<oatpp::web::server::handler::DefaultBearerAuthorizationObject>, authObject),
REQUEST(std::shared_ptr<IncomingRequest>, request)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth,
type::Scope::upload), Status::CODE_403, "Not allowed");
auto mp =
std::make_shared<oatpp::web::mime::multipart::Multipart>
(request->getHeaders());
oatpp::web::mime::multipart::Reader mp_reader(mp.get());
mp_reader.setPartReader("file",
oatpp::web::mime::multipart::createInMemoryPartReader(m_dataSize));
request->transferBody(&mp_reader);
auto file = mp->getNamedPart("file");
OATPP_ASSERT_HTTP(file, Status::CODE_400, "file is null");
auto buff = std::make_unique<const char*>(file->getInMemoryData()->c_str());
auto buffSize = file->getKnownSize();
std::vector<unsigned char> data(*buff, *buff + buffSize);
model::Song sng;
sng.data = std::move(data);
manager::SongManager songMgr(m_bConf);
const auto result = songMgr.saveSong(sng);
if (!result.first) {
switch (result.second) {
case type::SongUpload::AlreadyExist:
return createResponse(Status::CODE_400, "Song already exists");
default:
break;
}
}
auto songDto = dto::conversion::DtoConversions::toSongDto(sng);
return createDtoResponse(Status::CODE_200, songDto);
}
// endpoint for retrieving all song records in json format
ENDPOINT("GET", "/api/v1/song", songRecords,
REQUEST(std::shared_ptr<IncomingRequest>, request)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth, type::Scope::retrieveSong), Status::CODE_403, "Not allowed");
std::cout << "starting process of retrieving songs\n";
database::SongRepository songRepo(m_bConf);
auto songsDb = songRepo.retrieveRecords();
auto songs = oatpp::data::mapping::type::List<dto::SongDto::ObjectWrapper>::createShared();
std::cout << "creating object to send\n";
for (auto& songDb : songsDb) {
auto song = dto::conversion::DtoConversions::toSongDto(songDb);
songs->pushBack(song);
}
return createDtoResponse(Status::CODE_200, songs);
}
// endpoint for retrieving song record by the song id in json format
ENDPOINT("GET", "/api/v1/song/{id}", songRecord,
REQUEST(std::shared_ptr<IncomingRequest>, request), PATH(Int32, id)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth, type::Scope::retrieveSong), Status::CODE_403, "Not allowed");
database::SongRepository songRepo(m_bConf);
model::Song songDb(id);
if (!songRepo.doesSongExist(songDb, type::SongFilter::id)) {
return songDoesNotExist();
}
std::cout << "song exists\n";
songDb = songRepo.retrieveRecord(songDb, type::SongFilter::id);
auto song = dto::conversion::DtoConversions::toSongDto(songDb);
return createDtoResponse(Status::CODE_200, song);
}
ENDPOINT("GET", "/api/v1/song/data/{id}", downloadSong,
REQUEST(std::shared_ptr<IncomingRequest>, request), PATH(Int32, id)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth,
type::Scope::download), Status::CODE_403, "Not allowed");
database::SongRepository songRepo(m_bConf);
model::Song songDb(id);
if (!songRepo.doesSongExist(songDb, type::SongFilter::id)) {
return songDoesNotExist();
}
songDb = songRepo.retrieveRecord(songDb, type::SongFilter::id);
auto rawSong = oatpp::base::StrBuffer::loadFromFile(songDb.songPath.c_str());
auto response = createResponse(Status::CODE_200, rawSong);
response->putHeader(Header::CONTENT_TYPE, "audio/mpeg");
return response;
}
ENDPOINT("UPDATE", "/api/v1/song/{id}", songUpdate,
REQUEST(std::shared_ptr<IncomingRequest>, request),
BODY_DTO(dto::SongDto::ObjectWrapper, songDto), PATH(Int32, id)) {
songDto->id = id;
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth,
type::Scope::updateSong), Status::CODE_403, "Not allowed");
auto updatedSong = dto::conversion::DtoConversions::toSong(songDto);
manager::SongManager songMgr(m_bConf);
auto result = songMgr.updateSong(updatedSong);
if (!result) {
return songDoesNotExist();
}
return createResponse(Status::CODE_200, "OK");
}
// endpoint to delete a song
ENDPOINT("DELETE", "api/v1/song/data/{id}", songDelete,
REQUEST(std::shared_ptr<IncomingRequest>, request), PATH(Int32, id)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth,
type::Scope::deleteSong), Status::CODE_403, "Not allowed");
model::Song song(id);
manager::SongManager sngMgr(m_bConf);
auto result = sngMgr.deleteSong(song);
if (!result) {
return songDoesNotExist();
}
return createResponse(Status::CODE_200, "OK");
}
// endpoint for streaming a song
ENDPOINT("GET", "/api/v1/song/stream/{id}", streamSong,
REQUEST(std::shared_ptr<IncomingRequest>, request), PATH(Int32, id)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth,
type::Scope::stream), Status::CODE_403, "Not allowed");
database::SongRepository songRepo(m_bConf);
model::Song songDb(id);
if (!songRepo.doesSongExist(songDb, type::SongFilter::id)) {
return songDoesNotExist();
}
songDb = songRepo.retrieveRecord(songDb, type::SongFilter::id);
auto dSize = 1024;
auto callback = std::make_shared<callback::StreamCallback>(songDb.songPath);
auto db = std::make_shared<oatpp::web::protocol::http::outgoing::StreamingBody>(
callback
);
auto response = OutgoingResponse::createShared(Status::CODE_200, db);
response->putHeader(Header::CONNECTION, Header::Value::CONNECTION_KEEP_ALIVE);
response->putHeader(Header::CONTENT_TYPE, "audio/mpeg");
return response;
}
#include OATPP_CODEGEN_END(ApiController)
private:
std::shared_ptr<oatpp::web::protocol::http::outgoing::Response>
songDoesNotExist() {
return createResponse(Status::CODE_404, "Song not found");
}
model::BinaryPath m_bConf;
const long m_dataSize = std::numeric_limits<long long int>::max();
};
}
#endif
+97
View File
@@ -0,0 +1,97 @@
#ifndef YEARCONTROLLER_H_
#define YEARCONTROLLER_H_
#include <filesystem>
#include <iostream>
#include <fstream>
#include <limits>
#include <string>
#include <memory>
#include <vector>
#include "oatpp/core/data/stream/ChunkedBuffer.hpp"
#include "oatpp/core/data/stream/FileStream.hpp"
#include "oatpp/core/macro/codegen.hpp"
#include "oatpp/core/macro/component.hpp"
#include "oatpp/web/mime/multipart/InMemoryPartReader.hpp"
#include "oatpp/web/mime/multipart/Reader.hpp"
#include "oatpp/web/server/api/ApiController.hpp"
#include "database/YearRepository.h"
#include "dto/YearDto.hpp"
#include "manager/YearManager.h"
#include "model/Models.h"
#include "type/Scopes.h"
#include "type/YearFilter.h"
namespace fs = std::filesystem;
namespace controller {
class YearController : public oatpp::web::server::api::ApiController {
public:
YearController(const model::BinaryPath& bConf,
OATPP_COMPONENT(std::shared_ptr<ObjectMapper>, objectMapper))
: oatpp::web::server::api::ApiController(objectMapper), m_bConf(bConf) { }
#include OATPP_CODEGEN_BEGIN(ApiController)
// endpoint for retrieving all year records in json format
ENDPOINT("GET", "/api/v1/year", yearRecords,
REQUEST(std::shared_ptr<IncomingRequest>, request)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth,
type::Scope::retrieveYear), Status::CODE_403, "Not allowed");
std::cout << "starting process of retrieving year\n";
database::YearRepository yrRepo(m_bConf);
auto yrsDb = yrRepo.retrieveRecords();
auto yearRecs = oatpp::data::mapping::type::
List<dto::YearDto::ObjectWrapper>::createShared();
for (auto& yrDb : yrsDb) {
auto yr = dto::YearDto::createShared();
yr->id = yrDb.id;
yr->year = yrDb.year;
yearRecs->pushBack(yr);
}
return createDtoResponse(Status::CODE_200, yearRecs);
}
// endpoint for retrieving single year record by the year id in json format
ENDPOINT("GET", "/api/v1/year/{id}", yearRecord,
REQUEST(std::shared_ptr<IncomingRequest>, request), PATH(Int32, id)) {
auto authHeader = request->getHeader("Authorization");
OATPP_ASSERT_HTTP(authHeader, Status::CODE_403, "Nope");
auto auth = authHeader->std_str();
manager::TokenManager tok;
OATPP_ASSERT_HTTP(tok.isTokenValid(auth,
type::Scope::retrieveYear), Status::CODE_403, "Not allowed");
database::YearRepository yrRepo(m_bConf);
model::Year yrDb(id);
OATPP_ASSERT_HTTP(yrRepo.doesYearExist(yrDb,
type::YearFilter::id) , Status::CODE_403, "year does not exist");
std::cout << "year exist\n";
yrDb = yrRepo.retrieveRecord(yrDb, type::YearFilter::id);
auto year = dto::YearDto::createShared();
year->id = yrDb.id;
year->year= yrDb.year;
return createDtoResponse(Status::CODE_200, year);
}
#include OATPP_CODEGEN_END(ApiController)
private:
model::BinaryPath m_bConf;
};
}
#endif

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