diff --git a/crates/api/src/routes/songs.rs b/crates/api/src/routes/songs.rs index 4b45f6c..3a5c55f 100644 --- a/crates/api/src/routes/songs.rs +++ b/crates/api/src/routes/songs.rs @@ -48,12 +48,36 @@ pub async fn list_songs( .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: e.to_string() }))) } +#[derive(serde::Deserialize)] +pub struct UpdateSongRequest { + pub title: Option, + pub artist: Option, + pub original_key: Option, +} + pub async fn update_song( - State(_state): State>, - Path(_id): Path, - Json(_body): Json, -) -> StatusCode { - StatusCode::NOT_IMPLEMENTED + State(state): State>, + Path(id): Path, + Json(body): Json, +) -> Result, (StatusCode, Json)> { + let uuid = Uuid::parse_str(&id).map_err(|_| { + (StatusCode::BAD_REQUEST, Json(ErrorResponse { error: "Invalid ID".into() })) + })?; + + state.songs + .update_meta( + uuid, + body.title.as_deref(), + body.artist.as_deref(), + body.original_key.as_deref(), + ) + .await + .map(Json) + .map_err(|e| match e { + domain::RepositoryError::NotFound => + (StatusCode::NOT_FOUND, Json(ErrorResponse { error: "Not found".into() })), + e => (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: e.to_string() })), + }) } pub async fn get_song( diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 737e1cd..ff9e033 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -25,6 +25,16 @@ impl SongService { pub async fn delete(&self, id: Uuid) -> Result<(), RepositoryError> { self.repo.delete(id).await } + + pub async fn update_meta( + &self, + id: Uuid, + title: Option<&str>, + artist: Option<&str>, + original_key: Option<&str>, + ) -> Result { + self.repo.update_meta(id, title, artist, original_key).await + } } pub struct SongSearchService { diff --git a/crates/domain/src/ports.rs b/crates/domain/src/ports.rs index 8ff560f..a4bcc08 100644 --- a/crates/domain/src/ports.rs +++ b/crates/domain/src/ports.rs @@ -53,6 +53,13 @@ pub trait SongRepositoryPort: Send + Sync { async fn list(&self) -> Result, RepositoryError>; async fn get(&self, id: Uuid) -> Result, RepositoryError>; async fn delete(&self, id: Uuid) -> Result<(), RepositoryError>; + async fn update_meta( + &self, + id: Uuid, + title: Option<&str>, + artist: Option<&str>, + original_key: Option<&str>, + ) -> Result; } #[async_trait] diff --git a/crates/infrastructure/persistence/src/lib.rs b/crates/infrastructure/persistence/src/lib.rs index bab8e5c..74faed7 100644 --- a/crates/infrastructure/persistence/src/lib.rs +++ b/crates/infrastructure/persistence/src/lib.rs @@ -102,6 +102,58 @@ impl SongRepositoryPort for SqliteSongRepository { Ok(()) } } + + async fn update_meta( + &self, + id: Uuid, + title: Option<&str>, + artist: Option<&str>, + original_key: Option<&str>, + ) -> Result { + let id_str = id.to_string(); + + let row = sqlx::query_as::<_, SongRow>( + "SELECT id, title, artist, original_key, preview_chords, body FROM songs WHERE id = ?" + ) + .bind(&id_str) + .fetch_optional(&self.pool) + .await + .map_err(|e| RepositoryError::Internal(e.to_string()))? + .ok_or(RepositoryError::NotFound)?; + + let mut song: Song = serde_json::from_str(&row.body) + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + if let Some(t) = title { song.meta.title = t.to_string(); } + if let Some(a) = artist { song.meta.artist = a.to_string(); } + if let Some(k) = original_key { song.meta.original_key = Some(k.to_string()); } + let new_body = serde_json::to_string(&song) + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + + let new_title = title.unwrap_or(&row.title); + let new_artist = artist.unwrap_or(&row.artist); + let new_key: Option<&str> = original_key.or(row.original_key.as_deref()); + + sqlx::query( + "UPDATE songs SET title = ?, artist = ?, original_key = ?, body = ? WHERE id = ?" + ) + .bind(new_title) + .bind(new_artist) + .bind(new_key) + .bind(&new_body) + .bind(&id_str) + .execute(&self.pool) + .await + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + + let preview_chords: Vec = serde_json::from_str(&row.preview_chords) + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + + Ok(SongSummary { + id, + meta: song.meta, + preview_chords, + }) + } } #[async_trait]