From 805df87f5b8c3d7680d7acd65e90c09415cff284 Mon Sep 17 00:00:00 2001 From: KD Date: Sun, 24 Aug 2025 19:58:38 -0400 Subject: [PATCH] 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 --- Cargo.lock | 181 +++++++++++++++++++++++++++++++++- Cargo.toml | 4 +- src/callers/coverart.rs | 144 ++++++++++++++++++++++++--- src/callers/metadata.rs | 48 +++++++-- src/callers/song.rs | 210 ++++++++++++++++++++++++++++++++++++---- src/main.rs | 36 +++++++ 6 files changed, 580 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7524b89..313aceb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,15 @@ 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]] name = "atoi" version = "2.0.0" @@ -378,6 +387,17 @@ dependencies = [ "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]] name = "digest" version = "0.10.7" @@ -476,6 +496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -844,6 +865,8 @@ dependencies = [ "tower-http", "tracing-subscriber", "url", + "utoipa", + "utoipa-swagger-ui", "uuid", ] @@ -866,14 +889,15 @@ dependencies = [ [[package]] name = "icarus_models" -version = "0.5.5" -source = "git+ssh://git@git.kundeng.us/phoenix/icarus_models.git?tag=v0.5.5#f43bcaa314694c25cea65dbfcfbabc8fc7bf395a" +version = "0.5.6" +source = "git+ssh://git@git.kundeng.us/phoenix/icarus_models.git?tag=v0.5.6#2d6b550ae6721b41ecc3039799f6a5e873869077" dependencies = [ "josekit", "rand 0.9.1", "serde", "serde_json", "time", + "utoipa", "uuid", ] @@ -992,6 +1016,7 @@ checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown", + "serde", ] [[package]] @@ -1073,6 +1098,15 @@ dependencies = [ "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]] name = "linux-raw-sys" version = "0.9.4" @@ -1644,6 +1678,40 @@ dependencies = [ "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]] name = "rustc-demangle" version = "0.1.25" @@ -1675,6 +1743,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "schannel" version = "0.1.27" @@ -1824,6 +1901,12 @@ dependencies = [ "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" @@ -2473,6 +2556,49 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "uuid" version = "1.17.0" @@ -2503,6 +2629,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2608,6 +2744,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2953,3 +3098,35 @@ dependencies = [ "quote", "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", +] diff --git a/Cargo.toml b/Cargo.toml index 74ef755..2374089 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,10 @@ thiserror = "1.0" base64 = "0.21" 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.5" } +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] diff --git a/src/callers/coverart.rs b/src/callers/coverart.rs index a2b2aa0..b386c1b 100644 --- a/src/callers/coverart.rs +++ b/src/callers/coverart.rs @@ -1,14 +1,26 @@ // 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 id: uuid::Uuid, pub song_queue_id: uuid::Uuid, } pub mod request { + pub mod queue { + #[derive(utoipa::ToSchema)] + pub struct Request { + /// Filename + pub file: String, + #[schema(rename = "type")] + /// File type. Should be a file and not a value + pub file_type: String, + /// Raw data of the cover art file + pub value: Vec, + } + } pub mod link { - #[derive(Debug, Default, serde::Deserialize, serde::Serialize)] + #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Request { pub coverart_id: uuid::Uuid, pub song_queue_id: uuid::Uuid, @@ -16,7 +28,7 @@ pub mod request { } 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 id: Option, pub song_queue_id: Option, @@ -24,7 +36,7 @@ pub mod request { } pub mod fetch_coverart_with_data { - #[derive(Debug, serde::Deserialize, serde::Serialize)] + #[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Params { pub id: Option, pub song_queue_id: Option, @@ -32,7 +44,7 @@ pub mod request { } pub mod create_coverart { - #[derive(Debug, serde::Deserialize, serde::Serialize)] + #[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Request { pub song_id: uuid::Uuid, pub coverart_queue_id: uuid::Uuid, @@ -40,14 +52,14 @@ pub mod request { } 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 coverart_queue_id: uuid::Uuid, } } pub mod get_coverart { - #[derive(Debug, serde::Deserialize, serde::Serialize)] + #[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Params { pub id: Option, } @@ -55,20 +67,20 @@ pub mod request { } pub mod response { - #[derive(Debug, Default, serde::Deserialize, serde::Serialize)] + #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Response { pub message: String, pub data: Vec, } pub mod link { - #[derive(Debug, serde::Deserialize, serde::Serialize)] + #[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Id { pub coverart_id: uuid::Uuid, pub song_queue_id: uuid::Uuid, } - #[derive(Debug, Default, serde::Deserialize, serde::Serialize)] + #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Response { pub message: String, pub data: Vec, @@ -76,7 +88,7 @@ pub mod response { } 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 message: String, pub data: Vec, @@ -84,7 +96,7 @@ pub mod response { } 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 message: String, pub data: Vec>, @@ -92,7 +104,7 @@ pub mod response { } pub mod create_coverart { - #[derive(Debug, Default, serde::Deserialize, serde::Serialize)] + #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Response { pub message: String, pub data: Vec, @@ -100,7 +112,7 @@ pub mod response { } 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 message: String, pub data: Vec, @@ -108,7 +120,7 @@ pub mod response { } pub mod get_coverart { - #[derive(Debug, Default, serde::Deserialize, serde::Serialize)] + #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Response { pub message: String, pub data: Vec, @@ -465,6 +477,18 @@ pub mod endpoint { 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( axum::Extension(pool): axum::Extension, mut multipart: axum::extract::Multipart, @@ -510,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( axum::Extension(pool): axum::Extension, axum::Json(payload): axum::Json, @@ -537,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( axum::Extension(pool): axum::Extension, axum::extract::Query(params): axum::extract::Query< @@ -584,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), + (status = 400, description = "Error fetching queued cover art data", body = Vec) + ) + )] pub async fn fetch_coverart_with_data( axum::Extension(pool): axum::Extension, axum::extract::Path(id): axum::extract::Path, @@ -612,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( axum::Extension(pool): axum::Extension, axum::Json(payload): axum::Json, @@ -672,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( axum::Extension(pool): axum::Extension, axum::Json(payload): axum::Json, @@ -701,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( axum::Extension(pool): axum::Extension, axum::extract::Query(params): axum::extract::Query, @@ -729,6 +831,18 @@ 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), + (status = 404, description = "Cover art not found", body = Vec) + ) + )] pub async fn download_coverart( axum::Extension(pool): axum::Extension, axum::extract::Path(id): axum::extract::Path, diff --git a/src/callers/metadata.rs b/src/callers/metadata.rs index 6825fbd..72f5d10 100644 --- a/src/callers/metadata.rs +++ b/src/callers/metadata.rs @@ -1,10 +1,9 @@ // TODO: Explicitly make this module target queueing a song's metadata pub mod request { - pub mod queue_metadata { use serde::{Deserialize, Serialize}; - #[derive(Debug, Default, Deserialize, Serialize, sqlx::FromRow)] + #[derive(Debug, Default, Deserialize, Serialize, sqlx::FromRow, utoipa::ToSchema)] pub struct Request { pub song_queue_id: uuid::Uuid, pub album: String, @@ -43,7 +42,13 @@ pub mod request { pub mod fetch_metadata { #[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 id: Option, @@ -53,11 +58,10 @@ pub mod request { } pub mod response { - pub mod queue_metadata { use serde::{Deserialize, Serialize}; - #[derive(Default, Deserialize, Serialize)] + #[derive(Default, Deserialize, Serialize, utoipa::ToSchema)] pub struct Response { pub message: String, pub data: Vec, @@ -67,7 +71,7 @@ pub mod response { pub mod fetch_metadata { use serde::{Deserialize, Serialize}; - #[derive(Default, Deserialize, Serialize)] + #[derive(Default, Deserialize, Serialize, utoipa::ToSchema)] pub struct Response { pub message: String, pub data: Vec, @@ -83,7 +87,7 @@ pub mod metadata_queue { 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 id: uuid::Uuid, pub metadata: serde_json::Value, @@ -204,9 +208,24 @@ pub mod metadata_queue { } } +/// Module for metadata related endpoints pub mod endpoint { 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( axum::Extension(pool): axum::Extension, Json(payload): Json, @@ -221,7 +240,7 @@ pub mod endpoint { response.message = if response.data.is_empty() { String::from("Error") } else { - String::from("Success") + String::from(super::super::response::SUCCESSFUL) }; (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( axum::Extension(pool): axum::Extension, axum::extract::Query(params): axum::extract::Query, diff --git a/src/callers/song.rs b/src/callers/song.rs index 2bfc552..2051fda 100644 --- a/src/callers/song.rs +++ b/src/callers/song.rs @@ -7,8 +7,21 @@ pub mod request { 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, + } + } + pub mod update_status { - #[derive(Default, serde::Deserialize, serde::Serialize)] + #[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Request { pub id: uuid::Uuid, pub status: String, @@ -16,7 +29,7 @@ pub mod request { } pub mod create_metadata { - #[derive(Debug, serde::Deserialize, serde::Serialize)] + #[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Request { pub title: String, pub artist: String, @@ -79,14 +92,14 @@ pub mod request { } 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 song_queue_id: uuid::Uuid, } } pub mod link_user_id { - #[derive(Debug, serde::Deserialize, serde::Serialize)] + #[derive(Debug, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Request { pub song_queue_id: uuid::Uuid, pub user_id: uuid::Uuid, @@ -94,7 +107,7 @@ pub mod request { } pub mod get_songs { - #[derive(Debug, Default, serde::Deserialize, serde::Serialize)] + #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Params { pub id: Option, } @@ -104,16 +117,18 @@ pub mod request { pub mod response { use serde::{Deserialize, Serialize}; - #[derive(Default, Deserialize, Serialize)] + /// Song queue response + #[derive(Default, Deserialize, Serialize, utoipa::ToSchema)] pub struct Response { pub message: String, + /// Id of the queued song pub data: Vec, } pub mod fetch_queue_song { use serde::{Deserialize, Serialize}; - #[derive(Default, Deserialize, Serialize)] + #[derive(Default, Deserialize, Serialize, utoipa::ToSchema)] pub struct Response { pub message: String, pub data: Vec, @@ -121,13 +136,13 @@ pub mod response { } pub mod update_status { - #[derive(serde::Deserialize, serde::Serialize)] + #[derive(serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct ChangedStatus { pub old_status: String, pub new_status: String, } - #[derive(Default, serde::Deserialize, serde::Serialize)] + #[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Response { pub message: String, pub data: Vec, @@ -135,7 +150,7 @@ pub mod response { } pub mod update_song_queue { - #[derive(Default, serde::Deserialize, serde::Serialize)] + #[derive(Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Response { pub message: String, pub data: Vec, @@ -143,7 +158,7 @@ pub mod response { } pub mod create_metadata { - #[derive(Debug, Default, serde::Deserialize, serde::Serialize)] + #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Response { pub message: String, pub data: Vec, @@ -151,7 +166,7 @@ pub mod response { } 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 message: String, pub data: Vec, @@ -159,7 +174,7 @@ pub mod response { } 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 message: String, pub data: Vec, @@ -167,7 +182,7 @@ pub mod response { } pub mod get_songs { - #[derive(Debug, Default, serde::Deserialize, serde::Serialize)] + #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Response { pub message: String, pub data: Vec, @@ -175,13 +190,13 @@ pub mod response { } pub mod delete_song { - #[derive(Debug, Default, serde::Deserialize, serde::Serialize)] + #[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)] + #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct Response { pub message: String, pub data: Vec, @@ -561,7 +576,8 @@ mod song_queue { 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 id: uuid::Uuid, pub filename: String, @@ -854,12 +870,27 @@ mod song_queue { } } +/// Module for song related endpoints pub mod endpoint { use axum::{Json, http::StatusCode, response::IntoResponse}; + use std::io::Write; 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( axum::Extension(pool): axum::Extension, mut multipart: axum::extract::Multipart, @@ -908,6 +939,20 @@ pub mod endpoint { (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( axum::Extension(pool): axum::Extension, axum::Json(payload): axum::Json, @@ -939,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( axum::Extension(pool): axum::Extension, ) -> ( @@ -961,6 +1015,16 @@ 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), + (status = 400, description = "Linkage failed", body = Vec) + ) + )] pub async fn download_flac( axum::Extension(pool): axum::Extension, axum::extract::Path(id): axum::extract::Path, @@ -989,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( axum::Extension(pool): axum::Extension, axum::Json(payload): axum::Json, @@ -1041,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( axum::extract::Path(id): axum::extract::Path, axum::Extension(pool): axum::Extension, @@ -1087,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( axum::Extension(pool): axum::Extension, axum::Json(payload): axum::Json, @@ -1170,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( axum::Extension(pool): axum::Extension, axum::Json(payload): axum::Json, @@ -1200,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( axum::Extension(pool): axum::Extension, axum::extract::Query(params): axum::extract::Query, @@ -1228,6 +1363,15 @@ 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, ) -> ( @@ -1249,6 +1393,16 @@ pub mod endpoint { } } + /// 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), + (status = 500, description = "Error streaming song", body = (u64, String)) + ) + )] pub async fn stream_song( axum::Extension(pool): axum::Extension, axum::extract::Path(id): axum::extract::Path, @@ -1296,6 +1450,17 @@ pub mod endpoint { } } + /// 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)), + (status = 404, description = "Song not found", body = (u64, Vec)), + (status = 400, description = "Error downloading song", body = (u64, Vec)) + ) + )] pub async fn download_song( axum::Extension(pool): axum::Extension, axum::extract::Path(id): axum::extract::Path, @@ -1331,6 +1496,17 @@ pub mod endpoint { } } + /// 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, axum::extract::Path(id): axum::extract::Path, diff --git a/src/main.rs b/src/main.rs index f016dbf..369894f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,12 +48,44 @@ pub mod init { use axum::routing::{delete, get, patch, post}; use std::time::Duration; 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 { axum::Router::new() .route(crate::ROOT, get(crate::root)) @@ -211,6 +243,10 @@ pub mod init { routes() .await + .merge( + utoipa_swagger_ui::SwaggerUi::new("/swagger-ui") + .url("/api-docs/openapi.json", ApiDoc::openapi()), + ) .layer(axum::Extension(pool)) .layer(axum::extract::DefaultBodyLimit::max(1024 * 1024 * 1024)) .layer(TimeoutLayer::new(Duration::from_secs(300)))