diff --git a/Cargo.lock b/Cargo.lock index be020e7..eeb8c42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -630,6 +630,16 @@ dependencies = [ "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]] name = "heck" version = "0.5.0" @@ -752,7 +762,7 @@ dependencies = [ [[package]] name = "icarus" -version = "0.1.95" +version = "0.1.96" dependencies = [ "axum", "common-multipart-rfc7578", @@ -760,6 +770,7 @@ dependencies = [ "icarus_envy", "icarus_meta", "icarus_models", + "mime_guess", "serde", "serde_json", "sqlx", @@ -2125,9 +2136,13 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "hdrhistogram", + "indexmap", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", diff --git a/Cargo.toml b/Cargo.toml index fd0eb73..456f330 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "icarus" -version = "0.1.95" +version = "0.1.96" edition = "2024" rust-version = "1.88" @@ -8,12 +8,13 @@ rust-version = "1.88" axum = { version = "0.8.4", features = ["multipart"] } serde = { version = "1.0.219", features = ["derive"] } serde_json = { version = "1.0.140" } -tower = { version = "0.5.2" } +tower = { version = "0.5.2", 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"] } tracing-subscriber = "0.3.19" futures = { version = "0.3.31" } +mime_guess = { version = "2.0.5" } uuid = { version = "1.17.0", features = ["v4", "serde"] } sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio-native-tls", "time", "uuid"] } time = { version = "0.3.41", features = ["formatting", "macros", "parsing", "serde"] } diff --git a/src/callers/coverart.rs b/src/callers/coverart.rs index 31f8045..7a3398a 100644 --- a/src/callers/coverart.rs +++ b/src/callers/coverart.rs @@ -562,7 +562,10 @@ pub mod endpoint { match super::cov_db::create(&pool, &coverart, &song.id).await { Ok(id) => { + // TODO: Populate song_id coverart.id = id; + println!("Cover Art created"); + response.message = String::from("Successful"); response.data.push(coverart); diff --git a/src/callers/mod.rs b/src/callers/mod.rs index d81789b..b04f7a2 100644 --- a/src/callers/mod.rs +++ b/src/callers/mod.rs @@ -17,6 +17,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 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 b752536..ab175c3 100644 --- a/src/callers/song.rs +++ b/src/callers/song.rs @@ -745,6 +745,7 @@ pub mod endpoint { } } + // TODO: Rename pub async fn download_flac( axum::Extension(pool): axum::Extension, axum::extract::Path(id): axum::extract::Path, @@ -1011,4 +1012,51 @@ pub mod endpoint { } } } + + pub async fn stream_song( + axum::Extension(pool): axum::Extension, + axum::extract::Path(id): axum::extract::Path, + ) -> 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", + )), + } + } } diff --git a/src/main.rs b/src/main.rs index 4e8798e..d1d99f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -123,6 +123,10 @@ pub mod init { crate::callers::endpoints::GETCOVERART, get(crate::callers::coverart::endpoint::get_coverart), ) + .route( + crate::callers::endpoints::STREAMSONG, + get(crate::callers::song::endpoint::stream_song), + ) } pub async fn app() -> axum::Router { @@ -1819,6 +1823,7 @@ mod tests { } pub mod after_song_queue { + use futures::StreamExt; use tower::ServiceExt; #[tokio::test] @@ -1840,15 +1845,7 @@ mod tests { let app = super::init::app(pool).await; - let mut id = uuid::Uuid::nil(); - match uuid::Uuid::parse_str("44cf7940-34ff-489f-9124-d0ec90a55af9") { - Ok(val) => { - id = val; - } - Err(err) => { - assert!(false, "Error: {err:?}"); - } - }; + let id = test_data::song_id().await.unwrap(); let uri = format!("{}?id={id}", crate::callers::endpoints::GETSONGS); @@ -1901,16 +1898,7 @@ mod tests { let app = super::init::app(pool).await; - let mut id = uuid::Uuid::nil(); - - match uuid::Uuid::parse_str("996122cd-5ae9-4013-9934-60768d3006ed") { - Ok(val) => { - id = val; - } - Err(err) => { - assert!(false, "Error: {err:?}"); - } - }; + let id = test_data::coverart_id().await.unwrap(); let uri = format!("{}?id={id}", crate::callers::endpoints::GETCOVERART); @@ -1943,5 +1931,71 @@ mod tests { let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await; } + + pub mod test_data { + pub async fn song_id() -> Result { + uuid::Uuid::parse_str("44cf7940-34ff-489f-9124-d0ec90a55af9") + } + + pub async fn coverart_id() -> Result { + 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) + .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; + } } }