Compare commits

...

13 Commits

Author SHA1 Message Date
KD 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
KD 1ae2f88beb tsk-171: Updated Readme (#181) 2025-08-29 22:14:03 -04:00
KD 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
KD 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
KD 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
KD 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
KD 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
KD 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
KD 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
KD 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
KD e98e055de0 Stream song (#164)
* Added crates and dependency features

* Added endpoint to stream songs

* Changed name of function

* Song stream endpoint is now available

* Endpoint is now working

* Added test

* Warning fixes

* Code formatting

* Added TODO for later

* Test fix

* Version bump
2025-07-27 14:20:42 -04:00
15 changed files with 2124 additions and 113 deletions
+3 -1
View File
@@ -68,7 +68,9 @@ jobs:
env: env:
ROOT_DIRECTORY: "/tmp" ROOT_DIRECTORY: "/tmp"
DATABASE_URL: "postgres://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}" DATABASE_URL: "postgres://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}"
run: cargo test --verbose run: |
cat .env.sample | head -1 > .env
cargo test --verbose --
- name: Run clippy - name: Run clippy
run: cargo clippy -- -D warnings run: cargo clippy -- -D warnings
Generated
+378 -9
View File
@@ -17,12 +17,36 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "allocator-api2" name = "allocator-api2"
version = "0.2.21" version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]] [[package]]
name = "atoi" name = "atoi"
version = "2.0.0" version = "2.0.0"
@@ -93,6 +117,29 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "axum-extra"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d"
dependencies = [
"axum",
"axum-core",
"bytes",
"cookie",
"futures-util",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"serde",
"tower",
"tower-layer",
"tower-service",
]
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.75" version = "0.3.75"
@@ -108,6 +155,12 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@@ -222,6 +275,17 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@@ -323,6 +387,17 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@@ -421,6 +496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"libz-rs-sys",
"miniz_oxide", "miniz_oxide",
] ]
@@ -588,8 +664,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi 0.11.1+wasi-snapshot-preview1", "wasi 0.11.1+wasi-snapshot-preview1",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -630,6 +708,16 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "hdrhistogram"
version = "7.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d"
dependencies = [
"byteorder",
"num-traits",
]
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@@ -752,18 +840,24 @@ dependencies = [
[[package]] [[package]]
name = "icarus" name = "icarus"
version = "0.1.95" version = "0.2.0"
dependencies = [ dependencies = [
"axum", "axum",
"axum-extra",
"base64 0.21.7",
"common-multipart-rfc7578", "common-multipart-rfc7578",
"futures", "futures",
"icarus_envy", "icarus_envy",
"icarus_meta", "icarus_meta",
"icarus_models", "icarus_models",
"josekit",
"jsonwebtoken",
"mime_guess",
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
"tempfile", "tempfile",
"thiserror 1.0.69",
"time", "time",
"tokio", "tokio",
"tokio-util", "tokio-util",
@@ -771,13 +865,15 @@ dependencies = [
"tower-http", "tower-http",
"tracing-subscriber", "tracing-subscriber",
"url", "url",
"utoipa",
"utoipa-swagger-ui",
"uuid", "uuid",
] ]
[[package]] [[package]]
name = "icarus_envy" name = "icarus_envy"
version = "0.3.0" version = "0.3.2"
source = "git+ssh://git@git.kundeng.us/phoenix/icarus_envy.git?tag=v0.3.0-devel-d73fba9899-006#d73fba9899372b0655a90cb426645930135152da" source = "git+ssh://git@git.kundeng.us/phoenix/icarus_envy.git?tag=v0.3.2#d84a8144aedf02e1b459d67c4023a7e0833f89fd"
dependencies = [ dependencies = [
"const_format", "const_format",
"dotenvy", "dotenvy",
@@ -786,20 +882,22 @@ dependencies = [
[[package]] [[package]]
name = "icarus_meta" name = "icarus_meta"
version = "0.3.0" version = "0.3.0"
source = "git+ssh://git@git.kundeng.us/phoenix/icarus_meta.git?tag=v0.3.0-devel-f4b71de969-680#f4b71de9692029a706b1ce82c39f6715c560158e" source = "git+ssh://git@git.kundeng.us/phoenix/icarus_meta.git?tag=v0.3.0#f87c049ab3139995230485b33038a0425f2e7430"
dependencies = [ dependencies = [
"lofty", "lofty",
] ]
[[package]] [[package]]
name = "icarus_models" name = "icarus_models"
version = "0.5.1" version = "0.5.6"
source = "git+ssh://git@git.kundeng.us/phoenix/icarus_models.git?tag=v0.5.1-devel-1c5de9dc26-111#1c5de9dc26099ac5f21e1dc7a1f7d0dbb892b34b" source = "git+ssh://git@git.kundeng.us/phoenix/icarus_models.git?tag=v0.5.6#2d6b550ae6721b41ecc3039799f6a5e873869077"
dependencies = [ dependencies = [
"josekit",
"rand 0.9.1", "rand 0.9.1",
"serde", "serde",
"serde_json", "serde_json",
"time", "time",
"utoipa",
"uuid", "uuid",
] ]
@@ -918,6 +1016,7 @@ checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
"serde",
] ]
[[package]] [[package]]
@@ -926,6 +1025,23 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "josekit"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a808e078330e6af222eb0044b71d4b1ff981bfef43e7bc8133a88234e0c86a0c"
dependencies = [
"anyhow",
"base64 0.22.1",
"flate2",
"openssl",
"regex",
"serde",
"serde_json",
"thiserror 2.0.12",
"time",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.77" version = "0.3.77"
@@ -936,6 +1052,21 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "jsonwebtoken"
version = "9.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
dependencies = [
"base64 0.22.1",
"js-sys",
"pem",
"ring",
"serde",
"serde_json",
"simple_asn1",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -967,6 +1098,15 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "libz-rs-sys"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
dependencies = [
"zlib-rs",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.9.4" version = "0.9.4"
@@ -1123,6 +1263,16 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-bigint-dig" name = "num-bigint-dig"
version = "0.8.4" version = "0.8.4"
@@ -1285,6 +1435,16 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pem"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
dependencies = [
"base64 0.22.1",
"serde",
]
[[package]] [[package]]
name = "pem-rfc7468" name = "pem-rfc7468"
version = "0.7.0" version = "0.7.0"
@@ -1455,6 +1615,49 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.16",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.8" version = "0.9.8"
@@ -1475,6 +1678,40 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rust-embed"
version = "8.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "8.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594"
dependencies = [
"sha2",
"walkdir",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.25" version = "0.1.25"
@@ -1506,6 +1743,15 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "schannel" name = "schannel"
version = "0.1.27" version = "0.1.27"
@@ -1570,6 +1816,7 @@ version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [ dependencies = [
"indexmap",
"itoa", "itoa",
"memchr", "memchr",
"ryu", "ryu",
@@ -1654,6 +1901,24 @@ dependencies = [
"rand_core 0.6.4", "rand_core 0.6.4",
] ]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simple_asn1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [
"num-bigint",
"num-traits",
"thiserror 2.0.12",
"time",
]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.10" version = "0.4.10"
@@ -1717,7 +1982,7 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [ dependencies = [
"base64", "base64 0.22.1",
"bytes", "bytes",
"crc", "crc",
"crossbeam-queue", "crossbeam-queue",
@@ -1793,7 +2058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64", "base64 0.22.1",
"bitflags", "bitflags",
"byteorder", "byteorder",
"bytes", "bytes",
@@ -1837,7 +2102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64", "base64 0.22.1",
"bitflags", "bitflags",
"byteorder", "byteorder",
"crc", "crc",
@@ -2125,9 +2390,13 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-util", "futures-util",
"hdrhistogram",
"indexmap",
"pin-project-lite", "pin-project-lite",
"slab",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-util",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -2264,6 +2533,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.4" version = "2.5.4"
@@ -2281,6 +2556,49 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utoipa"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993"
dependencies = [
"indexmap",
"serde",
"serde_json",
"utoipa-gen",
]
[[package]]
name = "utoipa-gen"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn",
"uuid",
]
[[package]]
name = "utoipa-swagger-ui"
version = "9.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55"
dependencies = [
"axum",
"base64 0.22.1",
"mime_guess",
"regex",
"rust-embed",
"serde",
"serde_json",
"url",
"utoipa",
"zip",
]
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.17.0" version = "1.17.0"
@@ -2311,6 +2629,16 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.1+wasi-snapshot-preview1" version = "0.11.1+wasi-snapshot-preview1"
@@ -2416,6 +2744,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
@@ -2761,3 +3098,35 @@ dependencies = [
"quote", "quote",
"syn", "syn",
] ]
[[package]]
name = "zip"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308"
dependencies = [
"arbitrary",
"crc32fast",
"flate2",
"indexmap",
"memchr",
"zopfli",
]
[[package]]
name = "zlib-rs"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
[[package]]
name = "zopfli"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
]
+15 -7
View File
@@ -1,25 +1,33 @@
[package] [package]
name = "icarus" name = "icarus"
version = "0.1.95" version = "0.2.0"
edition = "2024" edition = "2024"
rust-version = "1.88" rust-version = "1.88"
[dependencies] [dependencies]
axum = { version = "0.8.4", features = ["multipart"] } axum = { version = "0.8.4", features = ["multipart"] }
axum-extra = { version = "0.10.1", features = ["cookie"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = { version = "1.0.140" } serde_json = { version = "1.0.140" }
tower = { version = "0.5.2" } tower = { version = "0.5.2", features = ["full"] }
tokio = { version = "1.45.1", features = ["full"] } tokio = { version = "1.45.1", features = ["full"] }
tokio-util = { version = "0.7.15" } tokio-util = { version = "0.7.15", features = ["io"] }
tower-http = { version = "0.6.6", features = ["timeout"] } tower-http = { version = "0.6.6", features = ["cors", "timeout"] }
tracing-subscriber = "0.3.19" tracing-subscriber = "0.3.19"
futures = { version = "0.3.31" } futures = { version = "0.3.31" }
mime_guess = { version = "2.0.5" }
uuid = { version = "1.17.0", features = ["v4", "serde"] } uuid = { version = "1.17.0", features = ["v4", "serde"] }
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio-native-tls", "time", "uuid"] } sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio-native-tls", "time", "uuid"] }
time = { version = "0.3.41", features = ["formatting", "macros", "parsing", "serde"] } time = { version = "0.3.41", features = ["formatting", "macros", "parsing", "serde"] }
icarus_meta = { git = "ssh://git@git.kundeng.us/phoenix/icarus_meta.git", tag = "v0.3.0-devel-f4b71de969-680" } thiserror = "1.0"
icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.5.1-devel-1c5de9dc26-111" } base64 = "0.21"
icarus_envy = { git = "ssh://git@git.kundeng.us/phoenix/icarus_envy.git", tag = "v0.3.0-devel-d73fba9899-006" } jsonwebtoken = { version = "9.3.1" }
josekit = { version = "0.10.1" }
utoipa = { version = "5.4.0", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
icarus_meta = { git = "ssh://git@git.kundeng.us/phoenix/icarus_meta.git", tag = "v0.3.0" }
icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.5.6" }
icarus_envy = { git = "ssh://git@git.kundeng.us/phoenix/icarus_envy.git", tag = "v0.3.2" }
[dev-dependencies] [dev-dependencies]
common-multipart-rfc7578 = { version = "0.7.0" } common-multipart-rfc7578 = { version = "0.7.0" }
+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.
+14 -6
View File
@@ -1,17 +1,25 @@
# Icarus
Web API for the Icarus project.
# Getting Started ## Getting Started
## Docker Quickest way to get started is with docker. Make sure `icarus_auth` and `songparser` repositories
Make sure `icarus_auth` is located in the root of the parent directory if using docker. are located in the root of the parent directory. Check the respective repositories to ensure they
are setup correctly before configuring `Icarus`.
Create a `.env` file for both projects - `icarus_auth` and `icarus` - in the root of each project. 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`.
+24 -7
View File
@@ -17,7 +17,7 @@ services:
main_db: main_db:
condition: service_healthy condition: service_healthy
networks: networks:
- main-api-network - icarus-network
restart: unless-stopped # Optional: Restart policy restart: unless-stopped # Optional: Restart policy
# --- Web API auth --- # --- Web API auth ---
@@ -39,11 +39,30 @@ services:
auth_db: auth_db:
condition: service_healthy condition: service_healthy
networks: networks:
- auth-api-network - icarus-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:
- icarus-network
# PostgreSQL Database Service # PostgreSQL Database Service
# --- icarus web api db --- # --- icarus web api db ---
main_db: main_db:
@@ -69,7 +88,7 @@ services:
start_period: 10s start_period: 10s
restart: always # Optional: Restart policy restart: always # Optional: Restart policy
networks: networks:
- main-api-network - icarus-network
# --- icarus web auth api db --- # --- icarus web auth api db ---
auth_db: auth_db:
@@ -95,7 +114,7 @@ services:
start_period: 10s start_period: 10s
restart: always # Optional: Restart policy restart: always # Optional: Restart policy
networks: networks:
- auth-api-network - icarus-network
# Define the named volume for data persistence # Define the named volume for data persistence
volumes: volumes:
@@ -106,7 +125,5 @@ volumes:
# Define the network (optional, but good practice) # Define the network (optional, but good practice)
networks: networks:
main-api-network: icarus-network:
driver: bridge
auth-api-network:
driver: bridge driver: bridge
+121
View File
@@ -0,0 +1,121 @@
// use std::collections::BTreeMap;
// use std::sync::Arc;
use axum::{
Json,
http::{Request, StatusCode},
middleware::Next,
response::IntoResponse,
};
use axum_extra::extract::cookie::CookieJar;
use jsonwebtoken::{DecodingKey, Validation, decode};
use serde::{Deserialize, Serialize};
// use serde_json::{json, Value};
use thiserror::Error;
// use time::OffsetDateTime;
fn deserialize_i64_from_f64<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
D: serde::Deserializer<'de>,
{
let val = f64::deserialize(deserializer)?;
// Handle NaN and infinity cases
if val.is_nan() || val.is_infinite() {
return Err(serde::de::Error::custom("invalid float value"));
}
// Round to nearest integer and convert
let rounded = val.round();
// Check if the rounded value can fit in i64
if rounded < (i64::MIN as f64) || rounded > (i64::MAX as f64) {
return Err(serde::de::Error::custom("float out of i64 range"));
}
Ok(rounded as i64)
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UserClaims {
pub iss: String,
pub aud: String, // Audience
pub sub: String, // Subject (user ID)
#[serde(deserialize_with = "deserialize_i64_from_f64")]
pub exp: i64, // Expiration time (UTC timestamp)
#[serde(deserialize_with = "deserialize_i64_from_f64")]
pub iat: i64, // Issued at (UTC timestamp)
// pub azp: String,
// pub gty: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub roles: Option<Vec<String>>, // Optional roles
}
#[derive(Error, Debug)]
pub enum JwtError {
#[error("Token creation failed")]
TokenCreation,
#[error("Token verification failed")]
TokenVerification,
#[error("Invalid token")]
InvalidToken,
#[error("Token expired")]
ExpiredToken,
#[error("Invalid key")]
InvalidKey,
}
#[derive(Debug, 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>)> {
println!("Cookie: {cookie_jar:?}");
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| {
println!("Auth value: {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 = icarus_envy::environment::get_secret_main_key().await;
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
validation.set_audience(&["icarus"]); // Must match exactly what's in the token
let claims = decode::<UserClaims>(
&token,
// TODO: Replace with code to get secret from env
&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;
println!("Claims: {claims:?}");
Ok(next.run(req).await)
}
+250 -15
View File
@@ -1,14 +1,26 @@
// TODO: Separate queue and coverart endpoints // TODO: Separate queue and coverart endpoints
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)] #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct CoverArtQueue { pub struct CoverArtQueue {
pub id: uuid::Uuid, pub id: uuid::Uuid,
pub song_queue_id: uuid::Uuid, pub song_queue_id: uuid::Uuid,
} }
pub mod request { 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 { pub mod link {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)] #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request { pub struct Request {
pub coverart_id: uuid::Uuid, pub coverart_id: uuid::Uuid,
pub song_queue_id: uuid::Uuid, pub song_queue_id: uuid::Uuid,
@@ -16,7 +28,7 @@ pub mod request {
} }
pub mod fetch_coverart_no_data { pub mod fetch_coverart_no_data {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)] #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Params { pub struct Params {
pub id: Option<uuid::Uuid>, pub id: Option<uuid::Uuid>,
pub song_queue_id: Option<uuid::Uuid>, pub song_queue_id: Option<uuid::Uuid>,
@@ -24,7 +36,7 @@ pub mod request {
} }
pub mod fetch_coverart_with_data { pub mod fetch_coverart_with_data {
#[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>,
pub song_queue_id: Option<uuid::Uuid>, pub song_queue_id: Option<uuid::Uuid>,
@@ -32,7 +44,7 @@ pub mod request {
} }
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,
@@ -40,14 +52,14 @@ pub mod request {
} }
pub mod wipe_data_from_coverart_queue { pub mod wipe_data_from_coverart_queue {
#[derive(Debug, serde::Deserialize, serde::Serialize)] #[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request { pub struct Request {
pub coverart_queue_id: uuid::Uuid, 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,20 +67,20 @@ pub mod request {
} }
pub mod response { pub mod response {
#[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<uuid::Uuid>, pub data: Vec<uuid::Uuid>,
} }
pub mod link { pub mod link {
#[derive(Debug, serde::Deserialize, serde::Serialize)] #[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Id { pub struct Id {
pub coverart_id: uuid::Uuid, pub coverart_id: uuid::Uuid,
pub song_queue_id: uuid::Uuid, pub song_queue_id: uuid::Uuid,
} }
#[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<Id>, pub data: Vec<Id>,
@@ -76,7 +88,7 @@ pub mod response {
} }
pub mod fetch_coverart_no_data { pub mod fetch_coverart_no_data {
#[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<super::super::CoverArtQueue>, pub data: Vec<super::super::CoverArtQueue>,
@@ -84,7 +96,7 @@ pub mod response {
} }
pub mod fetch_coverart_with_data { pub mod fetch_coverart_with_data {
#[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<Vec<u8>>, pub data: Vec<Vec<u8>>,
@@ -92,7 +104,7 @@ pub mod response {
} }
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<icarus_models::coverart::CoverArt>,
@@ -100,7 +112,7 @@ pub mod response {
} }
pub mod wipe_data_from_coverart_queue { pub mod wipe_data_from_coverart_queue {
#[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<uuid::Uuid>, pub data: Vec<uuid::Uuid>,
@@ -108,7 +120,7 @@ pub mod response {
} }
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<icarus_models::coverart::CoverArt>,
@@ -376,6 +388,88 @@ pub mod cov_db {
Err(_) => Err(sqlx::Error::RowNotFound), Err(_) => Err(sqlx::Error::RowNotFound),
} }
} }
pub async fn get_coverart_with_song_id(
pool: &sqlx::PgPool,
song_id: &uuid::Uuid,
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, title, path, 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(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 async fn delete_coverart(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<icarus_models::coverart::CoverArt, sqlx::Error> {
let result = sqlx::query(
r#"
DELETE FROM "coverart"
WHERE id = $1
RETURNING id, title, path, song_id
"#,
)
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error deleting 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(),
song_id: row
.try_get("song_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
}),
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
} }
pub mod endpoint { pub mod endpoint {
@@ -383,6 +477,18 @@ pub mod endpoint {
use axum::response::IntoResponse; use axum::response::IntoResponse;
/// 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::Response),
(status = 400, description = "Error queueing cover art", body = super::response::Response)
)
)]
pub async fn queue( pub async fn queue(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
mut multipart: axum::extract::Multipart, mut multipart: axum::extract::Multipart,
@@ -428,6 +534,20 @@ pub mod endpoint {
} }
} }
/// 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( pub async fn link(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::link::Request>, axum::Json(payload): axum::Json<super::request::link::Request>,
@@ -455,6 +575,19 @@ pub mod endpoint {
} }
} }
/// 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( pub async fn fetch_coverart_no_data(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query< axum::extract::Query(params): axum::extract::Query<
@@ -502,6 +635,16 @@ pub mod endpoint {
} }
} }
/// 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( pub async fn fetch_coverart_with_data(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>, axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
@@ -530,6 +673,20 @@ pub mod endpoint {
} }
} }
/// Endpoint to create cover art
#[utoipa::path(
post,
path = super::super::endpoints::CREATECOVERART,
request_body(
content = super::request::create_coverart::Request,
description = "Data required to create cover art",
content_type = "application/json"
),
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>,
@@ -562,7 +719,10 @@ pub mod endpoint {
match super::cov_db::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.id = id; coverart.id = id;
println!("Cover Art created");
response.message = String::from("Successful"); response.message = String::from("Successful");
response.data.push(coverart); response.data.push(coverart);
@@ -587,6 +747,21 @@ pub mod endpoint {
} }
} }
/// 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( pub async fn wipe_data_from_coverart_queue(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::wipe_data_from_coverart_queue::Request>, axum::Json(payload): axum::Json<super::request::wipe_data_from_coverart_queue::Request>,
@@ -616,6 +791,18 @@ pub mod endpoint {
} }
} }
/// Endpoint to get cover art with criteria
#[utoipa::path(
get,
path = super::super::endpoints::GETCOVERART,
params(
("id" = uuid::Uuid, Path, description = "Cover art Id")
),
responses(
(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)
)
)]
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>,
@@ -643,4 +830,52 @@ 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 super::cov_db::get_coverart(&pool, &id).await {
Ok(coverart) => match coverart.to_data() {
Ok(data) => {
let bytes = axum::body::Bytes::from(data);
let mut response = bytes.into_response();
let headers = response.headers_mut();
// TODO: Address hard coding
headers.insert(
axum::http::header::CONTENT_TYPE,
"audio/jpg".parse().unwrap(),
);
headers.insert(
axum::http::header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{id}.jpg\"")
.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(),
),
}
}
} }
+40 -8
View File
@@ -1,10 +1,9 @@
// TODO: Explicitly make this module target queueing a song's metadata // TODO: Explicitly make this module target queueing a song's metadata
pub mod request { pub mod request {
pub mod queue_metadata { pub mod queue_metadata {
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Deserialize, Serialize, sqlx::FromRow)] #[derive(Debug, Default, Deserialize, Serialize, sqlx::FromRow, utoipa::ToSchema)]
pub struct Request { pub struct Request {
pub song_queue_id: uuid::Uuid, pub song_queue_id: uuid::Uuid,
pub album: String, pub album: String,
@@ -43,7 +42,13 @@ pub mod request {
pub mod fetch_metadata { pub mod fetch_metadata {
#[derive( #[derive(
Debug, Default, serde::Deserialize, serde::Serialize, sqlx::FromRow, sqlx::Decode, Debug,
Default,
serde::Deserialize,
serde::Serialize,
sqlx::FromRow,
sqlx::Decode,
utoipa::ToSchema,
)] )]
pub struct Params { pub struct Params {
pub id: Option<uuid::Uuid>, pub id: Option<uuid::Uuid>,
@@ -53,11 +58,10 @@ pub mod request {
} }
pub mod response { pub mod response {
pub mod queue_metadata { pub mod queue_metadata {
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize)] #[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct Response { pub struct Response {
pub message: String, pub message: String,
pub data: Vec<uuid::Uuid>, pub data: Vec<uuid::Uuid>,
@@ -67,7 +71,7 @@ pub mod response {
pub mod fetch_metadata { pub mod fetch_metadata {
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize)] #[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct Response { pub struct Response {
pub message: String, pub message: String,
pub data: Vec<crate::callers::metadata::metadata_queue::MetadataQueue>, pub data: Vec<crate::callers::metadata::metadata_queue::MetadataQueue>,
@@ -83,7 +87,7 @@ pub mod metadata_queue {
pub id: uuid::Uuid, pub id: uuid::Uuid,
} }
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow)] #[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow, utoipa::ToSchema)]
pub struct MetadataQueue { pub struct MetadataQueue {
pub id: uuid::Uuid, pub id: uuid::Uuid,
pub metadata: serde_json::Value, pub metadata: serde_json::Value,
@@ -204,9 +208,24 @@ pub mod metadata_queue {
} }
} }
/// Module for metadata related endpoints
pub mod endpoint { pub mod endpoint {
use axum::{Json, http::StatusCode}; use axum::{Json, http::StatusCode};
/// 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( pub async fn queue_metadata(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
Json(payload): Json<super::request::queue_metadata::Request>, Json(payload): Json<super::request::queue_metadata::Request>,
@@ -221,7 +240,7 @@ pub mod endpoint {
response.message = if response.data.is_empty() { response.message = if response.data.is_empty() {
String::from("Error") String::from("Error")
} else { } else {
String::from("Success") String::from(super::super::response::SUCCESSFUL)
}; };
(StatusCode::OK, Json(response)) (StatusCode::OK, Json(response))
@@ -233,6 +252,19 @@ pub mod endpoint {
} }
} }
/// 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( pub async fn fetch_metadata(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<super::request::fetch_metadata::Params>, axum::extract::Query(params): axum::extract::Query<super::request::fetch_metadata::Params>,
+5
View File
@@ -17,8 +17,13 @@ pub mod endpoints {
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 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 {
+607 -15
View File
@@ -7,8 +7,21 @@ pub mod request {
pub message: String, pub message: String,
} }
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 { pub mod update_status {
#[derive(Default, serde::Deserialize, serde::Serialize)] #[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request { pub struct Request {
pub id: uuid::Uuid, pub id: uuid::Uuid,
pub status: String, pub status: String,
@@ -16,7 +29,7 @@ pub mod request {
} }
pub mod create_metadata { pub mod create_metadata {
#[derive(Debug, serde::Deserialize, serde::Serialize)] #[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request { pub struct Request {
pub title: String, pub title: String,
pub artist: String, pub artist: String,
@@ -79,14 +92,14 @@ pub mod request {
} }
pub mod wipe_data_from_song_queue { pub mod wipe_data_from_song_queue {
#[derive(Debug, serde::Deserialize, serde::Serialize)] #[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request { pub struct Request {
pub song_queue_id: uuid::Uuid, pub song_queue_id: uuid::Uuid,
} }
} }
pub mod link_user_id { pub mod link_user_id {
#[derive(Debug, serde::Deserialize, serde::Serialize)] #[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Request { pub struct Request {
pub song_queue_id: uuid::Uuid, pub song_queue_id: uuid::Uuid,
pub user_id: uuid::Uuid, pub user_id: uuid::Uuid,
@@ -94,7 +107,7 @@ pub mod request {
} }
pub mod get_songs { pub mod get_songs {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)] #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Params { pub struct Params {
pub id: Option<uuid::Uuid>, pub id: Option<uuid::Uuid>,
} }
@@ -104,16 +117,18 @@ pub mod request {
pub mod response { pub mod response {
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize)] /// Song queue response
#[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct Response { pub struct Response {
pub message: String, pub message: String,
/// Id of the queued song
pub data: Vec<uuid::Uuid>, pub data: Vec<uuid::Uuid>,
} }
pub mod fetch_queue_song { pub mod fetch_queue_song {
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize)] #[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct Response { pub struct Response {
pub message: String, pub message: String,
pub data: Vec<crate::callers::song::song_queue::SongQueue>, pub data: Vec<crate::callers::song::song_queue::SongQueue>,
@@ -121,13 +136,13 @@ pub mod response {
} }
pub mod update_status { pub mod update_status {
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct ChangedStatus { pub struct ChangedStatus {
pub old_status: String, pub old_status: String,
pub new_status: String, pub new_status: String,
} }
#[derive(Default, serde::Deserialize, serde::Serialize)] #[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response { pub struct Response {
pub message: String, pub message: String,
pub data: Vec<ChangedStatus>, pub data: Vec<ChangedStatus>,
@@ -135,7 +150,7 @@ pub mod response {
} }
pub mod update_song_queue { pub mod update_song_queue {
#[derive(Default, serde::Deserialize, serde::Serialize)] #[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response { pub struct Response {
pub message: String, pub message: String,
pub data: Vec<uuid::Uuid>, pub data: Vec<uuid::Uuid>,
@@ -143,7 +158,7 @@ pub mod response {
} }
pub mod create_metadata { pub mod create_metadata {
#[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::song::Song>, pub data: Vec<icarus_models::song::Song>,
@@ -151,7 +166,7 @@ pub mod response {
} }
pub mod wipe_data_from_song_queue { pub mod wipe_data_from_song_queue {
#[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<uuid::Uuid>, pub data: Vec<uuid::Uuid>,
@@ -159,7 +174,7 @@ pub mod response {
} }
pub mod link_user_id { pub mod link_user_id {
#[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<uuid::Uuid>, pub data: Vec<uuid::Uuid>,
@@ -167,12 +182,26 @@ pub mod response {
} }
pub mod get_songs { pub mod get_songs {
#[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::song::Song>, pub data: Vec<icarus_models::song::Song>,
} }
} }
pub mod delete_song {
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct SongAndCoverArt {
pub song: icarus_models::song::Song,
pub coverart: icarus_models::coverart::CoverArt,
}
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<SongAndCoverArt>,
}
}
} }
// TODO: Might make a distinction between year and date in a song's tag at some point // TODO: Might make a distinction between year and date in a song's tag at some point
@@ -336,6 +365,207 @@ pub mod song_db {
Err(_) => Err(sqlx::Error::RowNotFound), Err(_) => Err(sqlx::Error::RowNotFound),
} }
} }
pub async fn get_all_songs(
pool: &sqlx::PgPool,
) -> Result<Vec<icarus_models::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<icarus_models::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 = icarus_models::song::Song {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
title: row
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
artist: row
.try_get("artist")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
album_artist: row
.try_get("album_artist")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
album: row
.try_get("album")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
genre: row
.try_get("genre")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
year: row
.try_get("year")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
track: row
.try_get("track")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
disc: row
.try_get("disc")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
track_count: row
.try_get("track_count")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
disc_count: row
.try_get("disc_count")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
duration: row
.try_get("duration")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
audio_type: row
.try_get("audio_type")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
directory: row
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
date_created: date_created_time.to_string(),
user_id: row
.try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
};
songs.push(song);
}
Ok(songs)
}
Err(_err) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn delete_song(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<icarus_models::song::Song, sqlx::Error> {
let result = sqlx::query(
// icarus_models::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(icarus_models::song::Song {
id: row
.try_get("id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
title: row
.try_get("title")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
artist: row
.try_get("artist")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
album_artist: row
.try_get("album_artist")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
album: row
.try_get("album")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
genre: row
.try_get("genre")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
year: row
.try_get("year")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
track: row
.try_get("track")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
disc: row
.try_get("disc")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
track_count: row
.try_get("track_count")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
disc_count: row
.try_get("disc_count")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
duration: row
.try_get("duration")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
audio_type: row
.try_get("audio_type")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
filename: row
.try_get("filename")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
directory: row
.try_get("directory")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
date_created: date_created_time.to_string(),
user_id: row
.try_get("user_id")
.map_err(|_e| sqlx::Error::RowNotFound)
.unwrap(),
data: Vec::new(),
})
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
} }
mod song_queue { mod song_queue {
@@ -346,7 +576,8 @@ mod song_queue {
pub id: uuid::Uuid, pub id: uuid::Uuid,
} }
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow)] // TODO: Move this somewhere else at some point
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow, utoipa::ToSchema)]
pub struct SongQueue { pub struct SongQueue {
pub id: uuid::Uuid, pub id: uuid::Uuid,
pub filename: String, pub filename: String,
@@ -639,12 +870,27 @@ mod song_queue {
} }
} }
/// Module for song related endpoints
pub mod endpoint { pub mod endpoint {
use axum::{Json, http::StatusCode, response::IntoResponse}; use axum::{Json, http::StatusCode, response::IntoResponse};
use std::io::Write; use std::io::Write;
use crate::callers::song::song_queue; use crate::callers::song::song_queue;
/// 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 = 200, description = "Song queued", body = super::response::Response)
)
)]
pub async fn queue_song( pub async fn queue_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
mut multipart: axum::extract::Multipart, mut multipart: axum::extract::Multipart,
@@ -693,6 +939,20 @@ pub mod endpoint {
(StatusCode::OK, Json(response)) (StatusCode::OK, 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( pub async fn link_user_id(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::link_user_id::Request>, axum::Json(payload): axum::Json<super::request::link_user_id::Request>,
@@ -724,6 +984,15 @@ pub mod endpoint {
} }
} }
/// 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( pub async fn fetch_queue_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
) -> ( ) -> (
@@ -745,6 +1014,17 @@ pub mod endpoint {
} }
} }
// TODO: Rename
/// 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_flac( pub async fn download_flac(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>, axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
@@ -773,6 +1053,20 @@ pub mod endpoint {
} }
} }
/// 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( pub async fn update_song_queue_status(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::update_status::Request>, axum::Json(payload): axum::Json<super::request::update_status::Request>,
@@ -825,6 +1119,22 @@ pub mod endpoint {
} }
} }
/// 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)
)
)]
pub async fn update_song_queue( pub async fn update_song_queue(
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>, axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
@@ -871,6 +1181,21 @@ pub mod endpoint {
} }
} }
/// Endpoint to create song
#[utoipa::path(
post,
path = super::super::endpoints::QUEUEMETADATA,
request_body(
content = super::request::create_metadata::Request,
description = "Data needed to create the song and save it to the filesystem",
content_type = "application/json"
),
responses(
(status = 200, description = "Song created", body = super::response::create_metadata::Response),
(status = 400, description = "Error", body = super::response::create_metadata::Response),
(status = 505, description = "Error creating song", body = super::response::create_metadata::Response)
)
)]
pub async fn create_metadata( pub async fn create_metadata(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::create_metadata::Request>, axum::Json(payload): axum::Json<super::request::create_metadata::Request>,
@@ -954,6 +1279,20 @@ pub mod endpoint {
} }
} }
/// 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( pub async fn wipe_data_from_song_queue(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::wipe_data_from_song_queue::Request>, axum::Json(payload): axum::Json<super::request::wipe_data_from_song_queue::Request>,
@@ -984,6 +1323,18 @@ pub mod endpoint {
} }
} }
// Endpoint to get songs
#[utoipa::path(
get,
path = super::super::endpoints::GETSONGS,
params(
("id" = uuid::Uuid, Path, description = "Id of song")
),
responses(
(status = 200, description = "Songs found", body = super::response::get_songs::Response),
(status = 400, description = "Error getting songs", body = super::response::get_songs::Response)
)
)]
pub async fn get_songs( pub async fn get_songs(
axum::Extension(pool): axum::Extension<sqlx::PgPool>, axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<super::request::get_songs::Params>, axum::extract::Query(params): axum::extract::Query<super::request::get_songs::Params>,
@@ -1011,4 +1362,245 @@ pub mod endpoint {
} }
} }
} }
/// Endpoint to get all songs
#[utoipa::path(
get,
path = super::super::endpoints::GETALLSONGS,
responses(
(status = 200, description = "Getting all songs", body = super::response::get_songs::Response),
(status = 404, description = "Song not found", body = super::response::get_songs::Response)
)
)]
pub async fn get_all_songs(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::get_songs::Response>,
) {
let mut response = super::response::get_songs::Response::default();
match super::song_db::get_all_songs(&pool).await {
Ok(songs) => {
response.message = String::from(super::super::response::SUCCESSFUL);
response.data = songs;
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
}
}
}
/// Ednpoint to stream song
#[utoipa::path(
get,
path = super::super::endpoints::STREAMSONG,
params(("id" = uuid::Uuid, Path, description = "Song Id")),
responses(
(status = 200, description = "Stream song", body = Vec<u8>),
(status = 500, description = "Error streaming song", body = (u64, String))
)
)]
pub async fn stream_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
) -> impl IntoResponse {
match super::song_db::get_song(&pool, &id).await {
Ok(song) => {
let song_path = song.song_path().unwrap();
let path = std::path::Path::new(&song_path);
if !path.starts_with(&song.directory) || !path.exists() {
return Err((axum::http::StatusCode::NOT_FOUND, "File not found"));
}
let file = match tokio::fs::File::open(&path).await {
Ok(file) => file,
Err(_) => return Err((axum::http::StatusCode::NOT_FOUND, "File not found")),
};
let file_size = match file.metadata().await {
Ok(meta) => meta.len(),
Err(_) => {
return Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
"Could not read file",
));
}
};
let mime = mime_guess::from_path(path).first_or_octet_stream();
let stream = tokio_util::io::ReaderStream::new(file);
let rep = axum::response::Response::builder()
.header("content-type", mime.to_string())
.header("accept-ranges", "bytes")
.header("content-length", file_size.to_string())
.body(axum::body::Body::from_stream(stream))
.unwrap();
Ok(rep)
}
Err(_err) => Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
"Could not find file",
)),
}
}
/// Endpoint to download song
#[utoipa::path(
get,
path = super::super::endpoints::DOWNLOADSONG,
params(("id" = uuid::Uuid, Path, description = "Song Id")),
responses(
(status = 200, description = "Download song", body = (u64, Vec<u8>)),
(status = 404, description = "Song not found", body = (u64, Vec<u8>)),
(status = 400, description = "Error downloading song", body = (u64, Vec<u8>))
)
)]
pub async fn download_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
) -> (axum::http::StatusCode, axum::response::Response) {
match super::song_db::get_song(&pool, &id).await {
Ok(song) => match song.to_data() {
Ok(data) => {
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,
"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::INTERNAL_SERVER_ERROR,
axum::response::Response::default(),
),
},
Err(_err) => (
axum::http::StatusCode::NOT_FOUND,
axum::response::Response::default(),
),
}
}
/// Endpoint to delete the song
#[utoipa::path(
delete,
path = super::super::endpoints::DELETESONG,
params(("id" = uuid::Uuid, Path, description = "Song Id")),
responses(
(status = 200, description = "Song deleted", body = super::response::delete_song::Response),
(status = 404, description = "Song not found", body = super::response::delete_song::Response),
(status = 500, description = "Error deleting song", body = super::response::delete_song::Response)
)
)]
pub async fn delete_song(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::delete_song::Response>,
) {
let mut response = super::response::delete_song::Response::default();
match super::song_db::get_song(&pool, &id).await {
Ok(song) => {
match super::super::coverart::cov_db::get_coverart_with_song_id(&pool, &song.id)
.await
{
Ok(coverart) => {
let coverart_path = std::path::Path::new(&coverart.path);
if coverart_path.exists() {
match song.song_path() {
Ok(song_path) => {
match super::song_db::delete_song(&pool, &song.id).await {
Ok(deleted_song) => {
match super::super::coverart::cov_db::delete_coverart(
&pool,
&coverart.id,
)
.await
{
Ok(deleted_coverart) => {
match std::fs::remove_file(song_path) {
Ok(_) => match std::fs::remove_file(
&coverart.path,
) {
Ok(_) => {
response.message = String::from(super::super::response::SUCCESSFUL);
response.data.push(super::response::delete_song::SongAndCoverArt{ song: deleted_song, coverart: deleted_coverart });
(
axum::http::StatusCode::OK,
axum::Json(response),
)
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(response))
}
},
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(response))
}
}
}
Err(err) => {
response.message = err.to_string();
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(response))
}
}
}
Err(err) => {
response.message = err.to_string();
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
}
Err(err) => {
response.message = err.to_string();
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
} else {
response.message =
String::from("Could not locate coverart on the filesystem");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
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))
}
}
}
} }
+642 -44
View File
@@ -1,3 +1,4 @@
pub mod auth;
pub mod callers; pub mod callers;
pub mod db { pub mod db {
@@ -44,84 +45,186 @@ async fn main() {
} }
pub mod init { pub mod init {
use axum::routing::{get, patch, post}; use axum::routing::{delete, get, patch, post};
use std::time::Duration; use std::time::Duration;
use tower_http::timeout::TimeoutLayer; use tower_http::timeout::TimeoutLayer;
use utoipa::OpenApi;
use axum::http::{
HeaderValue, Method,
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
};
use crate::callers::coverart as coverart_caller;
use crate::callers::metadata as metadata_caller;
use crate::callers::song as song_caller;
use coverart_caller::endpoint as coverart_endpoints;
use coverart_caller::response as coverart_responses;
use metadata_caller::endpoint as metadata_endpoints;
use metadata_caller::response as metadata_responses;
use song_caller::endpoint as song_endpoints;
use song_caller::response as song_responses;
#[derive(utoipa::OpenApi)]
#[openapi(
paths(song_endpoints::queue_song, song_endpoints::link_user_id, song_endpoints::fetch_queue_song, song_endpoints::download_flac,
song_endpoints::update_song_queue_status, song_endpoints::update_song_queue, song_endpoints::create_metadata, song_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_endpoints::queue, coverart_endpoints::link, coverart_endpoints::fetch_coverart_no_data,
coverart_endpoints::fetch_coverart_with_data, coverart_endpoints::create_coverart, coverart_endpoints::wipe_data_from_coverart_queue,
coverart_endpoints::get_coverart, coverart_endpoints::download_coverart,
metadata_endpoints::queue_metadata, metadata_endpoints::fetch_metadata),
components(schemas(song_responses::Response, song_responses::link_user_id::Response, song_responses::fetch_queue_song::Response,
song_responses::update_status::Response, song_responses::update_song_queue::Response, song_responses::create_metadata::Response,
song_responses::wipe_data_from_song_queue::Response, song_responses::get_songs::Response, song_responses::delete_song::Response,
coverart_responses::Response, coverart_responses::link::Response, coverart_responses::fetch_coverart_no_data::Response,
coverart_responses::fetch_coverart_with_data::Response, coverart_responses::create_coverart::Response,
coverart_responses::wipe_data_from_coverart_queue::Response, coverart_responses::get_coverart::Response,
metadata_responses::queue_metadata::Response, metadata_responses::fetch_metadata::Response)),
tags(
(name = "Icarus API", description = "Web API to manage music")
)
)]
struct ApiDoc;
pub async fn routes() -> axum::Router { pub async fn routes() -> axum::Router {
axum::Router::new() axum::Router::new()
.route(crate::ROOT, get(crate::root)) .route(crate::ROOT, get(crate::root))
.route( .route(
crate::callers::endpoints::QUEUESONG, crate::callers::endpoints::QUEUESONG,
post(crate::callers::song::endpoint::queue_song), post(crate::callers::song::endpoint::queue_song).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
) )
.route( .route(
crate::callers::endpoints::QUEUESONG, crate::callers::endpoints::QUEUESONG,
patch(crate::callers::song::endpoint::update_song_queue_status), patch(crate::callers::song::endpoint::update_song_queue_status).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
) )
.route( .route(
crate::callers::endpoints::QUEUESONGLINKUSERID, crate::callers::endpoints::QUEUESONGLINKUSERID,
patch(crate::callers::song::endpoint::link_user_id), patch(crate::callers::song::endpoint::link_user_id).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
) )
.route( .route(
crate::callers::endpoints::QUEUESONGDATA, crate::callers::endpoints::QUEUESONGDATA,
get(crate::callers::song::endpoint::download_flac), get(crate::callers::song::endpoint::download_flac).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
) )
.route( .route(
crate::callers::endpoints::NEXTQUEUESONG, crate::callers::endpoints::NEXTQUEUESONG,
get(crate::callers::song::endpoint::fetch_queue_song), get(crate::callers::song::endpoint::fetch_queue_song).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
) )
.route( .route(
crate::callers::endpoints::QUEUESONGUPDATE, crate::callers::endpoints::QUEUESONGUPDATE,
patch(crate::callers::song::endpoint::update_song_queue), patch(crate::callers::song::endpoint::update_song_queue).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
) )
.route( .route(
crate::callers::endpoints::QUEUESONGDATAWIPE, crate::callers::endpoints::QUEUESONGDATAWIPE,
patch(crate::callers::song::endpoint::wipe_data_from_song_queue), patch(crate::callers::song::endpoint::wipe_data_from_song_queue).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
) )
.route( .route(
crate::callers::endpoints::QUEUEMETADATA, crate::callers::endpoints::QUEUEMETADATA,
post(crate::callers::metadata::endpoint::queue_metadata), post(crate::callers::metadata::endpoint::queue_metadata).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
) )
.route( .route(
crate::callers::endpoints::QUEUEMETADATA, crate::callers::endpoints::QUEUEMETADATA,
get(crate::callers::metadata::endpoint::fetch_metadata), get(crate::callers::metadata::endpoint::fetch_metadata).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
) )
.route( .route(
crate::callers::endpoints::QUEUECOVERART, crate::callers::endpoints::QUEUECOVERART,
post(crate::callers::coverart::endpoint::queue), post(crate::callers::coverart::endpoint::queue).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
) )
.route( .route(
crate::callers::endpoints::QUEUECOVERARTDATA, crate::callers::endpoints::QUEUECOVERARTDATA,
get(crate::callers::coverart::endpoint::fetch_coverart_with_data), get(crate::callers::coverart::endpoint::fetch_coverart_with_data).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
) )
.route( .route(
crate::callers::endpoints::QUEUECOVERART, crate::callers::endpoints::QUEUECOVERART,
get(crate::callers::coverart::endpoint::fetch_coverart_no_data), get(crate::callers::coverart::endpoint::fetch_coverart_no_data).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
) )
.route( .route(
crate::callers::endpoints::QUEUECOVERARTLINK, crate::callers::endpoints::QUEUECOVERARTLINK,
patch(crate::callers::coverart::endpoint::link), patch(crate::callers::coverart::endpoint::link).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
) )
.route( .route(
crate::callers::endpoints::QUEUECOVERARTDATAWIPE, crate::callers::endpoints::QUEUECOVERARTDATAWIPE,
patch(crate::callers::coverart::endpoint::wipe_data_from_coverart_queue), patch(crate::callers::coverart::endpoint::wipe_data_from_coverart_queue)
.route_layer(axum::middleware::from_fn(
crate::auth::auth::<axum::body::Body>,
)),
) )
.route( .route(
crate::callers::endpoints::CREATESONG, crate::callers::endpoints::CREATESONG,
post(crate::callers::song::endpoint::create_metadata), post(crate::callers::song::endpoint::create_metadata).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
) )
.route( .route(
crate::callers::endpoints::CREATECOVERART, crate::callers::endpoints::CREATECOVERART,
post(crate::callers::coverart::endpoint::create_coverart), post(crate::callers::coverart::endpoint::create_coverart).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
) )
.route( .route(
crate::callers::endpoints::GETSONGS, crate::callers::endpoints::GETSONGS,
get(crate::callers::song::endpoint::get_songs), get(crate::callers::song::endpoint::get_songs).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
) )
.route( .route(
crate::callers::endpoints::GETCOVERART, crate::callers::endpoints::GETCOVERART,
get(crate::callers::coverart::endpoint::get_coverart), 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),
) )
} }
@@ -132,11 +235,22 @@ pub mod init {
// TODO: Look into handling this. Seems redundant to run migrations multiple times // TODO: Look into handling this. Seems redundant to run migrations multiple times
crate::db::migrations(&pool).await; crate::db::migrations(&pool).await;
let cors = tower_http::cors::CorsLayer::new()
.allow_origin("http://localhost:3000".parse::<HeaderValue>().unwrap())
.allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
.allow_credentials(true)
.allow_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE]);
routes() routes()
.await .await
.merge(
utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
.url("/api-docs/openapi.json", ApiDoc::openapi()),
)
.layer(axum::Extension(pool)) .layer(axum::Extension(pool))
.layer(axum::extract::DefaultBodyLimit::max(1024 * 1024 * 1024)) .layer(axum::extract::DefaultBodyLimit::max(1024 * 1024 * 1024))
.layer(TimeoutLayer::new(Duration::from_secs(300))) .layer(TimeoutLayer::new(Duration::from_secs(300)))
.layer(cors)
} }
} }
@@ -259,6 +373,45 @@ mod tests {
} }
} }
pub fn token_fields() -> (String, String, String) {
(
String::from("What a twist!"),
String::from("icarus_test"),
String::from("icarus"),
)
}
pub const TEST_USER_ID: uuid::Uuid = uuid::uuid!("cc938368-615a-4694-b2ca-6e122fa31c52");
pub async fn test_token() -> Result<String, josekit::JoseError> {
let key: String = icarus_envy::environment::get_secret_main_key().await;
let (message, issuer, audience) = token_fields();
let token_resource = icarus_models::token::TokenResource {
message: message,
issuer: issuer,
audiences: vec![audience],
id: TEST_USER_ID,
};
match icarus_models::token::create_token(&key, &token_resource, time::Duration::hours(1)) {
Ok((access_token, _some_time)) => Ok(access_token),
Err(err) => Err(err),
}
}
pub async fn bearer_auth() -> String {
let token = match test_token().await {
Ok(access_token) => access_token,
Err(err) => {
assert!(false, "Error: {err:?}");
String::new()
}
};
format!("Bearer {token}")
}
// TODO: Put the *_req() functions in their own module // TODO: Put the *_req() functions in their own module
async fn song_queue_req( async fn song_queue_req(
app: &axum::Router, app: &axum::Router,
@@ -274,6 +427,7 @@ mod tests {
.method(axum::http::Method::POST) .method(axum::http::Method::POST)
.uri(crate::callers::endpoints::QUEUESONG) .uri(crate::callers::endpoints::QUEUESONG)
.header(axum::http::header::CONTENT_TYPE, content_type) .header(axum::http::header::CONTENT_TYPE, content_type)
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
.body(axum::body::Body::from_stream(body)) .body(axum::body::Body::from_stream(body))
.unwrap(); .unwrap();
app.clone().oneshot(req).await app.clone().oneshot(req).await
@@ -293,6 +447,7 @@ mod tests {
.method(axum::http::Method::PATCH) .method(axum::http::Method::PATCH)
.uri(crate::callers::endpoints::QUEUESONGLINKUSERID) .uri(crate::callers::endpoints::QUEUESONGLINKUSERID)
.header(axum::http::header::CONTENT_TYPE, "application/json") .header(axum::http::header::CONTENT_TYPE, "application/json")
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
.body(axum::body::Body::from(payload.to_string())) .body(axum::body::Body::from(payload.to_string()))
.unwrap(); .unwrap();
@@ -306,6 +461,7 @@ mod tests {
.method(axum::http::Method::GET) .method(axum::http::Method::GET)
.uri(crate::callers::endpoints::NEXTQUEUESONG) .uri(crate::callers::endpoints::NEXTQUEUESONG)
.header(axum::http::header::CONTENT_TYPE, "application/json") .header(axum::http::header::CONTENT_TYPE, "application/json")
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
.body(axum::body::Body::empty()) .body(axum::body::Body::empty())
.unwrap(); .unwrap();
app.clone().oneshot(fetch_req).await app.clone().oneshot(fetch_req).await
@@ -321,6 +477,7 @@ mod tests {
.method(axum::http::Method::GET) .method(axum::http::Method::GET)
.uri(uri) .uri(uri)
.header(axum::http::header::CONTENT_TYPE, "application/json") .header(axum::http::header::CONTENT_TYPE, "application/json")
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
.body(axum::body::Body::empty()) .body(axum::body::Body::empty())
.unwrap(); .unwrap();
@@ -339,6 +496,7 @@ mod tests {
.method(axum::http::Method::GET) .method(axum::http::Method::GET)
.uri(uri) .uri(uri)
.header(axum::http::header::CONTENT_TYPE, "audio/flac") .header(axum::http::header::CONTENT_TYPE, "audio/flac")
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
.body(axum::body::Body::empty()) .body(axum::body::Body::empty())
.unwrap(); .unwrap();
@@ -349,7 +507,7 @@ mod tests {
app: &axum::Router, app: &axum::Router,
) -> Result<axum::response::Response, std::convert::Infallible> { ) -> Result<axum::response::Response, std::convert::Infallible> {
let mut form = MultipartForm::default(); let mut form = MultipartForm::default();
let _ = form.add_file("jpg", "tests/I/Coverart.jpg"); let _ = form.add_file("jpg", "tests/I/Coverart-1.jpg");
// Create request // Create request
let content_type = form.content_type(); let content_type = form.content_type();
@@ -359,6 +517,7 @@ mod tests {
.method(axum::http::Method::POST) .method(axum::http::Method::POST)
.uri(crate::callers::endpoints::QUEUECOVERART) .uri(crate::callers::endpoints::QUEUECOVERART)
.header(axum::http::header::CONTENT_TYPE, content_type) .header(axum::http::header::CONTENT_TYPE, content_type)
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
.body(axum::body::Body::from_stream(body)) .body(axum::body::Body::from_stream(body))
.unwrap(); .unwrap();
@@ -376,6 +535,7 @@ mod tests {
.method(axum::http::Method::POST) .method(axum::http::Method::POST)
.uri(crate::callers::endpoints::QUEUEMETADATA) .uri(crate::callers::endpoints::QUEUEMETADATA)
.header(axum::http::header::CONTENT_TYPE, "application/json") .header(axum::http::header::CONTENT_TYPE, "application/json")
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
.body(axum::body::Body::from(payload.to_string())) .body(axum::body::Body::from(payload.to_string()))
.unwrap(); .unwrap();
@@ -396,6 +556,7 @@ mod tests {
.method(axum::http::Method::PATCH) .method(axum::http::Method::PATCH)
.uri(crate::callers::endpoints::QUEUECOVERARTLINK) .uri(crate::callers::endpoints::QUEUECOVERARTLINK)
.header(axum::http::header::CONTENT_TYPE, "application/json") .header(axum::http::header::CONTENT_TYPE, "application/json")
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
.body(axum::body::Body::from(payload.to_string())) .body(axum::body::Body::from(payload.to_string()))
.unwrap(); .unwrap();
@@ -415,6 +576,7 @@ mod tests {
.method(axum::http::Method::POST) .method(axum::http::Method::POST)
.uri(crate::callers::endpoints::CREATECOVERART) .uri(crate::callers::endpoints::CREATECOVERART)
.header(axum::http::header::CONTENT_TYPE, "application/json") .header(axum::http::header::CONTENT_TYPE, "application/json")
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
.body(axum::body::Body::from(payload.to_string())) .body(axum::body::Body::from(payload.to_string()))
.unwrap(); .unwrap();
app.clone().oneshot(req).await app.clone().oneshot(req).await
@@ -431,6 +593,7 @@ mod tests {
.method(axum::http::Method::POST) .method(axum::http::Method::POST)
.uri(crate::callers::endpoints::CREATESONG) .uri(crate::callers::endpoints::CREATESONG)
.header(axum::http::header::CONTENT_TYPE, "application/json") .header(axum::http::header::CONTENT_TYPE, "application/json")
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
.body(axum::body::Body::from(payload.to_string())) .body(axum::body::Body::from(payload.to_string()))
.unwrap(); .unwrap();
@@ -450,6 +613,7 @@ mod tests {
.method(axum::http::Method::PATCH) .method(axum::http::Method::PATCH)
.uri(crate::callers::endpoints::QUEUESONG) .uri(crate::callers::endpoints::QUEUESONG)
.header(axum::http::header::CONTENT_TYPE, "application/json") .header(axum::http::header::CONTENT_TYPE, "application/json")
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
.body(axum::body::Body::from(payload.to_string())) .body(axum::body::Body::from(payload.to_string()))
.unwrap(); .unwrap();
@@ -470,6 +634,7 @@ mod tests {
.method(axum::http::Method::GET) .method(axum::http::Method::GET)
.uri(uri) .uri(uri)
.header(axum::http::header::CONTENT_TYPE, "application/json") .header(axum::http::header::CONTENT_TYPE, "application/json")
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
.body(axum::body::Body::empty()) .body(axum::body::Body::empty())
.unwrap(); .unwrap();
@@ -491,9 +656,7 @@ mod tests {
let song_queue_id = resp.data[0]; let song_queue_id = resp.data[0];
assert_eq!(false, song_queue_id.is_nil(), "Should not be empty"); assert_eq!(false, song_queue_id.is_nil(), "Should not be empty");
let user_id = uuid::Uuid::new_v4(); let user_id = super::TEST_USER_ID;
// match super::get_resp_data::<crate::callers::song::response::link_user_id::Response>(response).await {
match super::song_queue_link_req(&app, &song_queue_id, &user_id).await { match super::song_queue_link_req(&app, &song_queue_id, &user_id).await {
Ok(response) => { Ok(response) => {
@@ -650,6 +813,11 @@ mod tests {
serde_json::from_slice(&body).unwrap() serde_json::from_slice(&body).unwrap()
} }
pub async fn format_url_with_value(endpoint: &str, value: &uuid::Uuid) -> String {
let last = endpoint.len() - 5;
format!("{}/{value}", &endpoint[0..last])
}
// TODO: Change the name of the function to be more expressive and put into it's own module // TODO: Change the name of the function to be more expressive and put into it's own module
pub mod payload_data { pub mod payload_data {
pub async fn queue_metadata_payload_data(song_queue_id: &uuid::Uuid) -> serde_json::Value { pub async fn queue_metadata_payload_data(song_queue_id: &uuid::Uuid) -> serde_json::Value {
@@ -755,7 +923,7 @@ mod tests {
assert_eq!(false, resp.data[0].is_nil(), "Should not be empty"); assert_eq!(false, resp.data[0].is_nil(), "Should not be empty");
let song_queue_id = &resp.data[0]; let song_queue_id = &resp.data[0];
let user_id = uuid::Uuid::new_v4(); let user_id = TEST_USER_ID;
println!("User Id: {user_id:?}"); println!("User Id: {user_id:?}");
match song_queue_link_req(&app, &song_queue_id, &user_id).await { match song_queue_link_req(&app, &song_queue_id, &user_id).await {
@@ -929,6 +1097,10 @@ mod tests {
.method(axum::http::Method::PATCH) .method(axum::http::Method::PATCH)
.uri(uri) .uri(uri)
.header(axum::http::header::CONTENT_TYPE, content_type) .header(axum::http::header::CONTENT_TYPE, content_type)
.header(
axum::http::header::AUTHORIZATION,
bearer_auth().await,
)
.body(axum::body::Body::from_stream(body)) .body(axum::body::Body::from_stream(body))
.unwrap(), .unwrap(),
) )
@@ -1391,6 +1563,10 @@ mod tests {
.method(axum::http::Method::GET) .method(axum::http::Method::GET)
.uri(uri) .uri(uri)
.header(axum::http::header::CONTENT_TYPE, "image/jpeg") .header(axum::http::header::CONTENT_TYPE, "image/jpeg")
.header(
axum::http::header::AUTHORIZATION,
bearer_auth().await,
)
.body(axum::body::Body::empty()) .body(axum::body::Body::empty())
.unwrap(), .unwrap(),
) )
@@ -1659,6 +1835,7 @@ mod tests {
.method(axum::http::Method::PATCH) .method(axum::http::Method::PATCH)
.uri(crate::callers::endpoints::QUEUESONGDATAWIPE) .uri(crate::callers::endpoints::QUEUESONGDATAWIPE)
.header(axum::http::header::CONTENT_TYPE, "application/json") .header(axum::http::header::CONTENT_TYPE, "application/json")
.header(axum::http::header::AUTHORIZATION, bearer_auth().await)
.body(axum::body::Body::from(payload.to_string())) .body(axum::body::Body::from(payload.to_string()))
.unwrap(), .unwrap(),
) )
@@ -1779,6 +1956,10 @@ mod tests {
axum::http::header::CONTENT_TYPE, axum::http::header::CONTENT_TYPE,
"application/json", "application/json",
) )
.header(
axum::http::header::AUTHORIZATION,
bearer_auth().await,
)
.body(axum::body::Body::from(payload.to_string())) .body(axum::body::Body::from(payload.to_string()))
.unwrap(), .unwrap(),
) )
@@ -1818,7 +1999,8 @@ mod tests {
let _ = db_mgr::drop_database(&tm_pool, &db_name).await; let _ = db_mgr::drop_database(&tm_pool, &db_name).await;
} }
pub mod after_song_queue { pub mod zzz_after_song_queue {
use futures::StreamExt;
use tower::ServiceExt; use tower::ServiceExt;
#[tokio::test] #[tokio::test]
@@ -1840,15 +2022,7 @@ mod tests {
let app = super::init::app(pool).await; let app = super::init::app(pool).await;
let mut id = uuid::Uuid::nil(); let (id, _, _, _) = test_data::song_id().await.unwrap();
match uuid::Uuid::parse_str("44cf7940-34ff-489f-9124-d0ec90a55af9") {
Ok(val) => {
id = val;
}
Err(err) => {
assert!(false, "Error: {err:?}");
}
};
let uri = format!("{}?id={id}", crate::callers::endpoints::GETSONGS); let uri = format!("{}?id={id}", crate::callers::endpoints::GETSONGS);
@@ -1859,6 +2033,10 @@ mod tests {
.method(axum::http::Method::GET) .method(axum::http::Method::GET)
.uri(uri) .uri(uri)
.header(axum::http::header::CONTENT_TYPE, "application/json") .header(axum::http::header::CONTENT_TYPE, "application/json")
.header(
axum::http::header::AUTHORIZATION,
super::bearer_auth().await,
)
.body(axum::body::Body::empty()) .body(axum::body::Body::empty())
.unwrap(), .unwrap(),
) )
@@ -1901,16 +2079,7 @@ mod tests {
let app = super::init::app(pool).await; let app = super::init::app(pool).await;
let mut id = uuid::Uuid::nil(); let id = test_data::coverart_id().await.unwrap();
match uuid::Uuid::parse_str("996122cd-5ae9-4013-9934-60768d3006ed") {
Ok(val) => {
id = val;
}
Err(err) => {
assert!(false, "Error: {err:?}");
}
};
let uri = format!("{}?id={id}", crate::callers::endpoints::GETCOVERART); let uri = format!("{}?id={id}", crate::callers::endpoints::GETCOVERART);
@@ -1921,6 +2090,10 @@ mod tests {
.method(axum::http::Method::GET) .method(axum::http::Method::GET)
.uri(uri) .uri(uri)
.header(axum::http::header::CONTENT_TYPE, "application/json") .header(axum::http::header::CONTENT_TYPE, "application/json")
.header(
axum::http::header::AUTHORIZATION,
super::bearer_auth().await,
)
.body(axum::body::Body::empty()) .body(axum::body::Body::empty())
.unwrap(), .unwrap(),
) )
@@ -1943,5 +2116,430 @@ mod tests {
let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await; let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await;
} }
pub mod test_data {
pub async fn song_id() -> Result<(uuid::Uuid, String, String, String), uuid::Error> {
match uuid::Uuid::parse_str("44cf7940-34ff-489f-9124-d0ec90a55af9") {
Ok(id) => Ok((
id,
String::from("tests/I/"),
String::from("track01.flac"),
String::from("tests/I/Coverart-1.jpg"),
)),
Err(err) => Err(err),
}
}
pub async fn other_song_id() -> Result<(uuid::Uuid, String, String, String), uuid::Error>
{
match uuid::Uuid::parse_str("94cf7940-34ff-489f-9124-d0ec90a55af4") {
Ok(id) => Ok((
id,
String::from("tests/I/"),
String::from("track02.flac"),
String::from("tests/I/Coverart-2.jpg"),
)),
Err(err) => Err(err),
}
}
pub async fn coverart_id() -> Result<uuid::Uuid, uuid::Error> {
uuid::Uuid::parse_str("996122cd-5ae9-4013-9934-60768d3006ed")
}
}
#[tokio::test]
async fn test_stream_song() {
let tm_pool = super::db_mgr::get_pool().await.unwrap();
let db_name = super::db_mgr::generate_db_name().await;
match super::db_mgr::create_database(&tm_pool, &db_name).await {
Ok(_) => {
println!("Success");
}
Err(err) => {
assert!(false, "Error: {:?}", err);
}
}
let pool = super::db_mgr::connect_to_db(&db_name).await.unwrap();
super::db_mgr::migrations(&pool).await;
let app = super::init::app(pool).await;
let (id, _, _, _) = test_data::song_id().await.unwrap();
let my_url = crate::callers::endpoints::STREAMSONG;
let last = my_url.len() - 5;
let uri = format!("{}/{id}", &my_url[0..last]);
match app
.clone()
.oneshot(
axum::http::Request::builder()
.method(axum::http::Method::GET)
.uri(&uri)
.header(
axum::http::header::AUTHORIZATION,
super::bearer_auth().await,
)
.body(axum::body::Body::empty())
.unwrap(),
)
.await
{
Ok(response) => {
let e = response.into_body();
let mut data = e.into_data_stream();
while let Some(chunk) = data.next().await {
match chunk {
Ok(_data) => {}
Err(err) => {
assert!(false, "Error: {err:?}");
}
}
}
}
Err(err) => {
assert!(false, "Error: {err:?}");
}
}
let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await;
}
#[tokio::test]
async fn test_download_song() {
let tm_pool = super::db_mgr::get_pool().await.unwrap();
let db_name = super::db_mgr::generate_db_name().await;
match super::db_mgr::create_database(&tm_pool, &db_name).await {
Ok(_) => {
println!("Success");
}
Err(err) => {
assert!(false, "Error: {:?}", err);
}
}
let pool = super::db_mgr::connect_to_db(&db_name).await.unwrap();
super::db_mgr::migrations(&pool).await;
let app = super::init::app(pool).await;
let (id, _, _, _) = test_data::song_id().await.unwrap();
let uri =
super::format_url_with_value(crate::callers::endpoints::DOWNLOADSONG, &id).await;
match app
.clone()
.oneshot(
axum::http::Request::builder()
.method(axum::http::Method::GET)
.uri(&uri)
.header(
axum::http::header::AUTHORIZATION,
super::bearer_auth().await,
)
.body(axum::body::Body::empty())
.unwrap(),
)
.await
{
Ok(response) => {
let e = response.into_body();
let mut data = e.into_data_stream();
while let Some(chunk) = data.next().await {
match chunk {
Ok(_data) => {}
Err(err) => {
assert!(false, "Error: {err:?}");
}
}
}
}
Err(err) => {
assert!(false, "Error: {err:?}");
}
}
let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await;
}
#[tokio::test]
async fn test_download_coverart() {
let tm_pool = super::db_mgr::get_pool().await.unwrap();
let db_name = super::db_mgr::generate_db_name().await;
match super::db_mgr::create_database(&tm_pool, &db_name).await {
Ok(_) => {
println!("Success");
}
Err(err) => {
assert!(false, "Error: {:?}", err);
}
}
let pool = super::db_mgr::connect_to_db(&db_name).await.unwrap();
super::db_mgr::migrations(&pool).await;
let app = super::init::app(pool).await;
let id = test_data::coverart_id().await.unwrap();
let uri =
super::format_url_with_value(crate::callers::endpoints::DOWNLOADCOVERART, &id)
.await;
match app
.clone()
.oneshot(
axum::http::Request::builder()
.method(axum::http::Method::GET)
.uri(&uri)
.header(
axum::http::header::AUTHORIZATION,
super::bearer_auth().await,
)
.body(axum::body::Body::empty())
.unwrap(),
)
.await
{
Ok(response) => {
let e = response.into_body();
let mut data = e.into_data_stream();
while let Some(chunk) = data.next().await {
match chunk {
Ok(_data) => {}
Err(err) => {
assert!(false, "Error: {err:?}");
}
}
}
}
Err(err) => {
assert!(false, "Error: {err:?}");
}
}
let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await;
}
async fn get_test_data(
song_directory: &String,
song_filename: &String,
coverart_path: &String,
) -> Result<(Vec<u8>, Vec<u8>), std::io::Error> {
let song = icarus_models::song::Song {
directory: song_directory.clone(),
filename: song_filename.clone(),
..Default::default()
};
let coverart = icarus_models::coverart::CoverArt {
path: coverart_path.clone(),
..Default::default()
};
match song.to_data() {
Ok(song_data) => match coverart.to_data() {
Ok(coverart_data) => Ok((song_data, coverart_data)),
Err(err) => Err(err),
},
Err(err) => Err(err),
}
}
async fn save_test_again(
song_directory: &String,
song_filename: &String,
song_data: Vec<u8>,
coverart_path: &String,
coverart_data: Vec<u8>,
) -> Result<(), std::io::Error> {
let song = icarus_models::song::Song {
directory: song_directory.clone(),
filename: song_filename.clone(),
..Default::default()
};
let coverart = icarus_models::coverart::CoverArt {
path: coverart_path.clone(),
..Default::default()
};
use std::io::Write;
match song.song_path() {
Ok(song_path) => {
let song_p = std::path::Path::new(&song_path);
match std::fs::File::create(song_p) {
Ok(mut song_file) => match song_file.write_all(&song_data) {
Ok(_) => {}
Err(err) => {
return Err(err);
}
},
Err(err) => {
return Err(err);
}
}
}
Err(err) => {
return Err(err);
}
}
let coverart_p = std::path::Path::new(&coverart.path);
match std::fs::File::create(coverart_p) {
Ok(mut coverart_file) => match coverart_file.write_all(&coverart_data) {
Ok(_) => Ok(()),
Err(err) => Err(err),
},
Err(err) => Err(err),
}
}
#[tokio::test]
async fn test_last_delete_song() {
let tm_pool = super::db_mgr::get_pool().await.unwrap();
let db_name = super::db_mgr::generate_db_name().await;
match super::db_mgr::create_database(&tm_pool, &db_name).await {
Ok(_) => {
println!("Success");
}
Err(err) => {
assert!(false, "Error: {:?}", err);
}
}
let pool = super::db_mgr::connect_to_db(&db_name).await.unwrap();
super::db_mgr::migrations(&pool).await;
let app = super::init::app(pool).await;
let (id, song_directory, song_filename, coverart_path) =
test_data::other_song_id().await.unwrap();
let (song_data, coverart_data) =
get_test_data(&song_directory, &song_filename, &coverart_path)
.await
.unwrap();
let uri =
super::format_url_with_value(crate::callers::endpoints::DELETESONG, &id).await;
match app
.clone()
.oneshot(
axum::http::Request::builder()
.method(axum::http::Method::DELETE)
.uri(&uri)
.header(
axum::http::header::AUTHORIZATION,
super::bearer_auth().await,
)
.body(axum::body::Body::empty())
.unwrap(),
)
.await
{
Ok(response) => {
let resp = super::get_resp_data::<
crate::callers::song::response::delete_song::Response,
>(response)
.await;
assert_eq!(false, resp.data.is_empty(), "Response has no data");
let song_and_coverart = &resp.data[0];
assert_eq!(
id, song_and_coverart.song.id,
"Song Ids do not match {id:?} {:?}",
song_and_coverart.song.id
);
match save_test_again(
&song_directory,
&song_filename,
song_data,
&coverart_path,
coverart_data,
)
.await
{
Ok(_) => {}
Err(err) => {
assert!(false, "Error: {err:?}");
}
}
}
Err(err) => {
assert!(false, "Error: {err:?}");
}
}
let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await;
}
#[tokio::test]
async fn test_get_all_songs() {
let tm_pool = super::db_mgr::get_pool().await.unwrap();
let db_name = super::db_mgr::generate_db_name().await;
match super::db_mgr::create_database(&tm_pool, &db_name).await {
Ok(_) => {
println!("Success");
}
Err(err) => {
assert!(false, "Error: {:?}", err);
}
}
let pool = super::db_mgr::connect_to_db(&db_name).await.unwrap();
super::db_mgr::migrations(&pool).await;
let app = super::init::app(pool).await;
match app
.clone()
.oneshot(
axum::http::Request::builder()
.method(axum::http::Method::GET)
.uri(crate::callers::endpoints::GETALLSONGS)
.header(axum::http::header::CONTENT_TYPE, "application/json")
.header(
axum::http::header::AUTHORIZATION,
super::bearer_auth().await,
)
.body(axum::body::Body::empty())
.unwrap(),
)
.await
{
Ok(response) => {
let resp = super::get_resp_data::<
crate::callers::song::response::get_songs::Response,
>(response)
.await;
assert_eq!(false, resp.data.is_empty(), "Should not be empty");
let songs = &resp.data;
assert_eq!(
2,
songs.len(),
"Returned song count does not match. Returned song count {:?} song count {}",
songs.len(),
2
);
}
Err(err) => {
assert!(false, "Error: {err:?}");
}
}
let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await;
}
} }
} }
@@ -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" VALUES('996122cd-5ae9-4013-9934-60768d3006ed', 'I', 'tests/I/Coverart-1.jpg', '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" VALUES('d96122cd-5ae9-4013-9934-60768d3006e9', 'I', 'tests/I/Coverart-2.jpg', '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