Compare commits

..
Author SHA1 Message Date
phoenix e2a5e02aad Trying
soaricarus_api CI / Rustfmt (pull_request) Successful in 2m21s
soaricarus_api CI / Test Suite (pull_request) Successful in 3m55s
soaricarus_api CI / Check (pull_request) Successful in 4m15s
soaricarus_api CI / Clippy (pull_request) Successful in 4m22s
soaricarus_api CI / build (pull_request) Successful in 3m48s
2026-07-24 19:12:19 -04:00
phoenix 6574d0ee8f I am tired boss
soaricarus_api CI / Rustfmt (pull_request) Successful in 2m26s
soaricarus_api CI / Check (pull_request) Successful in 3m24s
soaricarus_api CI / Clippy (pull_request) Successful in 3m43s
soaricarus_api CI / Test Suite (pull_request) Successful in 6m32s
soaricarus_api CI / build (pull_request) Successful in 3m25s
2026-07-24 15:33:18 -04:00
phoenix 75f4d9fa76 Saving changes. No real progress yet
soaricarus_api CI / Test Suite (pull_request) Successful in 3m36s
soaricarus_api CI / Check (pull_request) Successful in 4m3s
soaricarus_api CI / Rustfmt (pull_request) Successful in 5m49s
soaricarus_api CI / Clippy (pull_request) Successful in 2m24s
soaricarus_api CI / build (pull_request) Successful in 4m32s
2026-07-24 12:23:48 -04:00
phoenix 3de818f280 Saving changes
soaricarus_api CI / Rustfmt (pull_request) Successful in 3m50s
soaricarus_api CI / Check (pull_request) Successful in 5m0s
soaricarus_api CI / Test Suite (pull_request) Successful in 6m7s
soaricarus_api CI / Clippy (pull_request) Successful in 3m28s
soaricarus_api CI / build (pull_request) Successful in 3m42s
2026-07-23 17:19:29 -04:00
phoenix f116adffdb Saving changes. Nothing yet 2026-07-23 16:26:14 -04:00
phoenix 3965d86d54 Saving script 2026-07-23 16:21:40 -04:00
phoenix 8fae71b086 Getting somewhere. Object Storage is initialized but not initializing hangs 2026-07-23 16:05:21 -04:00
phoenix 2bfb8c6aee Got garage to run in docker
soaricarus_api CI / Rustfmt (pull_request) Successful in 2m13s
soaricarus_api CI / Check (pull_request) Successful in 4m13s
soaricarus_api CI / Test Suite (pull_request) Successful in 5m55s
soaricarus_api CI / Clippy (pull_request) Successful in 4m30s
soaricarus_api CI / build (pull_request) Successful in 4m57s
2026-07-23 15:31:41 -04:00
phoenix 327d04e8a9 Revert "Removing garage.toml"
soaricarus_api CI / Clippy (pull_request) Canceled after 0s
soaricarus_api CI / build (pull_request) Canceled after 0s
soaricarus_api CI / Check (pull_request) Canceled after 2m22s
soaricarus_api CI / Rustfmt (pull_request) Canceled after 2m10s
soaricarus_api CI / Test Suite (pull_request) Canceled after 4m15s
This reverts commit 7a209eeb5e.
2026-07-23 15:28:58 -04:00
phoenix 7a209eeb5e Removing garage.toml 2026-07-23 15:15:11 -04:00
phoenix 7460f0d157 Updating some of the sample env
soaricarus_api CI / Rustfmt (pull_request) Successful in 3m17s
soaricarus_api CI / Check (pull_request) Successful in 4m30s
soaricarus_api CI / Test Suite (pull_request) Successful in 4m38s
soaricarus_api CI / Clippy (pull_request) Successful in 3m58s
soaricarus_api CI / build (pull_request) Successful in 3m8s
2026-07-22 17:20:58 -04:00
phoenix 4b7816e117 Adding image to store data 2026-07-22 17:20:37 -04:00
phoenix 59a7e31641 Updating name (#253)
soaricarus_api CI / Check (push) Successful in 3m21s
soaricarus_api CI / Test Suite (push) Successful in 4m25s
soaricarus_api CI / Rustfmt (push) Successful in 3m8s
soaricarus_api CI / Clippy (push) Successful in 3m56s
soaricarus_api CI / build (push) Successful in 3m3s
Reviewed-on: #253
2026-07-21 15:28:48 -04:00
phoenix 8a64a71339 Update postgres (#252)
Rust CI / Test Suite (push) Successful in 3m43s
Rust CI / Rustfmt (push) Successful in 2m2s
Rust CI / Check (push) Successful in 3m9s
Rust CI / build (push) Successful in 2m58s
Rust CI / Clippy (push) Successful in 4m28s
Reviewed-on: phoenix/icarus#252
2026-07-09 11:05:12 -04:00
phoenix 7ca4c655a4 Update (#251)
Rust CI / build (push) Failing after 2m53s
Reviewed-on: phoenix/icarus#251
2026-07-07 23:25:45 -04:00
phoenixandGitHub 50d0ea630f Update rust (#250)
Rust CI / build (pull_request) Failing after 2m21s
* bump: rust Cargo.toml

* bump: rust docker

* bump: rust ci

* bump: axum

* bump: axum-extra

* bump: tokio

* bump: tower-http

* bump: uuid

* bump: utoipa

* bump: icarus_meta

* bump: icarus_models

* bump: icarus_envy

* bump: icarus

* Updated base image in dockerfile

* Adding sample local env file

* Fixing postgresql volume issue in docker
2026-05-22 11:29:38 -04:00
phoenixandGitHub d273c801bf Update rust (#249)
* Updated rust version

* Updated rust in workflow

* Updating rust version in docker file

* Updating postgresql

* Updated crates

* cargo update

* Updated readme

* Version bump

* Warning fix
2026-04-05 18:13:49 -04:00
KDandGitHub bcd960d9a1 tsk-245: Modify result when getting songs but no songs are present (#247)
* tsk-245: Modify result when getting songs but no songs are present

* tsk-245: Fixed endpoint indicating success when no content is found

* tsk-245: Version bump
2025-11-07 11:48:07 -05:00
KDandGitHub dbbbf02acf tsk-244: Remove code causing migrations to be ran twice (#246)
* tsk-244: Remove code causing migrations to be ran twice

* tsk-244: Cleanup

* tsk-244: Version bump
2025-11-06 12:56:47 -05:00
KDandGitHub 01b34c9848 tsk-230: Using UserClaims from icarus_models (#243)
* tsk-230: Using UserClaims from icarus_models

* tsk-230: Cleanup

* tsk-230: Version bump
2025-11-05 09:59:13 -05:00
KDandGitHub 1b9bd4359c tsk-207: Move code elsewhere that is found in src/main.rs (#242)
* tsk-207: Move code elsewhere that is found in src/main.rs

* tsk-207: Some refactoring

* tsk-207: Moving file around

* tsk-207: Code formatting

* tsk-207: Version bump

* tsk-207: Test fix
2025-11-04 11:19:12 -05:00
KDandGitHub d05502dd8e tsk-227: Change statement when evaluating multipart form when queueing song (#241)
* tsk-227: Change statement when evaluating multipart form when queueing song

* tsk-227: Version bump
2025-11-04 10:47:51 -05:00
KDandGitHub 147a50c867 tsk-196: Moving SongQueue struct (#240)
* tsk-196: Moving SongQueue struct

* tsk-196: Code formatting

* tsk-196: Version bump
2025-11-03 13:56:18 -05:00
KDandGitHub a80e21b918 tsk-204: Changing name of test payload function (#239)
* tsk-204: Changing name of test payload function

* tsk-204: Added more test payload functions

* tsk-204: Version bump
2025-11-03 13:41:56 -05:00
KDandGitHub 731aae5240 tsk-206: Moving request functions to their own module in testing (#238)
* tsk-206: Moving request functions to their own module in testing

* tsk-206: Version bump

* tsk-206: Forgot some imports. Some cleanup

* tsk-206: Forgot some imports

* tsk-206: How did I miss these?

* tsk-206: Code formatting
2025-11-02 21:30:40 -05:00
KDandGitHub 79e909bfc0 tsk-86: Postgresql version bump 18.0 (#237)
* tsk-86: Postgresql version bump 18.0

* tsk-186: Updated exposed port on docker

* tsk-186: cargo update

* tsk-186: Version bump
2025-11-02 17:03:14 -05:00
KDandGitHub 7940527418 tsk-208: Moving config and endpoint code (#236)
* tsk-208: Moving config and endpoint code

* tsk-208: Code formatting

* tsk-208: Version bump

* tsk-208: Accidentally changed port
2025-11-01 14:57:40 -04:00
KDandGitHub f859eaf0a2 tsk-205: Put functions within a util module (#235)
* tsk-205: Put functions within a util module

* tsk-205: Code formatting

* tsk-205: Version bump

* tsk-205: Code cleanup
2025-11-01 14:32:53 -04:00
KDandGitHub 22080a2f29 tsk-201: Making content disposition more unique (#234)
* tsk-201: Making content disposition more unique

* tsk-201: Cleanup

* tsk-201: Version bump
2025-10-31 12:34:43 -04:00
KDandGitHub c630cb84b5 tsk-200: Utilizing mime for Content type for fetching queued CoverArt data (#233)
* tsk-200: Utilizing mime for Content type for fetching queued CoverArt data

* tsk-200: Cleanup

* tsk-200: Version bump
2025-10-31 12:20:11 -04:00
KDandGitHub ca38465f2d tsk-202: Make CoverArt filename random during creation (#232)
* tsk-202: Make downloaded CoverArt filename random

* tsk-202: Cleanup

* tsk-202: Version bump

* tsk-202: Version bump down
2025-10-30 16:48:26 -04:00
KDandGitHub fd4e661b3c tsk-224: Save file_type to CoverArt (#231)
* tsk-224: Save file_type to CoverArt

* tsk-224: Updated migrations

* tsk-224: Code formatting

* tsk-224: Fix test

* tsk-224: Test fix

* tsk-224: Version bump

* tsk-224: Warning fix
2025-10-30 16:25:59 -04:00
KDandGitHub 6995ed483c icarus_models version bump (#229)
* icarus_models version bump

* Fix test

* Build fixes

* Version bump

* Using standard version
2025-10-30 15:35:02 -04:00
KDandGitHub 2cc5e99590 tsk-192: jsonwebtoken version bump (#228)
* tsk-192: jsonwebtoken version bump

* tsk-192: Added feature to jsonwebtoken

* tsk-192: Code cleanup

* tsk-192: cargo update

* tsk-192: Version bump
2025-10-29 16:40:17 -04:00
KDandGitHub 09d7fa32c8 tsk-179: Modify status code of song queue endpoint (#226)
* tsk-179: Updating docs for endpoint status codes

* tsk-179: Updated status code

* tsk-179: Improved error handling

* tsk-179: Code formatting

* tsk-179: Version bump
2025-10-29 15:20:04 -04:00
KDandGitHub 27ef175828 tsk-213: Identify audio file type in initial step when queueing (#225)
* tsk-213: Adding song check when queueing songs

* tsk-213: Added check for updating queued song

* tsk-213: Removed todo

* tsk-213: Code cleanup

* tsk-213: Warning fix

* tsk-213: Version bump

* tsk-213: Minor status code changes
2025-10-29 14:58:34 -04:00
KDandGitHub 1871ebe40b tsk-203: Addressing hard coding when downloading coverart (#223)
* tsk-203: Addressing hard coding when downloading coverart

* tsk-203: Code formatting

* tsk-203: Version bump

* tsk-203: Make downloaded coverart file random

* tsk-203: Code formatting
2025-10-27 14:16:04 -04:00
KDandGitHub 1a5a9fcaaf tsk-194: Make distinction in MetadataQueue endpoints (#222)
* tsk-194: Moving metadata module

* tsk-194: Build fix

* tsk-194: Name changes

* tsk-194: Code formatting

* tsk-194: Version bump

* tsk-194: Test fixes
2025-10-27 13:25:30 -04:00
KDandGitHub 8b36bc8c49 tsk-218: Separate Song and SongQueue in the endpoints (#221)
* tsk-218: Added empty file

* tsk-218: Created song module in queue module

* tsk-218: Code formatting

* tsk-218: Version bump

* tsk-218: Cleanup

* tsk-218: Test fixes
2025-10-26 17:48:17 -04:00
KDandGitHub 003e5b77c1 tsk-197: Rename download_flac() function (#220)
* tsk-197: Renamed function

* tsk-197: Version bump

* tsk-197: icarus_models version bump
2025-10-25 21:34:33 -04:00
KDandGitHub 02d9ef013e tsk-199: Separate CoverArt queue endpoints (#219)
* tsk-199: Created queue module for queue endpoints

* tsk-199: Code formatting

* tsk-199: Test fixes

* tsk-199: Version bump

* tsk-199: Updated readme
2025-10-25 21:17:40 -04:00
KDandGitHub 351b4acb1c tsk-211: Fix SQL error when checking queue (#217)
* tsk-211: Fixing error on getting queue item

* tsk-211: Some cleanup

* tsk-211: Cleanup

* tsk-211: Updated readme

* tsk-211: Version bump
2025-10-22 21:42:19 -04:00
KDandGitHub 381dd49b56 tsk-209: Create repo module (#216)
* tsk-209: Refactored repo code

* tsk-209: Refactoring more code

* tsk-209: Code formatting

* tsk-209: Refactored metadata repo code

* tsk-209: Refactored the other code

* tsk-209: Code formatting

* tsk-209: Version bump

* tsk-209: Fix test
2025-10-22 20:37:15 -04:00
KDandGitHub a9ba1c5193 tsk-212: Post fixes (#215)
* tsk-212: icarus_meta version bump

* tsk-212: icarus_meta version bump

* tsk-212: Got functionality working

* tsk-212: Migration changes

Added file_type to CoverArtQueue

* tsk-212: Add support to save file_type from CoverArtQueue

* tsk-212: Warning fix

* tsk-212: Version bump

* tsk-212: icarus_meta version bump

* tsk-212: icarus_meta version bump

* tsk-212: icarus_meta related changes

* tsk-212: Replaced code with constants

* tsk-212: icarus_meta version bump
2025-10-21 22:42:32 -04:00
KDandGitHub fb360c8269 tsk-212: Identify type of CoverArt (#214)
* tsk-212: icarus_meta version bump

* tsk-212: icarus_meta version bump

* tsk-212: Got functionality working

* tsk-212: Migration changes

Added file_type to CoverArtQueue

* tsk-212: Add support to save file_type from CoverArtQueue

* tsk-212: Warning fix

* tsk-212: Version bump

* tsk-212: icarus_meta version bump

* tsk-212: icarus_meta version bump

* tsk-212: icarus_meta related changes

* tsk-212: Replaced code with constants
2025-10-21 22:24:52 -04:00
KDandGitHub e461e78661 icarus_models version bump (#210)
* icarus_models version bump

* icarus_models related changes

* Code formatting

* Test changeS:

* Test changes

* Migration changes

* Migration changes

* Warning fix

* Cleanup

* Updated readme

* Removed line from migrations

* Code cleanup

* Version bump

* cargo update

* icarus_models version bump
2025-10-20 11:19:06 -04:00
KDandGitHub 2023fc81f8 Version fix (#193)
* Version fix

* Workflow fix

* Test fix

* Test fix

* Removing code that was meant to be removed

Caused an extra file to be created when the request is called

* Updated env sample to address workflow

* Removed import

* env fix

* Version change

* Version bump

Going with the version I done earlier. I won't have the versioning match the api version in the endpoints

* Fixing versions for app dependencies
2025-10-14 22:02:26 -04:00
KDandGitHub 48a40a4821 tsk-182: Update rust version (#191)
* Updated rust version

* Warning fix

* Updating crates and dependencies

* Version bump

* tsk-182: cargo update

* Updated jsonwebtoken
2025-10-12 18:23:56 -04:00
KDandGitHub 8bf99be22c icarus_models version bump (#190)
* icarus_models version bump

* icarus_models changes

* Code refactoring

* Changes

* Version bump

* Fixed todo
2025-10-11 19:50:56 -04:00
KDandGitHub 8c12c8c761 icarus_envy version bump (#189)
* icarus_envy version bump

* icarus_envy related changes

* Code formatting

* Code cleanup

* Version bump
2025-10-10 19:04:01 -04:00
KDandGitHub caaec248db CORS support (#183)
* Changes to same .env file

* Updated sample env files

* icarus_envy version bump

* Adding CORS

* Added other urls

* Code formatting

* Changed allowed urls

* Modified docker compose. Port changes

* Changed port

* Cleanup

* Version bump
2025-10-10 16:33:15 -04:00
KDandGitHub 309d76785a Language change (#185)
* Adding skeleton file

* Adding content

* Still having issues with the language

* Some changes

* Ignoring .NET related files
2025-10-01 09:55:04 -04:00
KDandGitHub c83a37c20a Change language of project (#184)
* Adding skeleton file

* Adding content
2025-10-01 09:48:02 -04:00
KDandGitHub 032801b903 Merge pull request #153 from kdeng00/v0.2
v0.2 release
2025-08-30 13:34:06 -04:00
kdeng00 b844f06e82 Added license 2025-08-30 13:30:34 -04:00
kdeng00 517a01d60c Version bump 2025-08-30 13:28:37 -04:00
KDandGitHub 1ae2f88beb tsk-171: Updated Readme (#181) 2025-08-29 22:14:03 -04:00
KDandGitHub 805df87f5b tsk-170: API documentation (#178)
* tsk-170: Added utopia crates for API documentation

* tsk-170: Adding code to document API

* tsk-170: icarus_models version bump

Added schema capabilities

* tsk-170: crate changes

* tsk-170: Added documentation for song queue endpoint

* tsk-170: Swagger openapi docs are now up

Well, at least one

* tsk-170: Some documentation

* tsk-170: Code formatting

* tsk-170: Saving changes

* tsk-170: Added more documentation

* tsk-170: Added the rest of the documentation

* tsk-170: Formatting

* tsk-170: icarus_models version bump
2025-08-24 19:58:38 -04:00
KDandGitHub 598996cb8d tsk-176: Get all songs endpoint (#177)
* tsk-176: Added function to retrieve all songs

* tsk-176: Depdendency version bump

icarus_envy, icarus_meta, and icarus_models

* tsk-176: Fix after icarus_models change

* tsk-176: Test fix

* tsk-176: Formatting

* tsk-176: Endpoint is now available

* tsk-176: Added test

* tsk-176: Cleanup

* tsk-176: Version bump
2025-08-19 12:25:46 -04:00
KDandGitHub 21a97c150c tsk-172: Include songparser as docker image (#173)
* tsk-172: Updated docker compose to include songparser service

* tsk-172: Changed docker dependencies

* tsk-172: Cleanup
2025-08-16 19:14:01 -04:00
KDandGitHub 679a4ad322 Auth (#169)
* Added jwt crate

* Saving changes

Might go a different route later

* Crate changes

* Adding auth module

* Saving changes

* Added jsonwebtoken crate

* Saving changes

* Added feature to tower-http crate

* Using secret key from env file

* Locking down one endpoint to start

* Warning fix

* Got auth functioning

* Updated icarus_models

* Fixed auth in tests

* Warning fixes

* Code formatting

* Added auth to the other endpoints

* Version bump

* This should fix the pipeline test failure
2025-08-01 15:16:00 -04:00
KDandGitHub 9aefeace23 Use multiple files in tests (#168)
* File renamed

* Filename change in the code as well

* Added more migration sql for tests

* Using different song for a test

* Changes to test and helper functions

* Workflow change

* Code formatting

* Version bump
2025-07-30 15:28:47 -04:00
KDandGitHub a0c6ae65a3 Delete song endpoint (#167)
* Added initial code for delete song endpoint

* Saving changes

* More changes

* Made delete song endpoint available

* Response change

* Added test function

* Changes to code

Fixing errors

* How did I miss this?

* Got the test working

* Code cleanup and formatting

* Version bump

* Code changes

* Workflow change
2025-07-29 21:34:16 -04:00
KDandGitHub c7230d3b32 Download coverart endpoint (#166)
* Added download coverart endpoint

* Endpoint is now available

* Added test

* Code formatting

* Version bump
2025-07-28 17:05:42 -04:00
KDandGitHub 703fc49f32 Downloading song endpoint (#165)
* Added initial code for downloading song endpoint

* Finished download song endpoint

* Making download endpoint available

* Added test

* Code formatting

* Version bump
2025-07-28 16:40:14 -04:00
45 changed files with 5958 additions and 3240 deletions
+26 -4
View File
@@ -1,7 +1,29 @@
APP_ENV=development
BACKEND_PORT=8000
FRONTEND_URL=http://localhost:4200
RUST_LOG=debug
ALLOWED_ORIGINS=https://soaricarus.com,https://www.soaricarus.com
SECRET_MAIN_KEY=refero34o8rfhfjn983thf39fhc943rf923n3h SECRET_MAIN_KEY=refero34o8rfhfjn983thf39fhc943rf923n3h
ROOT_DIRECTORY=/home/icarus/mydata ROOT_DIRECTORY=/home/soaricarus/mydata
POSTGRES_MAIN_USER=icarus POSTGRES_MAIN_USER=soaricarus
POSTGRES_MAIN_PASSWORD=password POSTGRES_MAIN_PASSWORD=password
POSTGRES_MAIN_DB=icarus_db POSTGRES_MAIN_DB=soaricarus_db
POSTGRES_MAIN_HOST=main_db POSTGRES_MAIN_HOST=main_db
DATABASE_URL=postgres://${POSTGRES_MAIN_USER}:${POSTGRES_MAIN_PASSWORD}@${POSTGRES_MAIN_HOST}:5432/${POSTGRES_MAIN_DB} POSTGRES_MAIN_PORT=5432
DATABASE_URL=postgres://${POSTGRES_MAIN_USER}:${POSTGRES_MAIN_PASSWORD}@${POSTGRES_MAIN_HOST}:${POSTGRES_MAIN_PORT}/${POSTGRES_MAIN_DB}
GARAGE_RPC_SECRET=c2bde5c3f9c04116a7563e25ea5ba6fab25319daa2fe8a952ad57d0b8159b590
GARAGE_METADATA_DIR=/var/lib/garage/meta
GARAGE_DATA_DIR=/var/lib/garage/data
GARAGE_DB_ENGINE=sqlite
GARAGE_REPLICATION_FACTOR=1
GARAGE_RPC_BIND_ADDR=[::]:3901
GARAGE_S3_API_BIND_ADDR=[::]:3900
GARAGE_S3_WEB_BIND_ADDR=[::]:3902
GARAGE_S3_REGION=maze
GARAGE_ROOT_DOMAIN=.localhost
AWS_DEFAULT_REGION=maze
AWS_SECRET_ACCESS_KEY=3o4n7yfo34985yrfcq9238765ry3928475rcy349857ty394578cy
AWS_ACCESS_KEY_ID=2q39fnoy59q2347yfdrc327894ryfc0q2834rcyq023897
S3_BUCKET_NAME=soaricarus-storage
+13
View File
@@ -0,0 +1,13 @@
APP_ENV=development
BACKEND_PORT=8000
FRONTEND_URL=http://localhost:4200
RUST_LOG=debug
ALLOWED_ORIGINS=https://soaricarus.com,https://www.soaricarus.com
SECRET_MAIN_KEY=refero34o8rfhfjn983thf39fhc943rf923n3h
ROOT_DIRECTORY=/home/soaricarus/mydata
POSTGRES_MAIN_USER=soaricarus
POSTGRES_MAIN_PASSWORD=password
POSTGRES_MAIN_DB=soaricarus_db
POSTGRES_MAIN_HOST=main_db
POSTGRES_MAIN_PORT=5432
DATABASE_URL=postgres://${POSTGRES_MAIN_USER}:${POSTGRES_MAIN_PASSWORD}@${POSTGRES_MAIN_HOST}:${POSTGRES_MAIN_PORT}/${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}
+9
View File
@@ -0,0 +1,9 @@
# Override specific file types
*.cs linguist-language=Rust
*.sln linguist-documentation
*.csproj linguist-documentation
# Or vendor all non-Rust files
*.cs linguist-vendored
*.js linguist-vendored
*.py linguist-vendored
+2 -2
View File
@@ -9,14 +9,14 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v6
with: with:
fetch-depth: 0 # Important for git describe --tags fetch-depth: 0 # Important for git describe --tags
- name: Install Rust - name: Install Rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: 1.88.0 toolchain: 1.97
components: cargo components: cargo
- name: Extract Version from Cargo.toml - name: Extract Version from Cargo.toml
+120 -53
View File
@@ -1,23 +1,47 @@
name: Rust CI name: soaricarus_api CI
on: on:
push: push:
branches: [ "v0.2" ] branches: [ "main" ]
pull_request: pull_request:
branches: [ "v0.2" ] branches: [ "main" ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build: check:
name: Check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/soaricarus_models_deploy_key
chmod 600 ~/.ssh/soaricarus_models_deploy_key
ssh-keyscan ${{ secrets.MYHOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/soaricarus_models_deploy_key
cargo check
test:
name: Test Suite
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
services: services:
postgres: postgres:
image: postgres:17.5 image: postgres:18.4-alpine
env: env:
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} POSTGRES_USER: ${{ secrets.DB_TEST_USER || 'testuser' }}
POSTGRES_USER: ${{ secrets.POSTGRES_USER }} POSTGRES_PASSWORD: ${{ secrets.DB_TEST_PASSWORD || 'testpassword' }}
POSTGRES_DB: ${{ secrets.POSTGRES_DB }} POSTGRES_DB: ${{ secrets.DB_TEST_NAME || 'testdb' }}
ports:
- 5432:5432
options: >- options: >-
--health-cmd pg_isready --health-cmd pg_isready
--health-interval 10s --health-interval 10s
@@ -25,53 +49,96 @@ jobs:
--health-retries 5 --health-retries 5
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install Rust 1.88.0
uses: actions-rs/toolchain@v1
with: with:
toolchain: 1.88.0 toolchain: 1.97
components: clippy, rustfmt - uses: Swatinem/rust-cache@v2
override: true - name: Verify Docker Environment
- 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: | run: |
# Wait for PostgreSQL to be ready echo "Runner User Info:"
for i in {1..10}; do id
pg_isready -U ${{ secrets.POSTGRES_USER }} -d ${{ secrets.POSTGRES_DB }} && break echo "Checking Docker Version:"
sleep 2 docker --version
done echo "Checking Docker Daemon Status (info):"
docker info
# Run database migrations (if you use sqlx migrations) echo "Checking Docker Daemon Status (ps):"
cargo install sqlx-cli --no-default-features --features native-tls,postgres docker ps -a
sqlx migrate run --database-url $DATABASE_URL echo "Docker environment check complete."
- 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 - name: Run tests
env: env:
# Define DATABASE_URL for tests to use
DATABASE_URL: postgresql://${{ secrets.DB_TEST_USER || 'testuser' }}:${{ secrets.DB_TEST_PASSWORD || 'testpassword' }}@postgres:5432/${{ secrets.DB_TEST_NAME || 'testdb' }}
RUST_LOG: info # Optional: configure test log level
SECRET_MAIN_KEY: ${{ secrets.TOKEN_SECRET_KEY }}
SSH_AUTH_SOCK: ${{ env.SSH_AUTH_SOCK }}
ROOT_DIRECTORY: "/tmp" ROOT_DIRECTORY: "/tmp"
DATABASE_URL: "postgres://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}" run: |
run: cargo test --verbose mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/soaricarus_models_deploy_key
chmod 600 ~/.ssh/soaricarus_models_deploy_key
ssh-keyscan ${{ secrets.MYHOST }} >> ~/.ssh/known_hosts
- name: Run clippy eval $(ssh-agent -s)
run: cargo clippy -- -D warnings ssh-add -v ~/.ssh/soaricarus_models_deploy_key
- name: Run rustfmt cargo test
run: cargo fmt --all -- --check
fmt:
name: Rustfmt
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
- run: rustup component add rustfmt
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/soaricarus_models_deploy_key
chmod 600 ~/.ssh/soaricarus_models_deploy_key
ssh-keyscan ${{ secrets.MYHOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/soaricarus_models_deploy_key
cargo fmt --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
- run: rustup component add clippy
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/soaricarus_models_deploy_key
chmod 600 ~/.ssh/soaricarus_models_deploy_key
ssh-keyscan ${{ secrets.MYHOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/soaricarus_models_deploy_key
cargo clippy -- -D warnings
build:
name: build
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/soaricarus_models_deploy_key
chmod 600 ~/.ssh/soaricarus_models_deploy_key
ssh-keyscan ${{ secrets.MYHOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/soaricarus_models_deploy_key
cargo build --release
+4
View File
@@ -7,3 +7,7 @@
.DS_STORE .DS_STORE
Storage/ Storage/
*.sln
*.cs
*.csproj
Generated
+1202 -834
View File
File diff suppressed because it is too large Load Diff
+28 -20
View File
@@ -1,28 +1,36 @@
[package] [package]
name = "icarus" name = "soaricarus_api"
version = "0.1.96" version = "0.5.2"
edition = "2024" edition = "2024"
rust-version = "1.88" rust-version = "1.95"
license = "MIT"
description = "Core API for interacting with soaricarus"
[dependencies] [dependencies]
axum = { version = "0.8.4", features = ["multipart"] } axum = { version = "0.8.9", features = ["multipart"] }
serde = { version = "1.0.219", features = ["derive"] } axum-extra = { version = "0.12.6", features = ["cookie"] }
serde_json = { version = "1.0.140" } openssl = { version = "0.10.81", features = ["vendored"] }
tower = { version = "0.5.2", features = ["full"] } serde = { version = "1.0.228", features = ["derive"] }
tokio = { version = "1.45.1", features = ["full"] } serde_json = { version = "1.0.150" }
tokio-util = { version = "0.7.15", features = ["io"] } tokio = { version = "1.52.3", features = ["full"] }
tower-http = { version = "0.6.6", features = ["timeout"] } tokio-util = { version = "0.7.18", features = ["io"] }
tracing-subscriber = "0.3.19" tower = { version = "0.5.3", features = ["full"] }
futures = { version = "0.3.31" } tower-http = { version = "0.7.0", features = ["cors", "timeout"] }
tracing-subscriber = "0.3.23"
futures = { version = "0.3.32" }
mime_guess = { version = "2.0.5" } mime_guess = { version = "2.0.5" }
uuid = { version = "1.17.0", features = ["v4", "serde"] } uuid = { version = "1.23.5", features = ["v4", "serde"] }
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio-native-tls", "time", "uuid"] } sqlx = { version = "0.9.0", features = ["postgres", "runtime-tokio", "tls-native-tls", "time", "uuid"] }
time = { version = "0.3.41", features = ["formatting", "macros", "parsing", "serde"] } time = { version = "0.3.53", features = ["formatting", "macros", "parsing", "serde"] }
icarus_meta = { git = "ssh://git@git.kundeng.us/phoenix/icarus_meta.git", tag = "v0.3.0-devel-f4b71de969-680" } jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] }
icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.5.1-devel-1c5de9dc26-111" } josekit = { version = "0.10.3" }
icarus_envy = { git = "ssh://git@git.kundeng.us/phoenix/icarus_envy.git", tag = "v0.3.0-devel-d73fba9899-006" } utoipa = { version = "5.5.0", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
simeta = { git = "ssh://git@git.kundeng.us/phoenix/simeta.git", tag = "v0.6.1-main-b690995806-680" }
simodels = { git = "ssh://git@git.kundeng.us/phoenix/simodels.git", tag = "v0.11.3-main-fe9d101bd0-111" }
sienvy = { git = "ssh://git@git.kundeng.us/phoenix/sienvy.git", tag = "v0.8.0-main-d06c8fdf49-006" }
[dev-dependencies] [dev-dependencies]
common-multipart-rfc7578 = { version = "0.7.0" } common-multipart-rfc7578 = { version = "0.7.0" }
url = { version = "2.5.4" } url = { version = "2.5.8" }
tempfile = { version = "3.20.0" } tempfile = { version = "3.27.0" }
+10 -24
View File
@@ -1,5 +1,4 @@
# Stage 1: Build the application FROM rust:1.97 as builder
FROM rust:1.88 as builder
# Set the working directory inside the container # Set the working directory inside the container
WORKDIR /usr/src/app WORKDIR /usr/src/app
@@ -17,35 +16,29 @@ RUN mkdir -p -m 0700 ~/.ssh && \
echo " User git" >> ~/.ssh/config && \ echo " User git" >> ~/.ssh/config && \
chmod 600 ~/.ssh/config chmod 600 ~/.ssh/config
# << --- ADD HOST KEY HERE --- >>
RUN ssh-keyscan git.kundeng.us >> ~/.ssh/known_hosts RUN ssh-keyscan git.kundeng.us >> ~/.ssh/known_hosts
# Copy Cargo manifests # Copy Cargo manifests
COPY Cargo.toml Cargo.lock ./ 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 && \ RUN --mount=type=ssh mkdir src && \
echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs && \ echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs && \
cargo build --release --quiet && \ cargo build --release --quiet && \
rm -rf src target/release/deps/icarus* # Clean up dummy build artifacts (replace icarus) rm -rf src target/release/deps/soaricarus_api*
# Copy the actual source code # Copy the actual source code
COPY src ./src COPY src ./src
# If you have other directories like `templates` or `static`, copy them too
COPY .env ./.env COPY .env ./.env
COPY migrations ./migrations COPY migrations ./migrations
COPY scripts/init-garage.sh /scripts/init-garage.sh
# Make it executable
RUN chmod +x /scripts/init-garage.sh
# << --- 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 \ RUN --mount=type=ssh \
cargo build --release --quiet cargo build --release --quiet
# Stage 2: Create the final, smaller runtime image FROM debian:trixie-slim
# 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) # 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/* RUN apt-get update && apt-get install -y ca-certificates libssl-dev libssl3 && rm -rf /var/lib/apt/lists/*
@@ -53,18 +46,11 @@ RUN apt-get update && apt-get install -y ca-certificates libssl-dev libssl3 && r
# Set the working directory # Set the working directory
WORKDIR /usr/local/bin WORKDIR /usr/local/bin
# Copy the compiled binary from the builder stage COPY --from=builder /usr/src/app/target/release/soaricarus_api .
# 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/.env .
COPY --from=builder /usr/src/app/migrations ./migrations COPY --from=builder /usr/src/app/migrations ./migrations
# Expose the port your Axum app listens on (e.g., 3000 or 8000) EXPOSE 8000
EXPOSE 3000
# Set the command to run your application CMD ["./soaricarus_api"]
# Ensure this matches the binary name copied above
CMD ["./icarus"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 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.
+22 -6
View File
@@ -1,17 +1,33 @@
# soaricarus_api
Core web API for the soaricarus project.
# Getting Started ### Requires
## Docker `soaricarus_auth` v0.8.x
Make sure `icarus_auth` is located in the root of the parent directory if using docker. `songparser` v0.6.x
Create a `.env` file for both projects - `icarus_auth` and `icarus` - in the root of each project. ### Compatible with
`sidm` v0.10.x
## Getting Started
Quickest way to get started is with docker. Make sure `soaricarus_auth` and `songparser` repositories
are located in the root of the parent directory. Check the respective repositories to ensure they
are setup correctly before configuring `soaricarus_api`.
Copy the `.env.docker.sample` file to `.env`. Ensure that the `ROOT_DIRECTORY` variable is populated
and exists on the docker image's filesystem. The credentials for the database doesn't need to be
changed for development, but if deploying it, it should be modified.
Build containers Build containers
``` ```
docker compose build --ssh default api auth_api docker compose build --ssh default
``` ```
Bring it up Bring it up
``` ```
docker compose up -d --force-recreate api auth_api docker compose up -d --force-recreate
``` ```
To view the OpenAPI spec, run the project and access `/swagger-ui`. If running through docker,
the url would be something like `http://localhost:8000/swagger-ui`.
+90 -39
View File
@@ -3,62 +3,114 @@ version: '3.8' # Use a recent version
services: services:
# --- Web API --- # --- Web API ---
api: api:
build: # Tells docker-compose to build the Dockerfile in the current directory build:
context: . context: .
ssh: ["default"] # Uses host's SSH agent ssh: ["default"]
container_name: icarus # Optional: Give the container a specific name container_name: soaricarus
ports: ports:
# Map host port 8000 to container port 3000 (adjust as needed) - "8000:8000"
# Format: "HOST_PORT:CONTAINER_PORT"
- "8000:3000"
env_file: env_file:
- .env - .env
volumes:
- shared_credentials:/shared:ro # Mount as read-only
depends_on: depends_on:
main_db: main_db:
condition: service_healthy condition: service_healthy
networks: networks:
- main-api-network - soaricarus-network
restart: unless-stopped # Optional: Restart policy restart: unless-stopped
# --- Web API auth --- # --- Web API auth ---
auth_api: auth_api:
build: build:
context: ../icarus_auth # IMPORTANT: Relative path to the local checkout of your web API repo (containing the Dockerfile) context: ../soaricarus_auth # IMPORTANT: Relative path to the local checkout of your web API repo (containing the Dockerfile)
ssh: ["default"] # Uses host's SSH agent ssh: ["default"]
dockerfile: Dockerfile # Optional: Specify if your Dockerfile has a non-standard name dockerfile: Dockerfile # Optional: Specify if your Dockerfile has a non-standard name
container_name: auth_api container_name: auth_api
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8001:3000" # Map host port 8000 to container port 8000 (adjust container port based on your app's EXPOSE in Dockerfile) - "8001:8001"
# environment:
# Environment variables your API needs, e.g., database connection
# Add other necessary environment variables
env_file: env_file:
- ../icarus_auth/.env - ../soaricarus_auth/.env
depends_on: depends_on:
auth_db: auth_db:
condition: service_healthy condition: service_healthy
networks: networks:
- auth-api-network - soaricarus-network
# Optional: Mount local code for development (live reload) # Optional: Mount local code for development (live reload)
# volumes: # volumes:
# - ./path/to/your/web-api-repo:/app # - ./path/to/your/web-api-repo:/app
# --- songparser service ---
songparser:
build:
context: ../songparser
ssh: ["default"]
dockerfile: Dockerfile
container_name: songparser
restart: unless-stopped
env_file:
- ../songparser/.env
depends_on:
- api
- main_db
- auth_api
- auth_db
networks:
- soaricarus-network
# --- storage service ---
maze:
build:
context: ./garage
dockerfile: Dockerfile
container_name: maze
restart: unless-stopped
ports:
- "3900:3900" # S3 API
- "3901:3901" # RPC for cluster communication
- "3902:3902" # S3 web interface
- "3903:3903" # Admin API
volumes:
- ./garage.toml:/etc/garage.toml:ro
- ./garage/entrypoint.sh:/entrypoint.sh:ro
- shared_credentials:/shared
- object_data:/var/lib/garage
# - ./scripts:/scripts:ro # ← ADD THIS - mount the scripts directory
env_file:
- .env
environment:
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- GARAGE_RPC_SECRET=${GARAGE_RPC_SECRET}
- BUCKET_NAME=${S3_BUCKET_NAME}
# entrypoint: ["/entrypoint.sh"]
# command: []
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3901/v0/status || exit 1"]
# test: ["CMD-SHELL", "echo 'What is to be done?'"]
# test: ["CMD-SHELL", "nc -z localhost 3900 || exit 1"]
# test: ["CMD-SHELL", "/garage status | grep -q 'Health: ok' || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- soaricarus-network
# PostgreSQL Database Service # PostgreSQL Database Service
# --- icarus web api db ---
main_db: main_db:
image: postgres:17.5-alpine # Use an official Postgres image (Alpine variant is smaller) image: postgres:18.4-alpine
container_name: icarus_db # Optional: Give the container a specific name container_name: soaricarus_db
environment: environment:
# These MUST match the user, password, and database name in the DATABASE_URL above # These MUST match the user, password, and database name in the DATABASE_URL above
POSTGRES_USER: ${POSTGRES_MAIN_USER:-icarus} POSTGRES_USER: ${POSTGRES_MAIN_USER:-soaricarus}
POSTGRES_PASSWORD: ${POSTGRES_MAIN_PASSWORD:-password} POSTGRES_PASSWORD: ${POSTGRES_MAIN_PASSWORD:-password}
POSTGRES_DB: ${POSTGRES_MAIN_DB:-icarus_db} POSTGRES_DB: ${POSTGRES_MAIN_DB:-soaricarus_db}
volumes: volumes:
# Persist database data using a named volume # Persist database data using a named volume
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql
ports: ports:
# Optional: Expose port 5432 ONLY if you need to connect directly from your host machine (e.g., for debugging)
- "5432:5432" - "5432:5432"
healthcheck: healthcheck:
# Checks if Postgres is ready to accept connections # Checks if Postgres is ready to accept connections
@@ -67,24 +119,23 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
start_period: 10s start_period: 10s
restart: always # Optional: Restart policy restart: always
networks: networks:
- main-api-network - soaricarus-network
# --- icarus web auth api db --- # --- soaricarus web auth api db ---
auth_db: auth_db:
image: postgres:17.5-alpine # Use an official Postgres image (Alpine variant is smaller) image: postgres:18.4-alpine
container_name: icarus_auth_db # Optional: Give the container a specific name container_name: soaricarus_auth_db
environment: environment:
# These MUST match the user, password, and database name in the DATABASE_URL above # These MUST match the user, password, and database name in the DATABASE_URL above
POSTGRES_USER: ${POSTGRES_AUTH_USER:-icarus_op} POSTGRES_USER: ${POSTGRES_AUTH_USER:-soaricarus_op}
POSTGRES_PASSWORD: ${POSTGRES_AUTH_PASSWORD:-password} POSTGRES_PASSWORD: ${POSTGRES_AUTH_PASSWORD:-password}
POSTGRES_DB: ${POSTGRES_AUTH_DB:-icarus_auth_db} POSTGRES_DB: ${POSTGRES_AUTH_DB:-soaricarus_auth_db}
volumes: volumes:
# Persist database data using a named volume # Persist database data using a named volume
- postgres_data_auth:/var/lib/postgresql/data - postgres_data_auth:/var/lib/postgresql
ports: ports:
# Optional: Expose port 5432 ONLY if you need to connect directly from your host machine (e.g., for debugging)
- "5433:5432" - "5433:5432"
healthcheck: healthcheck:
# Checks if Postgres is ready to accept connections # Checks if Postgres is ready to accept connections
@@ -93,20 +144,20 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
start_period: 10s start_period: 10s
restart: always # Optional: Restart policy restart: always
networks: networks:
- auth-api-network - soaricarus-network
# Define the named volume for data persistence
volumes: volumes:
postgres_data: postgres_data:
driver: local # Use the default local driver driver: local # Use the default local driver
postgres_data_auth: postgres_data_auth:
driver: local # Use the default local driver driver: local # Use the default local driver
object_data:
driver: local # Use the default local driver
shared_credentials: # New shared volume
driver: local
# Define the network (optional, but good practice)
networks: networks:
main-api-network: soaricarus-network:
driver: bridge
auth-api-network:
driver: bridge driver: bridge
+16
View File
@@ -0,0 +1,16 @@
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "sqlite"
replication_factor = 1
rpc_bind_addr = "[::]:3901"
rpc_secret = "${GARAGE_RPC_SECRET}"
[s3_api]
s3_region = "maze"
api_bind_addr = "[::]:3900"
root_domain = ".localhost"
[s3_web]
bind_addr = "[::]:3902"
root_domain = ".localhost"
+18
View File
@@ -0,0 +1,18 @@
# Use a base image with a shell (Alpine)
FROM alpine:latest
# Install necessary tools
RUN apk add --no-cache curl
# Copy the Garage binary from the official image
COPY --from=dxflrs/garage:v2.3.0 /garage /usr/local/bin/garage
# Copy the entrypoint script
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Set the entrypoint
ENTRYPOINT ["/entrypoint.sh"]
# Pass through any arguments to Garage
CMD []
+103
View File
@@ -0,0 +1,103 @@
#!/bin/sh
set -e
BUCKET_NAME="${BUCKET_NAME:-soaricarus-storage}"
CREDENTIALS_FILE="/shared/garage-credentials.env"
SETUP_DONE="/shared/setup.done"
GARAGE_BIN="/usr/local/bin/garage"
# Function to write credentials
write_credentials() {
local access_key="$1"
local secret_key="$2"
mkdir -p "$(dirname "$CREDENTIALS_FILE")"
cat > "$CREDENTIALS_FILE" << EOF
GARAGE_ACCESS_KEY_ID=$access_key
GARAGE_SECRET_ACCESS_KEY=$secret_key
GARAGE_BUCKET_NAME=$BUCKET_NAME
GARAGE_ENDPOINT=http://localhost:3900
EOF
touch "$SETUP_DONE"
echo "✅ Credentials saved to $CREDENTIALS_FILE"
}
# Check if setup already completed
if [ -f "$SETUP_DONE" ]; then
echo "️ Setup already completed. Starting Garage..."
exec $GARAGE_BIN "$@"
fi
echo "🔧 First-time setup - initializing Garage..."
# Start Garage in background for setup (just the binary, no daemon subcommand)
echo "⏳ Starting Garage in background..."
$GARAGE_BIN "$@" &
GARAGE_PID=$!
# Wait for Garage to be ready
echo "⏳ Waiting for Garage API..."
until curl -s -f "http://localhost:3901/v0/status" > /dev/null 2>&1; do
echo " Not ready yet, sleeping 2s..."
sleep 2
done
echo "✅ Garage API is responding!"
# Apply cluster layout
if ! $GARAGE_BIN layout show 2>/dev/null | grep -q "Current cluster layout"; then
echo "📦 Applying cluster layout..."
NODE_ID=$($GARAGE_BIN node id | head -n1)
$GARAGE_BIN layout assign --version 1 "$NODE_ID"
$GARAGE_BIN layout apply --version 1
echo " ✅ Layout applied"
sleep 2
else
echo "️ Layout already applied"
fi
# Create bucket
if ! $GARAGE_BIN bucket info "$BUCKET_NAME" 2>/dev/null; then
echo "📦 Creating bucket: $BUCKET_NAME"
$GARAGE_BIN bucket create "$BUCKET_NAME"
else
echo "️ Bucket $BUCKET_NAME already exists"
fi
# Handle key
if [ -n "$AWS_ACCESS_KEY_ID" ] && [ -n "$AWS_SECRET_ACCESS_KEY" ]; then
echo "🔑 Using pre-defined credentials from .env"
if ! $GARAGE_BIN key info "$AWS_ACCESS_KEY_ID" 2>/dev/null; then
$GARAGE_BIN key import "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" "app-key"
echo " ✅ Key imported: $AWS_ACCESS_KEY_ID"
else
echo " ️ Key already exists: $AWS_ACCESS_KEY_ID"
fi
ACCESS_KEY="$AWS_ACCESS_KEY_ID"
SECRET_KEY="$AWS_SECRET_ACCESS_KEY"
else
echo "🔑 Generating new key..."
KEY_OUTPUT=$($GARAGE_BIN key create --name "app-key")
echo "$KEY_OUTPUT"
ACCESS_KEY=$(echo "$KEY_OUTPUT" | grep 'Key ID:' | awk '{print $3}')
SECRET_KEY=$(echo "$KEY_OUTPUT" | grep 'Secret key:' | awk '{print $3}')
echo " ✅ Key generated: $ACCESS_KEY"
fi
# Grant permissions
$GARAGE_BIN bucket allow --read --write --owner "$BUCKET_NAME" --key "$ACCESS_KEY"
echo " ✅ Permissions granted"
# Save credentials
write_credentials "$ACCESS_KEY" "$SECRET_KEY"
echo "🎉 Setup complete!"
echo " Bucket: $BUCKET_NAME"
echo " Access Key: $ACCESS_KEY"
# Stop background Garage
echo "⏹️ Stopping background Garage..."
kill "$GARAGE_PID"
wait "$GARAGE_PID" 2>/dev/null || true
# Start Garage in foreground
echo "🚀 Starting Garage in foreground..."
exec $GARAGE_BIN "$@"
+109
View File
@@ -0,0 +1,109 @@
#!/bin/sh
set -e
BUCKET_NAME="${BUCKET_NAME:-soaricarus-storage}"
CREDENTIALS_FILE="/shared/garage-credentials.env"
SETUP_DONE="/shared/setup.done"
# Function to write credentials to shared volume
write_credentials() {
local access_key="$1"
local secret_key="$2"
mkdir -p "$(dirname "$CREDENTIALS_FILE")"
cat > "$CREDENTIALS_FILE" << EOF
GARAGE_ACCESS_KEY_ID=$access_key
GARAGE_SECRET_ACCESS_KEY=$secret_key
GARAGE_BUCKET_NAME=$BUCKET_NAME
GARAGE_ENDPOINT=http://maze:3900
EOF
touch "$SETUP_DONE"
echo "✅ Credentials saved to $CREDENTIALS_FILE"
}
# Check if setup already done
if [ -f "$SETUP_DONE" ]; then
echo "️ Setup already completed. Starting Garage..."
# Source credentials if needed for the main Garage process
# shellcheck source=/dev/null
[ -f "$CREDENTIALS_FILE" ] && . "$CREDENTIALS_FILE"
exec /usr/local/bin/garage "$@"
fi
echo "🔧 Running initial setup..."
# Start Garage in background for setup
echo "⏳ Starting Garage in background..."
/usr/local/bin/garage "$@" &
GARAGE_PID=$!
# Wait for Garage API to be ready
echo "⏳ Waiting for Garage API..."
# until curl -s -f "http://localhost:3900/v0/status" > /dev/null 2>&1; do
# echo " Not ready yet, sleeping 2s..."
# sleep 2
#done
echo "✅ Garage API is responding!"
# Apply layout if not already applied
if ! /usr/local/bin/garage layout show 2>/dev/null | grep -q "Current cluster layout"; then
echo "📦 Applying cluster layout..."
NODE_ID=$(/usr/local/bin/garage node id | head -n1)
/usr/local/bin/garage layout assign --version 1 "$NODE_ID"
/usr/local/bin/garage layout apply --version 1
echo " ✅ Layout applied"
sleep 3
else
echo "️ Layout already applied"
fi
echo "Laid out"
# Create bucket if it doesn't exist
if ! /usr/local/bin/garage bucket info "$BUCKET_NAME" 2>/dev/null; then
echo "📦 Creating bucket: $BUCKET_NAME"
/usr/local/bin/garage bucket create "$BUCKET_NAME"
else
echo "️ Bucket $BUCKET_NAME already exists"
fi
# Create or import key
# We'll use the credentials from .env if provided, otherwise generate new ones
if [ -n "$AWS_ACCESS_KEY_ID" ] && [ -n "$AWS_SECRET_ACCESS_KEY" ]; then
echo "🔑 Using pre-defined AWS credentials from .env"
if ! /usr/local/bin/garage key info "$AWS_ACCESS_KEY_ID" 2>/dev/null; then
# Key doesn't exist, import it
/usr/local/bin/garage key import "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" "app-key"
echo " ✅ Key imported: $AWS_ACCESS_KEY_ID"
else
echo " ️ Key already exists: $AWS_ACCESS_KEY_ID"
fi
ACCESS_KEY="$AWS_ACCESS_KEY_ID"
SECRET_KEY="$AWS_SECRET_ACCESS_KEY"
else
echo "🔑 Generating new key..."
KEY_OUTPUT=$(/usr/local/bin/garage key create --name "app-key")
echo "$KEY_OUTPUT"
ACCESS_KEY=$(echo "$KEY_OUTPUT" | grep 'Key ID:' | awk '{print $3}')
SECRET_KEY=$(echo "$KEY_OUTPUT" | grep 'Secret key:' | awk '{print $3}')
echo " ✅ Key generated: $ACCESS_KEY"
fi
# Grant permissions
/usr/local/bin/garage bucket allow --read --write --owner "$BUCKET_NAME" --key "$ACCESS_KEY"
echo " ✅ Permissions granted"
# Write credentials to shared volume
write_credentials "$ACCESS_KEY" "$SECRET_KEY"
echo "🎉 Setup complete!"
echo " Bucket: $BUCKET_NAME"
echo " Access Key: $ACCESS_KEY"
# Stop the background Garage process
echo "⏹️ Stopping background Garage process..."
kill "$GARAGE_PID"
wait "$GARAGE_PID" 2>/dev/null || true
echo "🚀 Starting Garage in foreground..."
exec /usr/local/bin/garage "$@"
+36
View File
@@ -0,0 +1,36 @@
#!/bin/sh
set -e
BUCKET_NAME="${BUCKET_NAME:-soaricarus-storage}"
CREDENTIALS_FILE="/shared/garage-credentials.env"
SETUP_DONE="/shared/setup.done"
echo "🔧 Running initial setup..."
# Start Garage in background for setup
echo "⏳ Starting Garage in background..."
/usr/local/bin/garage "$@" &
GARAGE_PID=$!
# Wait for Garage API to be ready
echo "⏳ Waiting for Garage API..."
# until curl -s -f "http://localhost:3900/v0/status" > /dev/null 2>&1; do
# echo " Not ready yet, sleeping 2s..."
# sleep 2
#done
echo "✅ Garage API is responding!"
# Apply layout if not already applied
#if ! /usr/local/bin/garage layout show 2>/dev/null | grep -q "Current cluster layout"; then
# echo "📦 Applying cluster layout..."
# NODE_ID=$(/usr/local/bin/garage node id | head -n1)
# echo $NODE_ID
# /usr/local/bin/garage layout assign --version 1 "$NODE_ID"
# /usr/local/bin/garage layout apply --version 1
# echo " ✅ Layout applied"
# sleep 3
#else
# echo "️ Layout already applied"
# fi
echo "Laid out"
+4 -3
View File
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS "metadataQueue" (
CREATE TABLE IF NOT EXISTS "coverartQueue" ( CREATE TABLE IF NOT EXISTS "coverartQueue" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
data BYTEA NULL, data BYTEA NULL,
file_type TEXT NOT NULL,
song_queue_id UUID NULL song_queue_id UUID NULL
); );
@@ -55,8 +56,8 @@ CREATE TABLE IF NOT EXISTS "song" (
CREATE TABLE IF NOT EXISTS "coverart" ( CREATE TABLE IF NOT EXISTS "coverart" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL, title TEXT NOT NULL,
-- TODO: Separate path later directory TEXT NOT NULL,
path TEXT NOT NULL, filename TEXT NOT NULL,
file_type TEXT NOT NULL,
song_id UUID NOT NULL song_id UUID NOT NULL
-- TODO: Add type later
); );
+84
View File
@@ -0,0 +1,84 @@
#!/bin/sh
set -e
BUCKET_NAME="soaricarus-storage"
echo "⏳ Waiting for Garage to be ready..."
# until docker exec maze curl -s -f "http://localhost:3901/v0/status" > /dev/null 2>&1; do
until docker exec maze nc -z "localhost 3900" > /dev/null 2>&1; do
echo " Garage not ready yet, retrying in 2s..."
sleep 2
done
echo "✅ Garage API is responding!"
sleep 3
# Check if layout is already applied
if docker exec maze garage layout show 2>/dev/null | grep -q "Current cluster layout"; then
echo "️ Layout already applied, checking bucket..."
else
echo "📦 Applying cluster layout..."
# Get node ID
MAX_RETRIES=10
RETRY_COUNT=0
NODE_ID=""
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
NODE_ID=$(docker exec maze garage node id 2>/dev/null | head -n1)
if [ -n "$NODE_ID" ]; then
break
fi
RETRY_COUNT=$((RETRY_COUNT + 1))
echo " Waiting for node ID (attempt $RETRY_COUNT/$MAX_RETRIES)..."
sleep 2
done
if [ -z "$NODE_ID" ]; then
echo "❌ Failed to get node ID after $MAX_RETRIES attempts"
exit 1
fi
echo " Node ID: $NODE_ID"
docker exec maze garage layout assign --version 1 "$NODE_ID"
docker exec maze garage layout apply --version 1
echo " ✅ Layout applied"
sleep 3
fi
# Check if bucket exists
if docker exec maze garage bucket info "${BUCKET_NAME}" 2>/dev/null; then
echo "️ Bucket '${BUCKET_NAME}' already exists. Skipping setup."
exit 0
fi
echo "📦 Creating bucket and configuring permissions..."
# Create the bucket
docker exec maze garage bucket create "${BUCKET_NAME}"
echo " ✅ Bucket created: ${BUCKET_NAME}"
# Check if key exists
if docker exec maze garage key info "${AWS_ACCESS_KEY_ID}" 2>/dev/null; then
echo " ️ Key already exists: ${AWS_ACCESS_KEY_ID}"
else
docker exec maze garage key create \
--name "app-key" \
"${AWS_ACCESS_KEY_ID}" \
"${AWS_SECRET_ACCESS_KEY}"
echo " ✅ Key created: ${AWS_ACCESS_KEY_ID}"
fi
# Grant permissions
docker exec maze garage bucket allow \
--read \
--write \
--owner \
"${BUCKET_NAME}" \
--key "${AWS_ACCESS_KEY_ID}"
echo " ✅ Permissions granted"
echo "🎉 Setup complete!"
echo " Bucket: ${BUCKET_NAME}"
echo " Access Key: ${AWS_ACCESS_KEY_ID}"
echo " Endpoint: http://maze:3900"
+59
View File
@@ -0,0 +1,59 @@
use axum::{
Json,
http::{Request, StatusCode},
middleware::Next,
response::IntoResponse,
};
use axum_extra::extract::cookie::CookieJar;
use jsonwebtoken::{DecodingKey, Validation, decode};
#[derive(Debug, serde::Serialize)]
pub struct ErrorResponse {
pub status: &'static str,
pub message: String,
}
pub async fn auth<B>(
cookie_jar: CookieJar,
req: Request<axum::body::Body>,
next: Next,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
let token = cookie_jar
.get("token")
.map(|cookie| cookie.value().to_string())
.or_else(|| {
req.headers()
.get(axum::http::header::AUTHORIZATION)
.and_then(|auth_header| auth_header.to_str().ok())
.and_then(|auth_value| auth_value.strip_prefix("Bearer ").map(String::from))
});
let token = token.ok_or_else(|| {
let json_error = ErrorResponse {
status: "fail",
message: "You are not logged in, please provide token".to_string(),
};
(StatusCode::UNAUTHORIZED, Json(json_error))
})?;
let secret_key = sienvy::environment::get_secret_main_key().value;
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
validation.set_audience(&["soaricarus"]);
let _claims = decode::<simodels::token::UserClaims>(
&token,
&DecodingKey::from_secret(secret_key.as_ref()),
&validation,
)
.map_err(|err| {
eprintln!("Error: {err:?}");
let json_error = ErrorResponse {
status: "fail",
message: "Invalid token - Error decoding claims".to_string(),
};
(StatusCode::UNAUTHORIZED, Json(json_error))
})?
.claims;
Ok(next.run(req).await)
}
+155 -543
View File
@@ -1,53 +1,14 @@
// 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 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 { pub mod create_coverart {
#[derive(Debug, serde::Deserialize, serde::Serialize)] #[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request { pub struct Request {
pub song_id: uuid::Uuid, pub song_id: uuid::Uuid,
pub coverart_queue_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 get_coverart { pub mod get_coverart {
#[derive(Debug, serde::Deserialize, serde::Serialize)] #[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Params { pub struct Params {
pub id: Option<uuid::Uuid>, pub id: Option<uuid::Uuid>,
} }
@@ -55,481 +16,43 @@ pub mod request {
} }
pub mod response { 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 { pub mod create_coverart {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)] #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response { pub struct Response {
pub message: String, pub message: String,
pub data: Vec<icarus_models::coverart::CoverArt>, pub data: Vec<simodels::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 get_coverart { pub mod get_coverart {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)] #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response { pub struct Response {
pub message: String, pub message: String,
pub data: Vec<icarus_models::coverart::CoverArt>, pub data: Vec<simodels::coverart::CoverArt>,
}
}
}
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 async fn get_coverart(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, title, path, song_id FROM "coverart" WHERE id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(row) => Ok(icarus_models::coverart::CoverArt {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
title: row
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
path: row
.try_get("path")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
song_id: row
.try_get("song_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
}),
Err(_) => Err(sqlx::Error::RowNotFound),
} }
} }
} }
pub mod endpoint { pub mod endpoint {
use std::io::Write;
use axum::response::IntoResponse; use axum::response::IntoResponse;
pub async fn queue( use crate::repo;
axum::Extension(pool): axum::Extension<sqlx::PgPool>, use crate::repo::queue as repo_queue;
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 { /// Endpoint to create cover art
Ok(Some(field)) => { #[utoipa::path(
let name = field.name().unwrap().to_string(); post,
let file_name = field.file_name().unwrap().to_string(); path = super::super::endpoints::CREATECOVERART,
let content_type = field.content_type().unwrap().to_string(); request_body(
let data = field.bytes().await.unwrap(); content = super::request::create_coverart::Request,
let raw_data = data.to_vec(); description = "Data required to create cover art",
content_type = "application/json"
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(),
), ),
} responses(
} (status = 200, description = "Cover art created", body = super::response::create_coverart::Response),
(status = 400, description = "Failure in creating cover art", body = super::response::create_coverart::Response)
)
)]
pub async fn create_coverart( pub async fn create_coverart(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::create_coverart::Request>, axum::Json(payload): axum::Json<super::request::create_coverart::Request>,
@@ -540,29 +63,48 @@ pub mod endpoint {
let mut response = super::response::create_coverart::Response::default(); let mut response = super::response::create_coverart::Response::default();
let id = payload.coverart_queue_id; let id = payload.coverart_queue_id;
match super::db::get_coverart_queue_data_with_id(&pool, &id).await { match repo_queue::coverart::get_coverart_queue_data_with_id(&pool, &id).await {
Ok(data) => { Ok(data) => {
let song_id = payload.song_id; let song_id = payload.song_id;
match crate::callers::song::song_db::get_song(&pool, &song_id).await { match crate::repo::song::get_song(&pool, &song_id).await {
Ok(song) => { Ok(song) => {
let directory = icarus_envy::environment::get_root_directory().await; let directory = sienvy::environment::get_root_directory().value;
let dir = std::path::Path::new(&directory); let file_type =
simeta::detection::coverart::file_type_from_data(&data).unwrap();
let coverart_type = if file_type.file_type
== simeta::detection::coverart::constants::JPEG_TYPE
{
simodels::types::CoverArtType::JpegExtension
} else if file_type.file_type
== simeta::detection::coverart::constants::JPG_TYPE
{
simodels::types::CoverArtType::JpgExtension
} else if file_type.file_type
== simeta::detection::coverart::constants::PNG_TYPE
{
simodels::types::CoverArtType::PngExtension
} else {
response.message = String::from("Invalid CoverArt type");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
};
let filename =
simodels::coverart::generate_filename(coverart_type, true).unwrap();
// TODO: Make this random and the file extension should not be hard coded let mut coverart = simodels::coverart::init::init_coverart_dir_and_filename(
let filename = format!("{}-coverart.jpeg", &song.filename[..8]); &directory, &filename,
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.title = song.album.clone();
coverart.file_type = file_type.file_type;
coverart.data = data; coverart.data = data;
let mut file = std::fs::File::create(&save_path).unwrap(); match coverart.save_to_filesystem() {
file.write_all(&coverart.data).unwrap(); Ok(_) => {
match repo::coverart::create(&pool, &coverart, &song.id).await {
match super::cov_db::create(&pool, &coverart, &song.id).await {
Ok(id) => { Ok(id) => {
// TODO: Populate song_id coverart.song_id = song_id;
coverart.id = id; coverart.id = id;
println!("Cover Art created"); println!("Cover Art created");
@@ -577,6 +119,15 @@ pub mod endpoint {
} }
} }
} }
Err(err) => {
response.message = err.to_string();
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
}
Err(err) => { Err(err) => {
response.message = err.to_string(); response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) (axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
@@ -590,35 +141,18 @@ pub mod endpoint {
} }
} }
pub async fn wipe_data_from_coverart_queue( /// Endpoint to get cover art with criteria
axum::Extension(pool): axum::Extension<sqlx::PgPool>, #[utoipa::path(
axum::Json(payload): axum::Json<super::request::wipe_data_from_coverart_queue::Request>, get,
) -> ( path = super::super::endpoints::GETCOVERART,
axum::http::StatusCode, params(
axum::Json<super::response::wipe_data_from_coverart_queue::Response>, ("id" = uuid::Uuid, Path, description = "Cover art Id")
) { ),
let mut response = super::response::wipe_data_from_coverart_queue::Response::default(); responses(
let coverart_queue_id = payload.coverart_queue_id; (status = 200, description = "Cover art retrieved", body = super::response::get_coverart::Response),
(status = 400, description = "Error retrieving cover art", body = super::response::get_coverart::Response)
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))
}
}
}
pub async fn get_coverart( pub async fn get_coverart(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<super::request::get_coverart::Params>, axum::extract::Query(params): axum::extract::Query<super::request::get_coverart::Params>,
@@ -629,7 +163,7 @@ pub mod endpoint {
let mut response = super::response::get_coverart::Response::default(); let mut response = super::response::get_coverart::Response::default();
match params.id { match params.id {
Some(id) => match super::cov_db::get_coverart(&pool, &id).await { Some(id) => match repo::coverart::get_coverart(&pool, &id).await {
Ok(coverart) => { Ok(coverart) => {
response.data.push(coverart); response.data.push(coverart);
response.message = String::from(super::super::response::SUCCESSFUL); response.message = String::from(super::super::response::SUCCESSFUL);
@@ -646,4 +180,82 @@ pub mod endpoint {
} }
} }
} }
/// Endpoint to download cover art
#[utoipa::path(
get,
path = super::super::endpoints::DOWNLOADCOVERART,
params(
("id" = uuid::Uuid, Path, description = "Cover art Id")
),
responses(
(status = 200, description = "Cover art downloading", body = Vec<u8>),
(status = 404, description = "Cover art not found", body = Vec<u8>)
)
)]
pub async fn download_coverart(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
) -> (axum::http::StatusCode, axum::response::Response) {
match repo::coverart::get_coverart(&pool, &id).await {
Ok(coverart) => match simodels::coverart::io::to_data(&coverart) {
Ok(data) => {
let (file_type, img_type) =
match simeta::detection::coverart::file_type_from_data(&data) {
Ok(file_type) => {
if file_type.file_type
== simeta::detection::coverart::constants::JPEG_TYPE
{
(file_type, simodels::types::CoverArtType::JpegExtension)
} else if file_type.file_type
== simeta::detection::coverart::constants::JPG_TYPE
{
(file_type, simodels::types::CoverArtType::JpgExtension)
} else if file_type.file_type
== simeta::detection::coverart::constants::PNG_TYPE
{
(file_type, simodels::types::CoverArtType::PngExtension)
} else {
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::response::Response::default(),
);
}
}
Err(err) => {
eprintln!("Error: {err:?}");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::response::Response::default(),
);
}
};
let bytes = axum::body::Bytes::from(data);
let mut response = bytes.into_response();
let headers = response.headers_mut();
let filename = simodels::coverart::generate_filename(img_type, true).unwrap();
headers.insert(
axum::http::header::CONTENT_TYPE,
file_type.mime.parse().unwrap(),
);
headers.insert(
axum::http::header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{filename}\"")
.parse()
.unwrap(),
);
(axum::http::StatusCode::OK, response)
}
Err(_err) => (
axum::http::StatusCode::NOT_FOUND,
axum::response::Response::default(),
),
},
Err(_err) => (
axum::http::StatusCode::NOT_FOUND,
axum::response::Response::default(),
),
}
}
} }
-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))
}
},
}
}
}
+11 -12
View File
@@ -1,27 +1,26 @@
pub mod coverart; pub mod coverart;
pub mod metadata; pub mod queue;
pub mod song; pub mod song;
pub mod endpoints { pub mod endpoints {
pub const QUEUESONG: &str = "/api/v2/song/queue"; pub const ROOT: &str = "/";
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 CREATESONG: &str = "/api/v2/song";
pub const GETSONGS: &str = "/api/v2/song"; pub const GETSONGS: &str = "/api/v2/song";
pub const GETALLSONGS: &str = "/api/v2/song/all";
pub const STREAMSONG: &str = "/api/v2/song/stream/{id}"; pub const STREAMSONG: &str = "/api/v2/song/stream/{id}";
pub const DOWNLOADSONG: &str = "/api/v2/song/download/{id}";
pub const DELETESONG: &str = "/api/v2/song/{id}";
pub const CREATECOVERART: &str = "/api/v2/coverart"; pub const CREATECOVERART: &str = "/api/v2/coverart";
pub const GETCOVERART: &str = "/api/v2/coverart"; pub const GETCOVERART: &str = "/api/v2/coverart";
pub const DOWNLOADCOVERART: &str = "/api/v2/coverart/download/{id}";
} }
pub mod response { pub mod response {
pub const SUCCESSFUL: &str = "SUCCESSFUL"; pub const SUCCESSFUL: &str = "SUCCESSFUL";
} }
/// Basic handler that responds with a static string
pub async fn root() -> &'static str {
"Hello, World!"
}
+405
View File
@@ -0,0 +1,405 @@
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct CoverArtQueue {
pub id: uuid::Uuid,
pub file_type: String,
pub song_queue_id: uuid::Uuid,
}
pub mod request {
pub mod queue {
#[derive(utoipa::ToSchema)]
pub struct Request {
/// Filename
pub file: String,
#[schema(rename = "type")]
/// File type. Should be a file and not a value
pub file_type: String,
/// Raw data of the cover art file
pub value: Vec<u8>,
}
}
pub mod link {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
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, utoipa::ToSchema)]
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, utoipa::ToSchema)]
pub struct Params {
pub id: Option<uuid::Uuid>,
pub song_queue_id: Option<uuid::Uuid>,
}
}
pub mod wipe_data_from_coverart_queue {
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub coverart_queue_id: uuid::Uuid,
}
}
}
pub mod response {
pub mod queue {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
pub mod link {
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Id {
pub coverart_id: uuid::Uuid,
pub song_queue_id: uuid::Uuid,
}
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<Id>,
}
}
pub mod fetch_coverart_no_data {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
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, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<Vec<u8>>,
}
}
pub mod wipe_data_from_coverart_queue {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
}
mod helper {
pub fn is_coverart_file_type_valid(file_type: &String) -> bool {
let valid_file_types = vec![
String::from(simeta::detection::coverart::constants::JPEG_TYPE),
String::from(simeta::detection::coverart::constants::JPG_TYPE),
String::from(simeta::detection::coverart::constants::PNG_TYPE),
];
for valid_file_type in valid_file_types {
if valid_file_type == *file_type {
return true;
}
}
false
}
}
pub mod endpoint {
use axum::response::IntoResponse;
use crate::repo::queue as repo;
/// Endpoint to queue cover art
#[utoipa::path(
post,
path = super::super::endpoints::QUEUECOVERART,
request_body(
content = super::request::queue::Request,
),
responses(
(status = 200, description = "Successful", body = super::response::queue::Response),
(status = 400, description = "Error queueing cover art", body = super::response::queue::Response)
)
)]
pub async fn queue(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
mut multipart: axum::extract::Multipart,
) -> (
axum::http::StatusCode,
axum::Json<super::response::queue::Response>,
) {
let mut response = super::response::queue::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();
let file_type = match simeta::detection::coverart::file_type_from_data(&raw_data) {
Ok(file_type) => file_type,
Err(err) => {
eprintln!("Error: {err:?}");
response.message = err.to_string();
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
};
if !super::helper::is_coverart_file_type_valid(&file_type.file_type) {
response.message = format!("CoverArt file type not supported: {file_type:?}");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
} else {
println!(
"Received file '{}' (name = '{}', content-type = '{}', size = {}, file-type = {:?})",
file_name,
name,
content_type,
data.len(),
file_type
);
match repo::coverart::insert(&pool, &raw_data, &file_type.file_type).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))
}
}
}
/// Endpoint to link queued cover art
#[utoipa::path(
post,
path = super::super::endpoints::QUEUECOVERARTLINK,
request_body(
content = super::request::link::Request,
description = "Linking queued cover art to queued song",
content_type = "application/json"
),
responses(
(status = 200, description = "Queued cover art linked", body = super::response::link::Response),
(status = 400, description = "Linkage failed", body = super::response::link::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 repo::coverart::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))
}
}
}
/// Endpoint to fetch cover art details
#[utoipa::path(
get,
path = super::super::endpoints::QUEUECOVERART,
params(
("id" = uuid::Uuid, Path, description = "Queued cover art Id"),
("song_queue_id" = uuid::Uuid, Path, description = "Queued song Id")
),
responses(
(status = 200, description = "Queued song linked", body = super::response::fetch_coverart_no_data::Response),
(status = 400, description = "Linkage failed", body = super::response::fetch_coverart_no_data::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 repo::coverart::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 repo::coverart::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))
}
},
}
}
/// Endpoint to fetch the queued cover art data
#[utoipa::path(
get,
path = super::super::endpoints::QUEUECOVERARTDATA,
params(("id" = uuid::Uuid, Path, description = "Queued cover art Id")),
responses(
(status = 200, description = "Queued cover art data", body = Vec<u8>),
(status = 400, description = "Error fetching queued cover art data", body = Vec<u8>)
)
)]
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 repo::coverart::get_coverart_queue_data_with_id(&pool, &id).await {
Ok(data) => {
let file_type = simeta::detection::coverart::file_type_from_data(&data).unwrap();
let bytes = axum::body::Bytes::from(data);
let mut response = bytes.into_response();
let headers = response.headers_mut();
headers.insert(
axum::http::header::CONTENT_TYPE,
file_type.mime.parse().unwrap(),
);
let coverart_type = if file_type.file_type
== simeta::detection::coverart::constants::JPEG_TYPE
{
simodels::types::CoverArtType::JpegExtension
} else if file_type.file_type == simeta::detection::coverart::constants::JPG_TYPE {
simodels::types::CoverArtType::JpgExtension
} else if file_type.file_type == simeta::detection::coverart::constants::PNG_TYPE {
simodels::types::CoverArtType::PngExtension
} else {
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::response::Response::default(),
);
};
let filename = simodels::coverart::generate_filename(coverart_type, true).unwrap();
headers.insert(
axum::http::header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{filename}\"")
.parse()
.unwrap(),
);
(axum::http::StatusCode::OK, response)
}
Err(_err) => (
axum::http::StatusCode::BAD_REQUEST,
axum::response::Response::default(),
),
}
}
/// Endpoint to wipe data from the cover art queue
#[utoipa::path(
patch,
path = super::super::endpoints::QUEUECOVERARTDATAWIPE,
request_body(
content = super::request::wipe_data_from_coverart_queue::Request,
description = "Data required to wipe the data from the cover art queue",
content_type = "application/json"
),
responses(
(status = 200, description = "Data wiped from cover art queue", body = super::response::wipe_data_from_coverart_queue::Response),
(status = 400, description = "Error wiping the data", body = super::response::wipe_data_from_coverart_queue::Response),
(status = 404, description = "Cover art not found", body = super::response::wipe_data_from_coverart_queue::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 repo::coverart::get_coverart_queue_with_id(&pool, &coverart_queue_id).await {
Ok(coverart_queue) => {
match repo::coverart::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))
}
}
}
}
+185
View File
@@ -0,0 +1,185 @@
pub mod request {
pub mod queue_metadata {
#[derive(
Debug, Default, serde::Deserialize, serde::Serialize, sqlx::FromRow, utoipa::ToSchema,
)]
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,
utoipa::ToSchema,
)]
pub struct Params {
pub id: Option<uuid::Uuid>,
pub song_queue_id: Option<uuid::Uuid>,
}
}
}
pub mod response {
pub mod queue_metadata {
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
pub mod fetch_metadata {
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<crate::repo::queue::metadata::MetadataQueue>,
}
}
}
/// Module for metadata related endpoints
pub mod endpoint {
use crate::repo::queue as repo_queue;
/// Endpoint to create queued metadata
#[utoipa::path(
post,
path = super::super::endpoints::QUEUEMETADATA,
request_body(
content = super::request::queue_metadata::Request,
description = "Data required to create queued metadata",
content_type = "application/json"
),
responses(
(status = 200, description = "Queued metadata created", body = super::response::queue_metadata::Response),
(status = 400, description = "Error creating queued metadata", body = super::response::queue_metadata::Response)
)
)]
pub async fn queue_metadata(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::queue_metadata::Request>,
) -> (
axum::http::StatusCode,
axum::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 repo_queue::metadata::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(super::super::super::response::SUCCESSFUL)
};
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
/// Endpoint to get queued metadata
#[utoipa::path(
get,
path = super::super::endpoints::QUEUEMETADATA,
params(
("id" = uuid::Uuid, Path, description = "Id of queued metadata"),
("song_queue_id" = uuid::Uuid, Path, description = "Id of queued song")
),
responses(
(status = 200, description = "Queued metadata retrieved", body = super::response::fetch_metadata::Response),
(status = 400, description = "Error retrieving queued metadata", body = super::response::fetch_metadata::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>,
) -> (
axum::http::StatusCode,
axum::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 repo_queue::metadata::get_with_id(&pool, &id).await {
Ok(item) => {
response.message = String::from("Successful");
response.data.push(item);
(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) => {
println!("Song queue Id is probably not nil");
match repo_queue::metadata::get_with_song_queue_id(&pool, &song_queue_id).await
{
Ok(item) => {
response.message = String::from("Successful");
response.data.push(item);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
None => {
println!("What is going on?");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
},
}
}
}
+17
View File
@@ -0,0 +1,17 @@
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";
}
+517
View File
@@ -0,0 +1,517 @@
pub mod request {
pub mod song_queue {
#[derive(utoipa::ToSchema)]
pub struct SongQueueRequest {
/// Filename
pub file: String,
#[schema(rename = "type")]
/// File type. Should be a file and not a value
pub file_type: String,
/// Raw data of the flac file
pub value: Vec<u8>,
}
}
pub mod update_status {
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub id: uuid::Uuid,
pub status: String,
}
}
pub mod wipe_data_from_song_queue {
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub song_queue_id: uuid::Uuid,
}
}
pub mod link_user_id {
#[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request {
pub song_queue_id: uuid::Uuid,
pub user_id: uuid::Uuid,
}
}
}
pub mod response {
pub mod song_queue {
/// Song queue response
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
/// Id of the queued song
pub data: Vec<uuid::Uuid>,
}
}
pub mod fetch_queue_song {
use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<crate::repo::queue::song::dbtype::SongQueue>,
}
}
pub mod update_status {
#[derive(serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct ChangedStatus {
pub old_status: String,
pub new_status: String,
}
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<ChangedStatus>,
}
}
pub mod update_song_queue {
#[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
pub mod wipe_data_from_song_queue {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
pub mod link_user_id {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
}
}
pub async fn is_song_valid(data: &[u8]) -> Result<bool, std::io::Error> {
match simeta::detection::song::file_type_from_data(data) {
Ok(file_type) => {
if file_type.file_type == simeta::detection::song::constants::FLAC_TYPE {
Ok(true)
} else {
Ok(false)
}
}
Err(err) => Err(err),
}
}
pub mod endpoint {
use axum::response::IntoResponse;
use crate::repo::queue as repo;
/// Endpoint to queue a song. Starts the process and places the song in a queue
#[utoipa::path(
post,
path = super::super::endpoints::QUEUESONG,
request_body(
content = super::request::song_queue::SongQueueRequest,
description = "Multipart form data for uploading song",
content_type = "multipart/form-data"
),
responses(
(status = 201, description = "Song queued", body = super::response::song_queue::Response),
(status = 400, description = "Invalid request passed", body = super::response::song_queue::Response),
(status = 500, description = "Error queueing song", body = super::response::song_queue::Response)
)
)]
pub async fn queue_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
mut multipart: axum::extract::Multipart,
) -> (
axum::http::StatusCode,
axum::Json<super::response::song_queue::Response>,
) {
let mut results: Vec<uuid::Uuid> = Vec::new();
let mut response = super::response::song_queue::Response::default();
match multipart.next_field().await {
Ok(multipart_field) => {
if let Some(field) = multipart_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();
println!(
"Received file '{}' (name = '{}', content-type = '{}', size = {})",
file_name,
name,
content_type,
data.len()
);
let raw_data: Vec<u8> = data.to_vec();
match super::is_song_valid(&raw_data).await {
Ok(valid) => {
if valid {
match repo::song::insert(
&pool,
&raw_data,
&file_name,
&crate::repo::queue::song::status::PENDING.to_string(),
)
.await
{
Ok(queued_song) => {
results.push(queued_song);
}
Err(err) => {
response.message = err.to_string();
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
}
} else {
response.message = String::from("Invalid song type");
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response));
}
}
Err(err) => {
response.message = err.to_string();
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
}
response.data = results;
response.message = if response.data.is_empty() {
String::from("Error")
} else {
String::from(super::super::super::response::SUCCESSFUL)
};
(axum::http::StatusCode::CREATED, axum::Json(response))
} else {
response.message = String::from("No field found in multipart");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
/// Endpoint to link a user id to a queued song
#[utoipa::path(
patch,
path = super::super::endpoints::QUEUESONGLINKUSERID,
request_body(
content = super::request::link_user_id::Request,
description = "User Id and queued song id",
content_type = "application/json"
),
responses(
(status = 200, description = "Queued song linked", body = super::response::link_user_id::Response),
(status = 400, description = "Linkage failed", body = super::response::link_user_id::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 repo::song::get_song_queue(&pool, &payload.song_queue_id).await {
Ok(song_queue) => {
match repo::song::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))
}
}
}
/// Endpoint to fetch the next queued song as long as it is available
#[utoipa::path(
get,
path = super::super::endpoints::NEXTQUEUESONG,
responses(
(status = 200, description = "Queued song is present and available", body = super::response::fetch_queue_song::Response),
(status = 400, description = "Linkage failed", body = super::response::fetch_queue_song::Response)
)
)]
pub async fn fetch_queue_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::fetch_queue_song::Response>,
) {
let mut response = super::response::fetch_queue_song::Response::default();
match repo::song::get_most_recent_and_update(&pool).await {
Ok(item) => {
response.message = String::from("Successful");
response.data.push(item);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
/// Endpoint to download the queued song
#[utoipa::path(
get,
path = super::super::endpoints::QUEUESONGDATA,
params(("id" = uuid::Uuid, Path, description = "Queued song Id")),
responses(
(status = 200, description = "Queued song linked", body = Vec<u8>),
(status = 400, description = "Linkage failed", body = Vec<u8>)
)
)]
pub async fn download_queued_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
) -> (axum::http::StatusCode, axum::response::Response) {
println!("Id: {id}");
match repo::song::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(),
);
(axum::http::StatusCode::OK, response)
}
Err(_err) => (
axum::http::StatusCode::BAD_REQUEST,
axum::response::Response::default(),
),
}
}
/// Endpoint to update the status of a queued song
#[utoipa::path(
patch,
path = super::super::endpoints::QUEUESONG,
request_body(
content = super::request::update_status::Request,
description = "Update the status of a queued song",
content_type = "application/json"
),
responses(
(status = 200, description = "Status has been updated", body = super::response::update_status::Response),
(status = 400, description = "Error updating status of queued song", body = super::response::update_status::Response)
)
)]
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 repo::song::status::is_valid(&payload.status).await {
let id = payload.id;
if !id.is_nil() {
match repo::song::get_status_of_song_queue(&pool, &id).await {
Ok(old) => {
match repo::song::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))
}
}
/// Endpoint to update the queued song data
#[utoipa::path(
patch,
path = super::super::endpoints::QUEUESONGUPDATE,
request_body(
content = super::request::song_queue::SongQueueRequest,
description = "Multipart form data for uploading song",
content_type = "multipart/form-data"
),
params(("id" = uuid::Uuid, Path, description = "Queued song Id")),
responses(
(status = 200, description = "Queued song updated", body = super::response::update_song_queue::Response),
(status = 400, description = "Error updating queued song", body = super::response::update_song_queue::Response),
(status = 404, description = "Queued song not found", body = super::response::update_song_queue::Response),
(status = 500, description = "Error updating queued song", body = super::response::update_song_queue::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 super::is_song_valid(&raw_data).await {
Ok(valid) => {
if valid {
match repo::song::update(&pool, &raw_data, &id).await {
Ok(_) => {
response.message =
String::from(super::super::super::response::SUCCESSFUL);
response.data.push(id);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
} else {
response.message = String::from("Invalid song type");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
Err(err) => {
response.message = err.to_string();
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
} else {
response.message = String::from("No data provided");
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
}
}
/// Endpoint to wipe the data from a queued song
#[utoipa::path(
patch,
path = super::super::endpoints::QUEUESONGDATAWIPE,
request_body(
content = super::request::wipe_data_from_song_queue::Request,
description = "Pass the queued song Id to wipe the data",
content_type = "application/json"
),
responses(
(status = 200, description = "Queued song data wiped", body = super::response::wipe_data_from_song_queue::Response),
(status = 404, description = "Queued song cannot be found", body = super::response::wipe_data_from_song_queue::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 repo::song::get_song_queue(&pool, &id).await {
Ok(song_queue) => match repo::song::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))
}
}
}
}
+260 -847
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
/// Full hosting address that the application will be broadcasting
pub fn get_full() -> String {
format!("{ADDRESS}:{PORT}")
}
pub const ADDRESS: &str = "0.0.0.0";
pub const PORT: &str = "8000";
+254
View File
@@ -0,0 +1,254 @@
use std::time::Duration;
use axum::routing::{delete, get, patch, post};
use tower_http::timeout::TimeoutLayer;
use utoipa::OpenApi;
use crate::callers::coverart as coverart_caller;
use crate::callers::queue::coverart as coverart_queue_callers;
use crate::callers::queue::metadata as metadata_queue_caller;
use crate::callers::queue::song as song_queue_callers;
use crate::callers::song as song_caller;
use coverart_caller::endpoint as coverart_endpoints;
use coverart_caller::response as coverart_responses;
use coverart_queue_callers::endpoint as coverart_queue_endpoints;
use coverart_queue_callers::response as coverart_queue_responses;
use metadata_queue_caller::endpoint as metadata_queue_endpoints;
use metadata_queue_caller::response as metadata_queue_responses;
use song_caller::endpoint as song_endpoints;
use song_caller::response as song_responses;
use song_queue_callers::endpoint as song_queue_endpoints;
use song_queue_callers::response as song_queue_responses;
mod cors {
pub async fn configure_cors() -> tower_http::cors::CorsLayer {
let cors = tower_http::cors::CorsLayer::new()
.allow_methods([
axum::http::Method::GET,
axum::http::Method::POST,
axum::http::Method::PUT,
axum::http::Method::DELETE,
])
.allow_headers([
axum::http::header::CONTENT_TYPE,
axum::http::header::AUTHORIZATION,
])
.allow_credentials(true)
.max_age(std::time::Duration::from_secs(3600)); // Cache the preflight response for 1 hour:cite[2]
match std::env::var(sienvy::keys::APP_ENV).as_deref() {
Ok("production") => {
let allowed_origins_env = sienvy::environment::get_allowed_origins();
match sienvy::utility::delimitize(&allowed_origins_env) {
Ok(alwd) => {
let allowed_origins: Vec<axum::http::HeaderValue> = alwd
.into_iter()
.map(|a| a.parse::<axum::http::HeaderValue>().unwrap())
.collect();
cors.allow_origin(allowed_origins)
}
Err(err) => {
eprintln!("Error getting allowed origins: Error: {err:?}");
std::process::exit(-1);
}
}
}
_ => cors.allow_origin(vec![
"http://localhost:8000".parse().unwrap(),
"http://127.0.0.1:8000".parse().unwrap(),
"http://localhost:8001".parse().unwrap(),
"http://127.0.0.1:8001".parse().unwrap(),
"http://localhost:4200".parse().unwrap(),
"http://127.0.0.1:4200".parse().unwrap(),
]),
}
}
}
#[derive(utoipa::OpenApi)]
#[openapi(
paths(song_queue_endpoints::queue_song, song_queue_endpoints::link_user_id, song_queue_endpoints::fetch_queue_song, song_queue_endpoints::download_queued_song,
song_queue_endpoints::update_song_queue_status, song_queue_endpoints::update_song_queue, song_endpoints::create_metadata, song_queue_endpoints::wipe_data_from_song_queue, song_endpoints::get_songs, song_endpoints::get_all_songs, song_endpoints::stream_song, song_endpoints::download_song,
song_endpoints::delete_song, coverart_queue_endpoints::queue, coverart_queue_endpoints::link, coverart_queue_endpoints::fetch_coverart_no_data,
coverart_queue_endpoints::fetch_coverart_with_data, coverart_endpoints::create_coverart, coverart_queue_endpoints::wipe_data_from_coverart_queue,
coverart_endpoints::get_coverart, coverart_endpoints::download_coverart,
metadata_queue_endpoints::queue_metadata, metadata_queue_endpoints::fetch_metadata),
components(schemas(song_queue_callers::response::song_queue::Response, song_queue_callers::response::link_user_id::Response, song_queue_callers::response::fetch_queue_song::Response,
song_queue_responses::update_status::Response, song_queue_callers::response::update_song_queue::Response, song_responses::create_metadata::Response,
song_queue_callers::response::wipe_data_from_song_queue::Response, song_responses::get_songs::Response, song_responses::delete_song::Response,
coverart_queue_responses::queue::Response, coverart_queue_responses::link::Response, coverart_queue_responses::fetch_coverart_no_data::Response,
coverart_queue_responses::fetch_coverart_with_data::Response, coverart_responses::create_coverart::Response,
coverart_queue_responses::wipe_data_from_coverart_queue::Response, coverart_responses::get_coverart::Response,
metadata_queue_responses::queue_metadata::Response, metadata_queue_responses::fetch_metadata::Response)),
tags(
(name = "soaricarus API", description = "Web API to manage music")
)
)]
struct ApiDoc;
pub async fn routes() -> axum::Router {
axum::Router::new()
.route(crate::callers::endpoints::ROOT, get(crate::callers::root))
.route(
crate::callers::queue::endpoints::QUEUESONG,
post(crate::callers::queue::song::endpoint::queue_song).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUESONG,
patch(crate::callers::queue::song::endpoint::update_song_queue_status).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUESONGLINKUSERID,
patch(crate::callers::queue::song::endpoint::link_user_id).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUESONGDATA,
get(crate::callers::queue::song::endpoint::download_queued_song).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::NEXTQUEUESONG,
get(crate::callers::queue::song::endpoint::fetch_queue_song).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUESONGUPDATE,
patch(crate::callers::queue::song::endpoint::update_song_queue).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUESONGDATAWIPE,
patch(crate::callers::queue::song::endpoint::wipe_data_from_song_queue).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUEMETADATA,
post(crate::callers::queue::metadata::endpoint::queue_metadata).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUEMETADATA,
get(crate::callers::queue::metadata::endpoint::fetch_metadata).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUECOVERART,
post(crate::callers::queue::coverart::endpoint::queue).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUECOVERARTDATA,
get(crate::callers::queue::coverart::endpoint::fetch_coverart_with_data).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUECOVERART,
get(crate::callers::queue::coverart::endpoint::fetch_coverart_no_data).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUECOVERARTLINK,
patch(crate::callers::queue::coverart::endpoint::link).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::queue::endpoints::QUEUECOVERARTDATAWIPE,
patch(crate::callers::queue::coverart::endpoint::wipe_data_from_coverart_queue)
.route_layer(axum::middleware::from_fn(
crate::auth::auth::<axum::body::Body>,
)),
)
.route(
crate::callers::endpoints::CREATESONG,
post(crate::callers::song::endpoint::create_metadata).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::endpoints::CREATECOVERART,
post(crate::callers::coverart::endpoint::create_coverart).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::endpoints::GETSONGS,
get(crate::callers::song::endpoint::get_songs).route_layer(axum::middleware::from_fn(
crate::auth::auth::<axum::body::Body>,
)),
)
.route(
crate::callers::endpoints::GETCOVERART,
get(crate::callers::coverart::endpoint::get_coverart).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::endpoints::DOWNLOADCOVERART,
get(crate::callers::coverart::endpoint::download_coverart).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::endpoints::STREAMSONG,
get(crate::callers::song::endpoint::stream_song).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::endpoints::DOWNLOADSONG,
get(crate::callers::song::endpoint::download_song).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::endpoints::DELETESONG,
delete(crate::callers::song::endpoint::delete_song).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::callers::endpoints::GETALLSONGS,
get(crate::callers::song::endpoint::get_all_songs),
)
.layer(cors::configure_cors().await)
}
pub async fn app() -> axum::Router {
let pool = crate::db::create_pool()
.await
.expect("Failed to create pool");
crate::db::migrations(&pool).await;
let cors = cors::configure_cors().await;
routes()
.await
.merge(
utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
.url("/api-docs/openapi.json", ApiDoc::openapi()),
)
.layer(axum::Extension(pool))
.layer(axum::extract::DefaultBodyLimit::max(1024 * 1024 * 1024))
.layer(TimeoutLayer::with_status_code(
axum::http::StatusCode::OK,
Duration::from_secs(300),
))
.layer(cors)
}
+2
View File
@@ -0,0 +1,2 @@
pub mod host;
pub mod init;
+22
View File
@@ -0,0 +1,22 @@
use sqlx::postgres::PgPoolOptions;
pub mod connection_settings {
pub const MAXCONN: u32 = 10;
}
pub async fn create_pool() -> Result<sqlx::PgPool, sqlx::Error> {
let database_url = sienvy::environment::get_db_url().value;
println!("Database url: {database_url}");
PgPoolOptions::new()
.max_connections(connection_settings::MAXCONN)
.connect(&database_url)
.await
}
pub async fn migrations(pool: &sqlx::PgPool) {
sqlx::migrate!("./migrations")
.run(pool)
.await
.expect("Failed to run migrations");
}
+793 -374
View File
File diff suppressed because it is too large Load Diff
+180
View File
@@ -0,0 +1,180 @@
use sqlx::Row;
pub async fn create(
pool: &sqlx::PgPool,
coverart: &simodels::coverart::CoverArt,
song_id: &uuid::Uuid,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO "coverart" (title, directory, filename, file_type, song_id) VALUES($1, $2, $3, $4, $5) RETURNING id;
"#,
)
.bind(&coverart.title)
.bind(&coverart.directory)
.bind(&coverart.filename)
.bind(&coverart.file_type)
.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 async fn get_coverart(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<simodels::coverart::CoverArt, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, title, directory, filename, file_type, song_id FROM "coverart" WHERE id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(row) => Ok(simodels::coverart::CoverArt {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
title: row
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
directory: row
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
file_type: row
.try_get("file_type")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_id: row
.try_get("song_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
..Default::default()
}),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_coverart_with_song_id(
pool: &sqlx::PgPool,
song_id: &uuid::Uuid,
) -> Result<simodels::coverart::CoverArt, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, title, directory, filename, file_type, song_id FROM "coverart" WHERE song_id = $1;
"#,
)
.bind(song_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(row) => Ok(simodels::coverart::CoverArt {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
title: row
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
directory: row
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
file_type: row
.try_get("file_type")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
song_id: row
.try_get("song_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
}),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn delete_coverart(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<simodels::coverart::CoverArt, sqlx::Error> {
let result = sqlx::query(
r#"
DELETE FROM "coverart"
WHERE id = $1
RETURNING id, title, directory, filename, file_type, song_id
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error deleting data: {e:?}");
});
match result {
Ok(row) => Ok(simodels::coverart::CoverArt {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
title: row
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
directory: row
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
file_type: row
.try_get("file_type")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
song_id: row
.try_get("song_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
}),
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod coverart;
pub mod queue;
pub mod song;
+197
View File
@@ -0,0 +1,197 @@
use sqlx::Row;
pub async fn insert(
pool: &sqlx::PgPool,
data: &Vec<u8>,
file_type: &str,
) -> Result<uuid::Uuid, sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO "coverartQueue" (data, file_type) VALUES($1, $2) RETURNING id;
"#,
)
.bind(data)
.bind(file_type)
.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<crate::callers::queue::coverart::CoverArtQueue, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, file_type, 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(crate::callers::queue::coverart::CoverArtQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
file_type: row
.try_get("file_type")
.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<crate::callers::queue::coverart::CoverArtQueue, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, file_type, 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(crate::callers::queue::coverart::CoverArtQueue {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
file_type: row
.try_get("file_type")
.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),
}
}
+126
View File
@@ -0,0 +1,126 @@
use sqlx::Row;
#[derive(Debug, serde::Serialize, sqlx::FromRow)]
pub struct InsertedData {
pub id: uuid::Uuid,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow, utoipa::ToSchema)]
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),
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod coverart;
pub mod metadata;
pub mod song;
+303
View File
@@ -0,0 +1,303 @@
use sqlx::Row;
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 dbtype {
/// Maps to the songQueue table. Does not contain the data field
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow, utoipa::ToSchema)]
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<dbtype::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(status::PROCESSING)
.bind(status::READY)
.fetch_one(pool)
.await;
match result {
Ok(row) => {
let user_id_result = row.try_get("user_id");
let song_queue = dbtype::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<dbtype::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 = dbtype::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),
}
}
+345
View File
@@ -0,0 +1,345 @@
use sqlx::Row;
pub async fn insert(
pool: &sqlx::PgPool,
song: &simodels::song::Song,
) -> Result<(time::OffsetDateTime, 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;
Ok((date_created, id))
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_song(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<simodels::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(simodels::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: Some(date_created_time),
user_id: row
.try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
})
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_all_songs(pool: &sqlx::PgPool) -> Result<Vec<simodels::song::Song>, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT * FROM "song";
"#,
)
.fetch_all(pool)
.await
.map_err(|e| {
eprintln!("Error querying data: {e:?}");
});
match result {
Ok(rows) => {
let mut songs: Vec<simodels::song::Song> = Vec::new();
for row in rows {
let date_created_time: time::OffsetDateTime = row
.try_get("date_created")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap();
let song = simodels::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: Some(date_created_time),
user_id: row
.try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
};
songs.push(song);
}
Ok(songs)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn delete_song(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<simodels::song::Song, sqlx::Error> {
let result = sqlx::query(
// simodels::song::Song,
r#"
DELETE FROM "song"
WHERE id = $1
RETURNING id, title, artist, album, album_artist, genre, year, disc, track, track_count, disc_count, duration, audio_type, date_created, filename, directory, user_id
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error deleting 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(simodels::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: Some(date_created_time),
user_id: row
.try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
})
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
+4 -1
View File
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS "metadataQueue" (
CREATE TABLE IF NOT EXISTS "coverartQueue" ( CREATE TABLE IF NOT EXISTS "coverartQueue" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
data BYTEA NULL, data BYTEA NULL,
file_type TEXT NOT NULL,
song_queue_id UUID NULL song_queue_id UUID NULL
); );
@@ -53,6 +54,8 @@ CREATE TABLE IF NOT EXISTS "song" (
CREATE TABLE IF NOT EXISTS "coverart" ( CREATE TABLE IF NOT EXISTS "coverart" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL, title TEXT NOT NULL,
path TEXT NOT NULL, directory TEXT NOT NULL,
filename TEXT NOT NULL,
file_type TEXT NOT NULL,
song_id UUID NOT NULL song_id UUID NOT NULL
); );
@@ -1,3 +1,6 @@
-- Add migration script here -- Add migration script here
INSERT INTO "song" (id, title, artist, album_artist, album, genre, year, track, disc, track_count, disc_count, duration, audio_type, date_created, filename, directory, user_id) VALUES('44cf7940-34ff-489f-9124-d0ec90a55af9', 'Hypocrite Like The Rest', 'Kuoth', 'Kuoth', 'I', 'Alternative Hip-Hop', 2020, 1, 1, 9, 1, 139, 'flac', '2020-01-01 13:00:00-05', 'track01.flac', 'tests/I', '47491f9b-725a-4ba4-b9a5-711e1be46670'); INSERT INTO "song" (id, title, artist, album_artist, album, genre, year, track, disc, track_count, disc_count, duration, audio_type, date_created, filename, directory, user_id) VALUES('44cf7940-34ff-489f-9124-d0ec90a55af9', 'Hypocrite Like The Rest', 'Kuoth', 'Kuoth', 'I', 'Alternative Hip-Hop', 2020, 1, 1, 9, 1, 139, 'flac', '2020-01-01 13:00:00-05', 'track01.flac', 'tests/I', '47491f9b-725a-4ba4-b9a5-711e1be46670');
INSERT INTO "coverart" VALUES('996122cd-5ae9-4013-9934-60768d3006ed', 'I', 'tests/I/Coverart.jpg', '44cf7940-34ff-489f-9124-d0ec90a55af9'); INSERT INTO "coverart" (id, title, directory, filename, file_type, song_id) VALUES('996122cd-5ae9-4013-9934-60768d3006ed', 'I', 'tests/I', 'Coverart-1.jpg', 'jpeg', '44cf7940-34ff-489f-9124-d0ec90a55af9');
INSERT INTO "song" (id, title, artist, album_artist, album, genre, year, track, disc, track_count, disc_count, duration, audio_type, date_created, filename, directory, user_id) VALUES('94cf7940-34ff-489f-9124-d0ec90a55af4', 'It''s Too Late', 'Kuoth', 'Kuoth', 'I', 'Alternative Hip-Hop', 2020, 2, 1, 9, 1, 116, 'flac', '2020-01-01 13:01:00-05', 'track02.flac', 'tests/I', '47491f9b-725a-4ba4-b9a5-711e1be46670');
INSERT INTO "coverart" (id, title, directory, filename, file_type, song_id) VALUES('d96122cd-5ae9-4013-9934-60768d3006e9', 'I', 'tests/I', 'Coverart-2.jpg', 'jpeg', '94cf7940-34ff-489f-9124-d0ec90a55af4');

Before

Width:  |  Height:  |  Size: 6.7 MiB

After

Width:  |  Height:  |  Size: 6.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 MiB