diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 74b4171..d13914b 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -68,7 +68,7 @@ jobs: env: ROOT_DIRECTORY: "/tmp" DATABASE_URL: "postgres://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}" - run: cargo test --verbose + run: cargo test --verbose -- --test-threads=1 - name: Run clippy run: cargo clippy -- -D warnings diff --git a/Cargo.lock b/Cargo.lock index b76cb8f..623c074 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -762,7 +762,7 @@ dependencies = [ [[package]] name = "icarus" -version = "0.1.98" +version = "0.1.99" dependencies = [ "axum", "common-multipart-rfc7578", diff --git a/Cargo.toml b/Cargo.toml index 1c63dbb..244a60c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "icarus" -version = "0.1.98" +version = "0.1.99" edition = "2024" rust-version = "1.88" diff --git a/src/callers/coverart.rs b/src/callers/coverart.rs index 3818f57..a2b2aa0 100644 --- a/src/callers/coverart.rs +++ b/src/callers/coverart.rs @@ -376,6 +376,88 @@ pub mod cov_db { Err(_) => Err(sqlx::Error::RowNotFound), } } + + pub async fn get_coverart_with_song_id( + pool: &sqlx::PgPool, + song_id: &uuid::Uuid, + ) -> Result { + 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 { + 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 { diff --git a/src/callers/mod.rs b/src/callers/mod.rs index 3c6b223..cadc8ac 100644 --- a/src/callers/mod.rs +++ b/src/callers/mod.rs @@ -19,6 +19,7 @@ pub mod endpoints { 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 DELETESONG: &str = "/api/v2/song/{id}"; pub const CREATECOVERART: &str = "/api/v2/coverart"; pub const GETCOVERART: &str = "/api/v2/coverart"; pub const DOWNLOADCOVERART: &str = "/api/v2/coverart/download/{id}"; diff --git a/src/callers/song.rs b/src/callers/song.rs index 6d8d6e9..fcdcfd6 100644 --- a/src/callers/song.rs +++ b/src/callers/song.rs @@ -173,6 +173,20 @@ pub mod response { pub data: Vec, } } + + pub mod delete_song { + #[derive(Debug, Default, serde::Deserialize, serde::Serialize)] + pub struct SongAndCoverArt { + pub song: icarus_models::song::Song, + pub coverart: icarus_models::coverart::CoverArt, + } + + #[derive(Debug, Default, serde::Deserialize, serde::Serialize)] + pub struct Response { + pub message: String, + pub data: Vec, + } + } } // TODO: Might make a distinction between year and date in a song's tag at some point @@ -336,6 +350,105 @@ pub mod song_db { Err(_) => Err(sqlx::Error::RowNotFound), } } + + pub async fn delete_song( + pool: &sqlx::PgPool, + id: &uuid::Uuid, + ) -> Result { + 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 { @@ -1094,4 +1207,101 @@ pub mod endpoint { ), } } + + pub async fn delete_song( + axum::Extension(pool): axum::Extension, + axum::extract::Path(id): axum::extract::Path, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + 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)) + } + } + } } diff --git a/src/main.rs b/src/main.rs index e1c8186..8e9340f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,7 +44,7 @@ async fn main() { } pub mod init { - use axum::routing::{get, patch, post}; + use axum::routing::{delete, get, patch, post}; use std::time::Duration; use tower_http::timeout::TimeoutLayer; @@ -135,6 +135,10 @@ pub mod init { crate::callers::endpoints::DOWNLOADSONG, get(crate::callers::song::endpoint::download_song), ) + .route( + crate::callers::endpoints::DELETESONG, + delete(crate::callers::song::endpoint::delete_song), + ) } pub async fn app() -> axum::Router { @@ -1835,7 +1839,7 @@ mod tests { 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; @@ -2121,5 +2125,62 @@ mod tests { let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await; } + + #[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 = test_data::song_id().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) + .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 + ); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + let _ = super::db_mgr::drop_database(&tm_pool, &db_name).await; + } } }