From 703fc49f329acc66b5c1cdcc5071e17b2a48e331 Mon Sep 17 00:00:00 2001 From: KD Date: Mon, 28 Jul 2025 16:40:14 -0400 Subject: [PATCH] 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 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/callers/mod.rs | 1 + src/callers/song.rs | 35 +++++++++++++++++++++++++ src/main.rs | 64 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 102 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eeb8c42..1f02946 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -762,7 +762,7 @@ dependencies = [ [[package]] name = "icarus" -version = "0.1.96" +version = "0.1.97" dependencies = [ "axum", "common-multipart-rfc7578", diff --git a/Cargo.toml b/Cargo.toml index 456f330..2773535 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "icarus" -version = "0.1.96" +version = "0.1.97" edition = "2024" rust-version = "1.88" diff --git a/src/callers/mod.rs b/src/callers/mod.rs index b04f7a2..e0bfeda 100644 --- a/src/callers/mod.rs +++ b/src/callers/mod.rs @@ -18,6 +18,7 @@ pub mod endpoints { pub const CREATESONG: &str = "/api/v2/song"; pub const GETSONGS: &str = "/api/v2/song"; pub const STREAMSONG: &str = "/api/v2/song/stream/{id}"; + pub const DOWNLOADSONG: &str = "/api/v2/song/download/{id}"; pub const CREATECOVERART: &str = "/api/v2/coverart"; pub const GETCOVERART: &str = "/api/v2/coverart"; } diff --git a/src/callers/song.rs b/src/callers/song.rs index ab175c3..6d8d6e9 100644 --- a/src/callers/song.rs +++ b/src/callers/song.rs @@ -1059,4 +1059,39 @@ pub mod endpoint { )), } } + + pub async fn download_song( + axum::Extension(pool): axum::Extension, + axum::extract::Path(id): axum::extract::Path, + ) -> (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(), + ), + } + } } diff --git a/src/main.rs b/src/main.rs index d1d99f7..6a4a58d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -127,6 +127,10 @@ pub mod init { crate::callers::endpoints::STREAMSONG, get(crate::callers::song::endpoint::stream_song), ) + .route( + crate::callers::endpoints::DOWNLOADSONG, + get(crate::callers::song::endpoint::download_song), + ) } pub async fn app() -> axum::Router { @@ -654,6 +658,11 @@ mod tests { 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 pub mod payload_data { pub async fn queue_metadata_payload_data(song_queue_id: &uuid::Uuid) -> serde_json::Value { @@ -1997,5 +2006,60 @@ mod tests { 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) + .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; + } } }