Compare commits

..

84 Commits

Author SHA1 Message Date
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
121 changed files with 7873 additions and 6849 deletions
-21
View File
@@ -1,21 +0,0 @@
# Ignore build artifacts
target/
pkg/
# Ignore git directory
.git/
.github/
# Ignore environment files (configure via docker-compose instead)
.env*
# Ignore IDE/editor specific files
.idea/
.vscode/
# Ignore OS specific files
*.DS_Store
# Add any other files/directories you don't need in the image
# e.g., logs/, tmp/
-7
View File
@@ -1,7 +0,0 @@
SECRET_MAIN_KEY=refero34o8rfhfjn983thf39fhc943rf923n3h
ROOT_DIRECTORY=/home/icarus/mydata
POSTGRES_MAIN_USER=icarus
POSTGRES_MAIN_PASSWORD=password
POSTGRES_MAIN_DB=icarus_db
POSTGRES_MAIN_HOST=main_db
DATABASE_URL=postgres://${POSTGRES_MAIN_USER}:${POSTGRES_MAIN_PASSWORD}@${POSTGRES_MAIN_HOST}:5432/${POSTGRES_MAIN_DB}
-7
View File
@@ -1,7 +0,0 @@
SECRET_MAIN_KEY=refero34o8rfhfjn983thf39fhc943rf923n3h
ROOT_DIRECTORY=/home/icarus/mydata
POSTGRES_MAIN_USER=icarus
POSTGRES_MAIN_PASSWORD=password
POSTGRES_MAIN_DB=icarus_db
POSTGRES_MAIN_HOST=localhost
DATABASE_URL=postgres://${POSTGRES_MAIN_USER}:${POSTGRES_MAIN_PASSWORD}@${POSTGRES_MAIN_HOST}:5432/${POSTGRES_MAIN_DB}
-52
View File
@@ -1,52 +0,0 @@
name: Release Tagging
on:
push:
branches: [ "v0.2" ]
pull_request:
branches: [ "v0.2" ]
jobs:
release:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0 # Important for git describe --tags
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: 1.88.0
components: cargo
- name: Extract Version from Cargo.toml
id: version
run: |
VERSION=$(grep '^version = "' Cargo.toml | sed -E 's/version = "([^"]+)"/\1/')
PROJECT_COMMIT_HASH=$(git rev-parse HEAD | cut -c 1-10)
BRANCH_REF="${GITHUB_REF}"
BRANCH_NAME=$(echo "$BRANCH_REF" | cut -d '/' -f 3)
PROJECT_TAG_RELEASE="v$VERSION-$BRANCH_NAME-$PROJECT_COMMIT_HASH"
echo "::set-output name=project_tag_release::$PROJECT_TAG_RELEASE-961"
echo "Version: $VERSION"
echo "Hash: $PROJECT_COMMIT_HASH"
echo "Branch: $BRANCH_NAME"
echo "Tag Release: $PROJECT_TAG_RELEASE"
- name: Print version
id: print_version
run: |
echo "Printing version"
echo "Version: ${{ steps.version.outputs.project_tag_release }}"
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.version.outputs.project_tag_release }}
release_name: Release ${{ steps.version.outputs.project_tag_release }}
body: |
Release of version ${{ steps.version.outputs.project_tag_release }}
-77
View File
@@ -1,77 +0,0 @@
name: Rust CI
on:
push:
branches: [ "v0.2" ]
pull_request:
branches: [ "v0.2" ]
jobs:
build:
runs-on: ubuntu-24.04
services:
postgres:
image: postgres:17.5
env:
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Install Rust 1.88.0
uses: actions-rs/toolchain@v1
with:
toolchain: 1.88.0
components: clippy, rustfmt
override: true
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
- name: Install libpq
run: sudo apt-get install -y libpq-dev
- name: Setup test database
env:
DATABASE_URL: "postgres://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}"
run: |
# Wait for PostgreSQL to be ready
for i in {1..10}; do
pg_isready -U ${{ secrets.POSTGRES_USER }} -d ${{ secrets.POSTGRES_DB }} && break
sleep 2
done
# Run database migrations (if you use sqlx migrations)
cargo install sqlx-cli --no-default-features --features native-tls,postgres
sqlx migrate run --database-url $DATABASE_URL
- name: Build
run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/gitea_deploy_key
chmod 600 ~/.ssh/gitea_deploy_key
ssh-keyscan ${{ vars.MYHOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/gitea_deploy_key
cargo build --verbose --release
- name: Run tests
env:
ROOT_DIRECTORY: "/tmp"
DATABASE_URL: "postgres://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}"
run: cargo test --verbose
- name: Run clippy
run: cargo clippy -- -D warnings
- name: Run rustfmt
run: cargo fmt --all -- --check
+7 -8
View File
@@ -1,9 +1,8 @@
# Added by cargo
################################################################################
# This .gitignore file was automatically created by Microsoft(R) Visual Studio.
################################################################################
/target
.env
.env.docker
.env.local
.DS_STORE
Storage/
/.vs/Icarus
/bin/Debug/netcoreapp2.2
/Migrations
/obj
+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 feebd2fd54
Vendored Submodule
+1
Submodule 3rdparty/jwt-cpp added at 27f32983fb
Vendored Submodule
+1
Submodule 3rdparty/libbcrypt added at 8aa32ad94e
Vendored Submodule
+1
Submodule 3rdparty/oatpp added at 5b4b313a0b
Vendored Submodule
+1
Submodule 3rdparty/taglib added at 79bc9ccf8e
+149
View File
@@ -0,0 +1,149 @@
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/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
)
set (ORM_DIR
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/ormpp
)
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_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/libbcrypt)
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})
+67
View File
@@ -0,0 +1,67 @@
# Contributing
When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.
Please note the project has a code of conduct, please follow it in all your interactions with the project.
## Code of Conduct
### Pledge
In the interest of fostering an open and welcoming environment, I as
aintainers pledge to making participation in this project and
this community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
### Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
### Maintainers Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## How You Can Contribute
The project is always looking for people to help in anyway they can. The following would be greatly appreciated:
* Developers
* Graphic Designers
* Documenters
### Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
Generated
-2763
View File
File diff suppressed because it is too large Load Diff
-27
View File
@@ -1,27 +0,0 @@
[package]
name = "icarus"
version = "0.1.93"
edition = "2024"
rust-version = "1.88"
[dependencies]
axum = { version = "0.8.4", features = ["multipart"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = { version = "1.0.140" }
tower = { version = "0.5.2" }
tokio = { version = "1.45.1", features = ["full"] }
tokio-util = { version = "0.7.15" }
tower-http = { version = "0.6.6", features = ["timeout"] }
tracing-subscriber = "0.3.19"
futures = { version = "0.3.31" }
uuid = { version = "1.17.0", features = ["v4", "serde"] }
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio-native-tls", "time", "uuid"] }
time = { version = "0.3.41", features = ["formatting", "macros", "parsing", "serde"] }
icarus_meta = { git = "ssh://git@git.kundeng.us/phoenix/icarus_meta.git", tag = "v0.3.0-devel-f4b71de969-680" }
icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.4.5-devel-655d05dabb-111" }
icarus_envy = { git = "ssh://git@git.kundeng.us/phoenix/icarus_envy.git", tag = "v0.3.0-devel-d73fba9899-006" }
[dev-dependencies]
common-multipart-rfc7578 = { version = "0.7.0" }
url = { version = "2.5.4" }
tempfile = { version = "3.20.0" }
-70
View File
@@ -1,70 +0,0 @@
# Stage 1: Build the application
FROM rust:1.88 as builder
# Set the working directory inside the container
WORKDIR /usr/src/app
# Install build dependencies if needed (e.g., git for cloning)
RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config libssl3 \
ca-certificates \
openssh-client git \
&& rm -rf /var/lib/apt/lists/*
# Create .ssh/ directory for internal dependencies
RUN mkdir -p -m 0700 ~/.ssh && \
echo "Host git.kundeng.us" >> ~/.ssh/config && \
echo " User git" >> ~/.ssh/config && \
chmod 600 ~/.ssh/config
# << --- ADD HOST KEY HERE --- >>
RUN ssh-keyscan git.kundeng.us >> ~/.ssh/known_hosts
# Copy Cargo manifests
COPY Cargo.toml Cargo.lock ./
# Build *only* dependencies to leverage Docker cache
# This dummy build caches dependencies as a separate layer
RUN --mount=type=ssh mkdir src && \
echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs && \
cargo build --release --quiet && \
rm -rf src target/release/deps/icarus* # Clean up dummy build artifacts (replace icarus)
# Copy the actual source code
COPY src ./src
# If you have other directories like `templates` or `static`, copy them too
COPY .env ./.env
COPY migrations ./migrations
# << --- SSH MOUNT ADDED HERE --- >>
# Build *only* dependencies to leverage Docker cache
# This dummy build caches dependencies as a separate layer
# Mount the SSH agent socket for this command
RUN --mount=type=ssh \
cargo build --release --quiet
# Stage 2: Create the final, smaller runtime image
# Use a minimal base image like debian-slim or even distroless for security/size
FROM ubuntu:24.04
# Install runtime dependencies if needed (e.g., SSL certificates)
RUN apt-get update && apt-get install -y ca-certificates libssl-dev libssl3 && rm -rf /var/lib/apt/lists/*
# Set the working directory
WORKDIR /usr/local/bin
# Copy the compiled binary from the builder stage
# Replace 'icarus' with the actual name of your binary (usually the crate name)
COPY --from=builder /usr/src/app/target/release/icarus .
# Copy other necessary files like .env (if used for runtime config) or static assets
# It's generally better to configure via environment variables in Docker though
COPY --from=builder /usr/src/app/.env .
COPY --from=builder /usr/src/app/migrations ./migrations
# Expose the port your Axum app listens on (e.g., 3000 or 8000)
EXPOSE 3000
# Set the command to run your application
# Ensure this matches the binary name copied above
CMD ["./icarus"]
Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+21
View File
@@ -0,0 +1,21 @@
MIT License
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
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+169 -9
View File
@@ -1,17 +1,177 @@
# Icarus
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/amazing-username/mear) - Partially implemented (under development)
* [IcarusDownloadManager](https://github.com/amazing-username/IcarusDownloadManager)
* Create your own client to interact with the API
# Getting Started
## Docker
Make sure `icarus_auth` is located in the root of the parent directory if using docker.
Create a `.env` file for both projects - `icarus_auth` and `icarus` - in the root of each project.
## Built With
Build containers
```
docker compose build --ssh default api auth_api
* 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.
1. Auth0 API configuration
2. API filesystem paths
3. Database connection string
4. Migrations
### Auth0 API configuration
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>
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
"domain": "[domain].auth0.com",
"api_identifier": "https://[identifier]/api",
"client_id": "iamunique",
"client_secret": "Icankeepasecret"
```
Bring it up
For the sake of this section, I will not go over configuring the API to accept the signing algorithm since it has already been configured in the [Startip](Startup.cs).cs file. Click on permissions to create the permissions for the API.
<h1 align "center">
<img src="Images/Configuration/configure_api.png" width=100%>
</h1>
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
{
"domain": "somedomain.auth0.com",
"api_identifier": "https://squawk/api"
"client_id": "clientidhere",
"client_secret": "illkeepyoumydirtylittlesecret"
}
```
docker compose up -d --force-recreate api auth_api
### API filesystem paths
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
{
"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/"
}
```
* `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 `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 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
{
"server": "localhost",
"database": "my_db",
"username": "admin",
"password": "toughpassword"
}
```
* 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.
### Database
Prior to starting the API, the database must be created. The following tables are required:
* User
* Salt
* Song
* Album
* Artist
* Year
* Genre
* CoverArt
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
mysql -u dblikedecibel -p < Scripts/MySQL/create_database.sql
```
From this point the database has been successfully created. Metadata and song filesystem locations can be saved.
## Building and Running
```
git clone --recursive https://github.com/kdeng00/icarus
cd 3rdparty/libbcrypt/
make
cp bcrypt.a libbcrypt.a
cp bcrypt.o libbcrypt.o
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
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
+26
View File
@@ -0,0 +1,26 @@
echo "Adding migrations..."
echo "Adding User migration"
dotnet ef migrations add User --context UserContext
echo "Adding Song migration"
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 ef database update --context UserContext
echo "Updating Song migration"
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
+86
View File
@@ -0,0 +1,86 @@
CREATE DATABASE Icarus;
USE Icarus;
CREATE TABLE CoverArt (
CoverArtId INT NOT NULL AUTO_INCREMENT,
SongTitle TEXT NOT NULL,
ImagePath TEXT NOT NULL,
PRIMARY KEY (CoverArtId)
);
CREATE TABLE Album (
AlbumId INT NOT NULL AUTO_INCREMENT,
Title TEXT NOT NULL,
Year INT NOT NULL,
PRIMARY KEY (AlbumId)
);
CREATE TABLE Artist (
ArtistId INT NOT NULL AUTO_INCREMENT,
Artist TEXT NOT NULL,
PRIMARY KEY (ArtistId)
);
CREATE TABLE Genre (
GenreId INT NOT NULL AUTO_INCREMENT,
Category TEXT NOT NULL,
PRIMARY KEY (GenreId)
);
CREATE TABLE Year (
YearId INT NOT NULL AUTO_INCREMENT,
Year INT NOT NULL,
PRIMARY KEY (YearId)
);
CREATE TABLE Song (
SongId INT NOT NULL AUTO_INCREMENT,
Title TEXT NOT NULL,
Artist TEXT NOT NULL,
Album TEXT NOT NULL,
Genre TEXT NOT NULL,
Year INT NOT NULL,
Duration INT NOT NULL,
Track INT NOT NULL,
Disc INT NOT NULL,
SongPath TEXT NOT NULL,
CoverArtId INT NOT NULL,
ArtistId INT NOT NULL,
AlbumId INT NOT NULL,
GenreId INT NOT NULL,
YearId INT NOT NULL,
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 (
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,
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)
);
+6
View File
@@ -0,0 +1,6 @@
delete from Song;
delete from Album;
delete from Artist;
delete from Genre;
delete from Year;
delete from CoverArt;
+1
View File
@@ -0,0 +1 @@
select * from Song;
+22
View File
@@ -0,0 +1,22 @@
{
"Logging": {
"LogLevel": {
"Default": "Trace",
"Microsoft": "Information"
}
},
"AllowedHosts": "*",
"Auth0": {
"Domain": "[domain].auth0.com",
"ApiIdentifier": "https://[identifier]/api",
"ClientId": "",
"ClientSecret": ""
},
"ConnectionStrings": {
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
},
"RootMusicPath": "/music/path/",
"TemporaryMusicPath": "/music/temp/path/",
"ArchivePath": "/archive/path/",
"CoverArtPath": "/cover/art/path/"
}
+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.0@vthiery/stable
[generators]
cmake
+7
View File
@@ -0,0 +1,7 @@
{
"server": "localhost",
"database": "Icarus",
"username": "icarus-admin",
"password": "dreamofpeace"
}
-112
View File
@@ -1,112 +0,0 @@
version: '3.8' # Use a recent version
services:
# --- Web API ---
api:
build: # Tells docker-compose to build the Dockerfile in the current directory
context: .
ssh: ["default"] # Uses host's SSH agent
container_name: icarus # Optional: Give the container a specific name
ports:
# Map host port 8000 to container port 3000 (adjust as needed)
# Format: "HOST_PORT:CONTAINER_PORT"
- "8000:3000"
env_file:
- .env
depends_on:
main_db:
condition: service_healthy
networks:
- main-api-network
restart: unless-stopped # Optional: Restart policy
# --- Web API auth ---
auth_api:
build:
context: ../icarus_auth # IMPORTANT: Relative path to the local checkout of your web API repo (containing the Dockerfile)
ssh: ["default"] # Uses host's SSH agent
dockerfile: Dockerfile # Optional: Specify if your Dockerfile has a non-standard name
container_name: auth_api
restart: unless-stopped
ports:
- "8001:3000" # Map host port 8000 to container port 8000 (adjust container port based on your app's EXPOSE in Dockerfile)
# environment:
# Environment variables your API needs, e.g., database connection
# Add other necessary environment variables
env_file:
- ../icarus_auth/.env
depends_on:
auth_db:
condition: service_healthy
networks:
- auth-api-network
# Optional: Mount local code for development (live reload)
# volumes:
# - ./path/to/your/web-api-repo:/app
# PostgreSQL Database Service
# --- icarus web api db ---
main_db:
image: postgres:17.5-alpine # Use an official Postgres image (Alpine variant is smaller)
container_name: icarus_db # Optional: Give the container a specific name
environment:
# These MUST match the user, password, and database name in the DATABASE_URL above
POSTGRES_USER: ${POSTGRES_MAIN_USER:-icarus}
POSTGRES_PASSWORD: ${POSTGRES_MAIN_PASSWORD:-password}
POSTGRES_DB: ${POSTGRES_MAIN_DB:-icarus_db}
volumes:
# Persist database data using a named volume
- postgres_data:/var/lib/postgresql/data
ports:
# Optional: Expose port 5432 ONLY if you need to connect directly from your host machine (e.g., for debugging)
- "5432:5432"
healthcheck:
# Checks if Postgres is ready to accept connections
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: always # Optional: Restart policy
networks:
- main-api-network
# --- icarus web auth api db ---
auth_db:
image: postgres:17.5-alpine # Use an official Postgres image (Alpine variant is smaller)
container_name: icarus_auth_db # Optional: Give the container a specific name
environment:
# These MUST match the user, password, and database name in the DATABASE_URL above
POSTGRES_USER: ${POSTGRES_AUTH_USER:-icarus_op}
POSTGRES_PASSWORD: ${POSTGRES_AUTH_PASSWORD:-password}
POSTGRES_DB: ${POSTGRES_AUTH_DB:-icarus_auth_db}
volumes:
# Persist database data using a named volume
- postgres_data_auth:/var/lib/postgresql/data
ports:
# Optional: Expose port 5432 ONLY if you need to connect directly from your host machine (e.g., for debugging)
- "5433:5432"
healthcheck:
# Checks if Postgres is ready to accept connections
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: always # Optional: Restart policy
networks:
- auth-api-network
# Define the named volume for data persistence
volumes:
postgres_data:
driver: local # Use the default local driver
postgres_data_auth:
driver: local # Use the default local driver
# Define the network (optional, but good practice)
networks:
main-api-network:
driver: bridge
auth-api-network:
driver: bridge
+26
View File
@@ -0,0 +1,26 @@
#ifndef STREAMCALLBACK_H_
#define STREAMCALLBACK_H_
#include <string>
#include "oatpp/core/data/stream/FileStream.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::data::v_io_size read(void*, oatpp::data::v_io_size);
private:
std::string m_songPath;
long m_bytesRead;
long m_counter;
long m_fileSize;
};
}
#endif
+37
View File
@@ -0,0 +1,37 @@
#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(5002);
}());
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:
};
}
#endif
+102
View File
@@ -0,0 +1,102 @@
#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 "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(std::string p, OATPP_COMPONENT(std::shared_ptr<ObjectMapper>, objectMapper))
: oatpp::web::server::api::ApiController(objectMapper), m_exe_path(p)
{ }
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" << std::endl;
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::AlbumDto::createShared();
alb->id = albDb.id;
alb->title = albDb.title.c_str();
alb->year = albDb.year;
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" << std::endl;
albDb = albRepo.retrieveRecord(albDb, type::AlbumFilter::id);
auto album = dto::AlbumDto::createShared();
album->id = albDb.id;
album->title = albDb.title.c_str();
album->year = albDb.year;
return createDtoResponse(Status::CODE_200, album);
}
#include OATPP_CODEGEN_END(ApiController)
private:
std::string m_exe_path;
model::BinaryPath m_bConf;
};
}
#endif
+100
View File
@@ -0,0 +1,100 @@
#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(std::string p, OATPP_COMPONENT(std::shared_ptr<ObjectMapper>, objectMapper))
: oatpp::web::server::api::ApiController(objectMapper), m_exe_path(p)
{ }
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" << std::endl;
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" << std::endl;
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:
std::string m_exe_path;
model::BinaryPath m_bConf;
};
}
#endif
+121
View File
@@ -0,0 +1,121 @@
#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(std::string p, OATPP_COMPONENT(std::shared_ptr<ObjectMapper>, objectMapper))
: oatpp::web::server::api::ApiController(objectMapper), m_exe_path(p)
{ }
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" << std::endl;
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" << std::endl;
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:
std::string m_exe_path;
model::BinaryPath m_bConf;
};
}
#endif
+100
View File
@@ -0,0 +1,100 @@
#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(std::string p, OATPP_COMPONENT(std::shared_ptr<ObjectMapper>, objectMapper))
: oatpp::web::server::api::ApiController(objectMapper), m_exe_path(p)
{ }
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" << std::endl;
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" << std::endl;
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:
std::string m_exe_path;
model::BinaryPath m_bConf;
};
}
#endif
+61
View File
@@ -0,0 +1,61 @@
#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(std::string p, OATPP_COMPONENT(std::shared_ptr<ObjectMapper>, objectMapper))
: oatpp::web::server::api::ApiController(objectMapper), exe_path(p)
{ }
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" << std::endl;
return createDtoResponse(Status::CODE_401, logRes);
}
std::cout << "user exists" << std::endl;
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:
std::string exe_path;
model::BinaryPath m_bConf;
};
}
#endif
+41
View File
@@ -0,0 +1,41 @@
#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);
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
+240
View File
@@ -0,0 +1,240 @@
#ifndef SONGCONTROLLER_H_
#define SONGCONTROLLER_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/protocol/http/outgoing/ChunkedBody.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"
namespace fs = std::filesystem;
namespace controller {
class SongController : public oatpp::web::server::api::ApiController {
public:
SongController(std::string p, OATPP_COMPONENT(std::shared_ptr<ObjectMapper>, objectMapper))
: oatpp::web::server::api::ApiController(objectMapper), m_exe_path(p)
{ }
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,
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::unique_ptr<char>(new char[file->getKnownSize()]);
auto buffSize = file->getInputStream()->read(buff.get(), file->getKnownSize());
std::vector<unsigned char> data(buff.get(), buff.get() + buffSize);
model::Song sng;
sng.data = std::move(data);
manager::SongManager songMgr(m_bConf);
songMgr.saveSong(sng);
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" << std::endl;
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" << std::endl;
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" << std::endl;
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);
oatpp::data::v_io_size dSize = 1024;
auto db = std::make_shared<oatpp::web::protocol::http::outgoing::ChunkedBody>(
std::make_shared<callback::StreamCallback>(songDb.songPath), nullptr, dSize);
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");
}
std::string m_exe_path;
model::BinaryPath m_bConf;
const long m_dataSize = std::numeric_limits<long long int>::max();
};
}
#endif
+99
View File
@@ -0,0 +1,99 @@
#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(std::string p, OATPP_COMPONENT(std::shared_ptr<ObjectMapper>, objectMapper))
: oatpp::web::server::api::ApiController(objectMapper), m_exe_path(p)
{ }
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" << std::endl;
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" << std::endl;
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:
std::string m_exe_path;
model::BinaryPath m_bConf;
};
}
#endif
+40
View File
@@ -0,0 +1,40 @@
#ifndef ALBUMREPOSITORY_H_
#define ALBUMREPOSITORY_H_
#include <utility>
#include <vector>
#include "database/BaseRepository.h"
#include "model/Models.h"
#include "type/AlbumFilter.h"
namespace database
{
class AlbumRepository : public BaseRepository
{
public:
AlbumRepository(const model::BinaryPath&);
std::vector<model::Album> retrieveRecords();
std::pair<model::Album, int> retrieveRecordWithSongCount(model::Album&, type::AlbumFilter);
model::Album retrieveRecord(model::Album&, type::AlbumFilter);
bool doesAlbumExists(const model::Album&, type::AlbumFilter);
void saveAlbum(const model::Album&);
void deleteAlbum(const model::Album&, type::AlbumFilter);
private:
std::vector<model::Album> parseRecords(MYSQL_STMT*);
std::pair<model::Album, int> parseRecordWithSongCount(MYSQL_STMT*);
// TODO: after parseRecord(MYSQL_STMT*) is implemented remove
// parseRecord(MYSQL_RES*)
model::Album parseRecord(MYSQL_RES*);
model::Album parseRecord(MYSQL_STMT*);
};
}
#endif
+40
View File
@@ -0,0 +1,40 @@
#ifndef ARTISTREPOSITORY_H_
#define ARTISTREPOSITORY_H_
#include <utility>
#include <vector>
#include "database/BaseRepository.h"
#include "model/Models.h"
#include "type/ArtistFilter.h"
namespace database
{
class ArtistRepository : public BaseRepository
{
public:
ArtistRepository(const model::BinaryPath&);
std::vector<model::Artist> retrieveRecords();
std::pair<model::Artist, int> retrieveRecordWithSongCount(model::Artist&, type::ArtistFilter);
model::Artist retrieveRecord(model::Artist&, type::ArtistFilter);
bool doesArtistExist(const model::Artist&, type::ArtistFilter);
void saveRecord(const model::Artist&);
void deleteArtist(const model::Artist&, type::ArtistFilter);
private:
std::vector<model::Artist> parseRecords(MYSQL_STMT*);
std::pair<model::Artist, int> parseRecordWithSongCount(MYSQL_STMT*);
// TODO: After parseRecord(MYSQL_STMT*) is implemented
// remove parseRecord(MYSQL_RES*)
model::Artist parseRecord(MYSQL_RES*);
model::Artist parseRecord(MYSQL_STMT*);
};
}
#endif
+36
View File
@@ -0,0 +1,36 @@
#ifndef BASE_REPOSITORY_H_
#define BASE_REPOSITORY_H_
#include <string>
#include <mysql/mysql.h>
#include "model/Models.h"
namespace database
{
class BaseRepository
{
public:
BaseRepository();
BaseRepository(const std::string&);
BaseRepository(const model::BinaryPath&);
bool testConnection();
protected:
MYSQL* setupMysqlConnection();
MYSQL* setupMysqlConnection(model::DatabaseConnection);
MYSQL_RES* performMysqlQuery(MYSQL*, const std::string&);
model::DatabaseConnection details;
private:
void intitalizeDetails();
void initializeDetails(const model::BinaryPath&);
std::string path;
model::BinaryPath m_binConf;
};
}
#endif
+39
View File
@@ -0,0 +1,39 @@
#ifndef COVERARTREPOSITORY_H_
#define COVERARTREPOSITORY_H_
#include <vector>
#include <mysql/mysql.h>
#include "database/BaseRepository.h"
#include "model/Models.h"
#include "type/CoverFilter.h"
namespace database
{
class CoverArtRepository : public BaseRepository
{
public:
CoverArtRepository(const std::string&);
CoverArtRepository(const model::BinaryPath&);
std::vector<model::Cover> retrieveRecords();
model::Cover retrieveRecord(model::Cover&, type::CoverFilter);
bool doesCoverArtExist(const model::Cover&, type::CoverFilter);
void deleteRecord(const model::Cover&);
void saveRecord(const model::Cover&);
void updateRecord(const model::Cover&);
private:
std::vector<model::Cover> parseRecords(MYSQL_STMT*);
// TODO: After parseRecord(MYSQL_STMT*) is implemented
// remove parseRecord(MYSQL_RES*)
model::Cover parseRecord(MYSQL_RES*);
model::Cover parseRecord(MYSQL_STMT*);
};
}
#endif
+40
View File
@@ -0,0 +1,40 @@
#ifndef GENREREPOSITORY_H_
#define GENREREPOSITORY_H_
#include <utility>
#include <vector>
#include "database/BaseRepository.h"
#include "model/Models.h"
#include "type/GenreFilter.h"
namespace database
{
class GenreRepository : public BaseRepository
{
public:
GenreRepository(const model::BinaryPath&);
std::vector<model::Genre> retrieveRecords();
std::pair<model::Genre, int> retrieveRecordWithSongCount(model::Genre&, type::GenreFilter);
model::Genre retrieveRecord(model::Genre&, type::GenreFilter);
bool doesGenreExist(const model::Genre&, type::GenreFilter);
void saveRecord(const model::Genre&);
void deleteRecord(const model::Genre&, type::GenreFilter);
private:
std::vector<model::Genre> parseRecords(MYSQL_STMT*);
std::pair<model::Genre, int> parseRecordWithSongCount(MYSQL_STMT*);
// TODO: After parseRecord(MYSQL_STMT*) is implemented
// remove parseRecord(MYSQL_RES*)
model::Genre parseRecord(MYSQL_RES*);
model::Genre parseRecord(MYSQL_STMT*);
};
}
#endif
+39
View File
@@ -0,0 +1,39 @@
#ifndef SONGREPOSITORY_H_
#define SONGREPOSITORY_H_
#include <memory>
#include <vector>
#include <mysql/mysql.h>
#include "database/BaseRepository.h"
#include "model/Models.h"
#include "type/SongFilter.h"
namespace database
{
class SongRepository : public BaseRepository
{
public:
SongRepository(const std::string&);
SongRepository(const model::BinaryPath&);
std::vector<model::Song> retrieveRecords();
model::Song retrieveRecord(model::Song&, type::SongFilter);
bool doesSongExist(const model::Song&, type::SongFilter);
bool deleteRecord(const model::Song&);
void saveRecord(const model::Song&);
void updateRecord(const model::Song&);
private:
std::vector<model::Song> parseRecords(MYSQL_RES*); // TODO: to be removed
std::vector<model::Song> parseRecords(MYSQL_STMT*);
model::Song parseRecord(MYSQL_RES*); // TODO: to be removed
model::Song parseRecord(MYSQL_STMT*);
};
}
#endif
+58
View File
@@ -0,0 +1,58 @@
#ifndef USERREPOSITORY_H_
#define USERREPOSITORY_H_
#include <iostream>
#include <memory>
#include <tuple>
#include "database/BaseRepository.h"
#include "model/Models.h"
#include "type/SaltFilter.h"
#include "type/UserFilter.h"
namespace database {
class UserRepository : BaseRepository {
public:
UserRepository(const model::BinaryPath&);
model::User retrieveUserRecord(model::User&, type::UserFilter);
model::PassSec retrieverUserSaltRecord(model::PassSec&, type::SaltFilter);
bool doesUserRecordExist(const model::User&, type::UserFilter);
void saveUserRecord(const model::User&);
void saveUserSalt(const model::PassSec&);
private:
struct UserLengths;
struct SaltLengths;
struct UserLengths
{
unsigned long firstnameLength;
unsigned long lastnameLength;
unsigned long phoneLength;
unsigned long emailLength;
unsigned long usernameLength;
unsigned long passwordLength;
};
struct SaltLengths
{
unsigned long saltLength;
};
std::shared_ptr<MYSQL_BIND> insertUserValues(const model::User&, std::shared_ptr<UserLengths>);
std::shared_ptr<MYSQL_BIND> insertSaltValues(const model::PassSec&, std::shared_ptr<SaltLengths>);
std::shared_ptr<MYSQL_BIND> valueBind(model::User&, std::tuple<char*, char*, char*, char*, char*, char*>&);
std::shared_ptr<MYSQL_BIND> saltValueBind(model::PassSec&, char*);
std::shared_ptr<UserLengths> fetchUserLengths(const model::User&);
std::shared_ptr<SaltLengths> fetchSaltLengths(const model::PassSec&);
std::tuple<char*, char*, char*, char*, char*, char*> fetchUV();
model::User parseRecord(MYSQL_STMT*);
model::PassSec parseSaltRecord(MYSQL_STMT*);
};
}
#endif
+40
View File
@@ -0,0 +1,40 @@
#ifndef YEARREPOSITORY_H_
#define YEARREPOSITORY_H_
#include <utility>
#include <vector>
#include "database/BaseRepository.h"
#include "model/Models.h"
#include "type/YearFilter.h"
namespace database
{
class YearRepository : public BaseRepository
{
public:
YearRepository(const model::BinaryPath&);
std::vector<model::Year> retrieveRecords();
std::pair<model::Year, int> retrieveRecordWithSongCount(model::Year&, type::YearFilter);
model::Year retrieveRecord(model::Year&, type::YearFilter);
bool doesYearExist(const model::Year&, type::YearFilter);
void saveRecord(const model::Year&);
void deleteYear(const model::Year&, type::YearFilter);
private:
std::vector<model::Year> parseRecords(MYSQL_STMT*);
std::pair<model::Year, int> parseRecordWithSongCount(MYSQL_STMT*);
// TODO: After parseRecord(MYSQL_STMT*) is implemented
// remove parseRecord(MYSQL_RES*)
model::Year parseRecord(MYSQL_RES*);
model::Year parseRecord(MYSQL_STMT*);
};
}
#endif
+23
View File
@@ -0,0 +1,23 @@
#ifndef ALBUMDTO_H_
#define ALBUMDTO_H_
#include "oatpp/core/data/mapping/type/Object.hpp"
#include "oatpp/core/macro/codegen.hpp"
namespace dto
{
#include OATPP_CODEGEN_BEGIN(DTO)
class AlbumDto : public oatpp::data::mapping::type::Object
{
DTO_INIT(AlbumDto, Object)
DTO_FIELD(Int32, id);
DTO_FIELD(String, title);
DTO_FIELD(Int32, year);
};
#include OATPP_CODEGEN_END(DTO)
}
#endif
+22
View File
@@ -0,0 +1,22 @@
#ifndef ARTISTDTO_H_
#define ARTISTDTO_H_
#include "oatpp/core/data/mapping/type/Object.hpp"
#include "oatpp/core/macro/codegen.hpp"
namespace dto
{
#include OATPP_CODEGEN_BEGIN(DTO)
class ArtistDto : public oatpp::data::mapping::type::Object
{
DTO_INIT(ArtistDto, Object)
DTO_FIELD(Int32, id);
DTO_FIELD(String, artist);
};
#include OATPP_CODEGEN_END(DTO)
}
#endif
+22
View File
@@ -0,0 +1,22 @@
#ifndef COVERARTDTO_H_
#define COVERARTDTO_H_
#include "oatpp/core/data/mapping/type/Object.hpp"
#include "oatpp/core/macro/codegen.hpp"
namespace dto
{
#include OATPP_CODEGEN_BEGIN(DTO)
class CoverArtDto : public oatpp::data::mapping::type::Object
{
DTO_INIT(CoverArtDto, Object)
DTO_FIELD(Int32, id);
DTO_FIELD(String, songTitle);
};
#include OATPP_CODEGEN_END(DTO)
}
#endif
+22
View File
@@ -0,0 +1,22 @@
#ifndef GENREDTO_H_
#define GENREDTO_H_
#include "oatpp/core/data/mapping/type/Object.hpp"
#include "oatpp/core/macro/codegen.hpp"
namespace dto
{
#include OATPP_CODEGEN_BEGIN(DTO)
class GenreDto : public oatpp::data::mapping::type::Object
{
DTO_INIT(GenreDto, Object)
DTO_FIELD(Int32, id);
DTO_FIELD(String, category);
};
#include OATPP_CODEGEN_END(DTO)
}
#endif
+47
View File
@@ -0,0 +1,47 @@
#ifndef LOGINRESULTDTO_H_
#define LOGINRESULTDTO_H_
#include "oatpp/core/data/mapping/type/Object.hpp"
#include "oatpp/core/macro/codegen.hpp"
#include "model/Models.h"
namespace dto {
#include OATPP_CODEGEN_BEGIN(DTO)
class LoginResultDto : public oatpp::data::mapping::type::Object {
DTO_INIT(LoginResultDto, Object)
DTO_FIELD(Int32, id);
DTO_FIELD(String, username);
DTO_FIELD(String, token);
DTO_FIELD(String, token_type);
DTO_FIELD(Int32, expiration);
DTO_FIELD(String, message);
};
class RegisterResultDto : public oatpp::data::mapping::type::Object {
DTO_INIT(RegisterResultDto, Object)
DTO_FIELD(String, username);
DTO_FIELD(Boolean, registered);
DTO_FIELD(String, message);
};
class UserDto : public oatpp::data::mapping::type::Object {
DTO_INIT(UserDto, Object)
DTO_FIELD(Int32, userId);
DTO_FIELD(String, firstname);
DTO_FIELD(String, lastname);
DTO_FIELD(String, phone);
DTO_FIELD(String, email);
DTO_FIELD(String, username);
DTO_FIELD(String, password);
};
#include OATPP_CODEGEN_END(DTO)
}
#endif
+34
View File
@@ -0,0 +1,34 @@
#ifndef SONGDTO_H_
#define SONGDTO_H_
#include "oatpp/core/data/mapping/type/Object.hpp"
#include "oatpp/core/macro/codegen.hpp"
#include "model/Models.h"
namespace dto
{
#include OATPP_CODEGEN_BEGIN(DTO)
class SongDto : public oatpp::data::mapping::type::Object
{
DTO_INIT(SongDto, Object)
DTO_FIELD(Int32, id);
DTO_FIELD(String, title);
DTO_FIELD(String, artist);
DTO_FIELD(String, album);
DTO_FIELD(String, genre);
DTO_FIELD(Int32, track);
DTO_FIELD(Int32, disc);
DTO_FIELD(Int32, year);
DTO_FIELD(Int32, duration);
DTO_FIELD(Int32, coverart_id);
};
#include OATPP_CODEGEN_END(DTO)
}
#endif
+22
View File
@@ -0,0 +1,22 @@
#ifndef YEARDTO_H_
#define YEARDTO_H_
#include "oatpp/core/data/mapping/type/Object.hpp"
#include "oatpp/core/macro/codegen.hpp"
namespace dto
{
#include OATPP_CODEGEN_BEGIN(DTO)
class YearDto : public oatpp::data::mapping::type::Object
{
DTO_INIT(YearDto, Object)
DTO_FIELD(Int32, id);
DTO_FIELD(Int32, year);
};
#include OATPP_CODEGEN_END(DTO)
}
#endif
+24
View File
@@ -0,0 +1,24 @@
#ifndef DTOCONVERSIONS_H_
#define DTOCONVERSIONS_H_
#include "dto/LoginResultDto.hpp"
#include "dto/SongDto.hpp"
#include "model/Models.h"
namespace dto { namespace conversion {
class DtoConversions {
public:
static dto::LoginResultDto::ObjectWrapper toLoginResultDto(const model::User&, const model::Token&);
static dto::RegisterResultDto::ObjectWrapper toRegisterResultDto(
const model::RegisterResult&);
static dto::SongDto::ObjectWrapper toSongDto(const model::Song&);
static model::Song toSong(dto::SongDto::ObjectWrapper&);
static model::User toUser(dto::UserDto::ObjectWrapper&);
};
}}
#endif
+25
View File
@@ -0,0 +1,25 @@
#ifndef ALBUMMANAGER_H_
#define ALBUMMANAGER_H_
#include "model/Models.h"
namespace manager
{
class AlbumManager
{
public:
AlbumManager(const model::BinaryPath&);
model::Album retrieveAlbum(model::Album&);
model::Album saveAlbum(const model::Song&);
void deleteAlbum(const model::Song&);
void updateAlbum(model::Song&, const model::Song&);
static void printAlbum(const model::Album&);
private:
model::BinaryPath m_bConf;
};
}
#endif
+25
View File
@@ -0,0 +1,25 @@
#ifndef ARTISTMANAGER_H_
#define ARTISTMANAGER_H_
#include "model/Models.h"
namespace manager
{
class ArtistManager
{
public:
ArtistManager(const model::BinaryPath&);
model::Artist retrieveArtist(model::Artist&);
model::Artist saveArtist(const model::Song&);
void deleteArtist(const model::Song&);
void updateArtist(model::Song&, const model::Song&);
static void printArtist(const model::Artist&);
private:
model::BinaryPath m_bConf;
};
}
#endif
+32
View File
@@ -0,0 +1,32 @@
#ifndef COVERARTMANAGER_H_
#define COVERARTMANAGER_H_
#include <string>
#include <utility>
#include "model/Models.h"
namespace manager
{
class CoverArtManager
{
public:
CoverArtManager(const std::string&);
CoverArtManager(const model::BinaryPath& bConf);
model::Cover saveCover(const model::Song&);
std::pair<bool, std::string> defaultCover(const model::Cover&);
void deleteCover(const model::Song&);
void updateCover(const model::Song&, const model::Song&);
void updateCoverRecord(const model::Song&);
private:
std::string createImagePath(const model::Song&);
model::BinaryPath m_bConf;
std::string path;
};
}
#endif
+37
View File
@@ -0,0 +1,37 @@
#ifndef DIRECTORY_MANAGER_H_
#define DIRECTORY_MANAGER_H_
#include <string>
#include <string_view>
#include <nlohmann/json.hpp>
#include "model/Models.h"
#include "type/PathType.h"
namespace manager
{
class DirectoryManager
{
public:
static std::string createDirectoryProcess(model::Song, const std::string&);
static std::string createDirectoryProcess(const model::Song&, const model::BinaryPath&, type::PathType);
static std::string configPath(std::string_view);
static std::string configPath(const model::BinaryPath&);
static std::string contentOfPath(const std::string&);
static std::string retrievePathType(type::PathType);
static nlohmann::json credentialConfigContent(const model::BinaryPath&);
static nlohmann::json databaseConfigContent(const model::BinaryPath&);
static nlohmann::json pathConfigContent(const model::BinaryPath&);
static void deleteDirectories(model::Song, const std::string&);
void deleteCoverArtFile(const std::string&, const std::string&);
private:
void deleteSong(const model::Song);
};
}
#endif
+25
View File
@@ -0,0 +1,25 @@
#ifndef GENREMANAGER_H_
#define GENREMANAGER_H_
#include "model/Models.h"
namespace manager
{
class GenreManager
{
public:
GenreManager(const model::BinaryPath&);
model::Genre retrieveGenre(model::Genre&);
model::Genre saveGenre(const model::Song&);
void deleteGenre(const model::Song&);
void updateGenre(model::Song&, const model::Song&);
static void printGenre(const model::Genre&);
private:
model::BinaryPath m_bConf;
};
}
#endif
+51
View File
@@ -0,0 +1,51 @@
#ifndef SONGMANAGER_H_
#define SONGMANAGER_H_
#include <iostream>
#include <map>
#include <string>
#include <nlohmann/json.hpp>
#include "dto/SongDto.hpp"
#include "model/Models.h"
#include "type/SongChanged.h"
namespace manager {
class SongManager
{
public:
SongManager(std::string&);
SongManager(const model::BinaryPath&);
bool didSongChange(const model::Song&, const model::Song&);
bool requiresFilesystemChange(const model::Song&, const model::Song&);
bool deleteSong(model::Song&);
bool updateSong(model::Song&);
void saveSong(model::Song&);
static void printSong(const model::Song&);
private:
std::map<type::SongChanged, bool> changesInSong(const model::Song&, const model::Song&);
std::string createSongPath(const model::Song&);
void assignMiscId(model::Song&, const model::Song&);
void assignMiscFields(std::map<type::SongChanged, bool>&, model::Song&,
const model::Song&);
void saveSongTemp(model::Song&);
void saveMisc(model::Song&);
void deleteMisc(const model::Song&);
void deleteMiscExceptCoverArt(const model::Song&);
void updateMisc(const std::map<type::SongChanged, bool>&,
model::Song&, const model::Song&);
void modifySongOnFilesystem(model::Song&, const model::Song&);
model::BinaryPath m_bConf;
std::string exe_path;
};
}
#endif
+40
View File
@@ -0,0 +1,40 @@
#ifndef TOKEN_MANAGER_H_
#define TOKEN_MANAGER_H_
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <cpr/cpr.h>
#include <jwt-cpp/jwt.h>
#include <nlohmann/json.hpp>
#include "model/Models.h"
#include "type/Scopes.h"
namespace manager {
class TokenManager {
public:
TokenManager();
model::Token retrieveToken(const model::BinaryPath&);
bool isTokenValid(std::string&, type::Scope);
bool testAuth(const model::BinaryPath&);
private:
cpr::Response sendRequest(std::string_view, nlohmann::json&);
nlohmann::json createTokenBody(const model::AuthCredentials&);
model::AuthCredentials parseAuthCredentials(std::string_view);
model::AuthCredentials parseAuthCredentials(const model::BinaryPath&);
std::vector<std::string> extractScopes(const jwt::decoded_jwt&&);
std::pair<bool, std::vector<std::string>> fetchAuthHeader(const std::string&);
bool tokenSupportsScope(const std::vector<std::string>&, const std::string&&);
};
}
#endif
+25
View File
@@ -0,0 +1,25 @@
#ifndef USERMANAGER_H_
#define USERMANAGER_H_
#include <iostream>
#include "dto/LoginResultDto.hpp"
#include "model/Models.h"
namespace manager {
class UserManager {
public:
UserManager(const model::BinaryPath&);
model::RegisterResult registerUser(model::User&);
bool doesUserExist(const model::User&);
bool validatePassword(const model::User&);
void printUser(const model::User&);
private:
model::BinaryPath m_bConf;
};
}
#endif
+25
View File
@@ -0,0 +1,25 @@
#ifndef YEARMANAGER_H_
#define YEARMANAGER_H_
#include "model/Models.h"
namespace manager
{
class YearManager
{
public:
YearManager(const model::BinaryPath&);
model::Year retrieveYear(model::Year&);
model::Year saveYear(const model::Song&);
void deleteYear(const model::Song&);
void updateYear(model::Song&, const model::Song&);
static void printYear(const model::Year&);
private:
model::BinaryPath m_bConf;
};
}
#endif
+176
View File
@@ -0,0 +1,176 @@
#ifndef MODELS_H_
#define MODELS_H_
#include <string>
#include <vector>
namespace model {
struct Song {
Song() = default;
Song(const int id) : id(id) { }
int id;
std::string title;
std::string artist;
std::string album;
std::string genre;
int year;
int duration;
int track;
int disc;
std::string songPath;
std::vector<unsigned char> data;
int coverArtId;
int artistId;
int albumId;
int genreId;
int yearId;
};
struct Artist {
Artist() = default;
Artist(const Song& song)
{
id = song.artistId;
artist = song.artist;
}
Artist(const int id) : id(id) { }
int id;
std::string artist;
};
struct Album {
Album() = default;
Album(const Song& song)
{
id = song.albumId;
title = song.album;
year = song.year;
}
Album(const int id) : id(id) { }
int id;
std::string title;
int year;
std::vector<Song> songs;
};
struct Genre {
Genre() = default;
Genre(const Song& song)
{
id = song.genreId;
category = song.genre;
}
Genre(const int id) : id(id) { }
int id;
std::string category;
};
struct Year {
Year() = default;
Year(const Song& song)
{
id = song.yearId;
year = song.year;
}
Year(const int id) : id(id) { }
int id;
int year;
};
struct Cover {
Cover() = default;
Cover(const Song& song)
{
id = song.coverArtId;
songTitle = song.title;
}
Cover(const int id) : id(id) { }
int id;
std::string songTitle;
std::string imagePath;
// Not being used but it should be
std::vector<unsigned char> data;
};
class Token {
public:
Token() = default;
Token(const std::string& accessToken) :
accessToken(accessToken) { }
Token(const std::string& accessToken, const std::string& tokenType,
const int expiration) :
accessToken(accessToken), tokenType(tokenType),
expiration(expiration) { }
std::string accessToken;
int expiration;
std::string tokenType;
};
struct LoginResult {
int userId;
std::string username;
std::string accessToken;
std::string tokenType;
std::string message;
int expiration;
};
class RegisterResult {
public:
std::string username;
bool registered;
std::string message;
};
struct User {
int id;
std::string firstname;
std::string lastname;
std::string email;
std::string phone;
std::string username;
std::string password;
};
struct PassSec {
int id;
std::string hashPassword;
std::string salt;
int userId;
};
struct AuthCredentials {
std::string domain;
std::string apiIdentifier;
std::string clientId;
std::string clientSecret;
std::string uri;
std::string endpoint;
};
struct DatabaseConnection {
std::string server;
std::string username;
std::string password;
std::string database;
};
struct BinaryPath {
BinaryPath() = default;
BinaryPath(const char *p) : path(std::move(p)) { }
BinaryPath(std::string& p) : path(p) { }
BinaryPath(const std::string& p) : path(p) { }
std::string path;
};
}
#endif
+14
View File
@@ -0,0 +1,14 @@
#ifndef ALBUMFILTER_H_
#define ALBUMFILTER_H_
namespace type
{
enum class AlbumFilter
{
id = 0,
title,
year
};
}
#endif
+13
View File
@@ -0,0 +1,13 @@
#ifndef ARTISTFILTER_H_
#define ARTISTFILTER_H_
namespace type
{
enum class ArtistFilter
{
id = 0,
artist
};
}
#endif
+14
View File
@@ -0,0 +1,14 @@
#ifndef COVERFILTER_H_
#define COVERFILTER_H_
namespace type
{
enum class CoverFilter
{
id = 0,
songTitle,
imagePath
};
}
#endif
+13
View File
@@ -0,0 +1,13 @@
#ifndef GENREFILTER_H_
#define GENREFILTER_H_
namespace type
{
enum class GenreFilter
{
id = 0,
category
};
}
#endif
+15
View File
@@ -0,0 +1,15 @@
#ifndef PATHTYPE_H_
#define PATHTYPE_H_
namespace type
{
enum PathType
{
music = 0,
archive,
temp,
coverArt
};
}
#endif
+13
View File
@@ -0,0 +1,13 @@
#ifndef SALTFILTER_H_
#define SALTFILTER_H_
namespace type {
enum class SaltFilter
{
id = 0,
salt,
userId
};
}
#endif
+23
View File
@@ -0,0 +1,23 @@
#ifndef SCOPES_H_
#define SCOPES_H_
namespace type
{
enum class Scope
{
upload = 0,
download,
stream,
retrieveSong,
updateSong,
deleteSong,
retrieveAlbum,
retrieveArtist,
retrieveGenre,
retrieveYear,
downloadCoverArt
};
}
#endif
+16
View File
@@ -0,0 +1,16 @@
#ifndef SONGCHANGED_H_
#define SONGCHANGED_H_
namespace type
{
enum SongChanged
{
title = 0,
artist,
album,
genre,
year
};
}
#endif
+17
View File
@@ -0,0 +1,17 @@
#ifndef SONGFILTER_H_
#define SONGFILTER_H_
namespace type
{
enum class SongFilter
{
id = 0,
title,
album,
artist,
genre,
year
};
}
#endif
+12
View File
@@ -0,0 +1,12 @@
#ifndef USERFILTER_H_
#define USERFILTER_H_
namespace type {
enum class UserFilter
{
id = 0,
username
};
}
#endif
+13
View File
@@ -0,0 +1,13 @@
#ifndef YEARFILTER_H_
#define YEARFILTER_H_
namespace type
{
enum class YearFilter
{
id = 0,
year
};
}
#endif
+32
View File
@@ -0,0 +1,32 @@
#ifndef IMAGEFILE_H_
#define IMAGEFILE_H_
#include <iostream>
#include <attachedpictureframe.h>
#include <tag.h>
#include <tfile.h>
#include <tfilestream.h>
#include <fileref.h>
#include <tbytevector.h>
#include <tbytevectorstream.h>
#include <tpropertymap.h>
#include <id3v2tag.h>
namespace utility
{
class ImageFile : public TagLib::File
{
public:
ImageFile(const char *file);
TagLib::ByteVector data();
private:
virtual TagLib::Tag *tag() const { return 0; }
virtual TagLib::AudioProperties *audioProperties() const { return 0; }
virtual bool save() { return false; }
};
}
#endif
+26
View File
@@ -0,0 +1,26 @@
#ifndef METADATA_RETRIEVER_H_
#define METADATA_RETRIEVER_H_
#include <iostream>
#include <string>
#include "model/Models.h"
namespace utility {
class MetadataRetriever
{
public:
model::Song retrieveMetadata(std::string&);
model::Cover updateCoverArt(const model::Song&, model::Cover&, const std::string&);
model::Cover applyStockCoverArt(const model::Song&, model::Cover&, const std::string&);
model::Cover applyCoverArt(const model::Song&, model::Cover&);
bool songContainsCoverArt(const model::Song&);
void updateMetadata(model::Song&, const model::Song&);
private:
};
}
#endif
+19
View File
@@ -0,0 +1,19 @@
#ifndef PASSWORDENCRYPTION_H_
#define PASSWORDENCRYPTION_H_
#include <string>
#include "model/Models.h"
namespace utility {
class PasswordEncryption {
public:
model::PassSec hashPassword(const model::User&);
bool isPasswordValid(const model::User&, const model::PassSec&);
private:
int saltSize();
};
}
#endif
+25
View File
@@ -0,0 +1,25 @@
#ifndef INITIALIZATION_H_
#define INITIALIZATION_H_
#include <string>
#include "model/Models.h"
namespace verify
{
class Initialization
{
public:
static bool skipVerification(const std::string&);
static void checkIcarus(const model::BinaryPath&);
private:
static bool confirmConfigAuth(const model::BinaryPath&);
static bool confirmConfigDatabase(const model::BinaryPath&);
static bool confirmConfigPaths(const model::BinaryPath&);
static void failedConfirmation();
};
}
#endif
@@ -1,62 +0,0 @@
-- Add migration script here
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Table to store queued songs to process
CREATE TABLE IF NOT EXISTS "songQueue" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
filename TEXT NOT NULL,
status TEXT CHECK (status IN ('pending', 'ready', 'processing', 'done')),
data BYTEA NULL,
user_id UUID NULL
);
-- Table to store queued metadata
CREATE TABLE IF NOT EXISTS "metadataQueue" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
metadata jsonb NOT NULL,
created_at timestamptz DEFAULT now(),
song_queue_id UUID NOT NULL
);
-- Table to store queued coverart
CREATE TABLE IF NOT EXISTS "coverartQueue" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
data BYTEA NULL,
song_queue_id UUID NULL
);
-- Create an index for better query performance
CREATE INDEX metadata_queue_data_metadata ON "metadataQueue" USING gin (metadata);
-- Table to store a song's info
CREATE TABLE IF NOT EXISTS "song" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
artist TEXT NOT NULL,
album_artist TEXT NOT NULL,
album TEXT NOT NULL,
genre TEXT NOT NULL,
-- TODO: Address discrepancy of date and year at some point
-- date TEXT NOT NULL,
year INT NOT NULL,
track INT NOT NULL,
disc INT NOT NULL,
track_count INT NOT NULL,
disc_count INT NOT NULL,
duration INT NOT NULL,
audio_type TEXT NOT NULL,
date_created timestamptz DEFAULT now(),
filename TEXT NOT NULL,
directory TEXT NOT NULL,
user_id UUID NULL
-- TODO: Add coverart id later. This will allow multiple songs to be linked to a single cover art
);
CREATE TABLE IF NOT EXISTS "coverart" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
-- TODO: Separate path later
path TEXT NOT NULL,
song_id UUID NOT NULL
-- TODO: Add type later
);
+6
View File
@@ -0,0 +1,6 @@
{
"root_music_path": "/icarus/music/",
"temp_root_path": "/icarus/temp/",
"cover_root_path": "/icarus/cover/",
"archive_root_path": "icarus/archive/"
}
+82
View File
@@ -0,0 +1,82 @@
#include <exception>
#include <iostream>
#include <filesystem>
#include <memory>
#include <string>
#include <mysql/mysql.h>
#include <oatpp/network/server/Server.hpp>
#include "oatpp/network/server/SimpleTCPConnectionProvider.hpp"
#include "oatpp/web/server/HttpConnectionHandler.hpp"
#include "component/AppComponent.hpp"
#include "controller/ArtistController.hpp"
#include "controller/AlbumController.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 "model/Models.h"
#include "verify/Initialization.h"
namespace fs = std::filesystem;
void run(const model::BinaryPath& bConf)
{
component::AppComponent component;
OATPP_COMPONENT(std::shared_ptr<oatpp::web::server::HttpRouter>, router);
auto albumController = std::make_shared<controller::AlbumController>(bConf);
auto artistController = std::make_shared<controller::ArtistController>(bConf);
auto coverArtController = std::make_shared<controller::CoverArtController>(bConf);
auto gnrController = std::make_shared<controller::GenreController>(bConf);
auto logController = std::make_shared<controller::LoginController>(bConf);
auto regController = std::make_shared<controller::RegisterController>(bConf);
auto sngController = std::make_shared<controller::SongController>(bConf);
auto yearController = std::make_shared<controller::YearController>(bConf);
albumController->addEndpointsToRouter(router);
artistController->addEndpointsToRouter(router);
coverArtController->addEndpointsToRouter(router);
gnrController->addEndpointsToRouter(router);
logController->addEndpointsToRouter(router);
regController->addEndpointsToRouter(router);
sngController->addEndpointsToRouter(router);
yearController->addEndpointsToRouter(router);
OATPP_COMPONENT(std::shared_ptr<oatpp::network::server::ConnectionHandler>, connectionHandler);
OATPP_COMPONENT(std::shared_ptr<oatpp::network::ServerConnectionProvider>, connectionProvider);
oatpp::network::server::Server server(connectionProvider, connectionHandler);
OATPP_LOGI("icarus", "Server running on port %s", connectionProvider->getProperty("port").getData());
server.run();
}
int main(int argc, char **argv)
{
oatpp::base::Environment::init();
model::BinaryPath bConf(std::move(argv[0]));
if (argc > 1) {
if (!verify::Initialization::skipVerification(argv[1])) {
verify::Initialization::checkIcarus(bConf);
} else {
std::cout << "skiping verifyication" << std::endl;
}
} else {
verify::Initialization::checkIcarus(bConf);
}
run(bConf);
oatpp::base::Environment::destroy();
return 0;
}
+44
View File
@@ -0,0 +1,44 @@
#include "callback/StreamCallback.h"
#include <iostream>
#include <fstream>
#include <memory>
namespace callback {
StreamCallback::StreamCallback() :
m_counter(0) { }
StreamCallback::StreamCallback(const std::string& songPath) :
m_songPath(songPath),
m_counter(0),
m_bytesRead(0)
{
// getting file size
std::ifstream song(m_songPath.c_str(), std::ios::binary | std::ios::in | std::ios::ate);
m_fileSize = song.tellg();
std::cout << "file size is " << m_fileSize << " bytes" << std::endl;
}
oatpp::data::v_io_size StreamCallback::read(void *buff, oatpp::data::v_io_size count)
{
if (m_counter >= m_fileSize) {
std::cout << "done streaming song" << std::endl;
return 0;
}
std::fstream song(m_songPath.c_str(), std::ios::binary | std::ios::in);
song.seekg(m_counter);
std::unique_ptr<char[]> data(new char[count]);
song.read(data.get(), count);
std::memcpy(buff, data.get(), count);
m_counter += count;
song.close();
return count;
}
}
-563
View File
@@ -1,563 +0,0 @@
// TODO: Separate queue and coverart endpoints
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct CoverArtQueue {
pub id: uuid::Uuid,
pub song_queue_id: uuid::Uuid,
}
pub mod request {
pub mod link {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct Request {
pub coverart_id: uuid::Uuid,
pub song_queue_id: uuid::Uuid,
}
}
pub mod fetch_coverart_no_data {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct Params {
pub id: Option<uuid::Uuid>,
pub song_queue_id: Option<uuid::Uuid>,
}
}
pub mod fetch_coverart_with_data {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Params {
pub id: Option<uuid::Uuid>,
pub song_queue_id: Option<uuid::Uuid>,
}
}
pub mod create_coverart {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Request {
pub song_id: uuid::Uuid,
pub coverart_queue_id: uuid::Uuid,
}
}
pub mod wipe_data_from_coverart_queue {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Request {
pub coverart_queue_id: uuid::Uuid,
}
}
}
pub mod response {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
pub mod link {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Id {
pub coverart_id: uuid::Uuid,
pub song_queue_id: uuid::Uuid,
}
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct Response {
pub message: String,
pub data: Vec<Id>,
}
}
pub mod fetch_coverart_no_data {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct Response {
pub message: String,
pub data: Vec<super::super::CoverArtQueue>,
}
}
pub mod fetch_coverart_with_data {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct Response {
pub message: String,
pub data: Vec<Vec<u8>>,
}
}
pub mod create_coverart {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct Response {
pub message: String,
pub data: Vec<icarus_models::coverart::CoverArt>,
}
}
pub mod wipe_data_from_coverart_queue {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
}
pub mod db {
use sqlx::Row;
pub async fn insert(pool: &sqlx::PgPool, data: &Vec<u8>) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO "coverartQueue" (data) VALUES($1) RETURNING id;
"#,
)
.bind(data)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e:?}");
});
match result {
Ok(row) => {
let id: uuid::Uuid = row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(id)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn update(
pool: &sqlx::PgPool,
coverart_id: &uuid::Uuid,
song_queue_id: &uuid::Uuid,
) -> Result<i32, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "coverartQueue" SET song_queue_id = $1 WHERE id = $2;
"#,
)
.bind(song_queue_id)
.bind(coverart_id)
.execute(pool)
.await;
match result {
Ok(_) => Ok(0),
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart_queue_with_id(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<super::CoverArtQueue, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, song_queue_id FROM "coverartQueue" WHERE id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(row) => Ok(super::CoverArtQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_queue_id: row
.try_get("song_queue_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
}),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart_queue_with_song_queue_id(
pool: &sqlx::PgPool,
song_queue_id: &uuid::Uuid,
) -> Result<super::CoverArtQueue, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, song_queue_id FROM "coverartQueue" WHERE song_queue_id = $1;
"#,
)
.bind(song_queue_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(row) => Ok(super::CoverArtQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_queue_id: row
.try_get("song_queue_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
}),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart_queue_data_with_id(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<Vec<u8>, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT data FROM "coverartQueue" WHERE id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(row) => Ok(row
.try_get("data")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart_queue_data_with_song_queue_id(
pool: &sqlx::PgPool,
song_queue_id: &uuid::Uuid,
) -> Result<Vec<u8>, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT data FROM "coverartQueue" WHERE song_queue_id = $1;
"#,
)
.bind(song_queue_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e}");
});
match result {
Ok(row) => Ok(row
.try_get("data")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn wipe_data(
pool: &sqlx::PgPool,
coverart_queue_id: &uuid::Uuid,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "coverartQueue" SET data = NULL WHERE id = $1 RETURNING id;
"#,
)
.bind(coverart_queue_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error updating query: {e}");
});
match result {
Ok(row) => Ok(row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
}
pub mod cov_db {
use sqlx::Row;
pub async fn create(
pool: &sqlx::PgPool,
coverart: &icarus_models::coverart::CoverArt,
song_id: &uuid::Uuid,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO "coverart" (title, path, song_id) VALUES($1, $2, $3) RETURNING id;
"#,
)
.bind(&coverart.title)
.bind(&coverart.path)
.bind(song_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => {
let id: uuid::Uuid = row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(id)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
}
pub mod endpoint {
use std::io::Write;
use axum::response::IntoResponse;
pub async fn queue(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
mut multipart: axum::extract::Multipart,
) -> (
axum::http::StatusCode,
axum::Json<super::response::Response>,
) {
let mut response = super::response::Response::default();
match multipart.next_field().await {
Ok(Some(field)) => {
let name = field.name().unwrap().to_string();
let file_name = field.file_name().unwrap().to_string();
let content_type = field.content_type().unwrap().to_string();
let data = field.bytes().await.unwrap();
let raw_data = data.to_vec();
println!(
"Received file '{}' (name = '{}', content-type = '{}', size = {})",
file_name,
name,
content_type,
data.len()
);
match super::db::insert(&pool, &raw_data).await {
Ok(id) => {
response.message = String::from("Successful");
response.data.push(id);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
Ok(None) => (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)),
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
pub async fn link(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::link::Request>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::link::Response>,
) {
let mut response = super::response::link::Response::default();
let id = payload.coverart_id;
let song_id = payload.song_queue_id;
match super::db::update(&pool, &id, &song_id).await {
Ok(_o) => {
response.data.push(super::response::link::Id {
song_queue_id: song_id,
coverart_id: id,
});
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
pub async fn fetch_coverart_no_data(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<
super::request::fetch_coverart_no_data::Params,
>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::fetch_coverart_no_data::Response>,
) {
let mut response = super::response::fetch_coverart_no_data::Response::default();
match params.id {
Some(id) => match super::db::get_coverart_queue_with_id(&pool, &id).await {
Ok(cover_art_queue) => {
response.message = String::from("Successful");
response.data.push(cover_art_queue);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
},
_ => match params.song_queue_id {
Some(song_queue_id) => {
match super::db::get_coverart_queue_with_song_queue_id(&pool, &song_queue_id)
.await
{
Ok(cover_art_queue) => {
response.message = String::from("Successful");
response.data.push(cover_art_queue);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
None => {
response.message = String::from("No valid id provided");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
},
}
}
pub async fn fetch_coverart_with_data(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
) -> (axum::http::StatusCode, axum::response::Response) {
match super::db::get_coverart_queue_data_with_id(&pool, &id).await {
Ok(data) => {
let bytes = axum::body::Bytes::from(data);
let mut response = bytes.into_response();
let headers = response.headers_mut();
// TODO: Address this hard coding for the coverart content type
headers.insert(axum::http::header::CONTENT_TYPE, "image".parse().unwrap());
// TODO: Make the conent disposition more dynamic
headers.insert(
axum::http::header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{id}.jpg\"")
.parse()
.unwrap(),
);
(axum::http::StatusCode::OK, response)
}
Err(_err) => (
axum::http::StatusCode::BAD_REQUEST,
axum::response::Response::default(),
),
}
}
pub async fn create_coverart(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::create_coverart::Request>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::create_coverart::Response>,
) {
let mut response = super::response::create_coverart::Response::default();
let id = payload.coverart_queue_id;
match super::db::get_coverart_queue_data_with_id(&pool, &id).await {
Ok(data) => {
let song_id = payload.song_id;
match crate::callers::song::song_db::get_song(&pool, &song_id).await {
Ok(song) => {
let directory = icarus_envy::environment::get_root_directory().await;
let dir = std::path::Path::new(&directory);
// TODO: Make this random and the file extension should not be hard coded
let filename = format!("{}-coverart.jpeg", &song.filename[..8]);
let save_path = dir.join(&filename);
let path = String::from(save_path.to_str().unwrap());
let mut coverart =
icarus_models::coverart::init::init_coverart_only_path(path);
coverart.title = song.album.clone();
coverart.data = data;
let mut file = std::fs::File::create(&save_path).unwrap();
file.write_all(&coverart.data).unwrap();
match super::cov_db::create(&pool, &coverart, &song.id).await {
Ok(id) => {
coverart.id = id;
response.message = String::from("Successful");
response.data.push(coverart);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
pub async fn wipe_data_from_coverart_queue(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::wipe_data_from_coverart_queue::Request>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::wipe_data_from_coverart_queue::Response>,
) {
let mut response = super::response::wipe_data_from_coverart_queue::Response::default();
let coverart_queue_id = payload.coverart_queue_id;
match super::db::get_coverart_queue_with_id(&pool, &coverart_queue_id).await {
Ok(coverart_queue) => match super::db::wipe_data(&pool, &coverart_queue.id).await {
Ok(id) => {
response.message = String::from("Success");
response.data.push(id);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
},
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
}
}
}
}
-281
View File
@@ -1,281 +0,0 @@
// TODO: Explicitly make this module target queueing a song's metadata
pub mod request {
pub mod queue_metadata {
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Deserialize, Serialize, sqlx::FromRow)]
pub struct Request {
pub song_queue_id: uuid::Uuid,
pub album: String,
pub album_artist: String,
pub artist: String,
pub disc: i32,
pub disc_count: i32,
pub duration: i64,
pub genre: String,
pub title: String,
pub track: i32,
pub track_count: i32,
pub year: i32,
}
impl Request {
pub async fn to_json_value(&self) -> serde_json::Value {
serde_json::json!(
{
"song_queue_id": &self.song_queue_id,
"album": &self.album,
"album_artist": &self.album_artist,
"genre": &self.genre,
"year": &self.year,
"track_count": &self.track_count,
"disc_count": &self.disc_count,
"title": &self.title,
"artist": &self.artist,
"disc": &self.disc,
"track": &self.track,
"duration": &self.duration,
})
}
}
}
pub mod fetch_metadata {
#[derive(
Debug, Default, serde::Deserialize, serde::Serialize, sqlx::FromRow, sqlx::Decode,
)]
pub struct Params {
pub id: Option<uuid::Uuid>,
pub song_queue_id: Option<uuid::Uuid>,
}
}
}
pub mod response {
pub mod queue_metadata {
use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
pub mod fetch_metadata {
use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize)]
pub struct Response {
pub message: String,
pub data: Vec<crate::callers::metadata::metadata_queue::MetadataQueue>,
}
}
}
pub mod metadata_queue {
use sqlx::Row;
#[derive(Debug, serde::Serialize, sqlx::FromRow)]
pub struct InsertedData {
pub id: uuid::Uuid,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow)]
pub struct MetadataQueue {
pub id: uuid::Uuid,
pub metadata: serde_json::Value,
#[serde(with = "time::serde::rfc3339")]
pub created_at: time::OffsetDateTime,
pub song_queue_id: uuid::Uuid,
}
pub async fn insert(
pool: &sqlx::PgPool,
metadata: &serde_json::Value,
song_queue_id: &uuid::Uuid,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO "metadataQueue" (metadata, song_queue_id) VALUES($1, $2) RETURNING id;
"#,
)
.bind(metadata)
.bind(song_queue_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => {
let id: uuid::Uuid = row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(id)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_with_song_queue_id(
pool: &sqlx::PgPool,
song_queue_id: &uuid::Uuid,
) -> Result<MetadataQueue, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT * FROM "metadataQueue" WHERE song_queue_id = $1;
"#,
)
.bind(song_queue_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => Ok(MetadataQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
metadata: row
.try_get("metadata")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
created_at: row
.try_get("created_at")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_queue_id: row
.try_get("song_queue_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
}),
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_with_id(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<MetadataQueue, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, metadata, created_at, song_queue_id FROM "metadataQueue" WHERE id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => {
let data: serde_json::Value = row
.try_get("metadata")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(MetadataQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
metadata: data,
created_at: row
.try_get("created_at")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_queue_id: row
.try_get("song_queue_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
})
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
}
pub mod endpoint {
use axum::{Json, http::StatusCode};
pub async fn queue_metadata(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
Json(payload): Json<super::request::queue_metadata::Request>,
) -> (StatusCode, Json<super::response::queue_metadata::Response>) {
let mut results: Vec<uuid::Uuid> = Vec::new();
let mut response = super::response::queue_metadata::Response::default();
let meta = payload.to_json_value().await;
match super::metadata_queue::insert(&pool, &meta, &payload.song_queue_id).await {
Ok(metadata_queue_id) => {
results.push(metadata_queue_id);
response.data = results;
response.message = if response.data.is_empty() {
String::from("Error")
} else {
String::from("Success")
};
(StatusCode::OK, Json(response))
}
Err(err) => {
response.message = err.to_string();
(StatusCode::BAD_REQUEST, Json(response))
}
}
}
pub async fn fetch_metadata(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<super::request::fetch_metadata::Params>,
) -> (StatusCode, Json<super::response::fetch_metadata::Response>) {
let mut response = super::response::fetch_metadata::Response::default();
match params.id {
Some(id) => {
println!("Something works {id}");
match super::metadata_queue::get_with_id(&pool, &id).await {
Ok(item) => {
response.message = String::from("Successful");
response.data.push(item);
(StatusCode::OK, Json(response))
}
Err(err) => {
response.message = err.to_string();
(StatusCode::BAD_REQUEST, Json(response))
}
}
}
_ => match params.song_queue_id {
Some(song_queue_id) => {
println!("Song queue Id is probably not nil");
match super::metadata_queue::get_with_song_queue_id(&pool, &song_queue_id).await
{
Ok(item) => {
response.message = String::from("Successful");
response.data.push(item);
(StatusCode::OK, Json(response))
}
Err(err) => {
response.message = err.to_string();
(StatusCode::BAD_REQUEST, Json(response))
}
}
}
None => {
println!("What is going on?");
(StatusCode::BAD_REQUEST, Json(response))
}
},
}
}
}
-24
View File
@@ -1,24 +0,0 @@
pub mod coverart;
pub mod metadata;
pub mod song;
pub mod endpoints {
pub const QUEUESONG: &str = "/api/v2/song/queue";
pub const QUEUESONGLINKUSERID: &str = "/api/v2/song/queue/link";
pub const QUEUESONGDATA: &str = "/api/v2/song/queue/{id}";
pub const QUEUESONGUPDATE: &str = "/api/v2/song/queue/{id}";
pub const NEXTQUEUESONG: &str = "/api/v2/song/queue/next";
pub const QUEUEMETADATA: &str = "/api/v2/song/metadata/queue";
pub const QUEUECOVERART: &str = "/api/v2/coverart/queue";
pub const QUEUECOVERARTDATA: &str = "/api/v2/coverart/queue/data/{id}";
pub const QUEUECOVERARTLINK: &str = "/api/v2/coverart/queue/link";
pub const QUEUESONGDATAWIPE: &str = "/api/v2/song/queue/data/wipe";
pub const QUEUECOVERARTDATAWIPE: &str = "/api/v2/coverart/queue/data/wipe";
pub const CREATESONG: &str = "/api/v2/song";
pub const CREATECOVERART: &str = "/api/v2/coverart";
}
pub mod response {
pub const SUCCESSFUL: &str = "SUCCESSFUL";
}
-971
View File
@@ -1,971 +0,0 @@
// TODO: Separate queue and song endpoints
pub mod request {
use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize)]
pub struct Request {
pub message: String,
}
pub mod update_status {
#[derive(Default, serde::Deserialize, serde::Serialize)]
pub struct Request {
pub id: uuid::Uuid,
pub status: String,
}
}
pub mod create_metadata {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Request {
pub title: String,
pub artist: String,
pub album_artist: String,
pub album: String,
pub genre: String,
pub date: String,
pub track: i32,
pub disc: i32,
pub track_count: i32,
pub disc_count: i32,
pub duration: i32,
pub audio_type: String,
pub user_id: uuid::Uuid,
pub song_queue_id: uuid::Uuid,
}
impl Request {
pub fn is_valid(&self) -> bool {
!self.title.is_empty()
|| !self.artist.is_empty()
|| !self.album_artist.is_empty()
|| !self.album.is_empty()
|| !self.genre.is_empty()
|| !self.date.is_empty()
|| self.track > 0
|| self.disc > 0
|| self.track_count > 0
|| self.disc_count > 0
|| self.duration > 0
|| !self.audio_type.is_empty()
|| !self.user_id.is_nil()
|| !self.song_queue_id.is_nil()
}
pub fn to_song(&self) -> icarus_models::song::Song {
icarus_models::song::Song {
id: uuid::Uuid::nil(),
title: self.title.clone(),
artist: self.artist.clone(),
album_artist: self.album_artist.clone(),
album: self.album.clone(),
genre: self.genre.clone(),
year: self.date[..3].parse().unwrap(),
track: self.track,
disc: self.disc,
track_count: self.track_count,
disc_count: self.disc_count,
duration: self.duration,
audio_type: self.audio_type.clone(),
user_id: self.user_id,
// TODO: Change the type of this in icarus_models lib
date_created: String::new(),
filename: String::new(),
data: Vec::new(),
directory: String::new(),
}
}
}
}
pub mod wipe_data_from_song_queue {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Request {
pub song_queue_id: uuid::Uuid,
}
}
pub mod link_user_id {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Request {
pub song_queue_id: uuid::Uuid,
pub user_id: uuid::Uuid,
}
}
}
pub mod response {
use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
pub mod fetch_queue_song {
use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize)]
pub struct Response {
pub message: String,
pub data: Vec<crate::callers::song::song_queue::SongQueue>,
}
}
pub mod update_status {
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ChangedStatus {
pub old_status: String,
pub new_status: String,
}
#[derive(Default, serde::Deserialize, serde::Serialize)]
pub struct Response {
pub message: String,
pub data: Vec<ChangedStatus>,
}
}
pub mod update_song_queue {
#[derive(Default, serde::Deserialize, serde::Serialize)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
pub mod create_metadata {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct Response {
pub message: String,
pub data: Vec<icarus_models::song::Song>,
}
}
pub mod wipe_data_from_song_queue {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
pub mod link_user_id {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
}
// TODO: Might make a distinction between year and date in a song's tag at some point
pub mod status {
pub const PENDING: &str = "pending";
pub const READY: &str = "ready";
pub const PROCESSING: &str = "processing";
pub const DONE: &str = "done";
pub async fn is_valid(status: &str) -> bool {
status == PENDING || status == PROCESSING || status == DONE || status == READY
}
}
pub mod song_db {
use sqlx::Row;
// TODO: Change first parameter of return value from string to a time type
pub async fn insert(
pool: &sqlx::PgPool,
song: &icarus_models::song::Song,
) -> Result<(String, uuid::Uuid), sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO "song" (title, artist, album_artist, album, genre, year, track, disc, track_count, disc_count, duration, audio_type, filename, directory, user_id)
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING date_created, id;
"#
)
.bind(&song.title)
.bind(&song.artist)
.bind(&song.album_artist)
.bind(&song.album)
.bind(&song.genre)
.bind(song.year)
.bind(song.track)
.bind(song.disc)
.bind(song.track_count)
.bind(song.disc_count)
.bind(song.duration)
.bind(&song.audio_type)
.bind(&song.filename)
.bind(&song.directory)
.bind(song.user_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting query: {e}");
});
match result {
Ok(row) => {
let id: uuid::Uuid = row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
let date_created_time: time::OffsetDateTime = row
.try_get("date_created")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
let date_created = date_created_time.to_string();
Ok((date_created, id))
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_song(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<icarus_models::song::Song, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT * FROM "song" WHERE id = $1
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e}");
});
match result {
Ok(row) => {
let date_created_time: time::OffsetDateTime = row
.try_get("date_created")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(icarus_models::song::Song {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
title: row
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
artist: row
.try_get("artist")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
album_artist: row
.try_get("album_artist")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
album: row
.try_get("album")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
genre: row
.try_get("genre")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
year: row
.try_get("year")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
track: row
.try_get("track")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
disc: row
.try_get("disc")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
track_count: row
.try_get("track_count")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
disc_count: row
.try_get("disc_count")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
duration: row
.try_get("duration")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
audio_type: row
.try_get("audio_type")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
directory: row
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
date_created: date_created_time.to_string(),
user_id: row
.try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
})
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
}
mod song_queue {
use sqlx::Row;
#[derive(Debug, serde::Serialize, sqlx::FromRow)]
pub struct InsertedData {
pub id: uuid::Uuid,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow)]
pub struct SongQueue {
pub id: uuid::Uuid,
pub filename: String,
pub status: String,
pub user_id: uuid::Uuid,
}
pub async fn insert(
pool: &sqlx::PgPool,
data: &Vec<u8>,
filename: &String,
status: &String,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO "songQueue" (data, filename, status) VALUES($1, $2, $3) RETURNING id;
"#,
)
.bind(data)
.bind(filename)
.bind(status)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => {
let id: uuid::Uuid = row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(id)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn update(
pool: &sqlx::PgPool,
data: &Vec<u8>,
id: &uuid::Uuid,
) -> Result<Vec<u8>, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "songQueue" SET data = $1 WHERE id = $2 RETURNING data;
"#,
)
.bind(data)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => Ok(row
.try_get("data")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_most_recent_and_update(pool: &sqlx::PgPool) -> Result<SongQueue, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "songQueue"
SET status = $1
WHERE id = (
SELECT id FROM "songQueue"
WHERE status = $2
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING id, filename, status, user_id;
"#,
)
.bind(super::status::PROCESSING)
.bind(super::status::READY)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => {
let user_id_result = row.try_get("user_id");
let song_queue = SongQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
status: row
.try_get("status")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
user_id: match user_id_result {
Ok(id) => id,
Err(_) => uuid::Uuid::nil(),
},
};
Ok(song_queue)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_status_of_song_queue(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<String, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, status FROM "songQueue" WHERE id = $1
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error selecting: {e}");
});
match result {
Ok(row) => Ok(row
.try_get("status")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn update_song_queue_status(
pool: &sqlx::PgPool,
status: &String,
id: &uuid::Uuid,
) -> Result<String, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "songQueue" SET status = $1 WHERE id = $2 RETURNING status;
"#,
)
.bind(status)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error updating record {e}");
});
match result {
Ok(row) => Ok(row
.try_get("status")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn link_user_id(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
user_id: &uuid::Uuid,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "songQueue" SET user_id = $1 WHERE id = $2 RETURNING user_id;
"#,
)
.bind(user_id)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error updating record {e}");
});
match result {
Ok(row) => Ok(row
.try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_song_queue(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<SongQueue, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, filename, status, user_id FROM "songQueue" WHERE id = $1
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e}");
});
match result {
Ok(row) => {
let user_id_result = row.try_get("user_id");
let song_queue = SongQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
status: row
.try_get("status")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
user_id: match user_id_result {
Ok(id) => id,
Err(_) => uuid::Uuid::nil(),
},
};
Ok(song_queue)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn wipe_data(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "songQueue" SET data = NULL WHERE id = $1 RETURNING id;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error updating record: {e}");
});
match result {
Ok(row) => Ok(row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_data(pool: &sqlx::PgPool, id: &uuid::Uuid) -> Result<Vec<u8>, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT data FROM "songQueue"
WHERE id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting: {e}");
});
match result {
Ok(row) => {
let data = row
.try_get("data")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
Ok(data)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
}
pub mod endpoint {
use axum::{Json, http::StatusCode, response::IntoResponse};
use std::io::Write;
use crate::callers::song::song_queue;
pub async fn queue_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
mut multipart: axum::extract::Multipart,
) -> (StatusCode, Json<super::response::Response>) {
let mut results: Vec<uuid::Uuid> = Vec::new();
let mut response = super::response::Response::default();
while let Some(field) = multipart.next_field().await.unwrap() {
let name = field.name().unwrap().to_string();
let file_name = field.file_name().unwrap().to_string();
let content_type = field.content_type().unwrap().to_string();
let data = field.bytes().await.unwrap();
println!(
"Received file '{}' (name = '{}', content-type = '{}', size = {})",
file_name,
name,
content_type,
data.len()
);
// TODO: Remove this
// Save the file to disk
let mut file = std::fs::File::create(&file_name).unwrap();
file.write_all(&data).unwrap();
let raw_data: Vec<u8> = data.to_vec();
let queue_repo = song_queue::insert(
&pool,
&raw_data,
&file_name,
&super::status::PENDING.to_string(),
)
.await
.unwrap();
results.push(queue_repo);
}
response.data = results;
response.message = if response.data.is_empty() {
String::from("Error")
} else {
String::from("Success")
};
(StatusCode::OK, Json(response))
}
pub async fn link_user_id(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::link_user_id::Request>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::link_user_id::Response>,
) {
let mut response = super::response::link_user_id::Response::default();
match super::song_queue::get_song_queue(&pool, &payload.song_queue_id).await {
Ok(song_queue) => {
match super::song_queue::link_user_id(&pool, &song_queue.id, &payload.user_id).await
{
Ok(user_id) => {
response.message = String::from(crate::callers::response::SUCCESSFUL);
response.data.push(user_id);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
pub async fn fetch_queue_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
) -> (
StatusCode,
Json<super::response::fetch_queue_song::Response>,
) {
let mut response = super::response::fetch_queue_song::Response::default();
match song_queue::get_most_recent_and_update(&pool).await {
Ok(item) => {
response.message = String::from("Successful");
response.data.push(item);
(StatusCode::OK, Json(response))
}
Err(err) => {
response.message = err.to_string();
(StatusCode::BAD_REQUEST, Json(response))
}
}
}
pub async fn download_flac(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
) -> (StatusCode, axum::response::Response) {
println!("Id: {id}");
match song_queue::get_data(&pool, &id).await {
Ok(data) => {
let by = axum::body::Bytes::from(data);
let mut response = by.into_response();
let headers = response.headers_mut();
headers.insert(
axum::http::header::CONTENT_TYPE,
"audio/flac".parse().unwrap(),
);
headers.insert(
axum::http::header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{id}.flac\"")
.parse()
.unwrap(),
);
(StatusCode::OK, response)
}
Err(_err) => (StatusCode::BAD_REQUEST, axum::response::Response::default()),
}
}
pub async fn update_song_queue_status(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::update_status::Request>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::update_status::Response>,
) {
let mut response = super::response::update_status::Response::default();
if super::status::is_valid(&payload.status).await {
let id = payload.id;
if !id.is_nil() {
match super::song_queue::get_status_of_song_queue(&pool, &id).await {
Ok(old) => {
match super::song_queue::update_song_queue_status(
&pool,
&payload.status,
&id,
)
.await
{
Ok(new) => {
response.message = String::from("Successful");
response
.data
.push(super::response::update_status::ChangedStatus {
old_status: old,
new_status: new,
});
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
} else {
response.message = String::from("Id is nil");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
} else {
response.message = String::from("Status not valid");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
pub async fn update_song_queue(
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
mut multipart: axum::extract::Multipart,
) -> (
axum::http::StatusCode,
axum::Json<super::response::update_song_queue::Response>,
) {
let mut response = super::response::update_song_queue::Response::default();
if let Some(field) = multipart.next_field().await.unwrap() {
let name = field.name().unwrap().to_string();
let file_name = field.file_name().unwrap().to_string();
let content_type = match field.content_type() {
Some(ct) => ct.to_string(),
None => String::new(),
};
let data = field.bytes().await.unwrap();
println!(
"Received file '{}' (name = '{}', content-type = '{}', size = {})",
file_name,
name,
content_type,
data.len()
);
let raw_data: Vec<u8> = data.to_vec();
match song_queue::update(&pool, &raw_data, &id).await {
Ok(_) => {
response.message = String::from("Successful");
response.data.push(id);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
} else {
response.message = String::from("No data provided");
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
}
}
pub async fn create_metadata(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::create_metadata::Request>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::create_metadata::Response>,
) {
let mut response = super::response::create_metadata::Response::default();
if payload.is_valid() {
let mut song = payload.to_song();
song.filename =
song.generate_filename(icarus_models::types::MusicTypes::FlacExtension, true);
song.directory = icarus_envy::environment::get_root_directory().await;
match song_queue::get_data(&pool, &payload.song_queue_id).await {
Ok(data) => {
song.data = data;
let dir = std::path::Path::new(&song.directory);
if !dir.exists() {
println!("Creating directory");
match std::fs::create_dir_all(dir) {
Ok(_) => {
println!("Successfully created directory");
}
Err(err) => {
eprintln!("Error: Unable to create the directory {err:?}");
}
}
}
let save_path = dir.join(&song.filename);
match std::fs::File::create(&save_path) {
Ok(mut file) => {
file.write_all(&song.data).unwrap();
match song.song_path() {
Ok(_) => match super::song_db::insert(&pool, &song).await {
Ok((date_created, id)) => {
song.id = id;
song.date_created = date_created;
response.message = String::from("Successful");
response.data.push(song);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message =
format!("{:?} song {:?}", err.to_string(), song);
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
},
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
Err(err) => {
let song_path = song.song_path();
response.message = format!(
"{err:?} Song directory: {} Filename: {} Save Path: {:?} Song Path: {:?}",
song.directory, song.filename, save_path, song_path
);
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
} else {
response.message = String::from("Request body is not valid");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
pub async fn wipe_data_from_song_queue(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::wipe_data_from_song_queue::Request>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::wipe_data_from_song_queue::Response>,
) {
let mut response = super::response::wipe_data_from_song_queue::Response::default();
let id = payload.song_queue_id;
match super::song_queue::get_song_queue(&pool, &id).await {
Ok(song_queue) => match super::song_queue::wipe_data(&pool, &song_queue.id).await {
Ok(wiped_id) => {
response.message = String::from("Success");
response.data.push(wiped_id);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
}
},
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
}
}
}
}
+449
View File
@@ -0,0 +1,449 @@
#include "database/AlbumRepository.h"
#include <iostream>
#include <algorithm>
#include <memory>
#include <sstream>
#include <string>
#include <cstring>
database::AlbumRepository::AlbumRepository(const model::BinaryPath& bConf)
: BaseRepository(bConf)
{ }
std::vector<model::Album> database::AlbumRepository::retrieveRecords()
{
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
const std::string query = "SELECT * FROM Album";
mysql_stmt_prepare(stmt, query.c_str(), query.size());
mysql_stmt_execute(stmt);
auto albums = parseRecords(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
return albums;
}
std::pair<model::Album, int> database::AlbumRepository::retrieveRecordWithSongCount(model::Album& album, type::AlbumFilter filter = type::AlbumFilter::id)
{
std::cout << "retrieving album with song count" << std::endl;
std::stringstream qry;
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
MYSQL_BIND params[4];
std::memset(params, 0, sizeof(params));
qry << "SELECT alb.*, COUNT(*) AS SongCount FROM Album alb LEFT JOIN ";
qry << "Song sng ON alb.AlbumId=sng.AlbumId WHERE ";
switch (filter) {
case type::AlbumFilter::id:
qry << "sng.AlbumId = ?";
params[0].buffer_type = MYSQL_TYPE_LONG;
params[0].buffer = (char*)&album.id;
params[0].length = 0;
params[0].is_null = 0;
break;
default:
break;
}
qry << " GROUP BY alb.AlbumId LIMIT 1";
const auto query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
auto albWSC = parseRecordWithSongCount(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
return albWSC;
}
model::Album database::AlbumRepository::retrieveRecord(model::Album& album, type::AlbumFilter filter)
{
std::cout << "retrieving album record" << std::endl;
std::stringstream qry;
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
MYSQL_BIND params[1];
memset(params, 0, sizeof(params));
qry << "SELECT alb.* FROM Album alb WHERE ";
auto titleLength = album.title.size();
switch (filter) {
case type::AlbumFilter::id:
qry << "alb.AlbumId = ?";
params[0].buffer_type = MYSQL_TYPE_LONG;
params[0].buffer = (char*)&album.id;
params[0].length = 0;
params[0].is_null = 0;
break;
case type::AlbumFilter::title:
qry << "alb.Title = ?";
params[0].buffer_type = MYSQL_TYPE_STRING;
params[0].buffer = (char*)album.title.c_str();
params[0].length = &titleLength;
params[0].is_null = 0;
break;
default:
break;
}
qry << " LIMIT 1";
const auto query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
album = parseRecord(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
std::cout << "done" << std::endl;
return album;
}
bool database::AlbumRepository::doesAlbumExists(const model::Album& album, type::AlbumFilter filter)
{
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
MYSQL_BIND params[1];
memset(params, 0, sizeof(params));
std::stringstream qry;
qry << "SELECT * FROM Album WHERE ";
auto titleLength = album.title.size();
switch (filter) {
case type::AlbumFilter::id:
qry << "AlbumId = ?";
params[0].buffer_type = MYSQL_TYPE_LONG;
params[0].buffer = (char*)&album.id;
params[0].length = 0;
params[0].is_null = 0;
break;
case type::AlbumFilter::title:
qry << "Title = ?";
params[0].buffer_type = MYSQL_TYPE_STRING;
params[0].buffer = (char*)album.title.c_str();
params[0].length = &titleLength;
params[0].is_null = 0;
break;
default:
break;
}
qry << " LIMIT 1";
const auto query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
std::cout << "the query has been performed" << std::endl;
::mysql_stmt_store_result(stmt);
auto rowCount = ::mysql_stmt_num_rows(stmt);
::mysql_stmt_close(stmt);
::mysql_close(conn);
return (rowCount > 0) ? true : false;
}
void database::AlbumRepository::saveAlbum(const model::Album& album)
{
std::cout << "beginning to insert album record" << std::endl;
auto conn = setupMysqlConnection();
MYSQL_STMT *stmt = mysql_stmt_init(conn);
const std::string query = "INSERT INTO Album(Title, Year) VALUES(?, ?)";
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
MYSQL_BIND params[2];
memset(params, 0, sizeof(params));
params[0].buffer_type = MYSQL_TYPE_STRING;
params[0].buffer = (char*)album.title.c_str();
auto titleLength = album.title.size();
params[0].length= &titleLength;
params[0].is_null = 0;
params[1].buffer_type = MYSQL_TYPE_LONG;
params[1].buffer = (char*)&album.year;
params[1].length = 0;
params[1].is_null = 0;
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
std::cout << "done inserting album record" << std::endl;
}
void database::AlbumRepository::deleteAlbum(const model::Album& album, type::AlbumFilter filter = type::AlbumFilter::id)
{
std::cout << "deleting album record" << std::endl;
std::stringstream qry;
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
MYSQL_BIND params[1];
std::memset(params, 0, sizeof(params));
qry << "DELETE FROM Album WHERE ";
switch (filter) {
case type::AlbumFilter::id:
qry << "AlbumId = ?";
params[0].buffer_type = MYSQL_TYPE_LONG;
params[0].buffer = (char*)&album.id;
params[0].length = 0;
params[0].is_null = 0;
break;
default:
break;
}
const auto query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
std::cout << "execute delete query" << std::endl;
}
std::vector<model::Album> database::AlbumRepository::parseRecords(MYSQL_STMT* stmt)
{
std::cout << "parsing album record" << std::endl;
mysql_stmt_store_result(stmt);
std::vector<model::Album> albums;
albums.reserve(mysql_stmt_num_rows(stmt));
const auto valAmt = 3;
unsigned long len[valAmt];
my_bool nullRes[valAmt];
for (auto status = 0; status == 0; status = mysql_stmt_next_result(stmt)) {
if (mysql_stmt_field_count(stmt) > 0) {
model::Album alb;
auto res = mysql_stmt_result_metadata(stmt);
auto fields = mysql_fetch_fields(res);
const auto strLen = 1024;
MYSQL_BIND val[valAmt];
memset(val, 0, sizeof(val));
char title[strLen];
val[0].buffer_type = MYSQL_TYPE_LONG;
val[0].buffer = (char*)&alb.id;
val[0].length = &len[0];
val[0].is_null = &nullRes[0];
val[1].buffer_type = MYSQL_TYPE_STRING;
val[1].buffer = (char*)title;
val[1].buffer_length = strLen;
val[1].length = &len[1];
val[1].is_null = &nullRes[1];
val[2].buffer_type = MYSQL_TYPE_LONG;
val[2].buffer = (char*)&alb.year;
val[2].length = &len[2];
val[2].is_null = &nullRes[2];
status = mysql_stmt_bind_result(stmt, val);
while (true) {
std::cout << "fetching statement result" << std::endl;
status = mysql_stmt_fetch(stmt);
if (status == 1 || status == MYSQL_NO_DATA) {
break;
}
alb.title = title;
albums.push_back(std::move(alb));
}
}
std::cout << "fetching next result" << std::endl;
}
return albums;
}
// TODO: check to see if this is not used, if not then remove it
model::Album database::AlbumRepository::parseRecord(MYSQL_RES* results)
{
std::cout << "parsing album record" << std::endl;
model::Album album;
auto fieldNum = mysql_num_fields(results);
auto row = mysql_fetch_row(results);
for (auto i = 0; i != fieldNum; ++i) {
const std::string field(mysql_fetch_field(results)->name);
if (field.compare("AlbumId") == 0) {
album.id = std::stoi(row[i]);
}
if (field.compare("Title") == 0) {
album.title = row[i];
}
if (field.compare("Year") == 0) {
album.year = std::stoi(row[i]);
}
}
return album;
}
std::pair<model::Album, int> database::AlbumRepository::parseRecordWithSongCount(MYSQL_STMT *stmt)
{
std::cout << "parsing album record with song count" << std::endl;
mysql_stmt_store_result(stmt);
auto rowCount = mysql_stmt_num_rows(stmt);
model::Album album;
int songCount = 0;
if (rowCount == 0) {
std::cout << "no results" << std::endl;
return std::make_pair(album, songCount);
}
if (mysql_stmt_field_count(stmt) == 0) {
std::cout << "field count is 0, must be an incorrect query" << std::endl;
return std::make_pair(model::Album(), 0);
}
auto res = mysql_stmt_result_metadata(stmt);
constexpr auto valAmt = 4;
constexpr auto strLen = 1024;
unsigned long len[valAmt];
my_bool nullRes[valAmt];
MYSQL_BIND val[valAmt];
std::memset(val, 0, sizeof(val));
char title[strLen];
val[0].buffer_type = MYSQL_TYPE_LONG;
val[0].buffer = (char*)&album.id;
val[0].length = &len[0];
val[0].is_null = &nullRes[0];
val[1].buffer_type = MYSQL_TYPE_STRING;
val[1].buffer = (char*)title;
val[1].buffer_length = strLen;
val[1].length = &len[1];
val[1].is_null = &nullRes[1];
val[2].buffer_type = MYSQL_TYPE_LONG;
val[2].buffer = (char*)&album.year;
val[2].length = &len[2];
val[2].is_null = &nullRes[2];
val[3].buffer_type = MYSQL_TYPE_LONG;
val[3].buffer = (char*)&songCount;
val[3].length = &len[3];
val[3].is_null = &nullRes[3];
auto status = mysql_stmt_bind_result(stmt, val);
status = mysql_stmt_fetch(stmt);
album.title = std::move(title);
std::cout << "done parsing album record with song count" << std::endl;
auto albWSC = std::make_pair(album, songCount);
return albWSC;
}
model::Album database::AlbumRepository::parseRecord(MYSQL_STMT *stmt)
{
std::cout << "parsing album record" << std::endl;
mysql_stmt_store_result(stmt);
auto rows = mysql_stmt_num_rows(stmt);
model::Album album;
auto status = 0;
auto time = 0;
auto valAmt = 3;
unsigned long len[valAmt];
my_bool nullRes[valAmt];
while (status == 0) {
if (mysql_stmt_field_count(stmt) > 0) {
auto res = mysql_stmt_result_metadata(stmt);
auto fields = mysql_fetch_fields(res);
auto strLen = 1024;
MYSQL_BIND val[valAmt];
memset(val, 0, sizeof(val));
char title[strLen];
val[0].buffer_type = MYSQL_TYPE_LONG;
val[0].buffer = (char*)&album.id;
val[0].length = &len[0];
val[0].is_null = &nullRes[0];
val[1].buffer_type = MYSQL_TYPE_STRING;
val[1].buffer = (char*)title;
val[1].buffer_length = strLen;
val[1].length = &len[1];
val[1].is_null = &nullRes[1];
val[2].buffer_type = MYSQL_TYPE_LONG;
val[2].buffer = (char*)&album.year;
val[2].length = &len[2];
val[2].is_null = &nullRes[2];
status = mysql_stmt_bind_result(stmt, val);
mysql_stmt_store_result(stmt);
status = mysql_stmt_fetch(stmt);
album.title = title;
}
status = mysql_stmt_next_result(stmt);
}
std::cout << "done parsing album record" << std::endl;
return album;
}
+415
View File
@@ -0,0 +1,415 @@
#include "database/ArtistRepository.h"
#include <iostream>
#include <memory>
#include <sstream>
#include <string>
#include <cstring>
database::ArtistRepository::ArtistRepository(const model::BinaryPath& binConf)
: BaseRepository(binConf)
{
}
std::vector<model::Artist> database::ArtistRepository::retrieveRecords()
{
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
const std::string query = "SELECT * FROM Artist";
mysql_stmt_prepare(stmt, query.c_str(), query.size());
mysql_stmt_execute(stmt);
auto artists = parseRecords(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
return artists;
}
std::pair<model::Artist, int> database::ArtistRepository::retrieveRecordWithSongCount(model::Artist& artist, type::ArtistFilter filter = type::ArtistFilter::id)
{
std::cout << "retrieving artist record with song count" << std::endl;
std::stringstream qry;
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
qry << "SELECT art.*, COUNT(*) AS SongCount FROM Artist art LEFT JOIN ";
qry << "Song sng ON art.ArtistId=sng.ArtistId WHERE ";
MYSQL_BIND params[1];
std::memset(params, 0, sizeof(params));
switch (filter) {
case type::ArtistFilter::id:
qry << "sng.ArtistId = ?";
params[0].buffer_type = MYSQL_TYPE_LONG;
params[0].buffer = (char*)&artist.id;
params[0].length = 0;
params[0].is_null = 0;
break;
default:
break;
}
qry << " GROUP BY art.ArtistId LIMIT 1";
const auto query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
std::cout << "Artist record with song count query ";
std::cout << "has been performed" << std::endl;
auto artWSC = parseRecordWithSongCount(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
return artWSC;
}
model::Artist database::ArtistRepository::retrieveRecord(model::Artist& artist, type::ArtistFilter filter)
{
std::cout << "retrieving artist record" << std::endl;
std::stringstream qry;
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
qry << "SELECT art.* FROM Artist art WHERE ";
MYSQL_BIND params[1];
memset(params, 0, sizeof(params));
auto artistLength = artist.artist.size();
switch (filter) {
case type::ArtistFilter::id:
qry << "art.ArtistId = ?";
params[0].buffer_type = MYSQL_TYPE_LONG;
params[0].buffer = (char*)&artist.id;
params[0].length = 0;
params[0].is_null = 0;
break;
case type::ArtistFilter::artist:
qry << "art.Artist = ?";
params[0].buffer_type = MYSQL_TYPE_STRING;
params[0].buffer = (char*)artist.artist.c_str();
params[0].length = &artistLength;
params[0].is_null = 0;
break;
default:
break;
}
qry << " LIMIT 1";
const auto query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
artist = parseRecord(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
std::cout << "retrieved record" << std::endl;
return artist;
}
bool database::ArtistRepository::doesArtistExist(const model::Artist& artist, type::ArtistFilter filter)
{
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
MYSQL_BIND params[1];
memset(params, 0, sizeof(params));
std::stringstream qry;
qry << "SELECT * FROM Artist WHERE ";
auto artistLength = artist.artist.size();
switch (filter) {
case type::ArtistFilter::id:
qry << "ArtistId = ?";
params[0].buffer_type = MYSQL_TYPE_LONG;
params[0].buffer = (char*)&artist.id;
params[0].length = 0;
params[0].is_null = 0;
break;
case type::ArtistFilter::artist:
qry << "Artist = ?";
params[0].buffer_type = MYSQL_TYPE_STRING;
params[0].buffer = (char*)artist.artist.c_str();
params[0].length = &artistLength;
params[0].is_null = 0;
break;
default:
break;
}
qry << " LIMIT 1";
const auto query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
std::cout << "the query has been performed" << std::endl;
mysql_stmt_store_result(stmt);
auto rowCount = mysql_stmt_num_rows(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
return (rowCount > 0) ? true : false;
}
void database::ArtistRepository::saveRecord(const model::Artist& artist)
{
std::cout << "inserting artist record" << std::endl;
auto conn = setupMysqlConnection();
MYSQL_STMT *stmt = mysql_stmt_init(conn);
const std::string query = "INSERT INTO Artist(Artist) VALUES(?)";
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
MYSQL_BIND params[1];
memset(params, 0, sizeof(params));
params[0].buffer_type = MYSQL_TYPE_STRING;
params[0].buffer = (char*)artist.artist.c_str();
auto artistLength = artist.artist.size();
params[0].length = &artistLength;
params[0].is_null = 0;
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
std::cout<< "inserted artist record" << std::endl;
}
void database::ArtistRepository::deleteArtist(const model::Artist& artist, type::ArtistFilter filter = type::ArtistFilter::id) {
// TODO: implement this
std::cout << "delete Artist record" << std::endl;
std::stringstream qry;
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
qry << "DELETE FROM Artist WHERE ";
MYSQL_BIND params[1];
std::memset(params, 0, sizeof(params));
switch (filter) {
case type::ArtistFilter::id:
qry << "ArtistId = ?";
params[0].buffer_type = MYSQL_TYPE_LONG;
params[0].buffer = (char*)&artist.id;
params[0].length = 0;
params[0].is_null = 0;
break;
default:
break;
}
const auto query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
std::cout << "deleted artist record" << std::endl;
}
std::vector<model::Artist> database::ArtistRepository::parseRecords(MYSQL_STMT *stmt)
{
mysql_stmt_store_result(stmt);
std::vector<model::Artist> artists;
artists.reserve(mysql_stmt_num_rows(stmt));
if (mysql_stmt_field_count(stmt) == 0) {
std::cout << "field count is 0" << std::endl;
return artists;
}
model::Artist art;
const auto valAmt = 2;
unsigned long len[valAmt];
my_bool nullRes[valAmt];
auto res = mysql_stmt_result_metadata(stmt);
auto fields = mysql_fetch_fields(res);
const auto strLen = 1024;
char artist[strLen];
MYSQL_BIND val[valAmt];
memset(val, 0, sizeof(val));
val[0].buffer_type = MYSQL_TYPE_LONG;
val[0].buffer = (char*)&art.id;
val[0].length = &len[0];
val[0].is_null = &nullRes[0];
val[1].buffer_type = MYSQL_TYPE_STRING;
val[1].buffer = (char*)artist;
val[1].buffer_length = strLen;
val[1].length = &len[1];
val[1].is_null = &nullRes[1];
for (auto status = mysql_stmt_bind_result(stmt, val); status == 0; ) {
std::cout << "fetching statement result" << std::endl;
status = mysql_stmt_fetch(stmt);
if (status == 0) {
art.artist = artist;
artists.push_back(std::move(art));
}
}
return artists;
}
model::Artist database::ArtistRepository::parseRecord(MYSQL_RES* results)
{
std::cout << "parsing artist record" << std::endl;
model::Artist artist;
auto fieldNum = mysql_num_fields(results);
auto row = mysql_fetch_row(results);
for (auto i = 0; i != fieldNum; ++i) {
const std::string field(mysql_fetch_field(results)->name);
if (field.compare("ArtistId") == 0) {
artist.id = std::stoi(row[i]);
}
if (field.compare("Artist") == 0) {
artist.artist = row[i];
}
}
std::cout << "parsed artist record" << std::endl;
return artist;
}
std::pair<model::Artist, int> database::ArtistRepository::parseRecordWithSongCount(MYSQL_STMT *stmt)
{
std::cout << "parsing artist record with song count" << std::endl;
mysql_stmt_store_result(stmt);
const auto strLen = 1024;
const auto valAmt = 3;
unsigned long len[valAmt];
my_bool nullRes[valAmt];
model::Artist artist;
int songCount = 0;
if (mysql_stmt_num_rows(stmt) == 0) {
std::cout << "no results" << std::endl;
return std::make_pair(artist, songCount);
}
MYSQL_BIND params[valAmt];
std::memset(params, 0, sizeof(params));
char art[strLen];
params[0].buffer_type = MYSQL_TYPE_LONG;
params[0].buffer = (char*)&artist.id;
params[0].length = &len[0];
params[0].is_null = &nullRes[0];
params[1].buffer_type = MYSQL_TYPE_STRING;
params[1].buffer = (char*)art;
params[1].buffer_length = strLen;
params[1].length = &len[1];
params[1].is_null = &nullRes[1];
params[2].buffer_type = MYSQL_TYPE_LONG;
params[2].buffer = (char*)&songCount;
params[2].length = &len[2];
params[2].is_null = &nullRes[2];
mysql_stmt_bind_result(stmt, params);
mysql_stmt_fetch(stmt);
artist.artist = std::move(art);
std::cout << "parsed artist record with song count" << std::endl;
return std::make_pair(artist, songCount);
}
model::Artist database::ArtistRepository::parseRecord(MYSQL_STMT *stmt)
{
std::cout << "parsing artist record" << std::endl;
mysql_stmt_store_result(stmt);
model::Artist art;
auto status = 0;
auto time = 0;
auto valAmt = 2;
unsigned long len[valAmt];
my_bool nullRes[valAmt];
while (status == 0) {
if (mysql_stmt_field_count(stmt) > 0) {
auto res = mysql_stmt_result_metadata(stmt);
auto fields = mysql_fetch_fields(res);
auto strLen = 1024;
MYSQL_BIND val[valAmt];
memset(val, 0, sizeof(val));
char artist[strLen];
val[0].buffer_type = MYSQL_TYPE_LONG;
val[0].buffer = (char*)&art.id;
val[0].length = &len[0];
val[0].is_null = &nullRes[0];
val[1].buffer_type = MYSQL_TYPE_STRING;
val[1].buffer = (char*)&artist;
val[1].buffer_length = strLen;
val[1].length = &len[1];
val[1].is_null = &nullRes[1];
status = mysql_stmt_bind_result(stmt, val);
mysql_stmt_store_result(stmt);
status = mysql_stmt_fetch(stmt);
art.artist = artist;
}
status = mysql_stmt_next_result(stmt);
}
std::cout << "done parsing artist record" << std::endl;
return art;
}
+96
View File
@@ -0,0 +1,96 @@
#include "database/BaseRepository.h"
#include <iostream>
#include <nlohmann/json.hpp>
#include "manager/DirectoryManager.h"
database::BaseRepository::BaseRepository()
{ }
database::BaseRepository::BaseRepository(const std::string& path) : path(path)
{
intitalizeDetails();
}
database::BaseRepository::BaseRepository(const model::BinaryPath& bConf)
{
initializeDetails(bConf);
}
bool database::BaseRepository::testConnection()
{
auto conn = mysql_init(nullptr);
if (!mysql_real_connect(conn, details.server.c_str(), details.username.c_str(),
details.password.c_str(), details.database.c_str(), 0,
nullptr, 0)) {
std::cout << "failed to connect to the database" << std::endl;
return false;
}
mysql_close(conn);
return true;
}
MYSQL* database::BaseRepository::setupMysqlConnection()
{
MYSQL *conn = mysql_init(nullptr);
if (!mysql_real_connect(conn, details.server.c_str(), details.username.c_str(),
details.password.c_str(), details.database.c_str(), 0, nullptr, 0)) {
std::cout << "connection error" << std::endl;
}
return conn;
}
MYSQL* database::BaseRepository::setupMysqlConnection(model::DatabaseConnection details)
{
MYSQL *connection = mysql_init(NULL);
// connect to the database with the details attached.
if (!mysql_real_connect(connection,details.server.c_str(), details.username.c_str(), details.password.c_str(), details.database.c_str(), 0, NULL, 0)) {
printf("Conection error : %s\n", mysql_error(connection));
exit(1);
}
return connection;
}
MYSQL_RES* database::BaseRepository::performMysqlQuery(MYSQL *conn, const std::string& query)
{
// send the query to the database
if (mysql_query(conn, query.c_str()))
{
printf("MySQL query error : %s\n", mysql_error(conn));
exit(1);
}
return mysql_use_result(conn);
}
void database::BaseRepository::intitalizeDetails()
{
auto databaseConfig = manager::DirectoryManager::databaseConfigContent(path);
details.database = databaseConfig["database"].get<std::string>();
details.password = databaseConfig["password"].get<std::string>();
details.server = databaseConfig["server"].get<std::string>();
details.username = databaseConfig["username"].get<std::string>();
}
void database::BaseRepository::initializeDetails(const model::BinaryPath& bConf)
{
auto databaseConfig = manager::DirectoryManager::databaseConfigContent(bConf);
details.database = databaseConfig["database"].get<std::string>();
details.server = databaseConfig["server"].get<std::string>();
details.username = databaseConfig["username"].get<std::string>();
details.password = databaseConfig["password"].get<std::string>();
std::cout << "retrieved database details" << std::endl;
}
+374
View File
@@ -0,0 +1,374 @@
#include "database/CoverArtRepository.h"
#include <iostream>
#include <memory>
#include <sstream>
#include <string>
#include <cstring>
database::CoverArtRepository::CoverArtRepository(const std::string& path) : BaseRepository(path)
{ }
database::CoverArtRepository::CoverArtRepository(const model::BinaryPath& bConf) : BaseRepository(bConf)
{ }
std::vector<model::Cover> database::CoverArtRepository::retrieveRecords()
{
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
const std::string query = "SELECT * FROM CoverArt";
mysql_stmt_prepare(stmt, query.c_str(), query.size());
mysql_stmt_execute(stmt);
auto coverArts = parseRecords(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
return coverArts;
}
model::Cover database::CoverArtRepository::retrieveRecord(model::Cover& cov, type::CoverFilter filter = type::CoverFilter::id)
{
std::stringstream qry;
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
qry << "SELECT * FROM CoverArt WHERE ";
MYSQL_BIND params[1];
memset(params, 0, sizeof(params));
auto songTitleLength = cov.songTitle.size();
switch (filter) {
case type::CoverFilter::id:
qry << "CoverArtId = ?";
params[0].buffer_type = MYSQL_TYPE_LONG;
params[0].buffer = (char*)&cov.id;
params[0].length = 0;
params[0].is_null = 0;
break;
case type::CoverFilter::songTitle:
qry << "SongTitle = ?";
params[0].buffer_type = MYSQL_TYPE_STRING;
params[0].buffer = (char*)cov.songTitle.c_str();
params[0].length = &songTitleLength;
params[0].is_null = 0;
break;
default:
break;
}
qry << " LIMIT 1";
const std::string query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
auto covDb = parseRecord(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
std::cout << "retrieved cover art record" << std::endl;
return covDb;
}
bool database::CoverArtRepository::doesCoverArtExist(const model::Cover& cover, type::CoverFilter filter)
{
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
MYSQL_BIND params[1];
memset(params, 0, sizeof(params));
std::stringstream qry;
qry << "SELECT * FROM CoverArt WHERE ";
auto titleLength = cover.songTitle.size();
switch (filter) {
case type::CoverFilter::id:
qry << "CoverArtId = ?";
params[0].buffer_type = MYSQL_TYPE_LONG;
params[0].buffer = (char*)&cover.id;
params[0].length = 0;
params[0].is_null = 0;
break;
case type::CoverFilter::songTitle:
qry << "SongTitle = ?";
params[0].buffer_type = MYSQL_TYPE_STRING;
params[0].buffer = (char*)cover.songTitle.c_str();
params[0].length = &titleLength;
params[0].is_null = 0;
break;
default:
break;
}
qry << " LIMIT 1";
const auto query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
std::cout << "the query has been performed" << std::endl;
mysql_stmt_store_result(stmt);
auto rowCount = mysql_stmt_num_rows(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
return (rowCount > 0) ? true : false;
}
// TODO: change to prepared statement
void database::CoverArtRepository::deleteRecord(const model::Cover& cov)
{
auto conn = setupMysqlConnection();
const std::string query("DELETE FROM CoverArt WHERE CoverArtId = " + std::to_string(cov.id));
auto result = performMysqlQuery(conn, query);
mysql_close(conn);
}
void database::CoverArtRepository::saveRecord(const model::Cover& cov)
{
std::cout << "saving cover art record";
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
MYSQL_BIND params[2];
memset(params, 0, sizeof(params));
my_bool isNull;
const std::string query = "INSERT INTO CoverArt(SongTitle, ImagePath) VALUES(?, ?)";
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
params[0].buffer_type = MYSQL_TYPE_STRING;
params[0].buffer = (char*)cov.songTitle.c_str();
auto songTitleLength = cov.songTitle.size();
params[0].length = &songTitleLength;
params[0].is_null = 0;
params[1].buffer_type = MYSQL_TYPE_STRING;
params[1].buffer = (char*)cov.imagePath.c_str();
auto imagePathLength = cov.imagePath.size();
params[1].length = &imagePathLength;
params[1].is_null = 0;
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
std::cout << "saved cover art record" << std::endl;
}
void database::CoverArtRepository::updateRecord(const model::Cover& cover)
{
std::stringstream qry;
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
qry << "UPDATE CoverArt SET ";
qry << "SongTitle = ?, ";
qry << "ImagePath = ? ";
qry << "WHERE CoverArtId = ?";
MYSQL_BIND params[3];
memset(params, 0, sizeof(params));
auto songTitleLength = cover.songTitle.size();
auto imagePathLength = cover.imagePath.size();
params[0].buffer_type = MYSQL_TYPE_STRING;
params[0].buffer = (char*)cover.songTitle.c_str();
params[0].length = &songTitleLength;
params[0].is_null = 0;
params[1].buffer_type = MYSQL_TYPE_STRING;
params[1].buffer = (char*)cover.imagePath.c_str();
params[1].length = &imagePathLength;
params[1].is_null = 0;
params[2].buffer_type = MYSQL_TYPE_LONG;
params[2].buffer = (char*)&cover.id;
params[2].length = 0;
params[2].is_null = 0;
const std::string query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
std::cout << "updated cover art record" << std::endl;
}
std::vector<model::Cover> database::CoverArtRepository::parseRecords(MYSQL_STMT *stmt)
{
mysql_stmt_store_result(stmt);
auto rowCount = mysql_stmt_num_rows(stmt);
std::cout << "number of results " << rowCount << std::endl;
std::vector<model::Cover> coverArts;
coverArts.reserve(rowCount);
const auto valAmt = 3;
unsigned long len[valAmt];
my_bool nullRes[valAmt];
for (auto status = 0; status == 0; status = mysql_stmt_next_result(stmt)) {
if (mysql_stmt_field_count(stmt) > 0) {
model::Cover coverArt;
auto res = mysql_stmt_result_metadata(stmt);
auto fields = mysql_fetch_fields(res);
auto strLen = 1024;
MYSQL_BIND val[valAmt];
memset(val, 0, sizeof(val));
char songTitle[strLen];
char imagePath[strLen];
val[0].buffer_type = MYSQL_TYPE_LONG;
val[0].buffer = (char*)&coverArt.id;
val[0].length = &len[0];
val[0].is_null = &nullRes[0];
val[1].buffer_type = MYSQL_TYPE_STRING;
val[1].buffer = (char*)songTitle;
val[1].buffer_length = strLen;
val[1].length = &len[1];
val[1].is_null = &nullRes[1];
val[2].buffer_type = MYSQL_TYPE_STRING;
val[2].buffer = (char*)imagePath;
val[2].buffer_length = strLen;
val[2].length = &len[2];
val[2].is_null = &nullRes[2];
status = mysql_stmt_bind_result(stmt, val);
while (true) {
std::cout << "fetching statement result" << std::endl;
status = mysql_stmt_fetch(stmt);
if (status == 1 || status == MYSQL_NO_DATA) {
break;
}
coverArt.songTitle = songTitle;
coverArt.imagePath = imagePath;
coverArts.push_back(std::move(coverArt));
}
}
std::cout << "fetching next result" << std::endl;
}
return coverArts;
}
model::Cover database::CoverArtRepository::parseRecord(MYSQL_RES *results)
{
std::cout << "parsing record" << std::endl;
model::Cover cov;
auto fieldNum = mysql_num_fields(results);
MYSQL_ROW row = mysql_fetch_row(results);
for (auto i = 0; i != fieldNum; ++i) {
switch (i) {
case 0:
cov.id = std::stoi(row[i]);
break;
case 1:
cov.songTitle = row[i];
break;
case 2:
cov.imagePath = row[i];
break;
}
}
std::cout << "done parsing record" << std::endl;
return cov;
}
model::Cover database::CoverArtRepository::parseRecord(MYSQL_STMT *stmt)
{
std::cout << "parsing cover art record" << std::endl;
mysql_stmt_store_result(stmt);
model::Cover cover;
auto status = 0;
auto time = 0;
auto valAmt = 3;
unsigned long len[valAmt];
my_bool nullRes[valAmt];
while (status == 0) {
if (mysql_stmt_field_count(stmt) > 0) {
auto res = mysql_stmt_result_metadata(stmt);
auto fields = mysql_fetch_fields(res);
auto strLen = 1024;
MYSQL_BIND val[valAmt];
memset(val, 0, sizeof(val));
char songTitle[strLen];
char imagePath[strLen];
val[0].buffer_type = MYSQL_TYPE_LONG;
val[0].buffer = (char*)&cover.id;
val[0].length = &len[0];
val[0].is_null = &nullRes[0];
val[1].buffer_type = MYSQL_TYPE_STRING;
val[1].buffer = (char*)&songTitle;
val[1].buffer_length = strLen;
val[1].length = &len[1];
val[1].is_null = &nullRes[1];
val[2].buffer_type = MYSQL_TYPE_STRING;
val[2].buffer = (char*)&imagePath;
val[2].buffer_length = strLen;
val[2].length = &len[2];
val[2].is_null = &nullRes[2];
status = mysql_stmt_bind_result(stmt, val);
mysql_stmt_store_result(stmt);
status = mysql_stmt_fetch(stmt);
cover.songTitle = songTitle;
cover.imagePath = imagePath;
}
status = mysql_stmt_next_result(stmt);
}
std::cout << "done parsing cover art record" << std::endl;
return cover;
}
+359
View File
@@ -0,0 +1,359 @@
#include "database/GenreRepository.h"
#include <iostream>
#include <memory>
#include <sstream>
#include <cstring>
database::GenreRepository::GenreRepository(const model::BinaryPath& bConf)
: BaseRepository(bConf)
{ }
std::vector<model::Genre> database::GenreRepository::retrieveRecords()
{
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
const std::string query = "SELECT * FROM Genre";
mysql_stmt_prepare(stmt, query.c_str(), query.size());
mysql_stmt_execute(stmt);
auto genres = parseRecords(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
return genres;
}
std::pair<model::Genre, int> database::GenreRepository::retrieveRecordWithSongCount(model::Genre& genre, type::GenreFilter filter = type::GenreFilter::id)
{
std::cout << "retrieving genre record with song count" << std::endl;
std::stringstream qry;
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
qry << "SELECT gnr.*, COUNT(*) AS SongCount FROM Genre gnr LEFT JOIN ";
qry << "Song sng ON gnr.GenreId=sng.GenreId WHERE ";
MYSQL_BIND params[1];
std::memset(params, 0, sizeof(params));
switch (filter) {
case type::GenreFilter::id:
qry << "sng.GenreId = ?";
params[0].buffer_type = MYSQL_TYPE_LONG;
params[0].buffer = (char*)&genre.id;
params[0].length = 0;
params[0].is_null = 0;
break;
default:
break;
}
qry << " GROUP BY gnr.GenreId LIMIT 1";
const auto query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
auto gnrWSC = parseRecordWithSongCount(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
std::cout << "retrieved genre record with song count" << std::endl;
return gnrWSC;
}
model::Genre database::GenreRepository::retrieveRecord(model::Genre& genre, type::GenreFilter filter)
{
// TODO: change to prepared statement
std::cout << "retrieving genre record" << std::endl;
std::stringstream qry;
auto conn = setupMysqlConnection();
qry << "SELECT gnr.* FROM Genre gnr WHERE ";
std::unique_ptr<char*> param;
switch (filter) {
case type::GenreFilter::id:
qry << "gnr.GenreId = " << genre.id;
break;
case type::GenreFilter::category:
param = std::make_unique<char*>(new char[genre.category.size()]);
mysql_real_escape_string(conn, *param, genre.category.c_str(), genre.category.size());
qry << "gnr.Category ='" << *param << "'";
break;
default:
break;
}
qry << " ORDER BY GenreId DESC LIMIT 1";
const auto query = qry.str();
auto results = performMysqlQuery(conn, query);
genre = parseRecord(results);
mysql_close(conn);
std::cout << "retrieved record" << std::endl;
return genre;
}
bool database::GenreRepository::doesGenreExist(const model::Genre& genre, type::GenreFilter filter)
{
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
std::stringstream qry;
qry << "SELECT * FROM Genre WHERE ";
MYSQL_BIND params[1];
memset(params, 0, sizeof(params));
auto categoryLength = genre.category.size();
switch (filter) {
case type::GenreFilter::id:
qry << "GenreId = ?";
params[0].buffer_type = MYSQL_TYPE_LONG;
params[0].buffer = (char*)&genre.id;
params[0].length = 0;
params[0].is_null = 0;
break;
case type::GenreFilter::category:
qry << "Category = ?";
params[0].buffer_type = MYSQL_TYPE_STRING;
params[0].buffer = (char*)genre.category.c_str();
params[0].length = &categoryLength;
params[0].is_null = 0;
break;
default:
break;
}
qry << " LIMIT 1";
const auto query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
std::cout << "the query has been performed" << std::endl;
mysql_stmt_store_result(stmt);
auto rowCount = mysql_stmt_num_rows(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
return (rowCount > 0) ? true : false;
}
void database::GenreRepository::saveRecord(const model::Genre& genre)
{
std::cout << "inserting genre record" << std::endl;
auto conn = setupMysqlConnection();
MYSQL_STMT *stmt = mysql_stmt_init(conn);
const std::string query("INSERT INTO Genre(Category) VALUES(?)");
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
MYSQL_BIND params[1];
memset(params, 0, sizeof(params));
params[0].buffer_type = MYSQL_TYPE_STRING;
params[0].buffer = (char*)genre.category.c_str();
auto categoryLength = genre.category.size();
params[0].length = &categoryLength;
params[0].is_null = 0;
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
std::cout << "inserted record" << std::endl;
}
void database::GenreRepository::deleteRecord(const model::Genre& genre, type::GenreFilter filter = type::GenreFilter::id)
{
// TODO: implement this
std::cout << "deleting genre record" << std::endl;
std::stringstream qry;
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
qry << "DELETE FROM Genre WHERE ";
MYSQL_BIND params[1];
std::memset(params, 0, sizeof(params));
switch (filter) {
case type::GenreFilter::id:
qry << "GenreId = ?";
params[0].buffer_type = MYSQL_TYPE_LONG;
params[0].buffer = (char*)&genre.id;
params[0].length = 0;
params[0].is_null = 0;
break;
default:
break;
}
const auto query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
std::cout << "deleted genre record" << std::endl;
}
std::vector<model::Genre> database::GenreRepository::parseRecords(MYSQL_STMT *stmt)
{
mysql_stmt_store_result(stmt);
std::vector<model::Genre> genres;
genres.reserve(mysql_stmt_num_rows(stmt));
if (mysql_stmt_field_count(stmt) == 0) {
std::cout << "field count is 0" << std::endl;
return genres;
}
model::Genre gnr;
const auto valAmt = 2;
unsigned long len[valAmt];
my_bool nullRes[valAmt];
auto res = mysql_stmt_result_metadata(stmt);
auto fields = mysql_fetch_fields(res);
const auto strLen = 1024;
char category[strLen];
MYSQL_BIND val[valAmt];
memset(val, 0, sizeof(val));
val[0].buffer_type = MYSQL_TYPE_LONG;
val[0].buffer = (char*)&gnr.id;
val[0].length = &len[0];
val[0].is_null = &nullRes[0];
val[1].buffer_type = MYSQL_TYPE_STRING;
val[1].buffer = (char*)category;
val[1].buffer_length = strLen;
val[1].length = &len[1];
val[1].is_null = &nullRes[1];
for (auto status = mysql_stmt_bind_result(stmt, val); status == 0;) {
std::cout << "fetching statement result" << std::endl;
status = mysql_stmt_fetch(stmt);
if (status == 0) {
gnr.category = category;
genres.push_back(std::move(gnr));
}
}
return genres;
}
std::pair<model::Genre, int> database::GenreRepository::parseRecordWithSongCount(MYSQL_STMT *stmt)
{
std::cout << "parsing genre record with song count" << std::endl;
mysql_stmt_store_result(stmt);
const auto strLen = 1024;
const auto valAmt = 3;
unsigned long len[valAmt];
my_bool nullRes[valAmt];
model::Genre genre;
int songCount = 0;
if (mysql_stmt_num_rows(stmt) == 0) {
std::cout << "no results" << std::endl;
return std::make_pair(genre, songCount);
}
MYSQL_BIND val[valAmt];
std::memset(val, 0, sizeof(val));
char category[strLen] ;
val[0].buffer_type = MYSQL_TYPE_LONG;
val[0].buffer = (char*)&genre.id;
val[0].length = &len[0];
val[0].is_null = &nullRes[0];
val[1].buffer_type = MYSQL_TYPE_STRING;
val[1].buffer = (char*)category;
val[1].buffer_length = strLen;
val[1].length = &len[1];
val[1].is_null = &nullRes[1];
val[2].buffer_type = MYSQL_TYPE_LONG;
val[2].buffer = (char*)&songCount;
val[2].length = &len[2];
val[2].is_null = &nullRes[2];
mysql_stmt_bind_result(stmt, val);
mysql_stmt_fetch(stmt);
genre.category = std::move(category);
std::cout << "parsed genre record with song count" << std::endl;
return std::make_pair(genre, songCount);
}
model::Genre database::GenreRepository::parseRecord(MYSQL_RES* results)
{
std::cout << "parsing genre record" << std::endl;
model::Genre genre;
auto fieldNum = mysql_num_fields(results);
auto row = mysql_fetch_row(results);
for (auto i =0; i != fieldNum; ++i) {
const std::string field(mysql_fetch_field(results)->name);
if (field.compare("GenreId") == 0) {
genre.id = std::stoi(row[i]);
}
if (field.compare("Category") == 0) {
genre.category = row[i];
}
}
std::cout << "parsed genre record" << std::endl;
return genre;
}
model::Genre database::GenreRepository::parseRecord(MYSQL_STMT *stmt)
{
// TODO: implement this
model::Genre genre;
return genre;
}

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