diff --git a/src/api/album.rs b/src/api/album.rs index e718160..bb7cb88 100644 --- a/src/api/album.rs +++ b/src/api/album.rs @@ -1,4 +1,5 @@ use crate::models::frontend; +use crate::util::error::*; use crate::util::serverfn_client::Client; use leptos::prelude::*; @@ -7,14 +8,13 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { use diesel::prelude::*; - use leptos::server_fn::error::NoCustomError; use crate::util::database::get_db_conn; use crate::models::backend; } } #[server(endpoint = "album/get", client = Client)] -pub async fn get_album(id: i32) -> Result, ServerFnError> { +pub async fn get_album(id: i32) -> BackendResult> { use crate::models::backend::Album; use crate::schema::*; @@ -24,9 +24,7 @@ pub async fn get_album(id: i32) -> Result, ServerFnError .find(id) .first::(db_con) .optional() - .map_err(|e| { - ServerFnError::::ServerError(format!("Error getting album: {e}")) - })?; + .context("Error loading album from database")?; let Some(album) = album else { return Ok(None) }; @@ -34,7 +32,8 @@ pub async fn get_album(id: i32) -> Result, ServerFnError .filter(album_artists::album_id.eq(id)) .inner_join(artists::table.on(album_artists::artist_id.eq(artists::id))) .select(artists::all_columns) - .load(db_con)?; + .load(db_con) + .context("Error loading album artists from database")?; let img = album .image_path @@ -52,12 +51,14 @@ pub async fn get_album(id: i32) -> Result, ServerFnError } #[server(endpoint = "album/get_songs", client = Client)] -pub async fn get_songs(id: i32) -> Result, ServerFnError> { +pub async fn get_songs(id: i32) -> BackendResult> { use crate::api::auth::get_logged_in_user; use crate::schema::*; use std::collections::HashMap; - let user = get_logged_in_user().await?; + let user = get_logged_in_user() + .await + .context("Error getting logged-in user")?; let db_con = &mut get_db_conn(); @@ -94,7 +95,8 @@ pub async fn get_songs(id: i32) -> Result, ServerFnError> { song_dislikes::all_columns.nullable(), )) .order(songs::track.asc()) - .load(db_con)?; + .load(db_con) + .context("Error loading album songs from database")?; song_list } else { let song_list: Vec<( @@ -115,7 +117,8 @@ pub async fn get_songs(id: i32) -> Result, ServerFnError> { artists::all_columns.nullable(), )) .order(songs::track.asc()) - .load(db_con)?; + .load(db_con) + .context("Error loading album songs from database")?; let song_list: Vec<( backend::Album, diff --git a/src/api/albums.rs b/src/api/albums.rs index 6e161df..00767c5 100644 --- a/src/api/albums.rs +++ b/src/api/albums.rs @@ -1,7 +1,7 @@ +use crate::util::error::*; use crate::util::serverfn_client::Client; -use leptos::prelude::*; - use cfg_if::cfg_if; +use leptos::prelude::*; cfg_if! { if #[cfg(feature = "ssr")] { @@ -27,18 +27,17 @@ pub async fn add_album( album_title: String, release_date: Option, image_path: Option, -) -> Result<(), ServerFnError> { +) -> BackendResult<()> { use crate::models::backend::NewAlbum; use crate::schema::albums::{self}; - use leptos::server_fn::error::NoCustomError; let parsed_release_date = match release_date { Some(date) => match NaiveDate::parse_from_str(date.trim(), "%Y-%m-%d") { Ok(parsed_date) => Some(parsed_date), - Err(_e) => { - return Err(ServerFnError::::ServerError( - "Invalid release date".to_string(), - )) + Err(e) => { + return Err( + InputError::InvalidInput(format!("Error parsing release date: {e}")).into(), + ); } }, None => None, @@ -56,9 +55,7 @@ pub async fn add_album( diesel::insert_into(albums::table) .values(&new_album) .execute(db) - .map_err(|e| { - ServerFnError::::ServerError(format!("Error adding album: {e}")) - })?; + .context("Error inserting new album into database")?; Ok(()) } diff --git a/src/api/artists.rs b/src/api/artists.rs index 3e5a33f..5cf7331 100644 --- a/src/api/artists.rs +++ b/src/api/artists.rs @@ -4,6 +4,7 @@ use cfg_if::cfg_if; use crate::models::backend::Artist; use crate::models::frontend; +use crate::util::error::*; use crate::util::serverfn_client::Client; cfg_if! { @@ -26,9 +27,8 @@ cfg_if! { /// * `Result<(), Box>` - A empty result if successful, or an error /// #[server(endpoint = "artists/add-artist", client = Client)] -pub async fn add_artist(artist_name: String) -> Result<(), ServerFnError> { +pub async fn add_artist(artist_name: String) -> BackendResult<()> { use crate::schema::artists::dsl::*; - use leptos::server_fn::error::NoCustomError; let new_artist = NewArtist { name: artist_name }; @@ -36,26 +36,21 @@ pub async fn add_artist(artist_name: String) -> Result<(), ServerFnError> { diesel::insert_into(artists) .values(&new_artist) .execute(db) - .map_err(|e| { - ServerFnError::::ServerError(format!("Error adding artist: {e}")) - })?; + .context("Error inserting new artist into database")?; Ok(()) } #[server(endpoint = "artists/get", client = Client)] -pub async fn get_artist_by_id(artist_id: i32) -> Result, ServerFnError> { +pub async fn get_artist_by_id(artist_id: i32) -> BackendResult> { use crate::schema::artists::dsl::*; - use leptos::server_fn::error::NoCustomError; let db = &mut get_db_conn(); let artist = artists .filter(id.eq(artist_id)) .first::(db) .optional() - .map_err(|e| { - ServerFnError::::ServerError(format!("Error getting artist: {e}")) - })?; + .context("Error loading artist from database")?; Ok(artist) } @@ -64,18 +59,12 @@ pub async fn get_artist_by_id(artist_id: i32) -> Result, ServerFn pub async fn top_songs_by_artist( artist_id: i32, limit: Option, -) -> Result, ServerFnError> { +) -> BackendResult> { use crate::api::auth::get_user; use crate::models::backend::Song; use crate::schema::*; - use leptos::server_fn::error::NoCustomError; - let user_id = get_user() - .await - .map_err(|e| { - ServerFnError::ServerError::(format!("Error getting user: {e}")) - })? - .id; + let user_id = get_user().await.context("Error getting logged-in user")?.id; let db = &mut get_db_conn(); let song_play_counts: Vec<(i32, i64)> = if let Some(limit) = limit { @@ -175,8 +164,8 @@ pub async fn top_songs_by_artist( let plays = song_play_counts .get(&song.id) - .ok_or(ServerFnError::ServerError::( - "Song id not found in history counts".to_string(), + .ok_or(BackendError::InternalError( + "Song id not found in history counts", ))?; top_songs_map.insert(song.id, (songdata, *plays)); @@ -192,7 +181,7 @@ pub async fn top_songs_by_artist( pub async fn albums_by_artist( artist_id: i32, limit: Option, -) -> Result, ServerFnError> { +) -> BackendResult> { use crate::schema::*; let db = &mut get_db_conn(); @@ -219,7 +208,8 @@ pub async fn albums_by_artist( .on(albums::id.eq(album_artists::album_id)), ) .select((albums::all_columns, artists::all_columns)) - .load(db)?; + .load(db) + .context("Error loading album artists from database")?; for (album, artist) in album_artists { if let Some(stored_album) = albums_map.get_mut(&album.id) { diff --git a/src/api/auth.rs b/src/api/auth.rs index 60cb8d2..d9ca80b 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -4,7 +4,6 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { - use leptos::server_fn::error::NoCustomError; use leptos_axum::extract; use axum_login::AuthSession; use crate::util::auth_backend::AuthBackend; @@ -13,18 +12,17 @@ cfg_if! { use crate::api::users::UserCredentials; use crate::models::backend::{NewUser, User}; +use crate::util::error::*; use crate::util::serverfn_client::Client; /// Create a new user and log them in /// Takes in a NewUser struct, with the password in plaintext /// Returns a Result with the error message if the user could not be created #[server(endpoint = "signup", client = Client)] -pub async fn signup(new_user: NewUser) -> Result<(), ServerFnError> { +pub async fn signup(new_user: NewUser) -> BackendResult<()> { // Check LIBRETUNES_DISABLE_SIGNUP env var if std::env::var("LIBRETUNES_DISABLE_SIGNUP").is_ok_and(|v| v == "true") { - return Err(ServerFnError::::ServerError( - "Signup is disabled".to_string(), - )); + return Err(AuthError::SignupDisabled.into()); } use crate::api::users::create_user; @@ -35,13 +33,13 @@ pub async fn signup(new_user: NewUser) -> Result<(), ServerFnError> { ..new_user }; - create_user(&new_user).await.map_err(|e| { - ServerFnError::::ServerError(format!("Error creating user: {e}")) - })?; + create_user(&new_user) + .await + .context("Error creating user")?; - let mut auth_session = extract::>().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting auth session: {e}")) - })?; + let mut auth_session = extract::>() + .await + .context("Error extracting auth session")?; let credentials = UserCredentials { username_or_email: new_user.username.clone(), @@ -49,15 +47,12 @@ pub async fn signup(new_user: NewUser) -> Result<(), ServerFnError> { }; match auth_session.authenticate(credentials).await { - Ok(Some(user)) => auth_session.login(&user).await.map_err(|e| { - ServerFnError::::ServerError(format!("Error logging in user: {e}")) - }), - Ok(None) => Err(ServerFnError::::ServerError( - "Error authenticating user: User not found".to_string(), - )), - Err(e) => Err(ServerFnError::::ServerError(format!( - "Error authenticating user: {e}" - ))), + Ok(Some(user)) => auth_session + .login(&user) + .await + .map_err(|e| AuthError::AuthError(format!("Error logging in user: {e}")).into()), + Ok(None) => Err(AuthError::InvalidCredentials.into()), + Err(e) => Err(AuthError::AuthError(format!("Error authenticating user: {e}")).into()), } } @@ -65,21 +60,22 @@ pub async fn signup(new_user: NewUser) -> Result<(), ServerFnError> { /// Takes in a username or email and a password in plaintext /// Returns a Result with a boolean indicating if the login was successful #[server(endpoint = "login", client = Client)] -pub async fn login(credentials: UserCredentials) -> Result, ServerFnError> { +pub async fn login(credentials: UserCredentials) -> BackendResult> { use crate::api::users::validate_user; - let mut auth_session = extract::>().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting auth session: {e}")) - })?; + let mut auth_session = extract::>() + .await + .context("Error extracting auth session")?; - let user = validate_user(credentials).await.map_err(|e| { - ServerFnError::::ServerError(format!("Error validating user: {e}")) - })?; + let user = validate_user(credentials) + .await + .context("Error validating user credentials")?; if let Some(mut user) = user { - auth_session.login(&user).await.map_err(|e| { - ServerFnError::::ServerError(format!("Error logging in user: {e}")) - })?; + auth_session + .login(&user) + .await + .map_err(|e| AuthError::AuthError(format!("Error logging in user: {e}")))?; user.password = None; Ok(Some(user)) @@ -91,14 +87,15 @@ pub async fn login(credentials: UserCredentials) -> Result, ServerF /// Log a user out /// Returns a Result with the error message if the user could not be logged out #[server(endpoint = "logout", client = Client)] -pub async fn logout() -> Result<(), ServerFnError> { - let mut auth_session = extract::>().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting auth session: {e}")) - })?; +pub async fn logout() -> BackendResult<()> { + let mut auth_session = extract::>() + .await + .context("Error extracting auth session")?; - auth_session.logout().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting auth session: {e}")) - })?; + auth_session + .logout() + .await + .map_err(|e| AuthError::AuthError(format!("Error logging out user: {e}")))?; leptos_axum::redirect("/login"); Ok(()) @@ -107,10 +104,10 @@ pub async fn logout() -> Result<(), ServerFnError> { /// Check if a user is logged in /// Returns a Result with a boolean indicating if the user is logged in #[server(endpoint = "check_auth", client = Client)] -pub async fn check_auth() -> Result { - let auth_session = extract::>().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting auth session: {e}")) - })?; +pub async fn check_auth() -> BackendResult { + let auth_session = extract::>() + .await + .context("Error extracting auth session")?; Ok(auth_session.user.is_some()) } @@ -129,16 +126,17 @@ pub async fn check_auth() -> Result { /// } /// ``` #[cfg(feature = "ssr")] -pub async fn require_auth() -> Result<(), ServerFnError> { - check_auth().await.and_then(|logged_in| { - if logged_in { - Ok(()) - } else { - Err(ServerFnError::::ServerError( - "Unauthorized".to_string(), - )) - } - }) +pub async fn require_auth() -> BackendResult<()> { + check_auth() + .await + .context("Error checking authentication") + .and_then(|logged_in| { + if logged_in { + Ok(()) + } else { + Err(AuthError::Unauthorized.into()) + } + }) } /// Get the current logged-in user @@ -157,23 +155,19 @@ pub async fn require_auth() -> Result<(), ServerFnError> { /// } /// ``` #[cfg(feature = "ssr")] -pub async fn get_user() -> Result { - let auth_session = extract::>().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting auth session: {e}")) - })?; +pub async fn get_user() -> BackendResult { + let auth_session = extract::>() + .await + .context("Error extracting auth session")?; - auth_session - .user - .ok_or(ServerFnError::::ServerError( - "User not logged in".to_string(), - )) + auth_session.user.ok_or(AuthError::Unauthorized.into()) } #[server(endpoint = "get_logged_in_user", client = Client)] -pub async fn get_logged_in_user() -> Result, ServerFnError> { - let auth_session = extract::>().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting auth session: {e}")) - })?; +pub async fn get_logged_in_user() -> BackendResult> { + let auth_session = extract::>() + .await + .context("Error extracting auth session")?; let user = auth_session.user.map(|mut user| { user.password = None; @@ -186,10 +180,10 @@ pub async fn get_logged_in_user() -> Result, ServerFnError> { /// Check if a user is an admin /// Returns a Result with a boolean indicating if the user is logged in and an admin #[server(endpoint = "check_admin", client = Client)] -pub async fn check_admin() -> Result { - let auth_session = extract::>().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting auth session: {e}")) - })?; +pub async fn check_admin() -> BackendResult { + let auth_session = extract::>() + .await + .context("Error extracting auth session")?; Ok(auth_session.user.as_ref().map(|u| u.admin).unwrap_or(false)) } @@ -208,14 +202,12 @@ pub async fn check_admin() -> Result { /// } /// ``` #[cfg(feature = "ssr")] -pub async fn require_admin() -> Result<(), ServerFnError> { +pub async fn require_admin() -> BackendResult<()> { check_admin().await.and_then(|is_admin| { if is_admin { Ok(()) } else { - Err(ServerFnError::::ServerError( - "Unauthorized".to_string(), - )) + Err(AuthError::AdminRequired.into()) } }) } diff --git a/src/api/health.rs b/src/api/health.rs index d18668d..3883c59 100644 --- a/src/api/health.rs +++ b/src/api/health.rs @@ -1,23 +1,23 @@ +use crate::util::error::*; +use crate::util::serverfn_client::Client; use leptos::prelude::*; -use crate::util::serverfn_client::Client; - #[server(endpoint = "health", client = Client)] -pub async fn health() -> Result { +pub async fn health() -> BackendResult { use crate::util::database::get_db_conn; use crate::util::redis::get_redis_conn; use diesel::connection::SimpleConnection; - use server_fn::error::NoCustomError; use tower_sessions_redis_store::fred::interfaces::ClientLike; get_db_conn() .batch_execute("SELECT 1") - .map_err(|e| ServerFnError::::ServerError(format!("Database error: {e}")))?; + .context("Failed to execute database health check query")?; get_redis_conn() .ping::<()>(None) .await - .map_err(|e| ServerFnError::::ServerError(format!("Redis error: {e}")))?; + .map_err(|e| BackendError::InternalError(format!("{e}"))) + .context("Failed to execute Redis health check ping")?; Ok("ok".to_string()) } diff --git a/src/api/history.rs b/src/api/history.rs index 6988239..a12b5bb 100644 --- a/src/api/history.rs +++ b/src/api/history.rs @@ -1,5 +1,6 @@ use crate::models::backend::HistoryEntry; use crate::models::backend::Song; +use crate::util::error::*; use crate::util::serverfn_client::Client; use chrono::NaiveDateTime; use leptos::prelude::*; @@ -8,7 +9,6 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { - use leptos::server_fn::error::NoCustomError; use crate::util::database::get_db_conn; use crate::api::auth::get_user; } @@ -16,35 +16,39 @@ cfg_if! { /// Get the history of the current user. #[server(endpoint = "history/get", client = Client)] -pub async fn get_history(limit: Option) -> Result, ServerFnError> { - let user = get_user().await?; +pub async fn get_history(limit: Option) -> BackendResult> { + let user = get_user().await.context("Error getting logged-in user")?; + let db_con = &mut get_db_conn(); - let history = user.get_history(limit, db_con).map_err(|e| { - ServerFnError::::ServerError(format!("Error getting history: {e}")) - })?; + let history = user + .get_history(limit, db_con) + .context("Error getting history")?; + Ok(history) } /// Get the listen dates and songs of the current user. #[server(endpoint = "history/get_songs", client = Client)] -pub async fn get_history_songs( - limit: Option, -) -> Result, ServerFnError> { - let user = get_user().await?; +pub async fn get_history_songs(limit: Option) -> BackendResult> { + let user = get_user().await.context("Error getting logged-in user")?; + let db_con = &mut get_db_conn(); - let songs = user.get_history_songs(limit, db_con).map_err(|e| { - ServerFnError::::ServerError(format!("Error getting history songs: {e}")) - })?; + + let songs = user + .get_history_songs(limit, db_con) + .context("Error getting history songs")?; + Ok(songs) } /// Add a song to the history of the current user. #[server(endpoint = "history/add", client = Client)] -pub async fn add_history(song_id: i32) -> Result<(), ServerFnError> { - let user = get_user().await?; +pub async fn add_history(song_id: i32) -> BackendResult<()> { + let user = get_user().await.context("Error getting logged-in user")?; + let db_con = &mut get_db_conn(); - user.add_history(song_id, db_con).map_err(|e| { - ServerFnError::::ServerError(format!("Error adding history: {e}")) - })?; + user.add_history(song_id, db_con) + .context("Error adding song to history")?; + Ok(()) } diff --git a/src/api/playlists.rs b/src/api/playlists.rs index 49757b1..c99ef18 100644 --- a/src/api/playlists.rs +++ b/src/api/playlists.rs @@ -1,4 +1,5 @@ use crate::models::{backend, frontend}; +use crate::util::error::*; use crate::util::serverfn_client::Client; use cfg_if::cfg_if; use leptos::prelude::*; @@ -11,14 +12,13 @@ cfg_if! { use crate::util::database::get_db_conn; use crate::util::extract_field::extract_field; use std::collections::HashMap; - use server_fn::error::NoCustomError; use log::*; use crate::schema::*; } } #[cfg(feature = "ssr")] -async fn user_owns_playlist(user_id: i32, playlist_id: i32) -> Result { +async fn user_owns_playlist(user_id: i32, playlist_id: i32) -> BackendResult { let mut db_conn = get_db_conn(); let exists = playlists::table @@ -27,22 +27,15 @@ async fn user_owns_playlist(user_id: i32, playlist_id: i32) -> Result(&mut db_conn) .optional() - .map_err(|e| { - ServerFnError::::ServerError(format!("Error checking playlist: {e}")) - })? + .context("Error loading playlist from database")? .is_some(); Ok(exists) } #[server(endpoint = "playlists/get_all", client = Client)] -pub async fn get_playlists() -> Result, ServerFnError> { - let user_id = get_user() - .await - .map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })? - .id; +pub async fn get_playlists() -> BackendResult> { + let user_id = get_user().await.context("Error getting logged-in user")?.id; let mut db_conn = get_db_conn(); @@ -50,21 +43,14 @@ pub async fn get_playlists() -> Result, ServerFnError> { .filter(playlists::owner_id.eq(user_id)) .select(playlists::all_columns) .load::(&mut db_conn) - .map_err(|e| { - ServerFnError::::ServerError(format!("Error getting playlists: {e}")) - })?; + .context("Error loading playlists from database")?; Ok(playlists) } #[server(endpoint = "playlists/get", client = Client)] -pub async fn get_playlist(playlist_id: i32) -> Result { - let user_id = get_user() - .await - .map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })? - .id; +pub async fn get_playlist(playlist_id: i32) -> BackendResult { + let user_id = get_user().await.context("Error getting logged-in user")?.id; let mut db_conn = get_db_conn(); @@ -73,33 +59,23 @@ pub async fn get_playlist(playlist_id: i32) -> Result::ServerError(format!("Error getting playlist: {e}")) - })?; + .context("Error loading playlist from database")?; Ok(playlist) } #[server(endpoint = "playlists/get_songs", client = Client)] -pub async fn get_playlist_songs(playlist_id: i32) -> Result, ServerFnError> { - let user_id = get_user() - .await - .map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })? - .id; +pub async fn get_playlist_songs(playlist_id: i32) -> BackendResult> { + let user_id = get_user().await.context("Error getting logged-in user")?.id; // Check if the playlist exists and belongs to the user let valid_playlist = user_owns_playlist(user_id, playlist_id) .await - .map_err(|e| { - ServerFnError::::ServerError(format!("Error checking playlist: {e}")) - })?; + .context("Error checking if playlist exists and is owned by user")?; if !valid_playlist { - return Err(ServerFnError::::ServerError( - "Playlist does not exist or does not belong to the user".to_string(), - )); + return Err(AccessError::NotFoundOrUnauthorized + .context("Playlist does not exist or does not belong to the user")); } let mut db_conn = get_db_conn(); @@ -139,7 +115,8 @@ pub async fn get_playlist_songs(playlist_id: i32) -> Result, song_likes::all_columns.nullable(), song_dislikes::all_columns.nullable(), )) - .load(&mut db_conn)?; + .load(&mut db_conn) + .context("Error loading playlist songs from database")?; let mut playlist_songs: HashMap = HashMap::new(); @@ -185,24 +162,19 @@ pub async fn get_playlist_songs(playlist_id: i32) -> Result, } #[server(endpoint = "playlists/add_song", client = Client)] -pub async fn add_song_to_playlist(playlist_id: i32, song_id: i32) -> Result<(), ServerFnError> { +pub async fn add_song_to_playlist(playlist_id: i32, song_id: i32) -> BackendResult<()> { use crate::schema::*; - let user = get_user().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })?; + let user = get_user().await.context("Error getting logged-in user")?; // Check if the playlist exists and belongs to the user let valid_playlist = user_owns_playlist(user.id, playlist_id) .await - .map_err(|e| { - ServerFnError::::ServerError(format!("Error checking playlist: {e}")) - })?; + .context("Error checking if playlist exists and is owned by user")?; if !valid_playlist { - return Err(ServerFnError::::ServerError( - "Playlist does not exist or does not belong to the user".to_string(), - )); + return Err(AccessError::NotFoundOrUnauthorized + .context("Playlist does not exist or does not belong to the user")); } let mut db_conn = get_db_conn(); @@ -213,23 +185,17 @@ pub async fn add_song_to_playlist(playlist_id: i32, song_id: i32) -> Result<(), playlist_songs::song_id.eq(song_id), )) .execute(&mut db_conn) - .map_err(|e| { - ServerFnError::::ServerError(format!( - "Error adding song to playlist: {e}" - )) - })?; + .context("Error adding song to playlist in database")?; Ok(()) } #[server(input = MultipartFormData, endpoint = "playlists/create")] -pub async fn create_playlist(data: MultipartData) -> Result<(), ServerFnError> { +pub async fn create_playlist(data: MultipartData) -> BackendResult<()> { use crate::models::backend::NewPlaylist; use image_convert::{to_webp, ImageResource, WEBPConfig}; - let user = get_user().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })?; + let user = get_user().await.context("Error getting logged-in user")?; // Safe to unwrap - "On the server side, this always returns Some(_). On the client side, always returns None." let mut data = data.into_inner().unwrap(); @@ -246,20 +212,17 @@ pub async fn create_playlist(data: MultipartData) -> Result<(), ServerFnError> { } "picture" => { // Read the image - let bytes = field.bytes().await.map_err(|e| { - ServerFnError::::ServerError(format!( - "Error getting field bytes: {e}" - )) - })?; + let bytes = field + .bytes() + .await + .map_err(|e| InputError::FieldReadError(format!("{e}"))) + .context("Error reading bytes of the picture field")?; // Check if the image is empty if !bytes.is_empty() { let reader = std::io::Cursor::new(bytes); - let image_source = ImageResource::from_reader(reader).map_err(|e| { - ServerFnError::::ServerError(format!( - "Error creating image resource: {e}" - )) - })?; + let image_source = ImageResource::from_reader(reader) + .context("Error creating image resource from reader")?; picture_data = Some(image_source); } @@ -272,7 +235,7 @@ pub async fn create_playlist(data: MultipartData) -> Result<(), ServerFnError> { // Unwrap mandatory fields let name = playlist_name.ok_or_else(|| { - ServerFnError::::ServerError("Missing playlist name".to_string()) + InputError::MissingField("name".to_string()).context("Missing playlist name") })?; let new_playlist = NewPlaylist { @@ -288,33 +251,27 @@ pub async fn create_playlist(data: MultipartData) -> Result<(), ServerFnError> { let playlist = diesel::insert_into(playlists::table) .values(&new_playlist) .get_result::(db_conn) - .map_err(|e| { - ServerFnError::::ServerError(format!("Error creating playlist: {e}")) - })?; + .context("Error creating playlist in database")?; // If a picture was provided, save it to the database if let Some(image_source) = picture_data { let image_path = format!("assets/images/playlist/{}.webp", playlist.id); let mut image_target = ImageResource::from_path(&image_path); - to_webp(&mut image_target, &image_source, &WEBPConfig::new()).map_err(|e| { - ServerFnError::::ServerError(format!( - "Error converting image to webp: {e}" - )) - })?; + to_webp(&mut image_target, &image_source, &WEBPConfig::new()) + .map_err(|e| InputError::InvalidInput(format!("{e}"))) + .context("Error converting image to webp")?; } - Ok::<(), ServerFnError>(()) + Ok::<(), BackendError>(()) }) } #[server(input = MultipartFormData, endpoint = "playlists/edit_image")] -pub async fn edit_playlist_image(data: MultipartData) -> Result<(), ServerFnError> { +pub async fn edit_playlist_image(data: MultipartData) -> BackendResult<()> { use image_convert::{to_webp, ImageResource, WEBPConfig}; - let user = get_user().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })?; + let user = get_user().await.context("Error getting logged-in user")?; // Safe to unwrap - "On the server side, this always returns Some(_). On the client side, always returns None." let mut data = data.into_inner().unwrap(); @@ -331,20 +288,17 @@ pub async fn edit_playlist_image(data: MultipartData) -> Result<(), ServerFnErro } "picture" => { // Read the image - let bytes = field.bytes().await.map_err(|e| { - ServerFnError::::ServerError(format!( - "Error getting field bytes: {e}" - )) - })?; + let bytes = field + .bytes() + .await + .map_err(|e| InputError::FieldReadError(format!("{e}"))) + .context("Error reading bytes of the picture field")?; // Check if the image is empty if !bytes.is_empty() { let reader = std::io::Cursor::new(bytes); - let image_source = ImageResource::from_reader(reader).map_err(|e| { - ServerFnError::::ServerError(format!( - "Error creating image resource: {e}" - )) - })?; + let image_source = ImageResource::from_reader(reader) + .context("Error creating image resource from reader")?; picture_data = Some(image_source); } @@ -356,25 +310,22 @@ pub async fn edit_playlist_image(data: MultipartData) -> Result<(), ServerFnErro } // Unwrap mandatory fields - let playlist_id = playlist_id.ok_or_else(|| { - ServerFnError::::ServerError("Missing playlist name".to_string()) - })?; + let playlist_id = playlist_id + .ok_or_else(|| InputError::MissingField("id".to_string()).context("Missing playlist ID"))?; - let playlist_id: i32 = playlist_id.parse().map_err(|e| { - ServerFnError::::ServerError(format!("Invalid playlist ID: {e}")) - })?; + let playlist_id: i32 = playlist_id + .parse() + .map_err(|e| InputError::InvalidInput(format!("Invalid playlist ID: {e}"))) + .context("Error parsing playlist ID from string")?; // Make sure the playlist exists and belongs to the user let valid_playlist = user_owns_playlist(user.id, playlist_id) .await - .map_err(|e| { - ServerFnError::::ServerError(format!("Error checking playlist: {e}")) - })?; + .context("Error checking if playlist exists and is owned by user")?; if !valid_playlist { - return Err(ServerFnError::::ServerError( - "Playlist does not exist or does not belong to the user".to_string(), - )); + return Err(AccessError::NotFoundOrUnauthorized + .context("Playlist does not exist or does not belong to the user")); } // If a picture was provided, save it to the database @@ -382,65 +333,52 @@ pub async fn edit_playlist_image(data: MultipartData) -> Result<(), ServerFnErro let image_path = format!("assets/images/playlist/{playlist_id}.webp"); let mut image_target = ImageResource::from_path(&image_path); - to_webp(&mut image_target, &image_source, &WEBPConfig::new()).map_err(|e| { - ServerFnError::::ServerError(format!( - "Error converting image to webp: {e}" - )) - })?; + to_webp(&mut image_target, &image_source, &WEBPConfig::new()) + .map_err(|e| InputError::InvalidInput(format!("{e}"))) + .context("Error converting image to webp")?; } Ok(()) } #[server(endpoint = "playlists/delete", client = Client)] -pub async fn delete_playlist(playlist_id: i32) -> Result<(), ServerFnError> { +pub async fn delete_playlist(playlist_id: i32) -> BackendResult<()> { use crate::schema::*; - let user = get_user().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })?; + let user = get_user().await.context("Error getting logged-in user")?; // Check if the playlist exists and belongs to the user let valid_playlist = user_owns_playlist(user.id, playlist_id) .await - .map_err(|e| { - ServerFnError::::ServerError(format!("Error checking playlist: {e}")) - })?; + .context("Error checking if playlist exists and is owned by user")?; if !valid_playlist { - return Err(ServerFnError::::ServerError( - "Playlist does not exist or does not belong to the user".to_string(), - )); + return Err(AccessError::NotFoundOrUnauthorized + .context("Playlist does not exist or does not belong to the user")); } let mut db_conn = get_db_conn(); diesel::delete(playlists::table.find(playlist_id)) .execute(&mut db_conn) - .map_err(|e| { - ServerFnError::::ServerError(format!("Error deleting playlist: {e}")) - })?; + .context("Error deleting playlist from database")?; Ok(()) } #[server(endpoint = "playlists/rename", client = Client)] -pub async fn rename_playlist(id: i32, new_name: String) -> Result<(), ServerFnError> { +pub async fn rename_playlist(id: i32, new_name: String) -> BackendResult<()> { use crate::schema::*; - let user = get_user().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })?; + let user = get_user().await.context("Error getting logged-in user")?; // Check if the playlist exists and belongs to the user - let valid_playlist = user_owns_playlist(user.id, id).await.map_err(|e| { - ServerFnError::::ServerError(format!("Error checking playlist: {e}")) - })?; + let valid_playlist = user_owns_playlist(user.id, id) + .await + .context("Error checking if playlist exists and is owned by user")?; if !valid_playlist { - return Err(ServerFnError::::ServerError( - "Playlist does not exist or does not belong to the user".to_string(), - )); + return Err(AccessError::NotFoundOrUnauthorized.into()); } let mut db_conn = get_db_conn(); @@ -448,9 +386,7 @@ pub async fn rename_playlist(id: i32, new_name: String) -> Result<(), ServerFnEr diesel::update(playlists::table.find(id)) .set(playlists::name.eq(new_name)) .execute(&mut db_conn) - .map_err(|e| { - ServerFnError::::ServerError(format!("Error renaming playlist: {e}")) - })?; + .context("Error renaming playlist in database")?; Ok(()) } diff --git a/src/api/profile.rs b/src/api/profile.rs index 33841db..8dc97df 100644 --- a/src/api/profile.rs +++ b/src/api/profile.rs @@ -1,8 +1,8 @@ +use crate::util::error::*; +use cfg_if::cfg_if; use leptos::prelude::*; use server_fn::codec::{MultipartData, MultipartFormData}; -use cfg_if::cfg_if; - use crate::models::frontend; use crate::util::serverfn_client::Client; use chrono::NaiveDateTime; @@ -10,7 +10,6 @@ use chrono::NaiveDateTime; cfg_if! { if #[cfg(feature = "ssr")] { use crate::api::auth::get_user; - use server_fn::error::NoCustomError; use crate::util::database::get_db_conn; use diesel::prelude::*; @@ -25,46 +24,48 @@ cfg_if! { /// Handle a user uploading a profile picture. Converts the image to webp and saves it to the server. #[server(input = MultipartFormData, endpoint = "/profile/upload_picture")] -pub async fn upload_picture(data: MultipartData) -> Result<(), ServerFnError> { +pub async fn upload_picture(data: MultipartData) -> BackendResult<()> { // Safe to unwrap - "On the server side, this always returns Some(_). On the client side, always returns None." let mut data = data.into_inner().unwrap(); let field = data .next_field() .await - .map_err(|e| { - ServerFnError::::ServerError(format!("Error getting field: {e}")) - })? - .ok_or_else(|| ServerFnError::::ServerError("No field found".to_string()))?; + .map_err(|e| InputError::InvalidInput(format!("Error reading multipart data: {e}"))) + .context("Error getting next field from multipart data")? + .ok_or_else(|| { + InputError::InvalidInput("Expected a field in the multipart data".to_string()) + })?; if field.name() != Some("picture") { - return Err(ServerFnError::ServerError( - "Field name is not 'picture'".to_string(), - )); + return Err(InputError::InvalidInput(format!( + "Expected field 'picture', got '{:?}'", + field.name() + )) + .into()); } // Get user id from session - let user = get_user().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })?; + let user = get_user().await.context("Error getting logged-in user")?; // Read the image, and convert it to webp use image_convert::{to_webp, ImageResource, WEBPConfig}; - let bytes = field.bytes().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting field bytes: {e}")) - })?; + let bytes = field + .bytes() + .await + .map_err(|e| InputError::InvalidInput(format!("Error reading bytes from field: {e}"))) + .context("Error reading bytes of the picture field")?; let reader = std::io::Cursor::new(bytes); - let image_source = ImageResource::from_reader(reader).map_err(|e| { - ServerFnError::::ServerError(format!("Error creating image resource: {e}")) - })?; + let image_source = ImageResource::from_reader(reader) + .map_err(|e| InputError::InvalidInput(format!("Error creating image resource: {e}"))) + .context("Error creating image resource from reader")?; let profile_picture_path = format!("assets/images/profile/{}.webp", user.id); let mut image_target = ImageResource::from_path(&profile_picture_path); - to_webp(&mut image_target, &image_source, &WEBPConfig::new()).map_err(|e| { - ServerFnError::::ServerError(format!("Error converting image to webp: {e}")) - })?; + to_webp(&mut image_target, &image_source, &WEBPConfig::new()) + .map_err(|e| InputError::InvalidInput(format!("Error converting image to webp: {e}")))?; Ok(()) } @@ -78,13 +79,8 @@ pub async fn upload_picture(data: MultipartData) -> Result<(), ServerFnError> { pub async fn recent_songs( for_user_id: i32, limit: Option, -) -> Result, ServerFnError> { - let viewing_user_id = get_user() - .await - .map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })? - .id; +) -> BackendResult> { + let viewing_user_id = get_user().await.context("Error getting logged-in user")?.id; let mut db_con = get_db_conn(); @@ -138,7 +134,8 @@ pub async fn recent_songs( song_likes::all_columns.nullable(), song_dislikes::all_columns.nullable(), )) - .load(&mut db_con)?; + .load(&mut db_con) + .context("Error loading recent songs from database")?; // Process the history data into a map of song ids to song data let mut history_songs: HashMap = HashMap::new(); @@ -201,13 +198,8 @@ pub async fn top_songs( start_date: NaiveDateTime, end_date: NaiveDateTime, limit: Option, -) -> Result, ServerFnError> { - let viewing_user_id = get_user() - .await - .map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })? - .id; +) -> BackendResult> { + let viewing_user_id = get_user().await.context("Error getting logged-in user")?.id; let mut db_con = get_db_conn(); @@ -220,14 +212,16 @@ pub async fn top_songs( .select((song_history::song_id, count(song_history::song_id))) .order(count(song_history::song_id).desc()) .limit(limit) - .load(&mut db_con)? + .load(&mut db_con) + .context("Error loading top song ids and counts from database")? } else { song_history::table .filter(song_history::date.between(start_date, end_date)) .filter(song_history::user_id.eq(for_user_id)) .group_by(song_history::song_id) .select((song_history::song_id, count(song_history::song_id))) - .load(&mut db_con)? + .load(&mut db_con) + .context("Error loading top song ids and counts from database")? }; let history_counts: HashMap = history_counts.into_iter().collect(); @@ -265,7 +259,8 @@ pub async fn top_songs( song_likes::all_columns.nullable(), song_dislikes::all_columns.nullable(), )) - .load(&mut db_con)?; + .load(&mut db_con) + .context("Error loading top songs from database")?; // Process the history data into a map of song ids to song data let mut history_songs_map: HashMap = @@ -308,8 +303,8 @@ pub async fn top_songs( let plays = history_counts .get(&song.id) - .ok_or(ServerFnError::ServerError::( - "Song id not found in history counts".to_string(), + .ok_or(BackendError::InternalError( + "Song id not found in history counts", ))?; history_songs_map.insert(song.id, (*plays, songdata)); @@ -332,7 +327,7 @@ pub async fn top_artists( start_date: NaiveDateTime, end_date: NaiveDateTime, limit: Option, -) -> Result, ServerFnError> { +) -> BackendResult> { let mut db_con = get_db_conn(); let artist_counts: Vec<(i64, Artist)> = if let Some(limit) = limit { @@ -345,7 +340,8 @@ pub async fn top_artists( .select((count(artists::id), artists::all_columns)) .order(count(artists::id).desc()) .limit(limit) - .load(&mut db_con)? + .load(&mut db_con) + .context("Error loading top artists from database")? } else { song_history::table .filter(song_history::date.between(start_date, end_date)) @@ -355,7 +351,8 @@ pub async fn top_artists( .group_by(artists::id) .select((count(artists::id), artists::all_columns)) .order(count(artists::id).desc()) - .load(&mut db_con)? + .load(&mut db_con) + .context("Error loading top artists from database")? }; let artist_data: Vec<(i64, frontend::Artist)> = artist_counts @@ -376,13 +373,8 @@ pub async fn top_artists( } #[server(endpoint = "/profile/liked_songs", client = Client)] -pub async fn get_liked_songs() -> Result, ServerFnError> { - let user_id = get_user() - .await - .map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })? - .id; +pub async fn get_liked_songs() -> BackendResult> { + let user_id = get_user().await.context("Error getting logged-in user")?.id; let mut db_conn = get_db_conn(); @@ -407,7 +399,8 @@ pub async fn get_liked_songs() -> Result, ServerFnError> { albums::all_columns.nullable(), artists::all_columns.nullable(), )) - .load(&mut db_conn)?; + .load(&mut db_conn) + .context("Error loading liked songs from database")?; let mut liked_songs: HashMap = HashMap::new(); diff --git a/src/api/search.rs b/src/api/search.rs index 251d8a3..6062efb 100644 --- a/src/api/search.rs +++ b/src/api/search.rs @@ -1,4 +1,5 @@ use crate::models::frontend; +use crate::util::error::*; use crate::util::serverfn_client::Client; use leptos::prelude::*; @@ -49,7 +50,7 @@ pub type SearchResults = Vec<(T, f32)>; pub async fn search_albums( query: String, limit: i64, -) -> Result, ServerFnError> { +) -> BackendResult> { use crate::schema::*; let mut db_conn = get_db_conn(); @@ -59,7 +60,8 @@ pub async fn search_albums( .order_by(trgm_distance(albums::title, query.clone()).desc()) .limit(limit) .select(albums::id) - .load::(&mut db_conn)?; + .load::(&mut db_conn) + .context("Error loading album ids from database")?; let mut albums_map: HashMap = HashMap::new(); @@ -75,7 +77,8 @@ pub async fn search_albums( artists::all_columns, trgm_distance(albums::title, query.clone()), )) - .load(&mut db_conn)?; + .load(&mut db_conn) + .context("Error loading album artists from database")?; for (album, artist, score) in album_artists { if let Some((stored_album, _score)) = albums_map.get_mut(&album.id) { @@ -113,7 +116,7 @@ pub async fn search_albums( pub async fn search_artists( query: String, limit: i64, -) -> Result, ServerFnError> { +) -> BackendResult> { use crate::schema::*; let mut db_conn = get_db_conn(); @@ -123,7 +126,8 @@ pub async fn search_artists( .order_by(trgm_distance(artists::name, query.clone()).desc()) .limit(limit) .select((artists::all_columns, trgm_distance(artists::name, query))) - .load::<(backend::Artist, f32)>(&mut db_conn)?; + .load::<(backend::Artist, f32)>(&mut db_conn) + .context("Error loading artists from database")?; let artist_data = artist_list .into_iter() @@ -154,11 +158,13 @@ pub async fn search_artists( pub async fn search_songs( query: String, limit: i64, -) -> Result, ServerFnError> { +) -> BackendResult> { use crate::api::auth::get_logged_in_user; use crate::schema::*; - let user = get_logged_in_user().await?; + let user = get_logged_in_user() + .await + .context("Error getting logged-in user")?; let mut db_conn = get_db_conn(); @@ -198,7 +204,8 @@ pub async fn search_songs( song_dislikes::all_columns.nullable(), trgm_distance(songs::title, query.clone()), )) - .load(&mut db_conn)?; + .load(&mut db_conn) + .context("Error loading songs from database")?; song_list } else { @@ -223,7 +230,8 @@ pub async fn search_songs( artists::all_columns.nullable(), trgm_distance(songs::title, query.clone()), )) - .load(&mut db_conn)?; + .load(&mut db_conn) + .context("Error loading songs from database")?; song_list .into_iter() @@ -293,14 +301,11 @@ pub async fn search_songs( pub async fn search( query: String, limit: i64, -) -> Result< - ( - SearchResults, - SearchResults, - SearchResults, - ), - ServerFnError, -> { +) -> BackendResult<( + SearchResults, + SearchResults, + SearchResults, +)> { let albums = search_albums(query.clone(), limit); let artists = search_artists(query.clone(), limit); let songs = search_songs(query, limit); @@ -308,5 +313,10 @@ pub async fn search( use tokio::join; let (albums, artists, songs) = join!(albums, artists, songs); - Ok((albums?, artists?, songs?)) + + let albums = albums.context("Error searching for albums")?; + let artists = artists.context("Error searching for artists")?; + let songs = songs.context("Error searching for songs")?; + + Ok((albums, artists, songs)) } diff --git a/src/api/songs.rs b/src/api/songs.rs index ee9cebe..b8c0afb 100644 --- a/src/api/songs.rs +++ b/src/api/songs.rs @@ -3,11 +3,11 @@ use leptos::prelude::*; use cfg_if::cfg_if; use crate::models::frontend; +use crate::util::error::*; use crate::util::serverfn_client::Client; cfg_if! { if #[cfg(feature = "ssr")] { - use leptos::server_fn::error::NoCustomError; use crate::util::database::get_db_conn; use crate::api::auth::get_user; use crate::models::backend::{Song, Album, Artist}; @@ -17,66 +17,56 @@ cfg_if! { /// Like or unlike a song #[server(endpoint = "songs/set_like", client = Client)] -pub async fn set_like_song(song_id: i32, like: bool) -> Result<(), ServerFnError> { - let user = get_user().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })?; +pub async fn set_like_song(song_id: i32, like: bool) -> BackendResult<()> { + let user = get_user().await.context("Error getting logged-in user")?; let db_con = &mut get_db_conn(); user.set_like_song(song_id, like, db_con) .await - .map_err(|e| ServerFnError::::ServerError(format!("Error liking song: {e}"))) + .context("Error setting like status for song") } /// Dislike or remove dislike from a song #[server(endpoint = "songs/set_dislike", client = Client)] -pub async fn set_dislike_song(song_id: i32, dislike: bool) -> Result<(), ServerFnError> { - let user = get_user().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })?; +pub async fn set_dislike_song(song_id: i32, dislike: bool) -> BackendResult<()> { + let user = get_user().await.context("Error getting logged-in user")?; let db_con = &mut get_db_conn(); user.set_dislike_song(song_id, dislike, db_con) .await - .map_err(|e| { - ServerFnError::::ServerError(format!("Error disliking song: {e}")) - }) + .context("Error setting dislike status for song") } /// Get the like and dislike status of a song #[server(endpoint = "songs/get_like_dislike", client = Client)] -pub async fn get_like_dislike_song(song_id: i32) -> Result<(bool, bool), ServerFnError> { - let user = get_user().await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })?; +pub async fn get_like_dislike_song(song_id: i32) -> BackendResult<(bool, bool)> { + let user = get_user().await.context("Error getting logged-in user")?; let db_con = &mut get_db_conn(); // TODO this could probably be done more efficiently with a tokio::try_join, but // doing so is much more complicated than it would initially seem - let like = user.get_like_song(song_id, db_con).await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting song liked: {e}")) - })?; - let dislike = user.get_dislike_song(song_id, db_con).await.map_err(|e| { - ServerFnError::::ServerError(format!("Error getting song disliked: {e}")) - })?; + let like = user + .get_like_song(song_id, db_con) + .await + .context("Error getting song like status")?; + + let dislike = user + .get_dislike_song(song_id, db_con) + .await + .context("Error getting song dislike status")?; Ok((like, dislike)) } #[server(endpoint = "songs/get", client = Client)] -pub async fn get_song_by_id(song_id: i32) -> Result, ServerFnError> { +pub async fn get_song_by_id(song_id: i32) -> BackendResult> { use crate::schema::*; - let user_id: i32 = get_user() - .await - .map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })? - .id; + let user_id: i32 = get_user().await.context("Error getting logged-in user")?.id; let db_con = &mut get_db_conn(); @@ -111,7 +101,8 @@ pub async fn get_song_by_id(song_id: i32) -> Result, Serv song_likes::all_columns.nullable(), song_dislikes::all_columns.nullable(), )) - .load(db_con)?; + .load(db_con) + .context("Error loading song from database")?; let song = song_parts.first().cloned(); let artists = song_parts @@ -148,7 +139,7 @@ pub async fn get_song_by_id(song_id: i32) -> Result, Serv } #[server(endpoint = "songs/plays", client = Client)] -pub async fn get_song_plays(song_id: i32) -> Result { +pub async fn get_song_plays(song_id: i32) -> BackendResult { use crate::schema::*; let db_con = &mut get_db_conn(); @@ -157,23 +148,16 @@ pub async fn get_song_plays(song_id: i32) -> Result { .filter(song_history::song_id.eq(song_id)) .count() .get_result::(db_con) - .map_err(|e| { - ServerFnError::::ServerError(format!("Error getting song plays: {e}")) - })?; + .context("Error getting song plays")?; Ok(plays) } #[server(endpoint = "songs/my-plays", client = Client)] -pub async fn get_my_song_plays(song_id: i32) -> Result { +pub async fn get_my_song_plays(song_id: i32) -> BackendResult { use crate::schema::*; - let user_id: i32 = get_user() - .await - .map_err(|e| { - ServerFnError::::ServerError(format!("Error getting user: {e}")) - })? - .id; + let user_id: i32 = get_user().await.context("Error getting logged-in user")?.id; let db_con = &mut get_db_conn(); @@ -185,9 +169,7 @@ pub async fn get_my_song_plays(song_id: i32) -> Result { ) .count() .get_result::(db_con) - .map_err(|e| { - ServerFnError::::ServerError(format!("Error getting song plays: {e}")) - })?; + .context("Error getting song plays for user")?; Ok(plays) } diff --git a/src/api/upload.rs b/src/api/upload.rs index 24fbcc1..601a6ef 100644 --- a/src/api/upload.rs +++ b/src/api/upload.rs @@ -1,3 +1,4 @@ +use crate::util::error::*; use leptos::prelude::*; use server_fn::codec::{MultipartData, MultipartFormData}; @@ -10,7 +11,6 @@ cfg_if! { use crate::util::extract_field::extract_field; use diesel::prelude::*; use log::*; - use server_fn::error::NoCustomError; use chrono::NaiveDate; } } @@ -18,7 +18,7 @@ cfg_if! { /// Validate the artist ids in a multipart field /// Expects a field with a comma-separated list of artist ids, and ensures each is a valid artist id in the database #[cfg(feature = "ssr")] -async fn validate_artist_ids(artist_ids: Field<'static>) -> Result, ServerFnError> { +async fn validate_artist_ids(artist_ids: Field<'static>) -> BackendResult> { use crate::models::backend::Artist; use diesel::result::Error::NotFound; @@ -40,32 +40,24 @@ async fn validate_artist_ids(artist_ids: Field<'static>) -> Result, Ser match artist { Ok(_) => Ok(artist_id), - Err(NotFound) => Err(ServerFnError::::ServerError( - "Artist does not exist".to_string(), - )), - Err(e) => Err(ServerFnError::::ServerError(format!( - "Error finding artist id: {e}" - ))), + Err(NotFound) => Err(AccessError::NotFound.context("Artist not found")), + Err(e) => Err(e.context("Error finding artist id")), } } else { - Err(ServerFnError::::ServerError( - "Error parsing artist id".to_string(), - )) + Err(InputError::InvalidInput("Error parsing artist id".to_string()).into()) } }) .collect() } - Err(e) => Err(ServerFnError::::ServerError(format!( - "Error reading artist id: {e}" - ))), + Err(e) => Err(InputError::FieldReadError(format!("Error reading artist ids: {e}")).into()), } } /// Validate the album id in a multipart field /// Expects a field with an album id, and ensures it is a valid album id in the database #[cfg(feature = "ssr")] -async fn validate_album_id(album_id: Field<'static>) -> Result, ServerFnError> { +async fn validate_album_id(album_id: Field<'static>) -> BackendResult> { use crate::models::backend::Album; use diesel::result::Error::NotFound; @@ -86,29 +78,21 @@ async fn validate_album_id(album_id: Field<'static>) -> Result, Serv match album { Ok(_) => Ok(Some(album_id)), - Err(NotFound) => Err(ServerFnError::::ServerError( - "Album does not exist".to_string(), - )), - Err(e) => Err(ServerFnError::::ServerError(format!( - "Error finding album id: {e}" - ))), + Err(NotFound) => Err(AccessError::NotFound.context("Album not found")), + Err(e) => Err(e.context("Error finding album id")), } } else { - Err(ServerFnError::::ServerError( - "Error parsing album id".to_string(), - )) + Err(InputError::InvalidInput("Error parsing album id".to_string()).into()) } } - Err(e) => Err(ServerFnError::::ServerError(format!( - "Error reading album id: {e}" - ))), + Err(e) => Err(InputError::FieldReadError(format!("Error reading album id: {e}")).into()), } } /// Validate the track number in a multipart field /// Expects a field with a track number, and ensures it is a valid track number (non-negative integer) #[cfg(feature = "ssr")] -async fn validate_track_number(track_number: Field<'static>) -> Result, ServerFnError> { +async fn validate_track_number(track_number: Field<'static>) -> BackendResult> { match track_number.text().await { Ok(track_number) => { if track_number.is_empty() { @@ -117,30 +101,27 @@ async fn validate_track_number(track_number: Field<'static>) -> Result() { if track_number < 0 { - Err(ServerFnError::::ServerError( - "Track number must be positive or 0".to_string(), - )) + Err( + InputError::InvalidInput("Track number must be positive or 0".to_string()) + .into(), + ) } else { Ok(Some(track_number)) } } else { - Err(ServerFnError::::ServerError( - "Error parsing track number".to_string(), - )) + Err(InputError::InvalidInput("Error parsing track number".to_string()).into()) } } - Err(e) => Err(ServerFnError::::ServerError(format!( - "Error reading track number: {e}" - )))?, + Err(e) => { + Err(InputError::FieldReadError(format!("Error reading track number: {e}")).into()) + } } } /// Validate the release date in a multipart field /// Expects a field with a release date, and ensures it is a valid date in the format [year]-[month]-[day] #[cfg(feature = "ssr")] -async fn validate_release_date( - release_date: Field<'static>, -) -> Result, ServerFnError> { +async fn validate_release_date(release_date: Field<'static>) -> BackendResult> { match release_date.text().await { Ok(release_date) => { if release_date.trim().is_empty() { @@ -151,20 +132,19 @@ async fn validate_release_date( match release_date { Ok(release_date) => Ok(Some(release_date)), - Err(_) => Err(ServerFnError::::ServerError( - "Invalid release date".to_string(), - )), + Err(_) => Err(InputError::InvalidInput( + "Invalid release date format, expected YYYY-MM-DD".to_string(), + ) + .into()), } } - Err(e) => Err(ServerFnError::::ServerError(format!( - "Error reading release date: {e}" - ))), + Err(e) => Err(InputError::InvalidInput(format!("Error reading release date: {e}")).into()), } } /// Handle the file upload form #[server(input = MultipartFormData, endpoint = "/upload")] -pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> { +pub async fn upload(data: MultipartData) -> BackendResult<()> { // Safe to unwrap - "On the server side, this always returns Some(_). On the client side, always returns None." let mut data = data.into_inner().unwrap(); @@ -182,19 +162,39 @@ pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> { match name.as_str() { "title" => { - title = Some(extract_field(field).await?); + title = Some( + extract_field(field) + .await + .context("Error extracting title field")?, + ); } "artist_ids" => { - artist_ids = Some(validate_artist_ids(field).await?); + artist_ids = Some( + validate_artist_ids(field) + .await + .context("Error validating artist ids")?, + ); } "album_id" => { - album_id = Some(validate_album_id(field).await?); + album_id = Some( + validate_album_id(field) + .await + .context("Error validating album id")?, + ); } "track_number" => { - track = Some(validate_track_number(field).await?); + track = Some( + validate_track_number(field) + .await + .context("Error validating track number")?, + ); } "release_date" => { - release_date = Some(validate_release_date(field).await?); + release_date = Some( + validate_release_date(field) + .await + .context("Error validating release date")?, + ); } "file" => { use crate::util::audio::extract_metadata; @@ -206,11 +206,9 @@ pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> { // or behavior that we may wish to change in the future // Create file name - let title = title - .clone() - .ok_or(ServerFnError::::ServerError( - "Title field required and must precede file field".to_string(), - ))?; + let title = title.clone().ok_or(InputError::InvalidInput( + "Title field must be present and must precede file field".to_string(), + ))?; let clean_title = title.replace(" ", "_").replace("/", "_"); let date_str = chrono::Utc::now().format("%Y-%m-%d_%H:%M:%S").to_string(); @@ -226,28 +224,30 @@ pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> { .write(true) .create(true) .truncate(true) - .open(upload_path.clone())?; + .open(upload_path.clone()) + .context("Error opening file for upload")?; - while let Some(chunk) = field.chunk().await? { - file.write_all(&chunk)?; + while let Some(chunk) = field.chunk().await.map_err(|e| { + InputError::FieldReadError(format!("Error reading file chunk: {e}")) + })? { + file.write_all(&chunk) + .context("Error writing field chunk to file")?; } - file.flush()?; + file.flush().context("Error flusing file")?; // Rewind the file so the duration can be measured - file.rewind()?; + file.rewind().context("Error rewinding file")?; // Get the codec and duration of the file - let (file_codec, file_duration) = extract_metadata(file).map_err(|e| { - let msg = format!("Error measuring duration of audio file {upload_path}: {e}"); - warn!("{}", msg); - ServerFnError::::ServerError(msg) - })?; + let (file_codec, file_duration) = extract_metadata(file) + .context("Error extracting metadata from uploaded file")?; if file_codec != CODEC_TYPE_MP3 { - let msg = format!("Invalid uploaded audio file codec: {file_codec}"); - warn!("{}", msg); - return Err(ServerFnError::::ServerError(msg)); + return Err(InputError::InvalidInput(format!( + "Invalid uploaded audio file codec: {file_codec}" + )) + .into()); } duration = Some(file_duration); @@ -259,30 +259,23 @@ pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> { } // Unwrap mandatory fields - let title = title.ok_or(ServerFnError::::ServerError( - "Missing title".to_string(), - ))?; + let title = title.ok_or(InputError::MissingField("title".to_string()))?; let artist_ids = artist_ids.unwrap_or(vec![]); - let file_name = file_name.ok_or(ServerFnError::::ServerError( - "Missing file".to_string(), - ))?; - let duration = duration.ok_or(ServerFnError::::ServerError( - "Missing duration".to_string(), - ))?; - let duration = i32::try_from(duration).map_err(|e| { - ServerFnError::::ServerError(format!( - "Error converting duration to i32: {e}" - )) - })?; + let file_name = file_name.ok_or(InputError::MissingField("file".to_string()))?; + let duration = duration.ok_or(InputError::MissingField("duration".to_string()))?; + let duration = i32::try_from(duration) + .map_err(|e| InputError::InvalidInput(format!("Error parsing duration: {e}"))) + .context("Error converting duration to i32")?; let album_id = album_id.unwrap_or(None); let track = track.unwrap_or(None); let release_date = release_date.unwrap_or(None); if album_id.is_some() != track.is_some() { - return Err(ServerFnError::::ServerError( + return Err(InputError::InvalidInput( "Album id and track number must both be present or both be absent".to_string(), - )); + ) + .into()); } // Create the song @@ -302,11 +295,7 @@ pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> { let song = song .insert_into(crate::schema::songs::table) .get_result::(db_con) - .map_err(|e| { - let msg = format!("Error saving song to database: {e}"); - warn!("{}", msg); - ServerFnError::::ServerError(msg) - })?; + .context("Error adding song to database")?; // Save the song's artists to the database use crate::schema::song_artists; @@ -325,11 +314,7 @@ pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> { diesel::insert_into(crate::schema::song_artists::table) .values(&artist_ids) .execute(db_con) - .map_err(|e| { - let msg = format!("Error saving song artists to database: {e}"); - warn!("{}", msg); - ServerFnError::::ServerError(msg) - })?; + .context("Error saving song artists to database")?; Ok(()) } diff --git a/src/api/users.rs b/src/api/users.rs index 5e958da..c13ae12 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -16,6 +16,7 @@ cfg_if::cfg_if! { } use crate::models::backend::User; +use crate::util::error::*; use crate::util::serverfn_client::Client; use leptos::prelude::*; use serde::{Deserialize, Serialize}; @@ -29,9 +30,8 @@ pub struct UserCredentials { /// Get a user from the database by username or email /// Returns a Result with the user if found, None if not found, or an error if there was a problem #[cfg(feature = "ssr")] -pub async fn find_user(username_or_email: String) -> Result, ServerFnError> { +pub async fn find_user(username_or_email: String) -> BackendResult> { use crate::schema::users::dsl::*; - use leptos::server_fn::error::NoCustomError; // Look for either a username or email that matches the input, and return an option with None if no user is found let db_con = &mut get_db_conn(); @@ -40,11 +40,7 @@ pub async fn find_user(username_or_email: String) -> Result, Server .or_filter(email.eq(username_or_email)) .first::(db_con) .optional() - .map_err(|e| { - ServerFnError::::ServerError(format!( - "Error getting user from database: {e}" - )) - })?; + .context("Error loading user from database")?; Ok(user) } @@ -52,20 +48,15 @@ pub async fn find_user(username_or_email: String) -> Result, Server /// Get a user from the database by ID /// Returns a Result with the user if found, None if not found, or an error if there was a problem #[cfg(feature = "ssr")] -pub async fn find_user_by_id(user_id: i32) -> Result, ServerFnError> { +pub async fn find_user_by_id(user_id: i32) -> BackendResult> { use crate::schema::users::dsl::*; - use leptos::server_fn::error::NoCustomError; let db_con = &mut get_db_conn(); let user = users .filter(id.eq(user_id)) .first::(db_con) .optional() - .map_err(|e| { - ServerFnError::::ServerError(format!( - "Error getting user from database: {e}" - )) - })?; + .context("Error loading user from database")?; Ok(user) } @@ -73,25 +64,21 @@ pub async fn find_user_by_id(user_id: i32) -> Result, ServerFnError /// Create a new user in the database /// Returns an empty Result if successful, or an error if there was a problem #[cfg(feature = "ssr")] -pub async fn create_user(new_user: &NewUser) -> Result<(), ServerFnError> { +pub async fn create_user(new_user: &NewUser) -> BackendResult<()> { use crate::schema::users::dsl::*; - use leptos::server_fn::error::NoCustomError; let new_password = new_user .password .clone() - .ok_or(ServerFnError::::ServerError(format!( - "No password provided for user {}", - new_user.username + .ok_or(BackendError::InputError(InputError::MissingField( + "password".to_string(), )))?; let salt = SaltString::generate(&mut OsRng); let password_hash = Pbkdf2 .hash_password(new_password.as_bytes(), &salt) - .map_err(|_| { - ServerFnError::::ServerError("Error hashing password".to_string()) - })? + .map_err(|e| AuthError::AuthError(format!("Error hashing password: {e}")))? .to_string(); let new_user = NewUser { @@ -104,9 +91,7 @@ pub async fn create_user(new_user: &NewUser) -> Result<(), ServerFnError> { diesel::insert_into(users) .values(&new_user) .execute(db_con) - .map_err(|e| { - ServerFnError::::ServerError(format!("Error creating user: {e}")) - })?; + .context("Error inserting new user into database")?; Ok(()) } @@ -114,16 +99,10 @@ pub async fn create_user(new_user: &NewUser) -> Result<(), ServerFnError> { /// Validate a user's credentials /// Returns a Result with the user if the credentials are valid, None if not valid, or an error if there was a problem #[cfg(feature = "ssr")] -pub async fn validate_user(credentials: UserCredentials) -> Result, ServerFnError> { - use leptos::server_fn::error::NoCustomError; - +pub async fn validate_user(credentials: UserCredentials) -> BackendResult> { let db_user = find_user(credentials.username_or_email.clone()) .await - .map_err(|e| { - ServerFnError::::ServerError(format!( - "Error getting user from database: {e}" - )) - })?; + .context("Error finding user in database")?; // If the user is not found, return None let db_user = match db_user { @@ -131,18 +110,13 @@ pub async fn validate_user(credentials: UserCredentials) -> Result, None => return Ok(None), }; - let db_password = - db_user - .password - .clone() - .ok_or(ServerFnError::::ServerError(format!( - "No password found for user {}", - db_user.username - )))?; + let db_password = db_user.password.clone().ok_or(AuthError::AuthError( + "No password stored for user".to_string(), + ))?; - let password_hash = PasswordHash::new(&db_password).map_err(|e| { - ServerFnError::::ServerError(format!("Error hashing supplied password: {e}")) - })?; + let password_hash = PasswordHash::new(&db_password) + .map_err(|e| AuthError::AuthError(format!("{e}"))) + .context("Error parsing password hash from database")?; match Pbkdf2.verify_password(credentials.password.as_bytes(), &password_hash) { Ok(()) => {} @@ -150,9 +124,9 @@ pub async fn validate_user(credentials: UserCredentials) -> Result, return Ok(None); } Err(e) => { - return Err(ServerFnError::::ServerError(format!( - "Error verifying password: {e}" - ))); + return Err( + AuthError::AuthError(format!("{e}")).context("Error verifying password hash") + ); } } @@ -162,8 +136,10 @@ pub async fn validate_user(credentials: UserCredentials) -> Result, /// Get a user from the database by username or email /// Returns a Result with the user if found, None if not found, or an error if there was a problem #[server(endpoint = "find_user", client = Client)] -pub async fn get_user(username_or_email: String) -> Result, ServerFnError> { - let mut user = find_user(username_or_email).await?; +pub async fn get_user(username_or_email: String) -> BackendResult> { + let mut user = find_user(username_or_email) + .await + .context("Error finding user by username or email")?; // Remove the password hash before returning the user if let Some(user) = user.as_mut() { @@ -174,8 +150,10 @@ pub async fn get_user(username_or_email: String) -> Result, ServerF } #[server(endpoint = "get_user_by_id", client = Client)] -pub async fn get_user_by_id(user_id: i32) -> Result, ServerFnError> { - let mut user = find_user_by_id(user_id).await?; +pub async fn get_user_by_id(user_id: i32) -> BackendResult> { + let mut user = find_user_by_id(user_id) + .await + .context("Error finding user by ID")?; // Remove the password hash before returning the user if let Some(user) = user.as_mut() { diff --git a/src/components/sidebar.rs b/src/components/sidebar.rs index 7988381..e7aea66 100644 --- a/src/components/sidebar.rs +++ b/src/components/sidebar.rs @@ -2,6 +2,7 @@ use crate::components::error::Error; use crate::components::loading::*; use crate::components::menu::*; use crate::util::state::GlobalState; +use leptos::either::*; use leptos::html::Div; use leptos::prelude::*; use leptos_icons::*; @@ -121,49 +122,38 @@ pub fn Playlists() -> impl IntoView { } > - - message=error.to_string() - /> - } - }).collect::>() + + + +

"Liked Songs"

+
+ {move || GlobalState::playlists().get().map(|playlists| { + match playlists { + Ok(playlists) => Either::Left(view! { + {playlists.into_iter().map(|playlist| { + let active = Signal::derive(move || { + location.pathname.get().ends_with(&format!("/playlist/{}", playlist.id)) + }); + + view! { + + +

{playlist.name}

+
+ } + }).collect::>()} + }), + Err(error) => Either::Right(error.to_component()), } - > - - -

"Liked Songs"

-
- {move || GlobalState::playlists().get().map(|playlists| { - playlists.map(|playlists| { - - view! { - {playlists.into_iter().map(|playlist| { - let active = Signal::derive(move || { - location.pathname.get().ends_with(&format!("/playlist/{}", playlist.id)) - }); - - view! { - - -

{playlist.name}

-
- } - }).collect::>()} - } - }) - })} -
+ })}
diff --git a/src/models/backend/user.rs b/src/models/backend/user.rs index f2c7e33..e79c3b9 100644 --- a/src/models/backend/user.rs +++ b/src/models/backend/user.rs @@ -8,8 +8,8 @@ cfg_if! { if #[cfg(feature = "ssr")] { use diesel::prelude::*; use crate::util::database::*; - use std::error::Error; use crate::models::backend::{Song, HistoryEntry}; + use crate::util::error::*; } } @@ -59,7 +59,7 @@ impl User { &self, limit: Option, conn: &mut PgPooledConn, - ) -> Result, Box> { + ) -> BackendResult> { use crate::schema::song_history::dsl::*; let my_history = if let Some(limit) = limit { @@ -67,7 +67,8 @@ impl User { .filter(user_id.eq(self.id)) .order(date.desc()) .limit(limit) - .load(conn)? + .load(conn) + .context("Error getting user history")? } else { song_history.filter(user_id.eq(self.id)).load(conn)? }; @@ -96,7 +97,7 @@ impl User { &self, limit: Option, conn: &mut PgPooledConn, - ) -> Result, Box> { + ) -> BackendResult> { use crate::schema::song_history::dsl::*; use crate::schema::songs::dsl::*; @@ -107,14 +108,16 @@ impl User { .order(date.desc()) .limit(limit) .select((date, songs::all_columns())) - .load(conn)? + .load(conn) + .context("Error getting user history songs")? } else { song_history .inner_join(songs) .filter(user_id.eq(self.id)) .order(date.desc()) .select((date, songs::all_columns())) - .load(conn)? + .load(conn) + .context("Error getting user history songs")? }; Ok(my_history) @@ -135,7 +138,7 @@ impl User { /// * `Result<(), Box>` - A result indicating success with an empty value, or an error /// #[cfg(feature = "ssr")] - pub fn add_history(&self, song_id: i32, conn: &mut PgPooledConn) -> Result<(), Box> { + pub fn add_history(&self, song_id: i32, conn: &mut PgPooledConn) -> BackendResult<()> { use crate::schema::song_history; diesel::insert_into(song_history::table) @@ -143,7 +146,8 @@ impl User { song_history::user_id.eq(self.id), song_history::song_id.eq(song_id), )) - .execute(conn)?; + .execute(conn) + .context("Error adding song to history")?; Ok(()) } @@ -156,7 +160,7 @@ impl User { song_id: i32, like: bool, conn: &mut PgPooledConn, - ) -> Result<(), Box> { + ) -> BackendResult<()> { use log::*; debug!("Setting like for song {} to {}", song_id, like); @@ -169,7 +173,8 @@ impl User { song_likes::song_id.eq(song_id), song_likes::user_id.eq(self.id), )) - .execute(conn)?; + .execute(conn) + .context("Error liking song")?; // Remove dislike if it exists diesel::delete( @@ -179,7 +184,8 @@ impl User { .and(song_dislikes::user_id.eq(self.id)), ), ) - .execute(conn)?; + .execute(conn) + .context("Error removing dislike for song")?; } else { diesel::delete( song_likes::table.filter( @@ -188,7 +194,8 @@ impl User { .and(song_likes::user_id.eq(self.id)), ), ) - .execute(conn)?; + .execute(conn) + .context("Error removing like for song")?; } Ok(()) @@ -200,7 +207,7 @@ impl User { &self, song_id: i32, conn: &mut PgPooledConn, - ) -> Result> { + ) -> BackendResult { use crate::schema::song_likes; let like = song_likes::table @@ -210,7 +217,8 @@ impl User { .and(song_likes::user_id.eq(self.id)), ) .first::<(i32, i32)>(conn) - .optional()? + .optional() + .context("Error checking if song is liked")? .is_some(); Ok(like) @@ -224,7 +232,7 @@ impl User { song_id: i32, dislike: bool, conn: &mut PgPooledConn, - ) -> Result<(), Box> { + ) -> BackendResult<()> { use log::*; debug!("Setting dislike for song {} to {}", song_id, dislike); @@ -237,7 +245,8 @@ impl User { song_dislikes::song_id.eq(song_id), song_dislikes::user_id.eq(self.id), )) - .execute(conn)?; + .execute(conn) + .context("Error disliking song")?; // Remove like if it exists diesel::delete( @@ -247,7 +256,8 @@ impl User { .and(song_likes::user_id.eq(self.id)), ), ) - .execute(conn)?; + .execute(conn) + .context("Error removing like for song")?; } else { diesel::delete( song_dislikes::table.filter( @@ -256,7 +266,8 @@ impl User { .and(song_dislikes::user_id.eq(self.id)), ), ) - .execute(conn)?; + .execute(conn) + .context("Error removing dislike for song")?; } Ok(()) @@ -268,7 +279,7 @@ impl User { &self, song_id: i32, conn: &mut PgPooledConn, - ) -> Result> { + ) -> BackendResult { use crate::schema::song_dislikes; let dislike = song_dislikes::table @@ -278,7 +289,8 @@ impl User { .and(song_dislikes::user_id.eq(self.id)), ) .first::<(i32, i32)>(conn) - .optional()? + .optional() + .context("Error checking if song is disliked")? .is_some(); Ok(dislike) diff --git a/src/pages/album.rs b/src/pages/album.rs index 797edab..ee41443 100644 --- a/src/pages/album.rs +++ b/src/pages/album.rs @@ -6,7 +6,6 @@ use crate::models::frontend; use leptos::either::*; use leptos::prelude::*; use leptos_router::hooks::use_params_map; -use server_fn::error::NoCustomError; #[component] pub fn AlbumPage() -> impl IntoView { @@ -61,12 +60,7 @@ fn AlbumIdPage(#[prop(into)] id: Signal) -> impl IntoView { message=format!("Album with ID {} not found", id.get()) /> }), - Err(error) => EitherOf3::C(view! { - - title="Error Getting Album" - error - /> - }), + Err(error) => EitherOf3::C(error.to_component()), } })} @@ -98,21 +92,14 @@ fn AlbumSongs(#[prop(into)] id: Signal) -> impl IntoView { } > - {e.to_string()}

}) - .collect_view() - } - } - > - {move || songs.get().map(|songs| { - songs.map(|songs| { + {move || songs.get().map(|songs| { + match songs { + Ok(songs) => Either::Left( view! { } - }) - })} -
+ ), + Err(error) => Either::Right(error.to_component()), + } + })}
} } diff --git a/src/pages/artist.rs b/src/pages/artist.rs index e31171b..bc5f459 100644 --- a/src/pages/artist.rs +++ b/src/pages/artist.rs @@ -2,7 +2,6 @@ use leptos::either::*; use leptos::prelude::*; use leptos_icons::*; use leptos_router::hooks::use_params_map; -use server_fn::error::NoCustomError; use crate::models::backend::Artist; @@ -65,12 +64,7 @@ fn ArtistIdProfile(#[prop(into)] id: Signal) -> impl IntoView { message=format!("Artist with ID {} not found", id.get()) /> }), - Err(error) => EitherOf3::C(view! { - - title="Error Getting Artist" - error - /> - }), + Err(error) => EitherOf3::C(error.to_component()) } })} @@ -126,21 +120,16 @@ fn TopSongsByArtist(#[prop(into)] artist_id: Signal) -> impl IntoView { } > - {e.to_string()}

}) - .collect_view() - } + {move || top_songs.get().map(|top_songs| { + match top_songs { + Ok(top_songs) => { + Either::Left(view! { + + }) + }, + Err(error) => Either::Right(error.to_component()), } - > - {move || top_songs.get().map(|top_songs| { - top_songs.map(|top_songs| { - view! { } - }) - })} -
+ })}
} } @@ -162,27 +151,20 @@ fn AlbumsByArtist(#[prop(into)] artist_id: Signal) -> impl IntoView { } > - {e.to_string()}

}) - .collect_view() - } - } - > - {move || albums.get().map(|albums| { - albums.map(|albums| { + {move || albums.get().map(|albums| { + match albums { + Ok(albums) => Either::Left({ let tiles = albums.into_iter().map(|album| { - album.into() + album.into() }).collect::>(); view! { } - }) - })} -
+ }), + Err(error) => Either::Right(error.to_component()), + } + })}
} } diff --git a/src/pages/profile.rs b/src/pages/profile.rs index 956b2f0..3240ba1 100644 --- a/src/pages/profile.rs +++ b/src/pages/profile.rs @@ -2,7 +2,6 @@ use leptos::either::*; use leptos::prelude::*; use leptos_icons::*; use leptos_router::hooks::use_params_map; -use server_fn::error::NoCustomError; use crate::components::dashboard_row::DashboardRow; use crate::components::error::*; @@ -118,13 +117,7 @@ fn UserIdProfile(#[prop(into)] id: Signal) -> impl IntoView { }, Err(error) => { show_details.set(false); - - EitherOf3::C(view! { - - title="Error Getting User" - error - /> - }) + EitherOf3::C(error.to_component()) } } })} @@ -203,25 +196,18 @@ fn TopSongs(#[prop(into)] user_id: Signal) -> impl IntoView { } > - {e.to_string()}

}) - .collect_view() - } - } - > - {move || - top_songs.get().map(|top_songs| { - top_songs.map(|top_songs| { + {move || + top_songs.get().map(|top_songs| { + match top_songs { + Ok(top_songs) => Either::Left({ view! { } - }) - }) - } -
+ }), + Err(err) => Either::Right(err.to_component()), + } + }) + }
} } @@ -248,25 +234,16 @@ fn RecentSongs(#[prop(into)] user_id: Signal) -> impl IntoView { } > - {e.to_string()}

}) - .collect_view() + {move || + recent_songs.get().map(|recent_songs| { + match recent_songs { + Ok(recent_songs) => Either::Left(view! { + + }), + Err(err) => Either::Right(err.to_component()), } - } - > - {move || - recent_songs.get().map(|recent_songs| { - recent_songs.map(|recent_songs| { - view! { - - } - }) - }) - } -
+ }) + }
} } @@ -305,19 +282,10 @@ fn TopArtists(#[prop(into)] user_id: Signal) -> impl IntoView { } > - {format!("Top Artists {HISTORY_MESSAGE}")} - {move || errors.get() - .into_iter() - .map(|(_, e)| view! {

{e.to_string()}

}) - .collect_view() - } - } - > - {move || - top_artists.get().map(|top_artists| { - top_artists.map(|top_artists| { + {move || + top_artists.get().map(|top_artists| { + match top_artists { + Ok(top_artists) => Either::Left({ let tiles = top_artists.into_iter().map(|artist| { artist.into() }).collect::>(); @@ -325,10 +293,16 @@ fn TopArtists(#[prop(into)] user_id: Signal) -> impl IntoView { view! { } - }) - }) - } -
+ }), + Err(err) => Either::Right( + view! { +

{format!("Top Artists {HISTORY_MESSAGE}")}

+ {err.to_component()} + } + ) + } + }) + } } } diff --git a/src/pages/search.rs b/src/pages/search.rs index caa7547..6cc125a 100644 --- a/src/pages/search.rs +++ b/src/pages/search.rs @@ -1,8 +1,8 @@ use crate::api::search::search; use crate::components::dashboard_row::*; -use crate::components::error::*; use crate::components::loading::*; use crate::components::song_list::*; +use leptos::either::*; use leptos::html::Input; use leptos::prelude::*; use leptos_router::hooks::query_signal; @@ -35,20 +35,10 @@ pub fn Search() -> impl IntoView { } > - - message=error.to_string() - /> - } - }).collect::>() - } - > - {move || { - search.get().map(|results| { - results.map(|(albums, artists, songs)| { + {move || { + search.get().map(|results| { + match results { + Ok((albums, artists, songs)) => Either::Right( view! { { (albums.is_empty() && artists.is_empty() && songs.is_empty()).then(|| { @@ -86,10 +76,11 @@ pub fn Search() -> impl IntoView { }) } } - }) - }) - }} - + ), + Err(err) => Either::Left(err.to_component()), + } + }) + }} } } diff --git a/src/pages/song.rs b/src/pages/song.rs index 45fcc52..9e0c68d 100644 --- a/src/pages/song.rs +++ b/src/pages/song.rs @@ -2,7 +2,6 @@ use leptos::either::*; use leptos::prelude::*; use leptos_icons::*; use leptos_router::hooks::use_params_map; -use server_fn::error::NoCustomError; use crate::api::songs; use crate::api::songs::*; @@ -67,14 +66,7 @@ fn SongDetails(#[prop(into)] id: Signal) -> impl IntoView { /> }) }, - Err(error) => { - EitherOf3::C(view! { - - title="Error Fetching Song" - error - /> - }) - } + Err(error) => EitherOf3::C(error.to_component()), } })} @@ -153,14 +145,7 @@ fn SongPlays(#[prop(into)] id: Signal) -> impl IntoView {

{format!("Plays: {plays}")}

}) }, - Err(error) => { - Either::Right(view! { - - title="Error fetching song plays" - error - /> - }) - } + Err(error) => Either::Right(error.to_component()) } })} @@ -183,12 +168,7 @@ fn MySongPlays(#[prop(into)] id: Signal) -> impl IntoView { }) }, Err(error) => { - Either::Right(view! { - - title="Error fetching my song plays" - error - /> - }) + Either::Right(error.to_component()) } } })} diff --git a/src/util/audio.rs b/src/util/audio.rs index 2d6f642..06fb620 100644 --- a/src/util/audio.rs +++ b/src/util/audio.rs @@ -1,3 +1,4 @@ +use crate::util::error::*; use std::fs::File; use symphonia::core::codecs::CodecType; use symphonia::core::formats::FormatOptions; @@ -7,38 +8,44 @@ use symphonia::core::probe::Hint; /// Extract the codec and duration of an audio file /// This is combined into one function because the file object will be consumed -pub fn extract_metadata(file: File) -> Result<(CodecType, u64), Box> { +pub fn extract_metadata(file: File) -> BackendResult<(CodecType, u64)> { let source_stream = MediaSourceStream::new(Box::new(file), Default::default()); let hint = Hint::new(); let format_opts = FormatOptions::default(); let metadata_opts = MetadataOptions::default(); - let probe = symphonia::default::get_probe().format( - &hint, - source_stream, - &format_opts, - &metadata_opts, - )?; + let probe = symphonia::default::get_probe() + .format(&hint, source_stream, &format_opts, &metadata_opts) + .map_err(|e| InputError::InvalidInput(format!("{e}")))?; let reader = probe.format; if reader.tracks().len() != 1 { - return Err(format!("Expected 1 track, found {}", reader.tracks().len()).into()); + return Err(InputError::InvalidInput(format!( + "Expected 1 track, found {}", + reader.tracks().len() + )) + .into()); } let track = &reader.tracks()[0]; - let time_base = track.codec_params.time_base.ok_or("Missing time base")?; + let time_base = track + .codec_params + .time_base + .ok_or(InputError::InvalidInput("Missing time base".into()))?; let duration = track .codec_params .n_frames .map(|frames| track.codec_params.start_ts + frames) - .ok_or("Missing number of frames")?; + .ok_or(InputError::InvalidInput("Missing frame count".to_string()))?; let duration = duration .checked_mul(time_base.numer as u64) .and_then(|v| v.checked_div(time_base.denom as u64)) - .ok_or("Overflow while computing duration")?; + .ok_or(BackendError::InternalError( + "Duration calculation overflowed", + ))?; Ok((track.codec_params.codec, duration)) } diff --git a/src/util/extract_field.rs b/src/util/extract_field.rs index c7ffa7a..f1bdeb4 100644 --- a/src/util/extract_field.rs +++ b/src/util/extract_field.rs @@ -1,14 +1,10 @@ +use crate::util::error::*; use multer::Field; -use server_fn::{error::NoCustomError, ServerFnError}; /// Extract the text from a multipart field -pub async fn extract_field(field: Field<'static>) -> Result { - let field = match field.text().await { - Ok(field) => field, - Err(e) => Err(ServerFnError::::ServerError(format!( - "Error reading field: {e}" - )))?, - }; - - Ok(field) +pub async fn extract_field(field: Field<'static>) -> BackendResult { + field + .text() + .await + .map_err(|e| InputError::FieldReadError(format!("{e}")).into()) } diff --git a/src/util/state.rs b/src/util/state.rs index bbb25a2..d62e2d2 100644 --- a/src/util/state.rs +++ b/src/util/state.rs @@ -6,6 +6,7 @@ use crate::api::playlists::get_playlists; use crate::models::backend; use crate::models::backend::User; use crate::models::frontend::PlayStatus; +use crate::util::error::*; /// Global front-end state /// Contains anything frequently needed across multiple components @@ -22,7 +23,7 @@ pub struct GlobalState { pub play_status: RwSignal, /// A resource that fetches the playlists - pub playlists: Resource, ServerFnError>>, + pub playlists: Resource>>, } impl GlobalState { @@ -60,7 +61,7 @@ impl GlobalState { expect_context::().play_status } - pub fn playlists() -> Resource, ServerFnError>> { + pub fn playlists() -> Resource>> { expect_context::().playlists } }